first commit

This commit is contained in:
Zakaria
2026-09-22 16:13:21 -04:00
commit 7e49be27f1
297 changed files with 63162 additions and 0 deletions
+583
View File
@@ -0,0 +1,583 @@
import {describe, expect, test} from 'bun:test'
import type {EC2Client} from '@aws-sdk/client-ec2'
import {
AllocateAddressCommand,
AssociateAddressCommand,
AssociateRouteTableCommand,
AuthorizeSecurityGroupEgressCommand,
AuthorizeSecurityGroupIngressCommand,
CreateImageCommand,
CreateInternetGatewayCommand,
CreateKeyPairCommand,
CreateNatGatewayCommand,
CreateRouteCommand,
CreateRouteTableCommand,
CreateSecurityGroupCommand,
CreateSubnetCommand,
CreateTagsCommand,
CreateVpcCommand,
DeleteInternetGatewayCommand,
DeleteKeyPairCommand,
DeleteNatGatewayCommand,
DeleteRouteCommand,
DeleteSecurityGroupCommand,
DeleteSubnetCommand,
DeleteTagsCommand,
DeleteVpcCommand,
DeregisterImageCommand,
DescribeAddressesCommand,
DescribeAvailabilityZonesCommand,
DescribeImagesCommand,
DescribeInstanceTypesCommand,
DescribeInternetGatewaysCommand,
DescribeKeyPairsCommand,
DescribeNatGatewaysCommand,
DescribeRouteTablesCommand,
DescribeSecurityGroupsCommand,
DescribeSubnetsCommand,
DescribeVpcAttributeCommand,
DescribeVpcsCommand,
DetachInternetGatewayCommand,
DisassociateAddressCommand,
DisassociateRouteTableCommand,
GetConsoleOutputCommand,
RebootInstancesCommand,
ModifySubnetAttributeCommand,
ModifyVpcAttributeCommand,
ReleaseAddressCommand,
RevokeSecurityGroupEgressCommand,
RevokeSecurityGroupIngressCommand,
RunInstancesCommand,
StartInstancesCommand,
StopInstancesCommand,
TerminateInstancesCommand,
} from '@aws-sdk/client-ec2'
import {createEc2Service} from './ec2'
function fakeClient(responses: Record<string, unknown> = {}): EC2Client {
return {
send: async (cmd: {constructor: {name: string}}) => {
const name = cmd.constructor.name
if (name in responses) return responses[name]
return {}
},
} as unknown as EC2Client
}
describe('ec2Service.runInstance', () => {
test('sends RunInstancesCommand and returns mapped instance', async () => {
const sentCmds: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sentCmds.push(cmd)
return {
Instances: [{
InstanceId: 'i-new123',
Tags: [{Key: 'Name', Value: 'my-instance'}],
State: {Name: 'pending'},
}],
}
},
} as unknown as EC2Client
const service = createEc2Service(client)
const result = await service.runInstance({
name: 'my-instance',
imageId: 'ami-abc',
instanceType: 't3.micro',
})
expect(sentCmds[0]).toBeInstanceOf(RunInstancesCommand)
expect(result.instanceId).toBe('i-new123')
expect(result.name).toBe('my-instance')
expect(result.state).toBe('pending')
})
})
describe('ec2Service lifecycle', () => {
test('startInstance sends StartInstancesCommand with correct id', async () => {
const sentCmds: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sentCmds.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).startInstance('i-abc')
expect(sentCmds[0]).toBeInstanceOf(StartInstancesCommand)
})
test('stopInstance sends StopInstancesCommand with correct id', async () => {
const sentCmds: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sentCmds.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).stopInstance('i-abc')
expect(sentCmds[0]).toBeInstanceOf(StopInstancesCommand)
})
test('rebootInstance sends RebootInstancesCommand', async () => {
const sentCmds: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sentCmds.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).rebootInstance('i-abc')
expect(sentCmds[0]).toBeInstanceOf(RebootInstancesCommand)
})
test('terminateInstance sends TerminateInstancesCommand', async () => {
const sentCmds: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sentCmds.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).terminateInstance('i-abc')
expect(sentCmds[0]).toBeInstanceOf(TerminateInstancesCommand)
})
})
describe('ec2Service.updateInstanceTags', () => {
test('calls CreateTags for additions and DeleteTags for removals', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
const service = createEc2Service(client)
await service.updateInstanceTags('i-abc', [{key: 'Env', value: 'prod'}], ['OldKey'])
expect(sent[0]).toBeInstanceOf(CreateTagsCommand)
expect(sent[1]).toBeInstanceOf(DeleteTagsCommand)
})
test('skips CreateTags when nothing to add', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).updateInstanceTags('i-abc', [], ['OldKey'])
expect(sent[0]).toBeInstanceOf(DeleteTagsCommand)
expect(sent).toHaveLength(1)
})
})
describe('ec2Service.createImage', () => {
test('sends CreateImageCommand and returns imageId', async () => {
const client = fakeClient({CreateImageCommand: {ImageId: 'ami-new'}})
const result = await createEc2Service(client).createImage({instanceId: 'i-abc', name: 'snap-1'})
expect(result.imageId).toBe('ami-new')
})
})
describe('ec2Service.deregisterImage', () => {
test('sends DeregisterImageCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deregisterImage('ami-old')
expect(sent[0]).toBeInstanceOf(DeregisterImageCommand)
})
})
describe('ec2Service.listAmis', () => {
test('requests the complete image catalog and maps launchable AMIs', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
return {
Images: [{
ImageId: 'ami-amazonlinux2023',
Name: 'Amazon Linux 2023',
}],
}
},
} as unknown as EC2Client
const result = await createEc2Service(client).listAmis()
expect(sent[0]).toBeInstanceOf(DescribeImagesCommand)
expect((sent[0] as DescribeImagesCommand).input).toEqual({})
expect(result).toEqual([
expect.objectContaining({
imageId: 'ami-amazonlinux2023',
name: 'Amazon Linux 2023',
}),
])
})
})
describe('ec2Service.getConsoleOutput', () => {
test('decodes base64 output', async () => {
const encoded = Buffer.from('hello console').toString('base64')
const client = fakeClient({GetConsoleOutputCommand: {Output: encoded}})
const result = await createEc2Service(client).getConsoleOutput('i-abc')
expect(result.output).toBe('hello console')
})
test('returns empty output when operation is unsupported', async () => {
const client: EC2Client = {
send: async () => { throw new Error('GetConsoleOutput is not supported') },
} as unknown as EC2Client
const result = await createEc2Service(client).getConsoleOutput('i-abc')
expect(result.output).toBe('')
})
})
describe('ec2Service key pairs', () => {
test('listKeyPairs returns empty array when unsupported', async () => {
const client: EC2Client = {
send: async () => { throw new Error('DescribeKeyPairs is not supported') },
} as unknown as EC2Client
const result = await createEc2Service(client).listKeyPairs()
expect(result).toEqual([])
})
test('createKeyPair sends CreateKeyPairCommand and returns material', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
return {KeyPairId: 'kp-1', KeyName: 'my-key', KeyMaterial: '-----BEGIN RSA-----'}
},
} as unknown as EC2Client
const result = await createEc2Service(client).createKeyPair('my-key')
expect(sent[0]).toBeInstanceOf(CreateKeyPairCommand)
expect(result.keyName).toBe('my-key')
expect(result.keyMaterial).toBe('-----BEGIN RSA-----')
})
test('deleteKeyPair sends DeleteKeyPairCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deleteKeyPair('my-key')
expect(sent[0]).toBeInstanceOf(DeleteKeyPairCommand)
})
})
describe('ec2Service security groups', () => {
test('listSecurityGroups returns empty array when unsupported', async () => {
const client: EC2Client = {
send: async () => { throw new Error('DescribeSecurityGroups is not supported') },
} as unknown as EC2Client
expect(await createEc2Service(client).listSecurityGroups()).toEqual([])
})
test('createSecurityGroup sends CreateSecurityGroupCommand and returns groupId', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => { sent.push(cmd); return {GroupId: 'sg-new'} },
} as unknown as EC2Client
const result = await createEc2Service(client).createSecurityGroup('web', 'Web SG', 'vpc-1')
expect(sent[0]).toBeInstanceOf(CreateSecurityGroupCommand)
expect(result.groupId).toBe('sg-new')
})
test('deleteSecurityGroup sends DeleteSecurityGroupCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deleteSecurityGroup('sg-1')
expect(sent[0]).toBeInstanceOf(DeleteSecurityGroupCommand)
})
test('authorizeSecurityGroupIngress sends correct command', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).authorizeSecurityGroupIngress('sg-1', {protocol: 'tcp', fromPort: 80, toPort: 80, cidr: '0.0.0.0/0'})
expect(sent[0]).toBeInstanceOf(AuthorizeSecurityGroupIngressCommand)
})
test('revokeSecurityGroupIngress sends correct command', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).revokeSecurityGroupIngress('sg-1', {protocol: 'tcp', fromPort: 80, toPort: 80, cidr: '0.0.0.0/0'})
expect(sent[0]).toBeInstanceOf(RevokeSecurityGroupIngressCommand)
})
test('authorizeSecurityGroupEgress sends correct command', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).authorizeSecurityGroupEgress('sg-1', {protocol: 'tcp', fromPort: 443, toPort: 443, cidr: '0.0.0.0/0'})
expect(sent[0]).toBeInstanceOf(AuthorizeSecurityGroupEgressCommand)
})
test('revokeSecurityGroupEgress sends correct command', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).revokeSecurityGroupEgress('sg-1', {protocol: 'tcp', fromPort: 443, toPort: 443, cidr: '0.0.0.0/0'})
expect(sent[0]).toBeInstanceOf(RevokeSecurityGroupEgressCommand)
})
})
describe('ec2Service VPCs and subnets', () => {
test('listVpcs returns empty array when unsupported', async () => {
const client: EC2Client = {
send: async () => { throw new Error('DescribeVpcs is not supported') },
} as unknown as EC2Client
expect(await createEc2Service(client).listVpcs()).toEqual([])
})
test('createVpc sends CreateVpcCommand and returns mapped vpc', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => { sent.push(cmd); return {Vpc: {VpcId: 'vpc-new', CidrBlock: '10.0.0.0/16', IsDefault: false}} },
} as unknown as EC2Client
const result = await createEc2Service(client).createVpc('10.0.0.0/16')
expect(sent[0]).toBeInstanceOf(CreateVpcCommand)
expect(result.vpcId).toBe('vpc-new')
expect(result.cidrBlock).toBe('10.0.0.0/16')
})
test('deleteVpc sends DeleteVpcCommand as final command', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deleteVpc('vpc-1')
// deleteVpc first cleans up SGs, subnets, IGWs, RTBs then sends DeleteVpcCommand last
expect(sent[sent.length - 1]).toBeInstanceOf(DeleteVpcCommand)
})
test('listSubnets returns empty array when unsupported', async () => {
const client: EC2Client = {
send: async () => { throw new Error('DescribeSubnets is not supported') },
} as unknown as EC2Client
expect(await createEc2Service(client).listSubnets()).toEqual([])
})
test('createSubnet sends CreateSubnetCommand and returns mapped subnet', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
return {Subnet: {SubnetId: 'subnet-new', VpcId: 'vpc-1', CidrBlock: '10.0.1.0/24', AvailabilityZone: 'us-east-1a'}}
},
} as unknown as EC2Client
const result = await createEc2Service(client).createSubnet('vpc-1', '10.0.1.0/24', 'us-east-1a')
expect(sent[0]).toBeInstanceOf(CreateSubnetCommand)
expect(result.subnetId).toBe('subnet-new')
expect(result.availabilityZone).toBe('us-east-1a')
})
test('deleteSubnet sends DeleteSubnetCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deleteSubnet('subnet-1')
expect(sent[0]).toBeInstanceOf(DeleteSubnetCommand)
})
test('modifySubnetMapPublicIp sends ModifySubnetAttributeCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).modifySubnetMapPublicIp('subnet-1', true)
expect(sent[0]).toBeInstanceOf(ModifySubnetAttributeCommand)
})
test('getVpcAttributes returns dns settings from two parallel describe calls', async () => {
const client = fakeClient({
DescribeVpcAttributeCommand: {AttributeValue: {Value: true}},
})
const result = await createEc2Service(client).getVpcAttributes('vpc-1')
expect(result).toHaveProperty('enableDnsHostnames')
expect(result).toHaveProperty('enableDnsSupport')
})
test('modifyVpcAttribute sends ModifyVpcAttributeCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).modifyVpcAttribute('vpc-1', 'enableDnsHostnames', true)
expect(sent[0]).toBeInstanceOf(ModifyVpcAttributeCommand)
})
})
describe('ec2Service Internet Gateways', () => {
test('listInternetGateways returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeInternetGateways is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listInternetGateways()).toEqual([])
})
test('createInternetGateway sends CreateInternetGatewayCommand and returns mapped igw', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof CreateInternetGatewayCommand) return {InternetGateway: {InternetGatewayId: 'igw-0a1b2c3d', Attachments: [], Tags: []}}
return {}
},
} as unknown as EC2Client
const result = await createEc2Service(client).createInternetGateway()
expect(sent[0]).toBeInstanceOf(CreateInternetGatewayCommand)
expect(result.internetGatewayId).toBe('igw-0a1b2c3d')
})
test('deleteInternetGateway detaches all VPCs then deletes', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof DescribeInternetGatewaysCommand) {
return {InternetGateways: [{InternetGatewayId: 'igw-0a1b2c3d', Attachments: [{VpcId: 'vpc-0a1b2c3d', State: 'available'}]}]}
}
return {}
},
} as unknown as EC2Client
await createEc2Service(client).deleteInternetGateway('igw-0a1b2c3d')
expect(sent[0]).toBeInstanceOf(DescribeInternetGatewaysCommand)
expect(sent[1]).toBeInstanceOf(DetachInternetGatewayCommand)
expect(sent[2]).toBeInstanceOf(DeleteInternetGatewayCommand)
})
})
describe('ec2Service NAT Gateways', () => {
test('listNatGateways returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeNatGateways is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listNatGateways()).toEqual([])
})
test('createNatGateway auto-allocates EIP when allocationId is absent', async () => {
const sent: unknown[] = []
let attempt = 0
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof AllocateAddressCommand) return {AllocationId: 'eipalloc-0a1b2c3d', PublicIp: '1.2.3.4'}
if (cmd instanceof CreateNatGatewayCommand) return {NatGateway: {NatGatewayId: 'nat-0a1b2c3d', SubnetId: 'subnet-1', VpcId: 'vpc-1'}}
if (cmd instanceof DescribeNatGatewaysCommand) {
attempt++
return {NatGateways: [{NatGatewayId: 'nat-0a1b2c3d', SubnetId: 'subnet-1', VpcId: 'vpc-1', State: attempt >= 1 ? 'available' : 'pending'}]}
}
return {}
},
} as unknown as EC2Client
const result = await createEc2Service(client).createNatGateway({subnetId: 'subnet-1'})
expect(sent[0]).toBeInstanceOf(AllocateAddressCommand)
expect(sent[1]).toBeInstanceOf(CreateNatGatewayCommand)
expect(result.natGatewayId).toBe('nat-0a1b2c3d')
})
test('deleteNatGateway sends DeleteNatGatewayCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof DescribeNatGatewaysCommand) return {NatGateways: [{NatGatewayId: 'nat-0a1b2c3d', State: 'deleted'}]}
return {}
},
} as unknown as EC2Client
await createEc2Service(client).deleteNatGateway('nat-0a1b2c3d')
expect(sent[0]).toBeInstanceOf(DeleteNatGatewayCommand)
})
})
describe('ec2Service Route Tables', () => {
test('listRouteTables returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeRouteTables is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listRouteTables()).toEqual([])
})
test('createRoute sends CreateRouteCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).createRoute('rtb-0a1b2c3d', {destinationCidrBlock: '0.0.0.0/0', gatewayId: 'igw-0a1b2c3d'})
expect(sent[0]).toBeInstanceOf(CreateRouteCommand)
})
test('deleteRoute sends DeleteRouteCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).deleteRoute('rtb-0a1b2c3d', '0.0.0.0/0')
expect(sent[0]).toBeInstanceOf(DeleteRouteCommand)
})
test('associateRouteTable sends AssociateRouteTableCommand and returns associationId', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => { sent.push(cmd); return {AssociationId: 'rtbassoc-0a1b2c3d'} },
} as unknown as EC2Client
const id = await createEc2Service(client).associateRouteTable('rtb-0a1b2c3d', 'subnet-0a1b2c3d')
expect(sent[0]).toBeInstanceOf(AssociateRouteTableCommand)
expect(id).toBe('rtbassoc-0a1b2c3d')
})
test('disassociateRouteTable sends DisassociateRouteTableCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).disassociateRouteTable('rtbassoc-0a1b2c3d')
expect(sent[0]).toBeInstanceOf(DisassociateRouteTableCommand)
})
})
describe('ec2Service Elastic IPs', () => {
test('listElasticIps returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeAddresses is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listElasticIps()).toEqual([])
})
test('allocateElasticIp sends AllocateAddressCommand and returns eip', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof AllocateAddressCommand) return {AllocationId: 'eipalloc-0a1b2c3d', PublicIp: '54.0.0.1', Domain: 'vpc'}
return {}
},
} as unknown as EC2Client
const result = await createEc2Service(client).allocateElasticIp()
expect(sent[0]).toBeInstanceOf(AllocateAddressCommand)
expect(result.allocationId).toBe('eipalloc-0a1b2c3d')
expect(result.publicIp).toBe('54.0.0.1')
})
test('releaseElasticIp sends ReleaseAddressCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).releaseElasticIp('eipalloc-0a1b2c3d')
expect(sent[0]).toBeInstanceOf(ReleaseAddressCommand)
})
test('associateElasticIp sends AssociateAddressCommand and returns associationId', async () => {
const sent: unknown[] = []
const client: EC2Client = {
send: async (cmd: unknown) => {
sent.push(cmd)
if (cmd instanceof AssociateAddressCommand) return {AssociationId: 'eipassoc-test'}
return {}
},
} as unknown as EC2Client
const result = await createEc2Service(client).associateElasticIp('eipalloc-0a1b2c3d', 'i-test1')
expect(sent[0]).toBeInstanceOf(AssociateAddressCommand)
expect(result.associationId).toBe('eipassoc-test')
})
test('disassociateElasticIp sends DisassociateAddressCommand', async () => {
const sent: unknown[] = []
const client: EC2Client = {send: async (cmd: unknown) => { sent.push(cmd); return {} }} as unknown as EC2Client
await createEc2Service(client).disassociateElasticIp('eipassoc-test')
expect(sent[0]).toBeInstanceOf(DisassociateAddressCommand)
})
})
describe('ec2Service metadata', () => {
test('listAvailabilityZones returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeAvailabilityZones is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listAvailabilityZones()).toEqual([])
})
test('listAvailabilityZones returns mapped zones', async () => {
const client = fakeClient({
DescribeAvailabilityZonesCommand: {AvailabilityZones: [{ZoneName: 'us-east-1a', ZoneId: 'use1-az1', State: 'available'}]},
})
const result = await createEc2Service(client).listAvailabilityZones()
expect(result).toHaveLength(1)
expect(result[0].zoneName).toBe('us-east-1a')
expect(result[0].zoneId).toBe('use1-az1')
})
test('listInstanceTypes returns empty array when unsupported', async () => {
const client: EC2Client = {send: async () => { throw new Error('DescribeInstanceTypes is not supported') }} as unknown as EC2Client
expect(await createEc2Service(client).listInstanceTypes()).toEqual([])
})
test('listInstanceTypes returns mapped types', async () => {
const client = fakeClient({
DescribeInstanceTypesCommand: {InstanceTypes: [{InstanceType: 't3.micro', VCpuInfo: {DefaultVCpus: 2}, MemoryInfo: {SizeInMiB: 1024}}]},
})
const result = await createEc2Service(client).listInstanceTypes()
expect(result).toHaveLength(1)
expect(result[0].instanceType).toBe('t3.micro')
expect(result[0].vcpu).toBe(2)
expect(result[0].memoryMiB).toBe(1024)
})
})
// Suppress unused import warnings
void DescribeKeyPairsCommand
void DescribeSecurityGroupsCommand
void DescribeSubnetsCommand
void DescribeVpcAttributeCommand
void DescribeVpcsCommand
void DescribeInternetGatewaysCommand
void DescribeNatGatewaysCommand
void DescribeRouteTablesCommand
void DescribeAddressesCommand
void CreateRouteTableCommand
void ReleaseAddressCommand
File diff suppressed because it is too large Load Diff
+386
View File
@@ -0,0 +1,386 @@
import {
CreateFargateProfileCommand,
CreateNodegroupCommand,
DeleteFargateProfileCommand,
DeleteNodegroupCommand,
DescribeClusterCommand,
DescribeFargateProfileCommand,
DescribeNodegroupCommand,
EKSClient,
ListClustersCommand,
ListFargateProfilesCommand,
ListNodegroupsCommand,
type Cluster,
type FargateProfile,
type FargateProfileSelector,
type Nodegroup,
} from "@aws-sdk/client-eks";
import { awsClients } from "../aws";
export type EksVpcConfig = {
subnetIds: string[];
securityGroupIds: string[];
clusterSecurityGroupId?: string;
vpcId?: string;
endpointPublicAccess?: boolean;
endpointPrivateAccess?: boolean;
publicAccessCidrs: string[];
};
export type EksCluster = {
name: string;
arn?: string;
createdAt?: string;
version?: string;
endpoint?: string;
roleArn?: string;
status?: string;
platformVersion?: string;
certificateAuthority?: {
data?: string;
};
resourcesVpcConfig?: EksVpcConfig;
tags: Record<string, string>;
nodegroupCount?: number;
fargateProfileCount?: number;
};
export type EksNodegroup = {
name: string;
arn?: string;
clusterName: string;
version?: string;
releaseVersion?: string;
createdAt?: string;
modifiedAt?: string;
status?: string;
capacityType?: string;
instanceTypes: string[];
subnets: string[];
nodeRole?: string;
scalingConfig?: {
minSize?: number;
maxSize?: number;
desiredSize?: number;
};
labels: Record<string, string>;
tags: Record<string, string>;
};
export type EksFargateProfile = {
name: string;
arn?: string;
clusterName: string;
createdAt?: string;
status?: string;
podExecutionRoleArn?: string;
subnets: string[];
selectors: {
namespace?: string;
labels: Record<string, string>;
}[];
tags: Record<string, string>;
};
export type CreateEksNodegroupInput = {
name: string;
nodeRole: string;
subnets: string[];
instanceTypes?: string[];
scalingConfig?: {
minSize?: number;
maxSize?: number;
desiredSize?: number;
};
labels?: Record<string, string>;
tags?: Record<string, string>;
};
export type CreateEksFargateProfileInput = {
name: string;
podExecutionRoleArn: string;
subnets?: string[];
selectors: {
namespace?: string;
labels?: Record<string, string>;
}[];
tags?: Record<string, string>;
};
function toEksVpcConfig(
config: Cluster["resourcesVpcConfig"],
): EksVpcConfig | undefined {
if (!config) return undefined;
return {
subnetIds: config.subnetIds ?? [],
securityGroupIds: config.securityGroupIds ?? [],
clusterSecurityGroupId: config.clusterSecurityGroupId,
vpcId: config.vpcId,
endpointPublicAccess: config.endpointPublicAccess,
endpointPrivateAccess: config.endpointPrivateAccess,
publicAccessCidrs: config.publicAccessCidrs ?? [],
};
}
function toEksCluster(cluster: Cluster, nodegroupCount?: number): EksCluster {
return {
name: cluster.name ?? "",
arn: cluster.arn,
createdAt: cluster.createdAt?.toISOString(),
version: cluster.version,
endpoint: cluster.endpoint,
roleArn: cluster.roleArn,
status: cluster.status,
platformVersion: cluster.platformVersion,
certificateAuthority: cluster.certificateAuthority
? { data: cluster.certificateAuthority.data }
: undefined,
resourcesVpcConfig: toEksVpcConfig(cluster.resourcesVpcConfig),
tags: cluster.tags ?? {},
nodegroupCount,
};
}
function toEksNodegroup(nodegroup: Nodegroup): EksNodegroup {
return {
name: nodegroup.nodegroupName ?? "",
arn: nodegroup.nodegroupArn,
clusterName: nodegroup.clusterName ?? "",
version: nodegroup.version,
releaseVersion: nodegroup.releaseVersion,
createdAt: nodegroup.createdAt?.toISOString(),
modifiedAt: nodegroup.modifiedAt?.toISOString(),
status: nodegroup.status,
capacityType: nodegroup.capacityType,
instanceTypes: nodegroup.instanceTypes ?? [],
subnets: nodegroup.subnets ?? [],
nodeRole: nodegroup.nodeRole,
scalingConfig: nodegroup.scalingConfig
? {
minSize: nodegroup.scalingConfig.minSize,
maxSize: nodegroup.scalingConfig.maxSize,
desiredSize: nodegroup.scalingConfig.desiredSize,
}
: undefined,
labels: nodegroup.labels ?? {},
tags: nodegroup.tags ?? {},
};
}
function toEksFargateProfile(profile: FargateProfile): EksFargateProfile {
return {
name: profile.fargateProfileName ?? "",
arn: profile.fargateProfileArn,
clusterName: profile.clusterName ?? "",
createdAt: profile.createdAt?.toISOString(),
status: profile.status,
podExecutionRoleArn: profile.podExecutionRoleArn,
subnets: profile.subnets ?? [],
selectors: (profile.selectors ?? []).map(toEksFargateProfileSelector),
tags: profile.tags ?? {},
};
}
function toEksFargateProfileSelector(selector: FargateProfileSelector) {
return {
namespace: selector.namespace,
labels: selector.labels ?? {},
};
}
function getHttpStatus(error: unknown) {
if (typeof error !== "object" || error === null) return undefined;
const metadata = (error as { $metadata?: { httpStatusCode?: number } })
.$metadata;
return metadata?.httpStatusCode;
}
export function createEksService(client: EKSClient = awsClients.eks) {
async function listClusterNames(): Promise<string[]> {
const clusters: string[] = [];
let nextToken: string | undefined;
do {
const res = await client.send(new ListClustersCommand({ nextToken }));
clusters.push(...(res.clusters ?? []));
nextToken = res.nextToken;
} while (nextToken);
return clusters;
}
async function listNodegroupNames(clusterName: string): Promise<string[]> {
const nodegroups: string[] = [];
let nextToken: string | undefined;
try {
do {
const res = await client.send(
new ListNodegroupsCommand({ clusterName, nextToken }),
);
nodegroups.push(...(res.nodegroups ?? []));
nextToken = res.nextToken;
} while (nextToken);
} catch (error) {
if (getHttpStatus(error) === 404) return [];
throw error;
}
return nodegroups;
}
async function listFargateProfileNames(clusterName: string): Promise<string[]> {
const profiles: string[] = [];
let nextToken: string | undefined;
try {
do {
const res = await client.send(
new ListFargateProfilesCommand({ clusterName, nextToken }),
);
profiles.push(...(res.fargateProfileNames ?? []));
nextToken = res.nextToken;
} while (nextToken);
} catch (error) {
if (getHttpStatus(error) === 404) return [];
throw error;
}
return profiles;
}
return {
async listClusters(): Promise<EksCluster[]> {
const names = await listClusterNames();
return Promise.all(
names.map(async (name) => {
const [cluster, nodegroups, fargateProfiles] = await Promise.all([
this.describeCluster(name),
listNodegroupNames(name),
listFargateProfileNames(name),
]);
return {
...cluster,
nodegroupCount: nodegroups.length,
fargateProfileCount: fargateProfiles.length,
};
}),
);
},
async describeCluster(name: string): Promise<EksCluster> {
const res = await client.send(new DescribeClusterCommand({ name }));
return toEksCluster(res.cluster ?? {});
},
async listNodegroups(clusterName: string): Promise<EksNodegroup[]> {
const names = await listNodegroupNames(clusterName);
return Promise.all(
names.map((nodegroupName) =>
this.describeNodegroup(clusterName, nodegroupName),
),
);
},
async describeNodegroup(
clusterName: string,
nodegroupName: string,
): Promise<EksNodegroup> {
const res = await client.send(
new DescribeNodegroupCommand({ clusterName, nodegroupName }),
);
return toEksNodegroup(res.nodegroup ?? {});
},
async createNodegroup(
clusterName: string,
input: CreateEksNodegroupInput,
): Promise<EksNodegroup> {
const res = await client.send(
new CreateNodegroupCommand({
clusterName,
nodegroupName: input.name,
nodeRole: input.nodeRole,
subnets: input.subnets,
instanceTypes: input.instanceTypes,
scalingConfig: input.scalingConfig,
labels: input.labels,
tags: input.tags,
}),
);
return toEksNodegroup(res.nodegroup ?? {});
},
async deleteNodegroup(
clusterName: string,
nodegroupName: string,
): Promise<EksNodegroup> {
const res = await client.send(
new DeleteNodegroupCommand({ clusterName, nodegroupName }),
);
return toEksNodegroup(res.nodegroup ?? {});
},
async listFargateProfiles(clusterName: string): Promise<EksFargateProfile[]> {
const names = await listFargateProfileNames(clusterName);
return Promise.all(
names.map((profileName) =>
this.describeFargateProfile(clusterName, profileName),
),
);
},
async describeFargateProfile(
clusterName: string,
profileName: string,
): Promise<EksFargateProfile> {
const res = await client.send(
new DescribeFargateProfileCommand({
clusterName,
fargateProfileName: profileName,
}),
);
return toEksFargateProfile(res.fargateProfile ?? {});
},
async createFargateProfile(
clusterName: string,
input: CreateEksFargateProfileInput,
): Promise<EksFargateProfile> {
const res = await client.send(
new CreateFargateProfileCommand({
clusterName,
fargateProfileName: input.name,
podExecutionRoleArn: input.podExecutionRoleArn,
subnets: input.subnets,
selectors: input.selectors.map((selector) => ({
namespace: selector.namespace,
labels: selector.labels,
})),
tags: input.tags,
}),
);
return toEksFargateProfile(res.fargateProfile ?? {});
},
async deleteFargateProfile(
clusterName: string,
profileName: string,
): Promise<EksFargateProfile> {
const res = await client.send(
new DeleteFargateProfileCommand({
clusterName,
fargateProfileName: profileName,
}),
);
return toEksFargateProfile(res.fargateProfile ?? {});
},
};
}
export const eksService = createEksService();
+249
View File
@@ -0,0 +1,249 @@
import {
CreateDBInstanceCommand,
CreateDBSnapshotCommand,
DeleteDBInstanceCommand,
DescribeDBInstancesCommand,
DescribeDBSnapshotsCommand,
DescribeOrderableDBInstanceOptionsCommand,
ModifyDBInstanceCommand,
type CreateDBInstanceCommandInput,
type DBInstance,
type DBSnapshot,
type ModifyDBInstanceCommandInput,
type RDSClient,
} from "@aws-sdk/client-rds";
import { awsClients } from "../aws";
export type RdsEndpoint = {
address?: string;
port?: number;
hostedZoneId?: string;
};
export type RdsVpcSecurityGroup = {
id?: string;
status?: string;
};
export type RdsSubnet = {
identifier?: string;
availabilityZone?: string;
status?: string;
};
export type RdsDbSubnetGroup = {
name?: string;
vpcId?: string;
status?: string;
subnets: RdsSubnet[];
};
export type RdsInstance = {
identifier: string;
arn?: string;
resourceId?: string;
createdAt?: string;
status?: string;
engine?: string;
engineVersion?: string;
instanceClass?: string;
dbName?: string;
masterUsername?: string;
allocatedStorage?: number;
storageType?: string;
availabilityZone?: string;
multiAz?: boolean;
publiclyAccessible?: boolean;
iamDatabaseAuthenticationEnabled?: boolean;
preferredBackupWindow?: string;
preferredMaintenanceWindow?: string;
endpoint?: RdsEndpoint;
vpcSecurityGroups: RdsVpcSecurityGroup[];
subnetGroup?: RdsDbSubnetGroup;
optionGroupName?: string;
autoMinorVersionUpgrade?: boolean;
};
export type RdsSnapshot = {
identifier: string;
instanceIdentifier?: string;
arn?: string;
status?: string;
engine?: string;
engineVersion?: string;
allocatedStorage?: number;
snapshotType?: string;
createdAt?: string;
port?: number;
availabilityZone?: string;
vpcId?: string;
};
function toRdsInstance(instance: DBInstance): RdsInstance {
return {
identifier: instance.DBInstanceIdentifier ?? "",
arn: instance.DBInstanceArn,
resourceId: instance.DbiResourceId,
createdAt: instance.InstanceCreateTime?.toISOString(),
status: instance.DBInstanceStatus,
engine: instance.Engine,
engineVersion: instance.EngineVersion,
instanceClass: instance.DBInstanceClass,
dbName: instance.DBName,
masterUsername: instance.MasterUsername,
allocatedStorage: instance.AllocatedStorage,
storageType: instance.StorageType,
availabilityZone: instance.AvailabilityZone,
multiAz: instance.MultiAZ,
publiclyAccessible: instance.PubliclyAccessible,
iamDatabaseAuthenticationEnabled: instance.IAMDatabaseAuthenticationEnabled,
preferredBackupWindow: instance.PreferredBackupWindow,
preferredMaintenanceWindow: instance.PreferredMaintenanceWindow,
endpoint: instance.Endpoint
? {
address: instance.Endpoint.Address,
port: instance.Endpoint.Port,
hostedZoneId: instance.Endpoint.HostedZoneId,
}
: undefined,
vpcSecurityGroups: (instance.VpcSecurityGroups ?? []).map((group) => ({
id: group.VpcSecurityGroupId,
status: group.Status,
})),
subnetGroup: instance.DBSubnetGroup
? {
name: instance.DBSubnetGroup.DBSubnetGroupName,
vpcId: instance.DBSubnetGroup.VpcId,
status: instance.DBSubnetGroup.SubnetGroupStatus,
subnets: (instance.DBSubnetGroup.Subnets ?? []).map((subnet) => ({
identifier: subnet.SubnetIdentifier,
availabilityZone: subnet.SubnetAvailabilityZone?.Name,
status: subnet.SubnetStatus,
})),
}
: undefined,
optionGroupName: instance.OptionGroupMemberships?.[0]?.OptionGroupName,
autoMinorVersionUpgrade: instance.AutoMinorVersionUpgrade,
};
}
function toRdsSnapshot(snapshot: DBSnapshot): RdsSnapshot {
return {
identifier: snapshot.DBSnapshotIdentifier ?? "",
instanceIdentifier: snapshot.DBInstanceIdentifier,
arn: snapshot.DBSnapshotArn,
status: snapshot.Status,
engine: snapshot.Engine,
engineVersion: snapshot.EngineVersion,
allocatedStorage: snapshot.AllocatedStorage,
snapshotType: snapshot.SnapshotType,
createdAt: snapshot.SnapshotCreateTime?.toISOString(),
port: snapshot.Port,
availabilityZone: snapshot.AvailabilityZone,
vpcId: snapshot.VpcId,
};
}
export function createRdsService(client: RDSClient = awsClients.rds) {
return {
async listInstances(): Promise<RdsInstance[]> {
const instances: RdsInstance[] = [];
let marker: string | undefined;
do {
const res = await client.send(
new DescribeDBInstancesCommand({ Marker: marker }),
);
instances.push(...(res.DBInstances ?? []).map(toRdsInstance));
marker = res.Marker;
} while (marker);
return instances;
},
async describeInstance(identifier: string): Promise<RdsInstance> {
const res = await client.send(
new DescribeDBInstancesCommand({ DBInstanceIdentifier: identifier }),
);
return toRdsInstance(res.DBInstances?.[0] ?? {});
},
async createInstance(input: CreateDBInstanceCommandInput): Promise<RdsInstance> {
const res = await client.send(new CreateDBInstanceCommand(input));
return toRdsInstance(res.DBInstance ?? {});
},
async modifyInstance(input: ModifyDBInstanceCommandInput): Promise<RdsInstance> {
const res = await client.send(new ModifyDBInstanceCommand(input));
return toRdsInstance(res.DBInstance ?? {});
},
async deleteInstance(
identifier: string,
skipFinalSnapshot = true,
finalDBSnapshotIdentifier?: string,
): Promise<void> {
await client.send(
new DeleteDBInstanceCommand({
DBInstanceIdentifier: identifier,
SkipFinalSnapshot: skipFinalSnapshot,
...(finalDBSnapshotIdentifier
? { FinalDBSnapshotIdentifier: finalDBSnapshotIdentifier }
: {}),
}),
);
},
async listSnapshots(instanceIdentifier?: string): Promise<RdsSnapshot[]> {
const snapshots: RdsSnapshot[] = [];
let marker: string | undefined;
do {
const res = await client.send(
new DescribeDBSnapshotsCommand({
DBInstanceIdentifier: instanceIdentifier,
Marker: marker,
}),
);
snapshots.push(...(res.DBSnapshots ?? []).map(toRdsSnapshot));
marker = res.Marker;
} while (marker);
return snapshots;
},
async createSnapshot(instanceIdentifier: string, snapshotIdentifier: string): Promise<RdsSnapshot> {
const res = await client.send(
new CreateDBSnapshotCommand({
DBInstanceIdentifier: instanceIdentifier,
DBSnapshotIdentifier: snapshotIdentifier,
}),
);
return toRdsSnapshot(res.DBSnapshot ?? {});
},
async listOrderableInstanceClasses(engine: string): Promise<string[]> {
const classes = new Set<string>();
let marker: string | undefined;
do {
const res = await client.send(
new DescribeOrderableDBInstanceOptionsCommand({
Engine: engine,
Marker: marker,
}),
);
for (const option of res.OrderableDBInstanceOptions ?? []) {
if (option.DBInstanceClass) {
classes.add(option.DBInstanceClass);
}
}
marker = res.Marker;
} while (marker);
return Array.from(classes);
},
};
}
export const rdsService = createRdsService();