Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ manifest.json
ami-id
Pulumi*.yaml
/tools/bin/**
!/tools/bin/.gitkeep
!/tools/bin/.gitkeep
.claude
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@specs/project-context.md
98 changes: 98 additions & 0 deletions specs/api/aws/allocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# API: Allocation (AWS)

> Concept: [specs/api/concepts/allocation.md](../concepts/allocation.md)

**Package:** `github.com/redhat-developer/mapt/pkg/provider/aws/modules/allocation`

Single entry point for resolving where and on what instance type a target will run.
All AWS EC2 action `Create()` functions call this before any Pulumi stack is touched.

---

## Types

### `AllocationArgs`

> `ComputeRequestArgs` and `SpotArgs` are cross-provider types — see `specs/api/provider-interfaces.md`.

```go
type AllocationArgs struct {
ComputeRequest *cr.ComputeRequestArgs // required: hardware constraints
Prefix *string // required: used to name the spot stack
AMIProductDescription *string // optional: e.g. "Linux/UNIX" — used for spot price queries
AMIName *string // optional: scopes spot search to AMI availability
Spot *spotTypes.SpotArgs // nil = on-demand; non-nil = spot evaluation
}
```

### `AllocationResult`

```go
type AllocationResult struct {
Region *string // AWS region to deploy into
AZ *string // availability zone within that region
SpotPrice *float64 // nil when on-demand; set when spot was selected
InstanceTypes []string // one or more compatible instance type strings
}
```

---

## Functions

### `Allocation`

```go
func Allocation(mCtx *mc.Context, args *AllocationArgs) (*AllocationResult, error)
```

**Spot path** (`args.Spot != nil && args.Spot.Spot == true`):
- Creates or reuses a `spotOption-<projectName>` Pulumi stack
- Queries spot prices across eligible regions; selects best region/AZ/price
- Idempotent: if the stack already exists, returns its saved outputs without re-querying
- Returns `AllocationResult` with all four fields set

**On-demand path** (`args.Spot == nil` or `args.Spot.Spot == false`):
- Uses `mCtx.TargetHostingPlace()` as the region (set from provider default)
- Iterates AZs until one supports the required instance types
- Returns `AllocationResult` with `SpotPrice == nil`

**Error:** returns `ErrNoSupportedInstanceTypes` if no AZ in the region supports the requested types.

---

## Usage Pattern

```go
// In every AWS action Create():
r.allocationData, err = allocation.Allocation(mCtx, &allocation.AllocationArgs{
Prefix: &args.Prefix,
ComputeRequest: args.ComputeRequest,
AMIProductDescription: &amiProduct, // constant in the action's constants.go
Spot: args.Spot,
})

// Then pass results into the deploy function:
// r.allocationData.Region → NetworkArgs.Region, ComputeRequest credential region
// r.allocationData.AZ → NetworkArgs.AZ
// r.allocationData.InstanceTypes → ComputeRequest.InstaceTypes
// r.allocationData.SpotPrice → ComputeRequest.SpotPrice (when non-nil)
```

---

## Known Gaps

- `spot.Destroy()` uses `aws.DefaultCredentials` (not region-scoped); verify this is correct
when the selected spot region differs from the default AWS region
- No re-evaluation of spot selection when the persisted region becomes significantly more expensive
between runs (by design — idempotency wins; worth documenting in user docs)

---

## When to Extend This API

Open a spec under `specs/features/aws/` and update this file when:
- Adding a new allocation strategy (e.g. reserved instances, on-demand with fallback to spot)
- Adding a new field to `AllocationArgs` that all targets would benefit from
- Changing the idempotency behaviour of the spot stack
113 changes: 113 additions & 0 deletions specs/api/aws/bastion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# API: Bastion

**Package:** `github.com/redhat-developer/mapt/pkg/provider/aws/modules/bastion`

Creates a bastion host in the public subnet of an airgap network. Called automatically by
`network.Create()` when `Airgap=true` — action code never calls bastion directly during deploy.

Action code calls `bastion.WriteOutputs()` in `manageResults()` when airgap is enabled.

---

## Types

### `BastionArgs`

```go
type BastionArgs struct {
Prefix string
VPC *ec2.Vpc
Subnet *ec2.Subnet // must be the PUBLIC subnet, not the target subnet
}
```

### `BastionResult`

```go
type BastionResult struct {
Instance *ec2.Instance
PrivateKey *tls.PrivateKey
Usarname string // note: typo in source — "Usarname" not "Username"
Port int // always 22
}
```

---

## Functions

### `Create`

```go
func Create(ctx *pulumi.Context, mCtx *mc.Context, args *BastionArgs) (*BastionResult, error)
```

Called internally by `network.Create()`. Not called directly from action code.

Creates:
- Amazon Linux 2 `t2.small` instance in the public subnet
- Keypair for SSH access
- Security group allowing SSH ingress from `0.0.0.0/0`

Exports to Pulumi stack:
- `<prefix>-bastion_id_rsa`
- `<prefix>-bastion_username`
- `<prefix>-bastion_host`

### `WriteOutputs`

```go
func WriteOutputs(stackResult auto.UpResult, prefix string, destinationFolder string) error
```

Writes the three bastion stack outputs to files in `destinationFolder`:

| Stack output key | Output filename |
|---|---|
| `<prefix>-bastion_id_rsa` | `bastion_id_rsa` |
| `<prefix>-bastion_username` | `bastion_username` |
| `<prefix>-bastion_host` | `bastion_host` |

---

## Usage Pattern

```go
// In deploy(): bastion is returned as part of NetworkResult — no direct call needed
nw, err := network.Create(ctx, mCtx, &network.NetworkArgs{Airgap: true, ...})
// nw.Bastion is populated automatically

// Pass to Readiness() so SSH goes through the bastion:
c.Readiness(ctx, cmd, prefix, id, privateKey, username, nw.Bastion, deps)

// In manageResults(): write bastion files alongside target files
func manageResults(mCtx *mc.Context, stackResult auto.UpResult, prefix *string, airgap *bool) error {
if *airgap {
if err := bastion.WriteOutputs(stackResult, *prefix, mCtx.GetResultsOutputPath()); err != nil {
return err
}
}
return output.Write(stackResult, mCtx.GetResultsOutputPath(), results)
}
```

---

## Bastion Instance Spec (fixed, not configurable)

| Property | Value |
|---|---|
| AMI | Amazon Linux 2 (`amzn2-ami-hvm-*-x86_64-ebs`) |
| Instance type | `t2.small` |
| Disk | 100 GiB |
| SSH user | `ec2-user` |
| SSH port | 22 |

---

## When to Extend This API

Open a spec under `specs/features/aws/` and update this file when:
- Making bastion instance type or disk size configurable
- Adding bastion support to Azure targets
- Adding support for Session Manager as an alternative to bastion SSH
170 changes: 170 additions & 0 deletions specs/api/aws/compute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# API: Compute (AWS EC2)

> Concept: [specs/api/concepts/compute.md](../concepts/compute.md)

**Package:** `github.com/redhat-developer/mapt/pkg/provider/aws/modules/ec2/compute`

Creates the EC2 instance (on-demand) or Auto Scaling Group (spot). Always the last Pulumi
resource created in a `deploy()` function, after networking, keypair, and security groups.

---

## Types

### `ComputeRequest`

```go
type ComputeRequest struct {
MCtx *mc.Context
Prefix string
ID string // component ID — used in resource naming
VPC *ec2.Vpc // from network.NetworkResult.Vpc
Subnet *ec2.Subnet // from network.NetworkResult.Subnet
Eip *ec2.Eip // from network.NetworkResult.Eip
LB *lb.LoadBalancer // from network.NetworkResult.LoadBalancer; nil = on-demand
LBTargetGroups []int // TCP ports to register as LB target groups (e.g. []int{22, 3389})
AMI *ec2.LookupAmiResult
KeyResources *keypair.KeyPairResources
SecurityGroups pulumi.StringArray
InstaceTypes []string // from AllocationResult.InstanceTypes
InstanceProfile *iam.InstanceProfile // optional — required by SNC for SSM access
DiskSize *int // nil uses the module default (200 GiB)
Airgap bool
Spot bool // true when AllocationResult.SpotPrice != nil
SpotPrice float64 // only read when Spot=true
UserDataAsBase64 pulumi.StringPtrInput // cloud-init or PowerShell userdata
DependsOn []pulumi.Resource // explicit Pulumi dependencies
}
```

### `Compute`

```go
type Compute struct {
Instance *ec2.Instance // set when Spot=false
AutoscalingGroup *autoscaling.Group // set when Spot=true
Eip *ec2.Eip
LB *lb.LoadBalancer
Dependencies []pulumi.Resource // pass to Readiness() and RunCommand()
}
```

---

## Functions

### `NewCompute`

```go
func (r *ComputeRequest) NewCompute(ctx *pulumi.Context) (*Compute, error)
```

- `Spot=false`: creates `ec2.Instance` with direct EIP association
- `Spot=true`: creates `ec2.LaunchTemplate` + `autoscaling.Group` with mixed instances policy, forced spot, capacity-optimized allocation strategy; registers LB target groups

### `Readiness`

```go
func (c *Compute) Readiness(
ctx *pulumi.Context,
cmd string, // command.CommandCloudInitWait or command.CommandPing
prefix, id string,
mk *tls.PrivateKey,
username string,
b *bastion.BastionResult, // nil when not airgap
dependencies []pulumi.Resource,
) error
```

Runs `cmd` over SSH on the instance. Blocks Pulumi until it succeeds (timeout: 40 minutes).
Pass `c.Dependencies` as `dependencies`.

### `RunCommand`

```go
func (c *Compute) RunCommand(
ctx *pulumi.Context,
cmd string,
loggingCmdStd bool, // compute.LoggingCmdStd or compute.NoLoggingCmdStd
prefix, id string,
mk *tls.PrivateKey,
username string,
b *bastion.BastionResult,
dependencies []pulumi.Resource,
) (*remote.Command, error)
```

Like `Readiness` but returns the command resource for use as a dependency in subsequent steps.
Used by SNC to chain SSH → cluster ready → CA rotated → fetch kubeconfig.

### `GetHostDnsName`

```go
func (c *Compute) GetHostDnsName(public bool) pulumi.StringInput
```

Returns `LB.DnsName` when LB is set, otherwise `Eip.PublicDns` (public=true) or `Eip.PrivateDns` (public=false).
Export this as `<prefix>-host`.

### `GetHostIP`

```go
func (c *Compute) GetHostIP(public bool) pulumi.StringOutput
```

Returns `Eip.PublicIp` or `Eip.PrivateIp`. Used by SNC (needs IP not DNS for kubeconfig replacement).

---

## Readiness Commands

| Constant | Value | When to use |
|---|---|---|
| `command.CommandCloudInitWait` | `sudo cloud-init status --long --wait \|\| [[ $? -eq 2 \|\| $? -eq 0 ]]` | Linux targets with cloud-init |
| `command.CommandPing` | `echo ping` | Windows targets (no cloud-init) |

---

## Usage Pattern

```go
cr := compute.ComputeRequest{
MCtx: r.mCtx,
Prefix: *r.prefix,
ID: awsTargetID,
VPC: nw.Vpc,
Subnet: nw.Subnet,
Eip: nw.Eip,
LB: nw.LoadBalancer,
LBTargetGroups: []int{22}, // add 3389 for Windows
AMI: ami,
KeyResources: keyResources,
SecurityGroups: securityGroups,
InstaceTypes: r.allocationData.InstanceTypes,
DiskSize: &diskSize, // constant in constants.go
Airgap: *r.airgap,
UserDataAsBase64: udB64,
}
if r.allocationData.SpotPrice != nil {
cr.Spot = true
cr.SpotPrice = *r.allocationData.SpotPrice
}
c, err := cr.NewCompute(ctx)

ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, outputHost), c.GetHostDnsName(!*r.airgap))

return c.Readiness(ctx, command.CommandCloudInitWait,
*r.prefix, awsTargetID,
keyResources.PrivateKey, amiUserDefault,
nw.Bastion, c.Dependencies)
```

---

## When to Extend This API

Open a spec under `specs/features/aws/` and update this file when:
- Adding support for additional storage volumes
- Adding support for instance store (NVMe) configuration
- Exposing health check grace period as configurable (currently hardcoded at 1200s)
- Adding on-demand with spot fallback (noted as TODO in source)
Loading