puls-dev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +148 -0
  3. package/dist/core/checker.d.ts +5 -0
  4. package/dist/core/checker.js +148 -0
  5. package/dist/core/config.d.ts +35 -0
  6. package/dist/core/config.js +15 -0
  7. package/dist/core/decorators.d.ts +26 -0
  8. package/dist/core/decorators.js +86 -0
  9. package/dist/core/output.d.ts +8 -0
  10. package/dist/core/output.js +19 -0
  11. package/dist/core/resource.d.ts +20 -0
  12. package/dist/core/resource.js +77 -0
  13. package/dist/core/stack.d.ts +20 -0
  14. package/dist/core/stack.js +120 -0
  15. package/dist/index.d.ts +12 -0
  16. package/dist/index.js +12 -0
  17. package/dist/providers/aws/acm.d.ts +22 -0
  18. package/dist/providers/aws/acm.js +109 -0
  19. package/dist/providers/aws/api.d.ts +28 -0
  20. package/dist/providers/aws/api.js +36 -0
  21. package/dist/providers/aws/apigateway.d.ts +24 -0
  22. package/dist/providers/aws/apigateway.js +157 -0
  23. package/dist/providers/aws/cloudfront.d.ts +31 -0
  24. package/dist/providers/aws/cloudfront.js +205 -0
  25. package/dist/providers/aws/fargate.d.ts +43 -0
  26. package/dist/providers/aws/fargate.js +277 -0
  27. package/dist/providers/aws/index.d.ts +23 -0
  28. package/dist/providers/aws/index.js +29 -0
  29. package/dist/providers/aws/lambda.d.ts +30 -0
  30. package/dist/providers/aws/lambda.js +159 -0
  31. package/dist/providers/aws/list.d.ts +2 -0
  32. package/dist/providers/aws/list.js +44 -0
  33. package/dist/providers/aws/rds.d.ts +46 -0
  34. package/dist/providers/aws/rds.js +227 -0
  35. package/dist/providers/aws/route53.d.ts +38 -0
  36. package/dist/providers/aws/route53.js +218 -0
  37. package/dist/providers/aws/s3.d.ts +20 -0
  38. package/dist/providers/aws/s3.js +165 -0
  39. package/dist/providers/aws/secrets.d.ts +25 -0
  40. package/dist/providers/aws/secrets.js +151 -0
  41. package/dist/providers/aws/sqs.d.ts +33 -0
  42. package/dist/providers/aws/sqs.js +178 -0
  43. package/dist/providers/do/api.d.ts +11 -0
  44. package/dist/providers/do/api.js +52 -0
  45. package/dist/providers/do/certificate.d.ts +7 -0
  46. package/dist/providers/do/certificate.js +36 -0
  47. package/dist/providers/do/domain.d.ts +21 -0
  48. package/dist/providers/do/domain.js +81 -0
  49. package/dist/providers/do/droplet.d.ts +35 -0
  50. package/dist/providers/do/droplet.js +180 -0
  51. package/dist/providers/do/firewall.d.ts +23 -0
  52. package/dist/providers/do/firewall.js +94 -0
  53. package/dist/providers/do/index.d.ts +15 -0
  54. package/dist/providers/do/index.js +21 -0
  55. package/dist/providers/do/list.d.ts +2 -0
  56. package/dist/providers/do/list.js +59 -0
  57. package/dist/providers/do/load_balancer.d.ts +12 -0
  58. package/dist/providers/do/load_balancer.js +62 -0
  59. package/dist/providers/firebase/api.d.ts +4 -0
  60. package/dist/providers/firebase/api.js +62 -0
  61. package/dist/providers/firebase/auth.d.ts +35 -0
  62. package/dist/providers/firebase/auth.js +147 -0
  63. package/dist/providers/firebase/firestore.d.ts +28 -0
  64. package/dist/providers/firebase/firestore.js +120 -0
  65. package/dist/providers/firebase/functions.d.ts +50 -0
  66. package/dist/providers/firebase/functions.js +163 -0
  67. package/dist/providers/firebase/hosting.d.ts +14 -0
  68. package/dist/providers/firebase/hosting.js +144 -0
  69. package/dist/providers/firebase/index.d.ts +15 -0
  70. package/dist/providers/firebase/index.js +15 -0
  71. package/dist/providers/firebase/remoteconfig.d.ts +22 -0
  72. package/dist/providers/firebase/remoteconfig.js +135 -0
  73. package/dist/providers/firebase/storage.d.ts +34 -0
  74. package/dist/providers/firebase/storage.js +117 -0
  75. package/dist/providers/proxmox/api.d.ts +12 -0
  76. package/dist/providers/proxmox/api.js +50 -0
  77. package/dist/providers/proxmox/index.d.ts +15 -0
  78. package/dist/providers/proxmox/index.js +10 -0
  79. package/dist/providers/proxmox/list.d.ts +2 -0
  80. package/dist/providers/proxmox/list.js +15 -0
  81. package/dist/providers/proxmox/vm.d.ts +61 -0
  82. package/dist/providers/proxmox/vm.js +482 -0
  83. package/dist/types/aws.d.ts +55 -0
  84. package/dist/types/aws.js +48 -0
  85. package/dist/types/do.d.ts +19 -0
  86. package/dist/types/do.js +19 -0
  87. package/dist/types/gcp.d.ts +9 -0
  88. package/dist/types/gcp.js +9 -0
  89. package/dist/types/inventory.d.ts +87 -0
  90. package/dist/types/inventory.js +2 -0
  91. package/dist/types/proxmox.d.ts +11 -0
  92. package/dist/types/proxmox.js +28 -0
  93. package/package.json +56 -0
