1250 lines
48 KiB
TypeScript
1250 lines
48 KiB
TypeScript
import {
|
||
AllocateAddressCommand,
|
||
AssociateAddressCommand,
|
||
AssociateRouteTableCommand,
|
||
AttachInternetGatewayCommand,
|
||
AuthorizeSecurityGroupEgressCommand,
|
||
AuthorizeSecurityGroupIngressCommand,
|
||
CreateImageCommand,
|
||
CreateInternetGatewayCommand,
|
||
CreateKeyPairCommand,
|
||
CreateNatGatewayCommand,
|
||
CreateRouteCommand,
|
||
CreateRouteTableCommand,
|
||
CreateSecurityGroupCommand,
|
||
CreateSubnetCommand,
|
||
CreateTagsCommand,
|
||
CreateVpcCommand,
|
||
DeleteInternetGatewayCommand,
|
||
DeleteKeyPairCommand,
|
||
DeleteNatGatewayCommand,
|
||
DeleteRouteCommand,
|
||
DeleteRouteTableCommand,
|
||
DeleteSecurityGroupCommand,
|
||
DeleteSubnetCommand,
|
||
DeleteTagsCommand,
|
||
DeleteVpcCommand,
|
||
DeregisterImageCommand,
|
||
DescribeAddressesCommand,
|
||
DescribeAvailabilityZonesCommand,
|
||
DescribeImagesCommand,
|
||
DescribeInstancesCommand,
|
||
DescribeInstanceTypesCommand,
|
||
DescribeInternetGatewaysCommand,
|
||
DescribeKeyPairsCommand,
|
||
DescribeNatGatewaysCommand,
|
||
DescribeRouteTablesCommand,
|
||
DescribeSecurityGroupsCommand,
|
||
DescribeSubnetsCommand,
|
||
DescribeVpcAttributeCommand,
|
||
DescribeVpcsCommand,
|
||
DetachInternetGatewayCommand,
|
||
DisassociateAddressCommand,
|
||
DisassociateRouteTableCommand,
|
||
GetConsoleOutputCommand,
|
||
ModifySubnetAttributeCommand,
|
||
ModifyInstanceAttributeCommand,
|
||
ModifyVpcAttributeCommand,
|
||
RebootInstancesCommand,
|
||
ReleaseAddressCommand,
|
||
RevokeSecurityGroupEgressCommand,
|
||
RevokeSecurityGroupIngressCommand,
|
||
RunInstancesCommand,
|
||
StartInstancesCommand,
|
||
StopInstancesCommand,
|
||
TerminateInstancesCommand,
|
||
type Address,
|
||
type EC2Client,
|
||
type Image,
|
||
type Instance,
|
||
type InternetGateway,
|
||
type IpPermission,
|
||
type KeyPairInfo,
|
||
type NatGateway,
|
||
type RouteTable,
|
||
type SecurityGroup,
|
||
type Subnet,
|
||
type Vpc,
|
||
} from '@aws-sdk/client-ec2'
|
||
import {awsClients} from '../aws'
|
||
|
||
export type Ec2Tag = {
|
||
key: string
|
||
value: string
|
||
}
|
||
|
||
export type Ec2SecurityGroupRef = {
|
||
id?: string
|
||
name?: string
|
||
}
|
||
|
||
export type Ec2NetworkInterface = {
|
||
networkInterfaceId: string
|
||
deviceIndex?: number
|
||
status?: string
|
||
privateIpAddress?: string
|
||
publicIpAddress?: string
|
||
privateDnsName?: string
|
||
subnetId?: string
|
||
vpcId?: string
|
||
securityGroups: Ec2SecurityGroupRef[]
|
||
}
|
||
|
||
export type Ec2IpPermission = {
|
||
protocol: string
|
||
fromPort: number | null
|
||
toPort: number | null
|
||
ipRanges: string[]
|
||
ipv6Ranges: string[]
|
||
}
|
||
|
||
export type Ec2SecurityGroup = {
|
||
groupId: string
|
||
groupName: string
|
||
description: string
|
||
vpcId?: string
|
||
inboundRules: Ec2IpPermission[]
|
||
outboundRules: Ec2IpPermission[]
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2Image = {
|
||
imageId: string
|
||
name: string
|
||
description?: string
|
||
state?: string
|
||
architecture?: string
|
||
platform?: string
|
||
virtualizationType?: string
|
||
rootDeviceType?: string
|
||
createdAt?: string
|
||
ownerId?: string
|
||
public?: boolean
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2Instance = {
|
||
instanceId: string
|
||
name: string
|
||
state?: string
|
||
instanceType?: string
|
||
availabilityZone?: string
|
||
publicIpAddress?: string
|
||
privateIpAddress?: string
|
||
vpcId?: string
|
||
subnetId?: string
|
||
imageId?: string
|
||
keyName?: string
|
||
launchTime?: string
|
||
architecture?: string
|
||
platform?: string
|
||
securityGroups: Ec2SecurityGroupRef[]
|
||
networkInterfaces: Ec2NetworkInterface[]
|
||
tags: Ec2Tag[]
|
||
connection?: Ec2InstanceConnection
|
||
}
|
||
|
||
export type Ec2InstanceConnection = {
|
||
sshHost?: string
|
||
sshPort?: number
|
||
containerId?: string
|
||
containerStatus: 'running' | 'stopped' | 'unavailable'
|
||
}
|
||
|
||
export type Ec2KeyPair = {
|
||
keyPairId: string
|
||
keyName: string
|
||
keyFingerprint?: string
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2KeyPairMaterial = {
|
||
keyPairId: string
|
||
keyName: string
|
||
keyMaterial: string
|
||
}
|
||
|
||
export type Ec2KeyPairType = 'rsa' | 'ed25519'
|
||
|
||
export type Ec2Vpc = {
|
||
vpcId: string
|
||
cidrBlock: string
|
||
state?: string
|
||
isDefault: boolean
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2Subnet = {
|
||
subnetId: string
|
||
vpcId: string
|
||
cidrBlock: string
|
||
availabilityZone: string
|
||
availableIpAddressCount?: number
|
||
mapPublicIpOnLaunch: boolean
|
||
state?: string
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2InternetGateway = {
|
||
internetGatewayId: string
|
||
attachments: Array<{vpcId: string; state: string}>
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2NatGateway = {
|
||
natGatewayId: string
|
||
subnetId: string
|
||
vpcId: string
|
||
state?: string
|
||
publicIp?: string
|
||
privateIp?: string
|
||
eipAllocationId?: string
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2Route = {
|
||
destinationCidrBlock?: string
|
||
gatewayId?: string
|
||
natGatewayId?: string
|
||
vpcPeeringConnectionId?: string
|
||
state?: string
|
||
origin?: string
|
||
}
|
||
|
||
export type Ec2RouteTableAssociation = {
|
||
associationId: string
|
||
subnetId?: string
|
||
isMain: boolean
|
||
}
|
||
|
||
export type Ec2RouteTable = {
|
||
routeTableId: string
|
||
vpcId: string
|
||
routes: Ec2Route[]
|
||
associations: Ec2RouteTableAssociation[]
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type Ec2ElasticIp = {
|
||
allocationId: string
|
||
publicIp: string
|
||
domain?: string
|
||
associationId?: string
|
||
instanceId?: string
|
||
networkInterfaceId?: string
|
||
tags: Ec2Tag[]
|
||
}
|
||
|
||
export type CreateNatGatewayInput = {
|
||
name?: string
|
||
subnetId: string
|
||
allocationId?: string
|
||
}
|
||
|
||
export type CreateRouteInput = {
|
||
destinationCidrBlock: string
|
||
gatewayId?: string
|
||
natGatewayId?: string
|
||
vpcPeeringConnectionId?: string
|
||
}
|
||
|
||
export type Ec2ConsoleOutput = {
|
||
instanceId: string
|
||
output: string
|
||
timestamp?: string
|
||
}
|
||
|
||
export type RunInstanceInput = {
|
||
name: string
|
||
imageId: string
|
||
instanceType: string
|
||
keyName?: string
|
||
subnetId?: string
|
||
securityGroupIds?: string[]
|
||
userData?: string
|
||
iamInstanceProfileName?: string
|
||
associatePublicIpAddress?: boolean
|
||
rootVolumeSize?: number
|
||
rootVolumeType?: string
|
||
}
|
||
|
||
export type CreateAmiInput = {
|
||
instanceId: string
|
||
name: string
|
||
description?: string
|
||
noReboot?: boolean
|
||
}
|
||
|
||
export type IpPermissionInput = {
|
||
protocol: string
|
||
fromPort: number
|
||
toPort: number
|
||
cidr: string
|
||
}
|
||
|
||
export type Ec2AvailabilityZone = {
|
||
zoneName: string
|
||
zoneId: string
|
||
state: string
|
||
}
|
||
|
||
export type Ec2InstanceType = {
|
||
instanceType: string
|
||
vcpu: number
|
||
memoryMiB: number
|
||
}
|
||
|
||
export type Ec2VpcAttributes = {
|
||
enableDnsHostnames: boolean
|
||
enableDnsSupport: boolean
|
||
}
|
||
|
||
function toTags(sdkTags: Array<{Key?: string; Value?: string}> | undefined): Ec2Tag[] {
|
||
return (sdkTags ?? []).map((t) => ({key: t.Key ?? '', value: t.Value ?? ''}))
|
||
}
|
||
|
||
function nameFromTags(tags: Array<{Key?: string; Value?: string}> | undefined): string {
|
||
return tags?.find((t) => t.Key === 'Name')?.Value ?? ''
|
||
}
|
||
|
||
function toEc2Instance(instance: Instance): Ec2Instance {
|
||
const tags = toTags(instance.Tags)
|
||
return {
|
||
instanceId: instance.InstanceId ?? '',
|
||
name: nameFromTags(instance.Tags) || (instance.InstanceId ?? ''),
|
||
state: instance.State?.Name,
|
||
instanceType: instance.InstanceType,
|
||
availabilityZone: instance.Placement?.AvailabilityZone,
|
||
publicIpAddress: instance.PublicIpAddress,
|
||
privateIpAddress: instance.PrivateIpAddress,
|
||
vpcId: instance.VpcId,
|
||
subnetId: instance.SubnetId,
|
||
imageId: instance.ImageId,
|
||
keyName: instance.KeyName,
|
||
launchTime: instance.LaunchTime?.toISOString(),
|
||
architecture: instance.Architecture,
|
||
platform: instance.Platform,
|
||
securityGroups: (instance.SecurityGroups ?? []).map((sg) => ({
|
||
id: sg.GroupId,
|
||
name: sg.GroupName,
|
||
})),
|
||
networkInterfaces: (instance.NetworkInterfaces ?? []).map((networkInterface) => ({
|
||
networkInterfaceId: networkInterface.NetworkInterfaceId ?? '',
|
||
deviceIndex: networkInterface.Attachment?.DeviceIndex,
|
||
status: networkInterface.Status,
|
||
privateIpAddress: networkInterface.PrivateIpAddress,
|
||
publicIpAddress: networkInterface.Association?.PublicIp,
|
||
privateDnsName: networkInterface.PrivateDnsName,
|
||
subnetId: networkInterface.SubnetId,
|
||
vpcId: networkInterface.VpcId,
|
||
securityGroups: (networkInterface.Groups ?? []).map((group) => ({
|
||
id: group.GroupId,
|
||
name: group.GroupName,
|
||
})),
|
||
})),
|
||
tags,
|
||
}
|
||
}
|
||
|
||
function toEc2Image(image: Image): Ec2Image {
|
||
return {
|
||
imageId: image.ImageId ?? '',
|
||
name: image.Name ?? image.ImageId ?? '',
|
||
description: image.Description,
|
||
state: image.State,
|
||
architecture: image.Architecture,
|
||
platform: image.Platform,
|
||
virtualizationType: image.VirtualizationType,
|
||
rootDeviceType: image.RootDeviceType,
|
||
createdAt: image.CreationDate,
|
||
ownerId: image.OwnerId,
|
||
public: image.Public,
|
||
tags: toTags(image.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2IpPermission(p: IpPermission): Ec2IpPermission {
|
||
return {
|
||
protocol: p.IpProtocol ?? '-1',
|
||
fromPort: p.FromPort ?? null,
|
||
toPort: p.ToPort ?? null,
|
||
ipRanges: (p.IpRanges ?? []).map((r) => r.CidrIp ?? '').filter(Boolean),
|
||
ipv6Ranges: (p.Ipv6Ranges ?? []).map((r) => r.CidrIpv6 ?? '').filter(Boolean),
|
||
}
|
||
}
|
||
|
||
function toEc2SecurityGroup(sg: SecurityGroup): Ec2SecurityGroup {
|
||
return {
|
||
groupId: sg.GroupId ?? '',
|
||
groupName: sg.GroupName ?? '',
|
||
description: sg.Description ?? '',
|
||
vpcId: sg.VpcId,
|
||
inboundRules: (sg.IpPermissions ?? []).map(toEc2IpPermission),
|
||
outboundRules: (sg.IpPermissionsEgress ?? []).map(toEc2IpPermission),
|
||
tags: toTags(sg.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2KeyPair(kp: KeyPairInfo): Ec2KeyPair {
|
||
return {
|
||
keyPairId: kp.KeyPairId ?? '',
|
||
keyName: kp.KeyName ?? '',
|
||
keyFingerprint: kp.KeyFingerprint,
|
||
tags: toTags(kp.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2Vpc(vpc: Vpc): Ec2Vpc {
|
||
return {
|
||
vpcId: vpc.VpcId ?? '',
|
||
cidrBlock: vpc.CidrBlock ?? '',
|
||
state: vpc.State,
|
||
isDefault: vpc.IsDefault ?? false,
|
||
tags: toTags(vpc.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2Subnet(subnet: Subnet): Ec2Subnet {
|
||
return {
|
||
subnetId: subnet.SubnetId ?? '',
|
||
vpcId: subnet.VpcId ?? '',
|
||
cidrBlock: subnet.CidrBlock ?? '',
|
||
availabilityZone: subnet.AvailabilityZone ?? '',
|
||
availableIpAddressCount: subnet.AvailableIpAddressCount,
|
||
mapPublicIpOnLaunch: subnet.MapPublicIpOnLaunch ?? false,
|
||
state: subnet.State,
|
||
tags: toTags(subnet.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2InternetGateway(igw: InternetGateway): Ec2InternetGateway {
|
||
return {
|
||
internetGatewayId: igw.InternetGatewayId ?? '',
|
||
attachments: (igw.Attachments ?? []).map((a) => ({
|
||
vpcId: a.VpcId ?? '',
|
||
state: a.State ?? '',
|
||
})),
|
||
tags: toTags(igw.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2NatGateway(nat: NatGateway): Ec2NatGateway {
|
||
const addr = nat.NatGatewayAddresses?.[0]
|
||
return {
|
||
natGatewayId: nat.NatGatewayId ?? '',
|
||
subnetId: nat.SubnetId ?? '',
|
||
vpcId: nat.VpcId ?? '',
|
||
state: nat.State,
|
||
publicIp: addr?.PublicIp,
|
||
privateIp: addr?.PrivateIp,
|
||
eipAllocationId: addr?.AllocationId,
|
||
tags: toTags(nat.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2RouteTable(rt: RouteTable): Ec2RouteTable {
|
||
return {
|
||
routeTableId: rt.RouteTableId ?? '',
|
||
vpcId: rt.VpcId ?? '',
|
||
routes: (rt.Routes ?? []).map((r) => ({
|
||
destinationCidrBlock: r.DestinationCidrBlock,
|
||
gatewayId: r.GatewayId,
|
||
natGatewayId: r.NatGatewayId,
|
||
vpcPeeringConnectionId: r.VpcPeeringConnectionId,
|
||
state: r.State,
|
||
origin: r.Origin,
|
||
})),
|
||
associations: (rt.Associations ?? []).map((a) => ({
|
||
associationId: a.RouteTableAssociationId ?? '',
|
||
subnetId: a.SubnetId,
|
||
isMain: a.Main ?? false,
|
||
})),
|
||
tags: toTags(rt.Tags),
|
||
}
|
||
}
|
||
|
||
function toEc2ElasticIp(addr: Address): Ec2ElasticIp {
|
||
return {
|
||
allocationId: addr.AllocationId ?? '',
|
||
publicIp: addr.PublicIp ?? '',
|
||
domain: addr.Domain,
|
||
associationId: addr.AssociationId,
|
||
instanceId: addr.InstanceId,
|
||
networkInterfaceId: addr.NetworkInterfaceId,
|
||
tags: toTags(addr.Tags),
|
||
}
|
||
}
|
||
|
||
function isUnsupportedOperation(error: unknown) {
|
||
return error instanceof Error && error.message.includes('is not supported')
|
||
}
|
||
|
||
export function createEc2Service(client: EC2Client = awsClients.ec2) {
|
||
return {
|
||
async listInstances(): Promise<Ec2Instance[]> {
|
||
const instances: Ec2Instance[] = []
|
||
let nextToken: string | undefined
|
||
|
||
do {
|
||
const res = await client.send(new DescribeInstancesCommand({NextToken: nextToken}))
|
||
for (const reservation of res.Reservations ?? []) {
|
||
instances.push(...(reservation.Instances ?? []).map(toEc2Instance))
|
||
}
|
||
nextToken = res.NextToken
|
||
} while (nextToken)
|
||
|
||
return instances
|
||
},
|
||
|
||
async describeInstance(instanceId: string): Promise<Ec2Instance> {
|
||
const res = await client.send(new DescribeInstancesCommand({InstanceIds: [instanceId]}))
|
||
const instance = res.Reservations?.[0]?.Instances?.[0]
|
||
return toEc2Instance(instance ?? {})
|
||
},
|
||
|
||
async runInstance(input: RunInstanceInput): Promise<Ec2Instance> {
|
||
const blockDeviceMappings = input.rootVolumeSize
|
||
? [{
|
||
DeviceName: '/dev/xvda',
|
||
Ebs: {
|
||
VolumeSize: input.rootVolumeSize,
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
VolumeType: (input.rootVolumeType ?? 'gp3') as any,
|
||
DeleteOnTermination: true,
|
||
},
|
||
}]
|
||
: undefined
|
||
const networkInterfaces = input.subnetId
|
||
? [{
|
||
DeviceIndex: 0,
|
||
SubnetId: input.subnetId,
|
||
AssociatePublicIpAddress: input.associatePublicIpAddress ?? false,
|
||
Groups: input.securityGroupIds?.length ? input.securityGroupIds : undefined,
|
||
}]
|
||
: undefined
|
||
const res = await client.send(new RunInstancesCommand({
|
||
ImageId: input.imageId,
|
||
MinCount: 1,
|
||
MaxCount: 1,
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
InstanceType: input.instanceType as any,
|
||
KeyName: input.keyName || undefined,
|
||
...(networkInterfaces
|
||
? {NetworkInterfaces: networkInterfaces}
|
||
: {
|
||
SubnetId: input.subnetId || undefined,
|
||
SecurityGroupIds: input.securityGroupIds?.length ? input.securityGroupIds : undefined,
|
||
}),
|
||
UserData: input.userData ? Buffer.from(input.userData).toString('base64') : undefined,
|
||
IamInstanceProfile: input.iamInstanceProfileName
|
||
? {Name: input.iamInstanceProfileName}
|
||
: undefined,
|
||
BlockDeviceMappings: blockDeviceMappings,
|
||
TagSpecifications: [{
|
||
ResourceType: 'instance',
|
||
Tags: [{Key: 'Name', Value: input.name}],
|
||
}],
|
||
}))
|
||
const instance = res.Instances?.[0]
|
||
if (!instance) throw new Error('RunInstances returned no instance')
|
||
return toEc2Instance(instance)
|
||
},
|
||
|
||
async startInstance(instanceId: string): Promise<void> {
|
||
await client.send(new StartInstancesCommand({InstanceIds: [instanceId]}))
|
||
},
|
||
|
||
async stopInstance(instanceId: string): Promise<void> {
|
||
await client.send(new StopInstancesCommand({InstanceIds: [instanceId]}))
|
||
},
|
||
|
||
async rebootInstance(instanceId: string): Promise<void> {
|
||
await client.send(new RebootInstancesCommand({InstanceIds: [instanceId]}))
|
||
},
|
||
|
||
async terminateInstance(instanceId: string): Promise<void> {
|
||
await client.send(new TerminateInstancesCommand({InstanceIds: [instanceId]}))
|
||
},
|
||
|
||
async updateInstanceTags(instanceId: string, toAdd: Ec2Tag[], toRemove: string[]): Promise<void> {
|
||
if (toAdd.length > 0) {
|
||
await client.send(new CreateTagsCommand({
|
||
Resources: [instanceId],
|
||
Tags: toAdd.map((t) => ({Key: t.key, Value: t.value})),
|
||
}))
|
||
}
|
||
if (toRemove.length > 0) {
|
||
await client.send(new DeleteTagsCommand({
|
||
Resources: [instanceId],
|
||
Tags: toRemove.map((k) => ({Key: k})),
|
||
}))
|
||
}
|
||
},
|
||
|
||
async updateInstanceSecurityGroups(instanceId: string, groupIds: string[]): Promise<void> {
|
||
await client.send(new ModifyInstanceAttributeCommand({
|
||
InstanceId: instanceId,
|
||
Groups: groupIds,
|
||
}))
|
||
},
|
||
|
||
async createImage(input: CreateAmiInput): Promise<{imageId: string}> {
|
||
const res = await client.send(new CreateImageCommand({
|
||
InstanceId: input.instanceId,
|
||
Name: input.name,
|
||
Description: input.description,
|
||
NoReboot: input.noReboot,
|
||
}))
|
||
return {imageId: res.ImageId ?? ''}
|
||
},
|
||
|
||
async deregisterImage(imageId: string): Promise<void> {
|
||
await client.send(new DeregisterImageCommand({ImageId: imageId}))
|
||
},
|
||
|
||
async listAmis(): Promise<Ec2Image[]> {
|
||
try {
|
||
const res = await client.send(new DescribeImagesCommand({}))
|
||
return (res.Images ?? []).map(toEc2Image)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async getConsoleOutput(instanceId: string): Promise<Ec2ConsoleOutput> {
|
||
try {
|
||
const res = await client.send(new GetConsoleOutputCommand({InstanceId: instanceId}))
|
||
const decoded = res.Output ? Buffer.from(res.Output, 'base64').toString('utf-8') : ''
|
||
return {instanceId, output: decoded, timestamp: res.Timestamp?.toISOString()}
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return {instanceId, output: ''}
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async listKeyPairs(): Promise<Ec2KeyPair[]> {
|
||
try {
|
||
const res = await client.send(new DescribeKeyPairsCommand({}))
|
||
return (res.KeyPairs ?? []).map(toEc2KeyPair)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createKeyPair(name: string, keyType: Ec2KeyPairType = 'rsa'): Promise<Ec2KeyPairMaterial> {
|
||
const res = await client.send(new CreateKeyPairCommand({KeyName: name, KeyType: keyType}))
|
||
return {
|
||
keyPairId: res.KeyPairId ?? '',
|
||
keyName: res.KeyName ?? name,
|
||
keyMaterial: res.KeyMaterial ?? '',
|
||
}
|
||
},
|
||
|
||
async deleteKeyPair(keyName: string): Promise<void> {
|
||
await client.send(new DeleteKeyPairCommand({KeyName: keyName}))
|
||
},
|
||
|
||
async listSecurityGroups(vpcId?: string): Promise<Ec2SecurityGroup[]> {
|
||
try {
|
||
const res = await client.send(new DescribeSecurityGroupsCommand({
|
||
Filters: vpcId ? [{Name: 'vpc-id', Values: [vpcId]}] : undefined,
|
||
}))
|
||
return (res.SecurityGroups ?? []).map(toEc2SecurityGroup)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createSecurityGroup(name: string, description: string, vpcId: string): Promise<{groupId: string}> {
|
||
const res = await client.send(new CreateSecurityGroupCommand({
|
||
GroupName: name,
|
||
Description: description,
|
||
VpcId: vpcId,
|
||
}))
|
||
return {groupId: res.GroupId ?? ''}
|
||
},
|
||
|
||
async deleteSecurityGroup(groupId: string): Promise<void> {
|
||
await client.send(new DeleteSecurityGroupCommand({GroupId: groupId}))
|
||
},
|
||
|
||
async authorizeSecurityGroupIngress(groupId: string, permission: IpPermissionInput): Promise<void> {
|
||
await client.send(new AuthorizeSecurityGroupIngressCommand({
|
||
GroupId: groupId,
|
||
IpPermissions: [{
|
||
IpProtocol: permission.protocol,
|
||
FromPort: permission.fromPort,
|
||
ToPort: permission.toPort,
|
||
IpRanges: [{CidrIp: permission.cidr}],
|
||
}],
|
||
}))
|
||
},
|
||
|
||
async revokeSecurityGroupIngress(groupId: string, permission: IpPermissionInput): Promise<void> {
|
||
await client.send(new RevokeSecurityGroupIngressCommand({
|
||
GroupId: groupId,
|
||
IpPermissions: [{
|
||
IpProtocol: permission.protocol,
|
||
FromPort: permission.fromPort,
|
||
ToPort: permission.toPort,
|
||
IpRanges: [{CidrIp: permission.cidr}],
|
||
}],
|
||
}))
|
||
},
|
||
|
||
async authorizeSecurityGroupEgress(groupId: string, permission: IpPermissionInput): Promise<void> {
|
||
await client.send(new AuthorizeSecurityGroupEgressCommand({
|
||
GroupId: groupId,
|
||
IpPermissions: [{
|
||
IpProtocol: permission.protocol,
|
||
FromPort: permission.fromPort,
|
||
ToPort: permission.toPort,
|
||
IpRanges: [{CidrIp: permission.cidr}],
|
||
}],
|
||
}))
|
||
},
|
||
|
||
async revokeSecurityGroupEgress(groupId: string, permission: IpPermissionInput): Promise<void> {
|
||
await client.send(new RevokeSecurityGroupEgressCommand({
|
||
GroupId: groupId,
|
||
IpPermissions: [{
|
||
IpProtocol: permission.protocol,
|
||
FromPort: permission.fromPort,
|
||
ToPort: permission.toPort,
|
||
IpRanges: [{CidrIp: permission.cidr}],
|
||
}],
|
||
}))
|
||
},
|
||
|
||
async listAvailabilityZones(): Promise<Ec2AvailabilityZone[]> {
|
||
try {
|
||
const res = await client.send(new DescribeAvailabilityZonesCommand({}))
|
||
return (res.AvailabilityZones ?? []).map((az) => ({
|
||
zoneName: az.ZoneName ?? '',
|
||
zoneId: az.ZoneId ?? '',
|
||
state: az.State ?? 'available',
|
||
}))
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async listInstanceTypes(): Promise<Ec2InstanceType[]> {
|
||
try {
|
||
const res = await client.send(new DescribeInstanceTypesCommand({}))
|
||
return (res.InstanceTypes ?? []).map((it) => ({
|
||
instanceType: it.InstanceType ?? '',
|
||
vcpu: it.VCpuInfo?.DefaultVCpus ?? 0,
|
||
memoryMiB: it.MemoryInfo?.SizeInMiB ?? 0,
|
||
}))
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async modifySubnetMapPublicIp(subnetId: string, mapPublicIpOnLaunch: boolean): Promise<void> {
|
||
await client.send(new ModifySubnetAttributeCommand({
|
||
SubnetId: subnetId,
|
||
MapPublicIpOnLaunch: {Value: mapPublicIpOnLaunch},
|
||
}))
|
||
},
|
||
|
||
async getVpcAttributes(vpcId: string): Promise<Ec2VpcAttributes> {
|
||
const [hostnamesRes, supportRes] = await Promise.all([
|
||
client.send(new DescribeVpcAttributeCommand({VpcId: vpcId, Attribute: 'enableDnsHostnames'})),
|
||
client.send(new DescribeVpcAttributeCommand({VpcId: vpcId, Attribute: 'enableDnsSupport'})),
|
||
])
|
||
return {
|
||
enableDnsHostnames: hostnamesRes.EnableDnsHostnames?.Value ?? false,
|
||
enableDnsSupport: supportRes.EnableDnsSupport?.Value ?? false,
|
||
}
|
||
},
|
||
|
||
async modifyVpcAttribute(vpcId: string, attribute: 'enableDnsHostnames' | 'enableDnsSupport', value: boolean): Promise<void> {
|
||
const cmd = attribute === 'enableDnsHostnames'
|
||
? new ModifyVpcAttributeCommand({VpcId: vpcId, EnableDnsHostnames: {Value: value}})
|
||
: new ModifyVpcAttributeCommand({VpcId: vpcId, EnableDnsSupport: {Value: value}})
|
||
await client.send(cmd)
|
||
},
|
||
|
||
async associateElasticIp(allocationId: string, instanceId: string): Promise<{associationId: string}> {
|
||
const res = await client.send(new AssociateAddressCommand({AllocationId: allocationId, InstanceId: instanceId}))
|
||
return {associationId: res.AssociationId ?? ''}
|
||
},
|
||
|
||
async disassociateElasticIp(associationId: string): Promise<void> {
|
||
await client.send(new DisassociateAddressCommand({AssociationId: associationId}))
|
||
},
|
||
|
||
async listVpcs(): Promise<Ec2Vpc[]> {
|
||
try {
|
||
const res = await client.send(new DescribeVpcsCommand({}))
|
||
return (res.Vpcs ?? []).map(toEc2Vpc)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createVpc(cidrBlock: string): Promise<Ec2Vpc> {
|
||
const res = await client.send(new CreateVpcCommand({CidrBlock: cidrBlock}))
|
||
return toEc2Vpc(res.Vpc ?? {})
|
||
},
|
||
|
||
async deleteVpc(vpcId: string): Promise<void> {
|
||
const vpcFilter = [{Name: 'vpc-id', Values: [vpcId]}]
|
||
|
||
// 1. Delete non-default security groups
|
||
try {
|
||
const sgsRes = await client.send(new DescribeSecurityGroupsCommand({Filters: vpcFilter}))
|
||
for (const sg of sgsRes.SecurityGroups ?? []) {
|
||
if (sg.GroupName !== 'default' && sg.GroupId) {
|
||
try { await client.send(new DeleteSecurityGroupCommand({GroupId: sg.GroupId})) } catch { /* skip */ }
|
||
}
|
||
}
|
||
} catch (error) { if (!isUnsupportedOperation(error)) throw error }
|
||
|
||
// 2. Delete subnets
|
||
try {
|
||
const subnetsRes = await client.send(new DescribeSubnetsCommand({Filters: vpcFilter}))
|
||
for (const subnet of subnetsRes.Subnets ?? []) {
|
||
if (subnet.SubnetId) {
|
||
try { await client.send(new DeleteSubnetCommand({SubnetId: subnet.SubnetId})) } catch { /* skip */ }
|
||
}
|
||
}
|
||
} catch (error) { if (!isUnsupportedOperation(error)) throw error }
|
||
|
||
// 3. Detach and delete internet gateways
|
||
try {
|
||
const igwRes = await client.send(new DescribeInternetGatewaysCommand({Filters: [{Name: 'attachment.vpc-id', Values: [vpcId]}]}))
|
||
for (const igw of igwRes.InternetGateways ?? []) {
|
||
if (!igw.InternetGatewayId) continue
|
||
for (const att of igw.Attachments ?? []) {
|
||
if (att.VpcId) {
|
||
try { await client.send(new DetachInternetGatewayCommand({InternetGatewayId: igw.InternetGatewayId, VpcId: att.VpcId})) } catch { /* skip */ }
|
||
}
|
||
}
|
||
try { await client.send(new DeleteInternetGatewayCommand({InternetGatewayId: igw.InternetGatewayId})) } catch { /* skip */ }
|
||
}
|
||
} catch (error) { if (!isUnsupportedOperation(error)) throw error }
|
||
|
||
// 4. Disassociate and delete non-main route tables
|
||
try {
|
||
const rtbRes = await client.send(new DescribeRouteTablesCommand({Filters: vpcFilter}))
|
||
for (const rt of rtbRes.RouteTables ?? []) {
|
||
if (!rt.RouteTableId) continue
|
||
const isMain = (rt.Associations ?? []).some((a) => a.Main)
|
||
if (isMain) continue
|
||
for (const assoc of rt.Associations ?? []) {
|
||
if (!assoc.Main && assoc.RouteTableAssociationId) {
|
||
try { await client.send(new DisassociateRouteTableCommand({AssociationId: assoc.RouteTableAssociationId})) } catch { /* skip */ }
|
||
}
|
||
}
|
||
try { await client.send(new DeleteRouteTableCommand({RouteTableId: rt.RouteTableId})) } catch { /* skip */ }
|
||
}
|
||
} catch (error) { if (!isUnsupportedOperation(error)) throw error }
|
||
|
||
// 5. Delete the VPC
|
||
await client.send(new DeleteVpcCommand({VpcId: vpcId}))
|
||
},
|
||
|
||
async listSubnets(vpcId?: string): Promise<Ec2Subnet[]> {
|
||
try {
|
||
const res = await client.send(new DescribeSubnetsCommand({
|
||
Filters: vpcId ? [{Name: 'vpc-id', Values: [vpcId]}] : undefined,
|
||
}))
|
||
return (res.Subnets ?? []).map(toEc2Subnet)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createSubnet(vpcId: string, cidrBlock: string, availabilityZone?: string): Promise<Ec2Subnet> {
|
||
const res = await client.send(new CreateSubnetCommand({
|
||
VpcId: vpcId,
|
||
CidrBlock: cidrBlock,
|
||
AvailabilityZone: availabilityZone || undefined,
|
||
}))
|
||
return toEc2Subnet(res.Subnet ?? {})
|
||
},
|
||
|
||
async deleteSubnet(subnetId: string): Promise<void> {
|
||
await client.send(new DeleteSubnetCommand({SubnetId: subnetId}))
|
||
},
|
||
|
||
// ── Internet Gateways ──────────────────────────────────────────────────
|
||
|
||
async listInternetGateways(): Promise<Ec2InternetGateway[]> {
|
||
try {
|
||
const res = await client.send(new DescribeInternetGatewaysCommand({}))
|
||
return (res.InternetGateways ?? []).map(toEc2InternetGateway)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createInternetGateway(name?: string): Promise<Ec2InternetGateway> {
|
||
const res = await client.send(new CreateInternetGatewayCommand({}))
|
||
const igw = res.InternetGateway ?? {}
|
||
if (name && igw.InternetGatewayId) {
|
||
await client.send(new CreateTagsCommand({
|
||
Resources: [igw.InternetGatewayId],
|
||
Tags: [{Key: 'Name', Value: name}],
|
||
}))
|
||
igw.Tags = [{Key: 'Name', Value: name}]
|
||
}
|
||
return toEc2InternetGateway(igw)
|
||
},
|
||
|
||
async attachInternetGateway(igwId: string, vpcId: string): Promise<void> {
|
||
await client.send(new AttachInternetGatewayCommand({InternetGatewayId: igwId, VpcId: vpcId}))
|
||
},
|
||
|
||
async detachInternetGateway(igwId: string, vpcId: string): Promise<void> {
|
||
await client.send(new DetachInternetGatewayCommand({InternetGatewayId: igwId, VpcId: vpcId}))
|
||
},
|
||
|
||
async deleteInternetGateway(igwId: string): Promise<void> {
|
||
// Auto-detach from any attached VPCs before deleting
|
||
const res = await client.send(new DescribeInternetGatewaysCommand({InternetGatewayIds: [igwId]}))
|
||
const attachments = res.InternetGateways?.[0]?.Attachments ?? []
|
||
for (const attachment of attachments) {
|
||
if (attachment.VpcId) {
|
||
await client.send(new DetachInternetGatewayCommand({InternetGatewayId: igwId, VpcId: attachment.VpcId}))
|
||
}
|
||
}
|
||
await client.send(new DeleteInternetGatewayCommand({InternetGatewayId: igwId}))
|
||
},
|
||
|
||
// ── NAT Gateways ───────────────────────────────────────────────────────
|
||
|
||
async listNatGateways(vpcId?: string): Promise<Ec2NatGateway[]> {
|
||
try {
|
||
const res = await client.send(new DescribeNatGatewaysCommand({
|
||
Filter: vpcId ? [{Name: 'vpc-id', Values: [vpcId]}] : undefined,
|
||
}))
|
||
return (res.NatGateways ?? []).filter((n) => n.State !== 'deleted').map(toEc2NatGateway)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createNatGateway(input: CreateNatGatewayInput): Promise<Ec2NatGateway> {
|
||
let allocationId = input.allocationId
|
||
if (!allocationId) {
|
||
const eipRes = await client.send(new AllocateAddressCommand({Domain: 'vpc'}))
|
||
allocationId = eipRes.AllocationId!
|
||
}
|
||
const natRes = await client.send(new CreateNatGatewayCommand({
|
||
SubnetId: input.subnetId,
|
||
AllocationId: allocationId,
|
||
ConnectivityType: 'public',
|
||
}))
|
||
const natId = natRes.NatGateway!.NatGatewayId!
|
||
if (input.name) {
|
||
await client.send(new CreateTagsCommand({Resources: [natId], Tags: [{Key: 'Name', Value: input.name}]}))
|
||
}
|
||
// Poll until available
|
||
for (let attempt = 0; attempt < 15; attempt++) {
|
||
const st = await client.send(new DescribeNatGatewaysCommand({NatGatewayIds: [natId]}))
|
||
const nat = st.NatGateways?.[0]
|
||
if (nat?.State === 'available') return toEc2NatGateway(nat)
|
||
if (nat?.State === 'failed') throw new Error(`NAT Gateway ${natId} failed to become available`)
|
||
await new Promise((r) => setTimeout(r, 1000))
|
||
}
|
||
// Return last known state after polling
|
||
const final = await client.send(new DescribeNatGatewaysCommand({NatGatewayIds: [natId]}))
|
||
return toEc2NatGateway(final.NatGateways?.[0] ?? {NatGatewayId: natId, SubnetId: input.subnetId})
|
||
},
|
||
|
||
async deleteNatGateway(natId: string): Promise<void> {
|
||
await client.send(new DeleteNatGatewayCommand({NatGatewayId: natId}))
|
||
// Poll until deleted
|
||
for (let attempt = 0; attempt < 15; attempt++) {
|
||
const st = await client.send(new DescribeNatGatewaysCommand({NatGatewayIds: [natId]}))
|
||
const state = st.NatGateways?.[0]?.State
|
||
if (!state || state === 'deleted') return
|
||
if (state === 'failed') throw new Error(`NAT Gateway ${natId} deletion failed`)
|
||
await new Promise((r) => setTimeout(r, 1000))
|
||
}
|
||
},
|
||
|
||
// ── Route Tables ───────────────────────────────────────────────────────
|
||
|
||
async listRouteTables(vpcId?: string): Promise<Ec2RouteTable[]> {
|
||
try {
|
||
const res = await client.send(new DescribeRouteTablesCommand({
|
||
Filters: vpcId ? [{Name: 'vpc-id', Values: [vpcId]}] : undefined,
|
||
}))
|
||
return (res.RouteTables ?? []).map(toEc2RouteTable)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async createRouteTable(vpcId: string, name?: string): Promise<Ec2RouteTable> {
|
||
const res = await client.send(new CreateRouteTableCommand({VpcId: vpcId}))
|
||
const rt = res.RouteTable ?? {}
|
||
if (name && rt.RouteTableId) {
|
||
await client.send(new CreateTagsCommand({
|
||
Resources: [rt.RouteTableId],
|
||
Tags: [{Key: 'Name', Value: name}],
|
||
}))
|
||
rt.Tags = [{Key: 'Name', Value: name}]
|
||
}
|
||
return toEc2RouteTable(rt)
|
||
},
|
||
|
||
async deleteRouteTable(rtbId: string): Promise<void> {
|
||
// Describe first to check associations and main flag
|
||
const res = await client.send(new DescribeRouteTablesCommand({RouteTableIds: [rtbId]}))
|
||
const rt = res.RouteTables?.[0]
|
||
if (!rt) throw new Error(`Route table ${rtbId} not found`)
|
||
const isMain = (rt.Associations ?? []).some((a) => a.Main)
|
||
if (isMain) throw new Error(`Cannot delete the main route table ${rtbId}. Detach subnets and update the VPC's main route table first.`)
|
||
// Disassociate all non-main associations
|
||
for (const assoc of rt.Associations ?? []) {
|
||
if (!assoc.Main && assoc.RouteTableAssociationId) {
|
||
await client.send(new DisassociateRouteTableCommand({AssociationId: assoc.RouteTableAssociationId}))
|
||
}
|
||
}
|
||
await client.send(new DeleteRouteTableCommand({RouteTableId: rtbId}))
|
||
},
|
||
|
||
async createRoute(rtbId: string, input: CreateRouteInput): Promise<void> {
|
||
await client.send(new CreateRouteCommand({
|
||
RouteTableId: rtbId,
|
||
DestinationCidrBlock: input.destinationCidrBlock,
|
||
GatewayId: input.gatewayId || undefined,
|
||
NatGatewayId: input.natGatewayId || undefined,
|
||
VpcPeeringConnectionId: input.vpcPeeringConnectionId || undefined,
|
||
}))
|
||
},
|
||
|
||
async deleteRoute(rtbId: string, destinationCidr: string): Promise<void> {
|
||
await client.send(new DeleteRouteCommand({
|
||
RouteTableId: rtbId,
|
||
DestinationCidrBlock: destinationCidr,
|
||
}))
|
||
},
|
||
|
||
async associateRouteTable(rtbId: string, subnetId: string): Promise<string> {
|
||
const res = await client.send(new AssociateRouteTableCommand({
|
||
RouteTableId: rtbId,
|
||
SubnetId: subnetId,
|
||
}))
|
||
return res.AssociationId ?? ''
|
||
},
|
||
|
||
async disassociateRouteTable(associationId: string): Promise<void> {
|
||
await client.send(new DisassociateRouteTableCommand({AssociationId: associationId}))
|
||
},
|
||
|
||
// ── Elastic IPs ────────────────────────────────────────────────────────
|
||
|
||
async listElasticIps(): Promise<Ec2ElasticIp[]> {
|
||
try {
|
||
const res = await client.send(new DescribeAddressesCommand({}))
|
||
return (res.Addresses ?? []).map(toEc2ElasticIp)
|
||
} catch (error) {
|
||
if (isUnsupportedOperation(error)) return []
|
||
throw error
|
||
}
|
||
},
|
||
|
||
async allocateElasticIp(name?: string): Promise<Ec2ElasticIp> {
|
||
const res = await client.send(new AllocateAddressCommand({Domain: 'vpc'}))
|
||
const allocationId = res.AllocationId ?? ''
|
||
if (name && allocationId) {
|
||
await client.send(new CreateTagsCommand({Resources: [allocationId], Tags: [{Key: 'Name', Value: name}]}))
|
||
}
|
||
return {
|
||
allocationId,
|
||
publicIp: res.PublicIp ?? '',
|
||
domain: res.Domain,
|
||
tags: name ? [{key: 'Name', value: name}] : [],
|
||
}
|
||
},
|
||
|
||
async releaseElasticIp(allocationId: string): Promise<void> {
|
||
await client.send(new ReleaseAddressCommand({AllocationId: allocationId}))
|
||
},
|
||
|
||
async createVpcWizard(input: VpcWizardInput): Promise<VpcWizardResult> {
|
||
const {name, cidrBlock, subnetGroups, natGateway, availabilityZone} = input
|
||
const groupCidrs = calculateSubnetCidrs(cidrBlock, subnetGroups)
|
||
|
||
// 1. VPC
|
||
const vpcRes = await client.send(new CreateVpcCommand({CidrBlock: cidrBlock}))
|
||
const vpcId = vpcRes.Vpc!.VpcId!
|
||
await client.send(new CreateTagsCommand({Resources: [vpcId], Tags: [{Key: 'Name', Value: name}]}))
|
||
|
||
// 2. Internet Gateway
|
||
const igwRes = await client.send(new CreateInternetGatewayCommand({}))
|
||
const igwId = igwRes.InternetGateway!.InternetGatewayId!
|
||
await client.send(new CreateTagsCommand({Resources: [igwId], Tags: [{Key: 'Name', Value: `${name}-igw`}]}))
|
||
await client.send(new AttachInternetGatewayCommand({InternetGatewayId: igwId, VpcId: vpcId}))
|
||
|
||
// 3. Create subnets per group
|
||
const createdGroups: Array<{name: string; isPublic: boolean; subnetIds: string[]}> = []
|
||
for (let gi = 0; gi < subnetGroups.length; gi++) {
|
||
const group = subnetGroups[gi]
|
||
const subnetIds: string[] = []
|
||
for (let i = 0; i < group.count; i++) {
|
||
const res = await client.send(new CreateSubnetCommand({
|
||
VpcId: vpcId, CidrBlock: groupCidrs[gi][i],
|
||
AvailabilityZone: group.az || availabilityZone || undefined,
|
||
}))
|
||
const sid = res.Subnet!.SubnetId!
|
||
await client.send(new CreateTagsCommand({Resources: [sid], Tags: [{Key: 'Name', Value: `${name}-${group.name}-${i + 1}`}]}))
|
||
subnetIds.push(sid)
|
||
}
|
||
createdGroups.push({name: group.name, isPublic: group.isPublic, subnetIds})
|
||
}
|
||
|
||
const publicSubnetIds = createdGroups.filter((g) => g.isPublic).flatMap((g) => g.subnetIds)
|
||
const privateSubnetIds = createdGroups.filter((g) => !g.isPublic).flatMap((g) => g.subnetIds)
|
||
|
||
// 4. Public route table → 0.0.0.0/0 → IGW (if any public subnets)
|
||
const pubRtRes = await client.send(new CreateRouteTableCommand({VpcId: vpcId}))
|
||
const publicRtbId = pubRtRes.RouteTable!.RouteTableId!
|
||
await client.send(new CreateTagsCommand({Resources: [publicRtbId], Tags: [{Key: 'Name', Value: `${name}-pub-rtb`}]}))
|
||
if (publicSubnetIds.length > 0) {
|
||
await client.send(new CreateRouteCommand({RouteTableId: publicRtbId, DestinationCidrBlock: '0.0.0.0/0', GatewayId: igwId}))
|
||
for (const sid of publicSubnetIds) {
|
||
await client.send(new AssociateRouteTableCommand({RouteTableId: publicRtbId, SubnetId: sid}))
|
||
}
|
||
}
|
||
|
||
// 5. NAT Gateway + private route table (if any private subnets and NAT requested)
|
||
// NAT Gateway is skipped silently when not supported (e.g. LocalStack free tier).
|
||
let natGatewayId: string | undefined
|
||
let eipAllocationId: string | undefined
|
||
let privateRtbId: string | undefined
|
||
|
||
if (natGateway && privateSubnetIds.length > 0 && publicSubnetIds.length > 0) {
|
||
try {
|
||
const eipRes = await client.send(new AllocateAddressCommand({Domain: 'vpc'}))
|
||
eipAllocationId = eipRes.AllocationId!
|
||
|
||
const natRes = await client.send(new CreateNatGatewayCommand({
|
||
SubnetId: publicSubnetIds[0], AllocationId: eipAllocationId, ConnectivityType: 'public',
|
||
}))
|
||
natGatewayId = natRes.NatGateway!.NatGatewayId!
|
||
await client.send(new CreateTagsCommand({Resources: [natGatewayId], Tags: [{Key: 'Name', Value: `${name}-nat`}]}))
|
||
|
||
// Poll until available
|
||
for (let attempt = 0; attempt < 15; attempt++) {
|
||
const st = await client.send(new DescribeNatGatewaysCommand({NatGatewayIds: [natGatewayId]}))
|
||
const state = st.NatGateways?.[0]?.State
|
||
if (state === 'available') break
|
||
if (state === 'failed') throw new Error(`NAT Gateway ${natGatewayId} failed to become available`)
|
||
await new Promise((r) => setTimeout(r, 1000))
|
||
}
|
||
|
||
const privRtRes = await client.send(new CreateRouteTableCommand({VpcId: vpcId}))
|
||
privateRtbId = privRtRes.RouteTable!.RouteTableId!
|
||
await client.send(new CreateTagsCommand({Resources: [privateRtbId], Tags: [{Key: 'Name', Value: `${name}-pri-rtb`}]}))
|
||
await client.send(new CreateRouteCommand({RouteTableId: privateRtbId, DestinationCidrBlock: '0.0.0.0/0', NatGatewayId: natGatewayId}))
|
||
for (const sid of privateSubnetIds) {
|
||
await client.send(new AssociateRouteTableCommand({RouteTableId: privateRtbId, SubnetId: sid}))
|
||
}
|
||
} catch (err) {
|
||
if (!isUnsupportedOperation(err)) throw err
|
||
// Not supported (e.g. LocalStack) — skip NAT GW silently
|
||
natGatewayId = undefined
|
||
eipAllocationId = undefined
|
||
}
|
||
}
|
||
|
||
return {
|
||
vpcId, igwId,
|
||
publicSubnetIds, privateSubnetIds,
|
||
subnetGroups: createdGroups,
|
||
natGatewayId, eipAllocationId,
|
||
publicRouteTableId: publicRtbId, privateRouteTableId: privateRtbId,
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
// ─── VPC Wizard types and CIDR helpers ───────────────────────────────────────
|
||
|
||
export type SubnetGroup = {
|
||
/** User-defined group name, e.g. "public", "private-app", "dmz". */
|
||
name: string
|
||
/** Number of subnets in this group (1–4). */
|
||
count: number
|
||
/** true → IGW route (public); false → NAT GW route (private). */
|
||
isPublic: boolean
|
||
/** Optional AZ override for subnets in this group, e.g. "us-east-1a". */
|
||
az?: string
|
||
/** Subnet prefix length override, e.g. 24 for /24. Defaults to min(vpcPrefix+4, 28). */
|
||
prefix?: number
|
||
}
|
||
|
||
export type VpcWizardInput = {
|
||
name: string
|
||
cidrBlock: string
|
||
subnetGroups: SubnetGroup[]
|
||
natGateway: boolean
|
||
availabilityZone?: string
|
||
}
|
||
|
||
export type VpcWizardResult = {
|
||
vpcId: string
|
||
igwId: string
|
||
/** Flat list of all public-type subnet IDs (backward compat). */
|
||
publicSubnetIds: string[]
|
||
/** Flat list of all private-type subnet IDs (backward compat). */
|
||
privateSubnetIds: string[]
|
||
/** Named group breakdown with the subnets that belong to each. */
|
||
subnetGroups: Array<{name: string; isPublic: boolean; subnetIds: string[]}>
|
||
natGatewayId?: string
|
||
eipAllocationId?: string
|
||
publicRouteTableId: string
|
||
privateRouteTableId?: string
|
||
}
|
||
|
||
/**
|
||
* Allocates non-overlapping subnet CIDRs across named groups sequentially.
|
||
* Each group gets `group.count` contiguous subnets. If `group.prefix` is set it is used
|
||
* as the subnet prefix; otherwise defaults to min(vpcPrefix+4, 28). Groups with different
|
||
* prefixes are laid out sequentially without gaps.
|
||
*/
|
||
export function calculateSubnetCidrs(
|
||
vpcCidr: string,
|
||
groups: SubnetGroup[],
|
||
): string[][] {
|
||
const m = vpcCidr.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/)
|
||
if (!m) throw new Error(`Invalid VPC CIDR: ${vpcCidr}`)
|
||
|
||
const vpcBase = Number(m[1]) * 16777216 + Number(m[2]) * 65536 + Number(m[3]) * 256 + Number(m[4])
|
||
const vpcPrefix = Number(m[5])
|
||
const defaultPrefix = Math.min(vpcPrefix + 4, 28)
|
||
|
||
function baseToCidr(base: number, prefix: number): string {
|
||
return `${Math.floor(base / 16777216)}.${Math.floor((base % 16777216) / 65536)}.${Math.floor((base % 65536) / 256)}.${base % 256}/${prefix}`
|
||
}
|
||
|
||
let currentBase = vpcBase
|
||
return groups.map((g) => {
|
||
const prefix = g.prefix ?? defaultPrefix
|
||
const size = Math.pow(2, 32 - prefix)
|
||
const cidrs = Array.from({length: g.count}, (_, i) => baseToCidr(currentBase + i * size, prefix))
|
||
currentBase += g.count * size
|
||
return cidrs
|
||
})
|
||
}
|
||
|
||
export const ec2Service = createEc2Service()
|