@@ -0,0 +1,205 @@
1
+ import { ListDistributionsCommand, GetDistributionCommand, CreateDistributionCommand, GetDistributionConfigCommand, CreateInvalidationCommand, } from '@aws-sdk/client-cloudfront';
2
+ import { BaseBuilder } from '../../core/resource.js';
3
+ import { S3BucketBuilder } from './s3.js';
4
+ import { getCFClient } from './api.js';
5
+ export class CloudFrontBuilder extends BaseBuilder {
6
+ resolvedArn = null;
7
+ resolvedId = null;
8
+ _origin;
9
+ _aliases = [];
10
+ _prefixes = [];
11
+ _zone;
12
+ _referenceId;
13
+ _kvsName;
14
+ _certRef;
15
+ _invalidatePaths;
16
+ constructor(name) {
17
+ super(name);
18
+ this.discoveryPromise = this.discoverDistribution(name);
19
+ }
20
+ async discoverDistribution(name) {
21
+ try {
22
+ const cf = getCFClient();
23
+ const list = await cf.send(new ListDistributionsCommand({}));
24
+ const items = list.DistributionList?.Items ?? [];
25
+ const match = items.find(d => d.Comment === name || d.Id === name);
26
+ if (match) {
27
+ this.resolvedId = match.Id;
28
+ this.resolvedArn = match.ARN;
29
+ }
30
+ return match ?? null;
31
+ }
32
+ catch (e) {
33
+ if (e.name === 'CredentialsProviderError')
34
+ return null;
35
+ throw e;
36
+ }
37
+ }
38
+ invalidate(paths) {
39
+ this._invalidatePaths = paths;
40
+ return this;
41
+ }
42
+ withRedirector(opts) {
43
+ this._kvsName = opts.kvs;
44
+ return this;
45
+ }
46
+ copyFrom(distributionId) {
47
+ this._referenceId = distributionId;
48
+ return this;
49
+ }
50
+ forDomain(zone, prefixes) {
51
+ this._zone = zone;
52
+ this._prefixes = prefixes;
53
+ this._aliases = prefixes.map(p => `${p}.${zone.zoneName}`);
54
+ const cert = zone.cert();
55
+ if (cert)
56
+ this._certRef = cert;
57
+ return this;
58
+ }
59
+ origin(source) {
60
+ this._origin = source;
61
+ return this;
62
+ }
63
+ dns(aliases) {
64
+ this._aliases = Array.isArray(aliases) ? aliases : [aliases];
65
+ return this;
66
+ }
67
+ async deploy() {
68
+ const dryRun = this.isDryRunActive();
69
+ const existing = await this.discoveryPromise;
70
+ const cf = getCFClient();
71
+ console.log(`\n⚔ Finalizing CloudFront Distribution "${this.name}"...`);
72
+ if (existing) {
73
+ this.resolvedId = existing.Id;
74
+ this.resolvedArn = existing.ARN;
75
+ console.log(` āœ… Distribution "${this.name}" exists (id=${this.resolvedId})`);
76
+ if (this._aliases.length)
77
+ console.log(` āœ… Aliases: [${this._aliases.join(', ')}]`);
78
+ if (this._invalidatePaths?.length) {
79
+ if (dryRun) {
80
+ console.log(` šŸ“ [PLAN] Invalidate: ${this._invalidatePaths.join(', ')}`);
81
+ }
82
+ else {
83
+ await cf.send(new CreateInvalidationCommand({
84
+ DistributionId: this.resolvedId,
85
+ InvalidationBatch: {
86
+ Paths: { Quantity: this._invalidatePaths.length, Items: this._invalidatePaths },
87
+ CallerReference: `puls-${Date.now()}`,
88
+ },
89
+ }));
90
+ console.log(` āœ… Invalidated: ${this._invalidatePaths.join(', ')}`);
91
+ }
92
+ }
93
+ return { id: this.resolvedId, arn: this.resolvedArn, name: this.name };
94
+ }
95
+ if (dryRun) {
96
+ console.log(` šŸ“ [PLAN] Create distribution "${this.name}"`);
97
+ if (this._referenceId)
98
+ console.log(` └─ Clone config from: ${this._referenceId}`);
99
+ if (this._aliases.length)
100
+ console.log(` └─ Aliases: [${this._aliases.join(', ')}]`);
101
+ const dryRunCert = this._certRef ?? this._zone?.cert();
102
+ if (dryRunCert?.resolvedArn)
103
+ console.log(` └─ Certificate: ${dryRunCert.resolvedArn}`);
104
+ if (this._kvsName)
105
+ console.log(` └─ KVS redirector: ${this._kvsName}`);
106
+ if (this._zone && this._prefixes.length) {
107
+ console.log(` šŸ“ [PLAN] Add Route53 CNAMEs in ${this._zone.zoneName}:`);
108
+ for (const p of this._prefixes) {
109
+ console.log(` └─ ${p}.${this._zone.zoneName} → <cf-domain>.cloudfront.net`);
110
+ }
111
+ }
112
+ this.resolvedArn = `arn:aws:cloudfront::DRYRUN:distribution/PENDING`;
113
+ this.resolvedId = 'PENDING';
114
+ return { id: this.resolvedId, arn: this.resolvedArn, name: this.name };
115
+ }
116
+ let distroConfig;
117
+ if (this._referenceId) {
118
+ // Clone from reference distribution
119
+ const ref = await cf.send(new GetDistributionConfigCommand({ Id: this._referenceId }));
120
+ distroConfig = ref.DistributionConfig;
121
+ console.log(` šŸ“‹ Cloning config from ${this._referenceId}`);
122
+ }
123
+ else {
124
+ distroConfig = this.buildBaseConfig();
125
+ }
126
+ // Override comment (used as our "name")
127
+ distroConfig.Comment = this.name;
128
+ // Override aliases + cert if provided
129
+ if (this._aliases.length) {
130
+ distroConfig.Aliases = { Quantity: this._aliases.length, Items: this._aliases };
131
+ }
132
+ // _certRef is set at construction time, but cert sidecar is added lazily in zone.deploy() —
133
+ // fall back to zone.cert() which is populated by the time CloudFront.deploy() runs
134
+ const certRef = this._certRef ?? this._zone?.cert();
135
+ if (certRef?.resolvedArn) {
136
+ distroConfig.ViewerCertificate = {
137
+ ACMCertificateArn: certRef.resolvedArn,
138
+ SSLSupportMethod: 'sni-only',
139
+ MinimumProtocolVersion: 'TLSv1.2_2021',
140
+ };
141
+ }
142
+ // Remove CallerReference from cloned config — AWS will reject it
143
+ delete distroConfig.CallerReference;
144
+ const distroInput = { ...distroConfig, CallerReference: `puls-${this.name}-${Date.now()}` };
145
+ // ACM ISSUED → CloudFront cert index replication can take up to ~5 min
146
+ const MAX_ATTEMPTS = 10;
147
+ let result;
148
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
149
+ try {
150
+ result = await cf.send(new CreateDistributionCommand({ DistributionConfig: distroInput }));
151
+ break;
152
+ }
153
+ catch (e) {
154
+ if (e.name === 'InvalidViewerCertificate' && attempt < MAX_ATTEMPTS) {
155
+ console.log(` ā³ Cert not yet visible to CloudFront — retrying in 30s (${attempt}/${MAX_ATTEMPTS - 1})...`);
156
+ await new Promise(r => setTimeout(r, 30_000));
157
+ }
158
+ else
159
+ throw e;
160
+ }
161
+ }
162
+ this.resolvedId = result.Distribution.Id;
163
+ this.resolvedArn = result.Distribution.ARN;
164
+ console.log(`šŸš€ Created distribution "${this.name}" (id=${this.resolvedId})`);
165
+ await this.waitFor(`distribution "${this.name}" to finish deploying`, async () => {
166
+ const d = await cf.send(new GetDistributionCommand({ Id: this.resolvedId }));
167
+ return d.Distribution?.Status === 'Deployed';
168
+ }, { intervalMs: 30_000, timeoutMs: 1_200_000 });
169
+ const cfDomain = result.Distribution.DomainName;
170
+ console.log(` 🌐 Domain: ${cfDomain}`);
171
+ if (this._zone && this._prefixes.length) {
172
+ await this._zone.upsertCnames(this._prefixes.map(p => ({ name: p, value: cfDomain })));
173
+ }
174
+ return { id: this.resolvedId, arn: this.resolvedArn, name: this.name };
175
+ }
176
+ buildBaseConfig() {
177
+ const originId = `puls-origin-${this.name}`;
178
+ const originDomain = this._origin instanceof S3BucketBuilder
179
+ ? `${this._origin.bucketName}.s3.amazonaws.com`
180
+ : this._origin ?? 'example.com';
181
+ return {
182
+ Comment: this.name,
183
+ Enabled: true,
184
+ HttpVersion: 'http2',
185
+ Origins: {
186
+ Quantity: 1,
187
+ Items: [{
188
+ Id: originId,
189
+ DomainName: originDomain,
190
+ S3OriginConfig: { OriginAccessIdentity: '' },
191
+ }],
192
+ },
193
+ DefaultCacheBehavior: {
194
+ TargetOriginId: originId,
195
+ ViewerProtocolPolicy: 'redirect-to-https',
196
+ CachePolicyId: '658327ea-f89d-4fab-a63d-7e88639e58f6', // Managed-CachingOptimized
197
+ AllowedMethods: { Quantity: 2, Items: ['GET', 'HEAD'], CachedMethods: { Quantity: 2, Items: ['GET', 'HEAD'] } },
198
+ Compress: true,
199
+ ForwardedValues: { QueryString: false, Cookies: { Forward: 'none' } },
200
+ MinTTL: 0,
201
+ },
202
+ PriceClass: 'PriceClass_All',
203
+ };
204
+ }
205
+ }
@@ -0,0 +1,43 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { SecretsBuilder } from './secrets.js';
3
+ export declare class FargateBuilder extends BaseBuilder {
4
+ private _image?;
5
+ private _cpu;
6
+ private _memory;
7
+ private _port?;
8
+ private _replicas;
9
+ private _env;
10
+ private _clusterName;
11
+ private _subnetIds?;
12
+ private _securityGroupIds?;
13
+ resolvedArn: string | null;
14
+ constructor(name: string);
15
+ image(img: string): this;
16
+ cpu(units: number): this;
17
+ memory(mb: number): this;
18
+ port(p: number): this;
19
+ replicas(n: number): this;
20
+ cluster(name: string): this;
21
+ subnets(ids: string[]): this;
22
+ securityGroups(ids: string[]): this;
23
+ env(vars: Record<string, string | SecretsBuilder>): this;
24
+ private discoverService;
25
+ private ensureCluster;
26
+ private ensureExecutionRole;
27
+ private ensureLogGroup;
28
+ private discoverDefaultVpc;
29
+ private ensureSecurityGroup;
30
+ private resolveNetworking;
31
+ deploy(): Promise<{
32
+ name: string;
33
+ arn: string;
34
+ cluster?: undefined;
35
+ } | {
36
+ name: string;
37
+ arn: string | null;
38
+ cluster: string;
39
+ }>;
40
+ destroy(): Promise<{
41
+ destroyed: string;
42
+ }>;
43
+ }
@@ -0,0 +1,277 @@
1
+ import { DescribeClustersCommand, CreateClusterCommand, DescribeServicesCommand, CreateServiceCommand, UpdateServiceCommand, DeleteServiceCommand, RegisterTaskDefinitionCommand, NetworkMode, LaunchType, SchedulingStrategy, } from '@aws-sdk/client-ecs';
2
+ import { DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, CreateSecurityGroupCommand, AuthorizeSecurityGroupIngressCommand, } from '@aws-sdk/client-ec2';
3
+ import { GetRoleCommand, CreateRoleCommand, AttachRolePolicyCommand, } from '@aws-sdk/client-iam';
4
+ import { DescribeLogGroupsCommand, CreateLogGroupCommand, } from '@aws-sdk/client-cloudwatch-logs';
5
+ import { BaseBuilder } from '../../core/resource.js';
6
+ import { getECSClient, getEC2Client, getIAMClient, getCWLogsClient } from './api.js';
7
+ import { resolveEnvVars } from './secrets.js';
8
+ import { Config } from '../../core/config.js';
9
+ const ECS_ASSUME_ROLE_POLICY = JSON.stringify({
10
+ Version: '2012-10-17',
11
+ Statement: [{ Effect: 'Allow', Principal: { Service: 'ecs-tasks.amazonaws.com' }, Action: 'sts:AssumeRole' }],
12
+ });
13
+ export class FargateBuilder extends BaseBuilder {
14
+ _image;
15
+ _cpu = 256;
16
+ _memory = 512;
17
+ _port;
18
+ _replicas = 1;
19
+ _env = {};
20
+ _clusterName = 'puls';
21
+ _subnetIds;
22
+ _securityGroupIds;
23
+ resolvedArn = null;
24
+ constructor(name) {
25
+ super(name);
26
+ this.discoveryPromise = this.discoverService(name);
27
+ }
28
+ image(img) { this._image = img; return this; }
29
+ cpu(units) { this._cpu = units; return this; }
30
+ memory(mb) { this._memory = mb; return this; }
31
+ port(p) { this._port = p; return this; }
32
+ replicas(n) { this._replicas = n; return this; }
33
+ cluster(name) { this._clusterName = name; return this; }
34
+ subnets(ids) { this._subnetIds = ids; return this; }
35
+ securityGroups(ids) { this._securityGroupIds = ids; return this; }
36
+ env(vars) { this._env = { ...this._env, ...vars }; return this; }
37
+ async discoverService(name) {
38
+ try {
39
+ const ecs = getECSClient();
40
+ const clusterDesc = await ecs.send(new DescribeClustersCommand({ clusters: [this._clusterName] }));
41
+ const cluster = clusterDesc.clusters?.find(c => c.status === 'ACTIVE');
42
+ if (!cluster)
43
+ return null;
44
+ const result = await ecs.send(new DescribeServicesCommand({
45
+ cluster: this._clusterName,
46
+ services: [name],
47
+ }));
48
+ const svc = result.services?.find(s => s.status !== 'INACTIVE');
49
+ if (svc)
50
+ this.resolvedArn = svc.serviceArn ?? null;
51
+ return svc ?? null;
52
+ }
53
+ catch (e) {
54
+ if (e.name === 'ClusterNotFoundException' || e.name === 'CredentialsProviderError')
55
+ return null;
56
+ throw e;
57
+ }
58
+ }
59
+ async ensureCluster() {
60
+ const ecs = getECSClient();
61
+ const desc = await ecs.send(new DescribeClustersCommand({ clusters: [this._clusterName] }));
62
+ const existing = desc.clusters?.find(c => c.status === 'ACTIVE');
63
+ if (existing)
64
+ return existing.clusterArn;
65
+ const created = await ecs.send(new CreateClusterCommand({ clusterName: this._clusterName }));
66
+ console.log(` āœ… Created ECS cluster: ${this._clusterName}`);
67
+ return created.cluster.clusterArn;
68
+ }
69
+ async ensureExecutionRole() {
70
+ const roleName = `puls-fargate-${this.name}-exec-role`;
71
+ const iam = getIAMClient();
72
+ try {
73
+ const existing = await iam.send(new GetRoleCommand({ RoleName: roleName }));
74
+ return existing.Role.Arn;
75
+ }
76
+ catch (e) {
77
+ if (e.name !== 'NoSuchEntityException')
78
+ throw e;
79
+ }
80
+ const created = await iam.send(new CreateRoleCommand({
81
+ RoleName: roleName,
82
+ AssumeRolePolicyDocument: ECS_ASSUME_ROLE_POLICY,
83
+ Description: `Task execution role for OpsDSL Fargate service "${this.name}"`,
84
+ }));
85
+ await iam.send(new AttachRolePolicyCommand({
86
+ RoleName: roleName,
87
+ PolicyArn: 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy',
88
+ }));
89
+ console.log(` āœ… Created execution role: ${roleName}`);
90
+ return created.Role.Arn;
91
+ }
92
+ async ensureLogGroup() {
93
+ const logGroupName = `/puls/${this.name}`;
94
+ const cw = getCWLogsClient();
95
+ const existing = await cw.send(new DescribeLogGroupsCommand({ logGroupNamePrefix: logGroupName }));
96
+ if (existing.logGroups?.some(g => g.logGroupName === logGroupName))
97
+ return logGroupName;
98
+ await cw.send(new CreateLogGroupCommand({ logGroupName }));
99
+ console.log(` āœ… Created log group: ${logGroupName}`);
100
+ return logGroupName;
101
+ }
102
+ async discoverDefaultVpc() {
103
+ const ec2 = getEC2Client();
104
+ const vpcs = await ec2.send(new DescribeVpcsCommand({
105
+ Filters: [{ Name: 'isDefault', Values: ['true'] }],
106
+ }));
107
+ const vpcId = vpcs.Vpcs?.[0]?.VpcId;
108
+ if (!vpcId)
109
+ throw new Error(`[Fargate:${this.name}] No default VPC found. Use .subnets(ids[]) to specify subnets.`);
110
+ const subnets = await ec2.send(new DescribeSubnetsCommand({
111
+ Filters: [{ Name: 'vpc-id', Values: [vpcId] }],
112
+ }));
113
+ const subnetIds = (subnets.Subnets ?? []).map(s => s.SubnetId).filter(Boolean);
114
+ return { vpcId, subnetIds };
115
+ }
116
+ async ensureSecurityGroup(vpcId) {
117
+ const ec2 = getEC2Client();
118
+ const sgName = `puls-${this.name}-sg`;
119
+ const existing = await ec2.send(new DescribeSecurityGroupsCommand({
120
+ Filters: [
121
+ { Name: 'group-name', Values: [sgName] },
122
+ { Name: 'vpc-id', Values: [vpcId] },
123
+ ],
124
+ }));
125
+ if (existing.SecurityGroups?.length)
126
+ return existing.SecurityGroups[0].GroupId;
127
+ const created = await ec2.send(new CreateSecurityGroupCommand({
128
+ GroupName: sgName,
129
+ Description: `OpsDSL Fargate service "${this.name}"`,
130
+ VpcId: vpcId,
131
+ }));
132
+ const sgId = created.GroupId;
133
+ if (this._port) {
134
+ await ec2.send(new AuthorizeSecurityGroupIngressCommand({
135
+ GroupId: sgId,
136
+ IpPermissions: [{
137
+ IpProtocol: 'tcp',
138
+ FromPort: this._port,
139
+ ToPort: this._port,
140
+ IpRanges: [{ CidrIp: '0.0.0.0/0' }],
141
+ Ipv6Ranges: [{ CidrIpv6: '::/0' }],
142
+ }],
143
+ }));
144
+ }
145
+ console.log(` āœ… Created security group: ${sgName} (${sgId})`);
146
+ return sgId;
147
+ }
148
+ async resolveNetworking() {
149
+ if (this._subnetIds && this._securityGroupIds) {
150
+ return { subnetIds: this._subnetIds, securityGroupIds: this._securityGroupIds };
151
+ }
152
+ const { vpcId, subnetIds } = await this.discoverDefaultVpc();
153
+ const sgId = this._securityGroupIds?.[0] ?? await this.ensureSecurityGroup(vpcId);
154
+ return {
155
+ subnetIds: this._subnetIds ?? subnetIds,
156
+ securityGroupIds: [sgId],
157
+ };
158
+ }
159
+ async deploy() {
160
+ const dryRun = this.isDryRunActive();
161
+ const existing = await this.discoveryPromise;
162
+ const region = Config.get().providers.aws?.region ?? 'us-east-1';
163
+ console.log(`\n⚔ Finalizing Fargate Service "${this.name}"...`);
164
+ if (!this._image)
165
+ throw new Error(`[Fargate:${this.name}] .image("...") is required`);
166
+ if (dryRun) {
167
+ console.log(` šŸ“ [PLAN] ${existing ? 'Update' : 'Create'} Fargate service "${this.name}"`);
168
+ console.log(` └─ Cluster: ${this._clusterName}`);
169
+ console.log(` └─ Image: ${this._image}`);
170
+ console.log(` └─ CPU: ${this._cpu} | Memory: ${this._memory}MB | Replicas: ${this._replicas}`);
171
+ if (this._port)
172
+ console.log(` └─ Port: ${this._port}`);
173
+ if (Object.keys(this._env).length)
174
+ console.log(` └─ Env vars: ${Object.keys(this._env).join(', ')}`);
175
+ this.resolvedArn = `arn:aws:ecs:${region}:000000000000:service/${this._clusterName}/DRYRUN`;
176
+ return { name: this.name, arn: this.resolvedArn };
177
+ }
178
+ const [clusterArn, executionRoleArn, logGroupName, networking] = await Promise.all([
179
+ this.ensureCluster(),
180
+ this.ensureExecutionRole(),
181
+ this.ensureLogGroup(),
182
+ this.resolveNetworking(),
183
+ ]);
184
+ // Register a new task definition revision
185
+ const containerDef = {
186
+ name: this.name,
187
+ image: this._image,
188
+ essential: true,
189
+ logConfiguration: {
190
+ logDriver: 'awslogs',
191
+ options: {
192
+ 'awslogs-group': logGroupName,
193
+ 'awslogs-region': region,
194
+ 'awslogs-stream-prefix': this.name,
195
+ },
196
+ },
197
+ };
198
+ if (this._port)
199
+ containerDef.portMappings = [{ containerPort: this._port, protocol: 'tcp' }];
200
+ if (Object.keys(this._env).length) {
201
+ const resolvedEnv = await resolveEnvVars(this._env);
202
+ containerDef.environment = Object.entries(resolvedEnv).map(([name, value]) => ({ name, value }));
203
+ }
204
+ const taskDef = await getECSClient().send(new RegisterTaskDefinitionCommand({
205
+ family: this.name,
206
+ networkMode: NetworkMode.AWSVPC,
207
+ requiresCompatibilities: ['FARGATE'],
208
+ cpu: String(this._cpu),
209
+ memory: String(this._memory),
210
+ executionRoleArn,
211
+ containerDefinitions: [containerDef],
212
+ }));
213
+ const taskDefArn = taskDef.taskDefinition.taskDefinitionArn;
214
+ console.log(` āœ… Registered task definition: ${this.name}:${taskDef.taskDefinition.revision}`);
215
+ const networkConfig = {
216
+ awsvpcConfiguration: {
217
+ subnets: networking.subnetIds,
218
+ securityGroups: networking.securityGroupIds,
219
+ assignPublicIp: 'ENABLED',
220
+ },
221
+ };
222
+ const ecs = getECSClient();
223
+ if (existing) {
224
+ await ecs.send(new UpdateServiceCommand({
225
+ cluster: this._clusterName,
226
+ service: this.name,
227
+ taskDefinition: taskDefArn,
228
+ desiredCount: this._replicas,
229
+ networkConfiguration: networkConfig,
230
+ forceNewDeployment: true,
231
+ }));
232
+ console.log(` āœ… Updated service "${this.name}" → rolling deployment started`);
233
+ }
234
+ else {
235
+ const created = await ecs.send(new CreateServiceCommand({
236
+ cluster: clusterArn,
237
+ serviceName: this.name,
238
+ taskDefinition: taskDefArn,
239
+ desiredCount: this._replicas,
240
+ launchType: LaunchType.FARGATE,
241
+ networkConfiguration: networkConfig,
242
+ schedulingStrategy: SchedulingStrategy.REPLICA,
243
+ }));
244
+ this.resolvedArn = created.service.serviceArn;
245
+ console.log(`šŸš€ Created Fargate service "${this.name}" (arn=${this.resolvedArn})`);
246
+ }
247
+ await this.deploySidecars();
248
+ return { name: this.name, arn: this.resolvedArn, cluster: this._clusterName };
249
+ }
250
+ async destroy() {
251
+ const dryRun = this.isDryRunActive();
252
+ const existing = await this.discoveryPromise;
253
+ console.log(`\nšŸ—‘ļø Destroying Fargate Service "${this.name}"...`);
254
+ if (!existing) {
255
+ console.log(` āœ… Service "${this.name}" does not exist — nothing to do`);
256
+ return { destroyed: this.name };
257
+ }
258
+ if (dryRun) {
259
+ console.log(` šŸ“ [PLAN] Drain and delete service "${this.name}" in cluster "${this._clusterName}"`);
260
+ return { destroyed: this.name };
261
+ }
262
+ const ecs = getECSClient();
263
+ // Drain before delete — required by ECS
264
+ await ecs.send(new UpdateServiceCommand({
265
+ cluster: this._clusterName,
266
+ service: this.name,
267
+ desiredCount: 0,
268
+ }));
269
+ await ecs.send(new DeleteServiceCommand({
270
+ cluster: this._clusterName,
271
+ service: this.name,
272
+ force: true,
273
+ }));
274
+ console.log(` āœ… Deleted service "${this.name}"`);
275
+ return { destroyed: this.name };
276
+ }
277
+ }
@@ -0,0 +1,23 @@
1
+ import { S3BucketBuilder } from "./s3.js";
2
+ import { Route53Builder } from "./route53.js";
3
+ import { CloudFrontBuilder } from "./cloudfront.js";
4
+ import { LambdaBuilder } from "./lambda.js";
5
+ import { APIGatewayBuilder } from "./apigateway.js";
6
+ import { FargateBuilder } from "./fargate.js";
7
+ import { RDSBuilder } from "./rds.js";
8
+ import { SQSBuilder } from "./sqs.js";
9
+ import { SecretsBuilder } from "./secrets.js";
10
+ export declare const AWS: {
11
+ init: (opts: {
12
+ region: string;
13
+ }) => void;
14
+ S3: (name: string) => S3BucketBuilder;
15
+ Route53: (name?: string) => Route53Builder;
16
+ CloudFront: (name: string) => CloudFrontBuilder;
17
+ Lambda: (name: string) => LambdaBuilder;
18
+ APIGateway: (name: string) => APIGatewayBuilder;
19
+ Fargate: (name: string) => FargateBuilder;
20
+ RDS: (name: string) => RDSBuilder;
21
+ SQS: (name: string) => SQSBuilder;
22
+ Secret: (secretId: string) => SecretsBuilder;
23
+ };
@@ -0,0 +1,29 @@
1
+ import { Config } from "../../core/config.js";
2
+ import { S3BucketBuilder } from "./s3.js";
3
+ import { Route53Builder } from "./route53.js";
4
+ import { CloudFrontBuilder } from "./cloudfront.js";
5
+ import { LambdaBuilder } from "./lambda.js";
6
+ import { APIGatewayBuilder } from "./apigateway.js";
7
+ import { FargateBuilder } from "./fargate.js";
8
+ import { RDSBuilder } from "./rds.js";
9
+ import { SQSBuilder } from "./sqs.js";
10
+ import { SecretsBuilder } from "./secrets.js";
11
+ export const AWS = {
12
+ init: (opts) => {
13
+ Config.set({
14
+ providers: {
15
+ ...Config.get().providers,
16
+ aws: opts,
17
+ },
18
+ });
19
+ },
20
+ S3: (name) => new S3BucketBuilder(name),
21
+ Route53: (name = "") => new Route53Builder(name),
22
+ CloudFront: (name) => new CloudFrontBuilder(name),
23
+ Lambda: (name) => new LambdaBuilder(name),
24
+ APIGateway: (name) => new APIGatewayBuilder(name),
25
+ Fargate: (name) => new FargateBuilder(name),
26
+ RDS: (name) => new RDSBuilder(name),
27
+ SQS: (name) => new SQSBuilder(name),
28
+ Secret: (secretId) => new SecretsBuilder(secretId),
29
+ };
@@ -0,0 +1,30 @@
1
+ import { BaseBuilder } from '../../core/resource.js';
2
+ import { SecretsBuilder } from './secrets.js';
3
+ export declare class LambdaBuilder extends BaseBuilder {
4
+ private _runtime;
5
+ private _handler;
6
+ private _memory;
7
+ private _timeout;
8
+ private _codePath?;
9
+ private _env;
10
+ private _roleArn?;
11
+ resolvedArn: string | null;
12
+ constructor(name: string);
13
+ private discoverFunction;
14
+ code(pathOrZip: string): this;
15
+ runtime(r: string): this;
16
+ handler(h: string): this;
17
+ memory(mb: number): this;
18
+ timeout(seconds: number): this;
19
+ role(arn: string): this;
20
+ env(vars: Record<string, string | SecretsBuilder>): this;
21
+ private ensureRole;
22
+ private buildZip;
23
+ deploy(): Promise<{
24
+ name: string;
25
+ arn: string | null;
26
+ }>;
27
+ destroy(): Promise<{
28
+ destroyed: string;
29
+ }>;
30
+ }