@stacksjs/ts-cloud-core 0.1.1
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.
- package/LICENSE.md +21 -0
- package/README.md +321 -0
- package/package.json +31 -0
- package/src/advanced-features.test.ts +465 -0
- package/src/aws/cloudformation.ts +421 -0
- package/src/aws/cloudfront.ts +158 -0
- package/src/aws/credentials.test.ts +132 -0
- package/src/aws/credentials.ts +545 -0
- package/src/aws/index.ts +87 -0
- package/src/aws/s3.test.ts +188 -0
- package/src/aws/s3.ts +1088 -0
- package/src/aws/signature.test.ts +670 -0
- package/src/aws/signature.ts +1155 -0
- package/src/backup/disaster-recovery.test.ts +726 -0
- package/src/backup/disaster-recovery.ts +500 -0
- package/src/backup/index.ts +34 -0
- package/src/backup/manager.test.ts +498 -0
- package/src/backup/manager.ts +432 -0
- package/src/cicd/circleci.ts +430 -0
- package/src/cicd/github-actions.ts +424 -0
- package/src/cicd/gitlab-ci.ts +255 -0
- package/src/cicd/index.ts +8 -0
- package/src/cli/history.ts +396 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/progress.ts +458 -0
- package/src/cli/repl.ts +454 -0
- package/src/cli/suggestions.ts +327 -0
- package/src/cli/table.test.ts +319 -0
- package/src/cli/table.ts +332 -0
- package/src/cloudformation/builder.test.ts +327 -0
- package/src/cloudformation/builder.ts +378 -0
- package/src/cloudformation/builders/api-gateway.ts +449 -0
- package/src/cloudformation/builders/cache.ts +334 -0
- package/src/cloudformation/builders/cdn.ts +278 -0
- package/src/cloudformation/builders/compute.ts +485 -0
- package/src/cloudformation/builders/database.ts +392 -0
- package/src/cloudformation/builders/functions.ts +343 -0
- package/src/cloudformation/builders/messaging.ts +140 -0
- package/src/cloudformation/builders/monitoring.ts +300 -0
- package/src/cloudformation/builders/network.ts +264 -0
- package/src/cloudformation/builders/queue.ts +147 -0
- package/src/cloudformation/builders/security.ts +399 -0
- package/src/cloudformation/builders/storage.ts +285 -0
- package/src/cloudformation/index.ts +30 -0
- package/src/cloudformation/types.ts +173 -0
- package/src/compliance/aws-config.ts +543 -0
- package/src/compliance/cloudtrail.ts +376 -0
- package/src/compliance/compliance.test.ts +423 -0
- package/src/compliance/guardduty.ts +446 -0
- package/src/compliance/index.ts +66 -0
- package/src/compliance/security-hub.ts +456 -0
- package/src/containers/build-optimization.ts +416 -0
- package/src/containers/containers.test.ts +508 -0
- package/src/containers/image-scanning.ts +360 -0
- package/src/containers/index.ts +9 -0
- package/src/containers/registry.ts +293 -0
- package/src/containers/service-mesh.ts +520 -0
- package/src/database/database.test.ts +762 -0
- package/src/database/index.ts +9 -0
- package/src/database/migrations.ts +444 -0
- package/src/database/performance.ts +528 -0
- package/src/database/replicas.ts +534 -0
- package/src/database/users.ts +494 -0
- package/src/dependency-graph.ts +143 -0
- package/src/deployment/ab-testing.ts +582 -0
- package/src/deployment/blue-green.ts +452 -0
- package/src/deployment/canary.ts +500 -0
- package/src/deployment/deployment.test.ts +526 -0
- package/src/deployment/index.ts +61 -0
- package/src/deployment/progressive.ts +62 -0
- package/src/dns/dns.test.ts +641 -0
- package/src/dns/dnssec.ts +315 -0
- package/src/dns/index.ts +8 -0
- package/src/dns/resolver.ts +496 -0
- package/src/dns/routing.ts +593 -0
- package/src/email/advanced/analytics.ts +445 -0
- package/src/email/advanced/index.ts +11 -0
- package/src/email/advanced/rules.ts +465 -0
- package/src/email/advanced/scheduling.ts +352 -0
- package/src/email/advanced/search.ts +412 -0
- package/src/email/advanced/shared-mailboxes.ts +404 -0
- package/src/email/advanced/templates.ts +455 -0
- package/src/email/advanced/threading.ts +281 -0
- package/src/email/analytics.ts +467 -0
- package/src/email/bounce-handling.ts +425 -0
- package/src/email/email.test.ts +431 -0
- package/src/email/handlers/__tests__/inbound.test.ts +38 -0
- package/src/email/handlers/__tests__/outbound.test.ts +37 -0
- package/src/email/handlers/converter.ts +227 -0
- package/src/email/handlers/feedback.ts +228 -0
- package/src/email/handlers/inbound.ts +169 -0
- package/src/email/handlers/outbound.ts +178 -0
- package/src/email/index.ts +15 -0
- package/src/email/reputation.ts +303 -0
- package/src/email/templates.ts +352 -0
- package/src/errors/index.test.ts +434 -0
- package/src/errors/index.ts +416 -0
- package/src/health-checks/index.ts +40 -0
- package/src/index.ts +360 -0
- package/src/intrinsic-functions.ts +118 -0
- package/src/lambda/concurrency.ts +330 -0
- package/src/lambda/destinations.ts +345 -0
- package/src/lambda/dlq.ts +425 -0
- package/src/lambda/index.ts +11 -0
- package/src/lambda/lambda.test.ts +840 -0
- package/src/lambda/layers.ts +263 -0
- package/src/lambda/versions.ts +376 -0
- package/src/lambda/vpc.ts +399 -0
- package/src/local/config.ts +114 -0
- package/src/local/index.ts +6 -0
- package/src/local/mock-aws.ts +351 -0
- package/src/modules/ai.ts +340 -0
- package/src/modules/api.ts +478 -0
- package/src/modules/auth.ts +805 -0
- package/src/modules/cache.ts +417 -0
- package/src/modules/cdn.ts +1062 -0
- package/src/modules/communication.ts +1094 -0
- package/src/modules/compute.ts +3348 -0
- package/src/modules/database.ts +554 -0
- package/src/modules/deployment.ts +1079 -0
- package/src/modules/dns.ts +337 -0
- package/src/modules/email.ts +1538 -0
- package/src/modules/filesystem.ts +515 -0
- package/src/modules/index.ts +32 -0
- package/src/modules/messaging.ts +486 -0
- package/src/modules/monitoring.ts +2086 -0
- package/src/modules/network.ts +664 -0
- package/src/modules/parameter-store.ts +325 -0
- package/src/modules/permissions.ts +1081 -0
- package/src/modules/phone.ts +494 -0
- package/src/modules/queue.ts +1260 -0
- package/src/modules/redirects.ts +464 -0
- package/src/modules/registry.ts +699 -0
- package/src/modules/search.ts +401 -0
- package/src/modules/secrets.ts +416 -0
- package/src/modules/security.ts +731 -0
- package/src/modules/sms.ts +389 -0
- package/src/modules/storage.ts +1120 -0
- package/src/modules/workflow.ts +680 -0
- package/src/multi-account/config.ts +521 -0
- package/src/multi-account/index.ts +7 -0
- package/src/multi-account/manager.ts +427 -0
- package/src/multi-region/cross-region.ts +410 -0
- package/src/multi-region/index.ts +8 -0
- package/src/multi-region/manager.ts +483 -0
- package/src/multi-region/regions.ts +435 -0
- package/src/network-security/index.ts +48 -0
- package/src/observability/index.ts +9 -0
- package/src/observability/logs.ts +522 -0
- package/src/observability/metrics.ts +460 -0
- package/src/observability/observability.test.ts +782 -0
- package/src/observability/synthetics.ts +568 -0
- package/src/observability/xray.ts +358 -0
- package/src/phone/advanced/analytics.ts +349 -0
- package/src/phone/advanced/callbacks.ts +428 -0
- package/src/phone/advanced/index.ts +8 -0
- package/src/phone/advanced/ivr-builder.ts +504 -0
- package/src/phone/advanced/recording.ts +310 -0
- package/src/phone/handlers/__tests__/incoming-call.test.ts +40 -0
- package/src/phone/handlers/incoming-call.ts +117 -0
- package/src/phone/handlers/missed-call.ts +116 -0
- package/src/phone/handlers/voicemail.ts +179 -0
- package/src/phone/index.ts +9 -0
- package/src/presets/api-backend.ts +134 -0
- package/src/presets/data-pipeline.ts +204 -0
- package/src/presets/extend.test.ts +295 -0
- package/src/presets/extend.ts +297 -0
- package/src/presets/fullstack-app.ts +144 -0
- package/src/presets/index.ts +27 -0
- package/src/presets/jamstack.ts +135 -0
- package/src/presets/microservices.ts +167 -0
- package/src/presets/ml-api.ts +208 -0
- package/src/presets/nodejs-server.ts +104 -0
- package/src/presets/nodejs-serverless.ts +114 -0
- package/src/presets/realtime-app.ts +184 -0
- package/src/presets/static-site.ts +64 -0
- package/src/presets/traditional-web-app.ts +339 -0
- package/src/presets/wordpress.ts +138 -0
- package/src/preview/github.test.ts +249 -0
- package/src/preview/github.ts +297 -0
- package/src/preview/index.ts +37 -0
- package/src/preview/manager.test.ts +440 -0
- package/src/preview/manager.ts +326 -0
- package/src/preview/notifications.test.ts +582 -0
- package/src/preview/notifications.ts +341 -0
- package/src/queue/batch-processing.ts +402 -0
- package/src/queue/dlq-monitoring.ts +402 -0
- package/src/queue/fifo.ts +342 -0
- package/src/queue/index.ts +9 -0
- package/src/queue/management.ts +428 -0
- package/src/queue/queue.test.ts +429 -0
- package/src/resource-mgmt/index.ts +39 -0
- package/src/resource-naming.ts +62 -0
- package/src/s3/index.ts +523 -0
- package/src/schema/cloud-config.schema.json +554 -0
- package/src/schema/index.ts +68 -0
- package/src/security/certificate-manager.ts +492 -0
- package/src/security/index.ts +9 -0
- package/src/security/scanning.ts +545 -0
- package/src/security/secrets-manager.ts +476 -0
- package/src/security/secrets-rotation.ts +456 -0
- package/src/security/security.test.ts +738 -0
- package/src/sms/advanced/ab-testing.ts +389 -0
- package/src/sms/advanced/analytics.ts +336 -0
- package/src/sms/advanced/campaigns.ts +523 -0
- package/src/sms/advanced/chatbot.ts +224 -0
- package/src/sms/advanced/index.ts +10 -0
- package/src/sms/advanced/link-tracking.ts +248 -0
- package/src/sms/advanced/mms.ts +308 -0
- package/src/sms/handlers/__tests__/send.test.ts +40 -0
- package/src/sms/handlers/delivery-status.ts +133 -0
- package/src/sms/handlers/receive.ts +162 -0
- package/src/sms/handlers/send.ts +174 -0
- package/src/sms/index.ts +9 -0
- package/src/stack-diff.ts +389 -0
- package/src/static-site/index.ts +85 -0
- package/src/template-builder.ts +110 -0
- package/src/template-validator.ts +574 -0
- package/src/utils/cache.ts +291 -0
- package/src/utils/diff.ts +269 -0
- package/src/utils/hash.ts +227 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/parallel.ts +294 -0
- package/src/validators/credentials.test.ts +274 -0
- package/src/validators/credentials.ts +233 -0
- package/src/validators/quotas.test.ts +434 -0
- package/src/validators/quotas.ts +217 -0
- package/test/ai.test.ts +327 -0
- package/test/api.test.ts +511 -0
- package/test/auth.test.ts +632 -0
- package/test/cache.test.ts +406 -0
- package/test/cdn.test.ts +247 -0
- package/test/compute.test.ts +861 -0
- package/test/database.test.ts +523 -0
- package/test/deployment.test.ts +499 -0
- package/test/dns.test.ts +270 -0
- package/test/email.test.ts +439 -0
- package/test/filesystem.test.ts +382 -0
- package/test/integration.test.ts +350 -0
- package/test/messaging.test.ts +514 -0
- package/test/monitoring.test.ts +634 -0
- package/test/network.test.ts +425 -0
- package/test/permissions.test.ts +488 -0
- package/test/queue.test.ts +484 -0
- package/test/registry.test.ts +306 -0
- package/test/security.test.ts +462 -0
- package/test/storage.test.ts +463 -0
- package/test/template-validator.test.ts +559 -0
- package/test/workflow.test.ts +592 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import type { ECRRepository, ECRLifecyclePolicy } from '@stacksjs/ts-cloud-aws-types'
|
|
2
|
+
import { generateLogicalId, generateResourceName } from '../resource-naming'
|
|
3
|
+
import type { EnvironmentType } from '@stacksjs/ts-cloud-types'
|
|
4
|
+
|
|
5
|
+
export interface RegistryOptions {
|
|
6
|
+
name: string
|
|
7
|
+
slug: string
|
|
8
|
+
environment: EnvironmentType
|
|
9
|
+
scanOnPush?: boolean
|
|
10
|
+
imageMutability?: 'MUTABLE' | 'IMMUTABLE'
|
|
11
|
+
encryption?: 'AES256' | 'KMS'
|
|
12
|
+
kmsKey?: string
|
|
13
|
+
lifecyclePolicy?: LifecyclePolicyConfig
|
|
14
|
+
tags?: Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LifecyclePolicyConfig {
|
|
18
|
+
maxImageCount?: number
|
|
19
|
+
maxImageAgeDays?: number
|
|
20
|
+
untaggedImageExpireDays?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Registry Module - ECR Container Registry Management
|
|
25
|
+
* Provides clean API for creating and configuring ECR repositories
|
|
26
|
+
*/
|
|
27
|
+
export class Registry {
|
|
28
|
+
/**
|
|
29
|
+
* Create an ECR repository with the specified options
|
|
30
|
+
*/
|
|
31
|
+
static createRepository(options: RegistryOptions): { repository: ECRRepository, logicalId: string } {
|
|
32
|
+
const {
|
|
33
|
+
name,
|
|
34
|
+
slug,
|
|
35
|
+
environment,
|
|
36
|
+
scanOnPush = true,
|
|
37
|
+
imageMutability = 'MUTABLE',
|
|
38
|
+
encryption = 'AES256',
|
|
39
|
+
kmsKey,
|
|
40
|
+
lifecyclePolicy,
|
|
41
|
+
tags,
|
|
42
|
+
} = options
|
|
43
|
+
|
|
44
|
+
const resourceName = generateResourceName({
|
|
45
|
+
slug,
|
|
46
|
+
environment,
|
|
47
|
+
resourceType: 'ecr',
|
|
48
|
+
suffix: name,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const logicalId = generateLogicalId(resourceName)
|
|
52
|
+
|
|
53
|
+
const repository: ECRRepository = {
|
|
54
|
+
Type: 'AWS::ECR::Repository',
|
|
55
|
+
Properties: {
|
|
56
|
+
RepositoryName: resourceName,
|
|
57
|
+
ImageTagMutability: imageMutability,
|
|
58
|
+
ImageScanningConfiguration: {
|
|
59
|
+
ScanOnPush: scanOnPush,
|
|
60
|
+
},
|
|
61
|
+
EncryptionConfiguration: {
|
|
62
|
+
EncryptionType: encryption,
|
|
63
|
+
...(kmsKey && encryption === 'KMS' ? { KmsKey: kmsKey } : {}),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add lifecycle policy if specified
|
|
69
|
+
if (lifecyclePolicy) {
|
|
70
|
+
repository.Properties!.LifecyclePolicy = {
|
|
71
|
+
LifecyclePolicyText: JSON.stringify(
|
|
72
|
+
Registry.generateLifecyclePolicy(lifecyclePolicy),
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add tags if specified
|
|
78
|
+
if (tags) {
|
|
79
|
+
repository.Properties!.Tags = Object.entries(tags).map(([Key, Value]) => ({
|
|
80
|
+
Key,
|
|
81
|
+
Value,
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
repository,
|
|
87
|
+
logicalId,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate lifecycle policy from config
|
|
93
|
+
*/
|
|
94
|
+
private static generateLifecyclePolicy(config: LifecyclePolicyConfig): ECRLifecyclePolicy {
|
|
95
|
+
const rules: ECRLifecyclePolicy['rules'] = []
|
|
96
|
+
|
|
97
|
+
// Rule for untagged images
|
|
98
|
+
if (config.untaggedImageExpireDays !== undefined) {
|
|
99
|
+
rules.push({
|
|
100
|
+
rulePriority: 1,
|
|
101
|
+
description: 'Delete untagged images',
|
|
102
|
+
selection: {
|
|
103
|
+
tagStatus: 'untagged',
|
|
104
|
+
countType: 'sinceImagePushed',
|
|
105
|
+
countNumber: config.untaggedImageExpireDays,
|
|
106
|
+
countUnit: 'days',
|
|
107
|
+
},
|
|
108
|
+
action: {
|
|
109
|
+
type: 'expire',
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Rule for max image count
|
|
115
|
+
if (config.maxImageCount !== undefined) {
|
|
116
|
+
rules.push({
|
|
117
|
+
rulePriority: rules.length + 1,
|
|
118
|
+
description: 'Keep only most recent images',
|
|
119
|
+
selection: {
|
|
120
|
+
tagStatus: 'any',
|
|
121
|
+
countType: 'imageCountMoreThan',
|
|
122
|
+
countNumber: config.maxImageCount,
|
|
123
|
+
},
|
|
124
|
+
action: {
|
|
125
|
+
type: 'expire',
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rule for max image age
|
|
131
|
+
if (config.maxImageAgeDays !== undefined) {
|
|
132
|
+
rules.push({
|
|
133
|
+
rulePriority: rules.length + 1,
|
|
134
|
+
description: 'Delete images older than specified days',
|
|
135
|
+
selection: {
|
|
136
|
+
tagStatus: 'any',
|
|
137
|
+
countType: 'sinceImagePushed',
|
|
138
|
+
countNumber: config.maxImageAgeDays,
|
|
139
|
+
countUnit: 'days',
|
|
140
|
+
},
|
|
141
|
+
action: {
|
|
142
|
+
type: 'expire',
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { rules }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Common lifecycle policy presets
|
|
152
|
+
*/
|
|
153
|
+
static readonly LifecyclePolicies = {
|
|
154
|
+
/**
|
|
155
|
+
* Keep only the 10 most recent images, delete untagged after 7 days
|
|
156
|
+
*/
|
|
157
|
+
production: {
|
|
158
|
+
maxImageCount: 10,
|
|
159
|
+
untaggedImageExpireDays: 7,
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Keep only the 5 most recent images, delete untagged after 3 days
|
|
164
|
+
*/
|
|
165
|
+
development: {
|
|
166
|
+
maxImageCount: 5,
|
|
167
|
+
untaggedImageExpireDays: 3,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Aggressive cleanup - keep 3 images, delete untagged after 1 day
|
|
172
|
+
*/
|
|
173
|
+
minimal: {
|
|
174
|
+
maxImageCount: 3,
|
|
175
|
+
untaggedImageExpireDays: 1,
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Long-term storage - keep 50 images, delete untagged after 30 days
|
|
180
|
+
*/
|
|
181
|
+
archive: {
|
|
182
|
+
maxImageCount: 50,
|
|
183
|
+
untaggedImageExpireDays: 30,
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Enable immutable tags on an existing repository
|
|
189
|
+
*/
|
|
190
|
+
static enableImmutableTags(repository: ECRRepository): ECRRepository {
|
|
191
|
+
if (!repository.Properties) {
|
|
192
|
+
repository.Properties = {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
repository.Properties.ImageTagMutability = 'IMMUTABLE'
|
|
196
|
+
|
|
197
|
+
return repository
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Enable scan on push
|
|
202
|
+
*/
|
|
203
|
+
static enableScanOnPush(repository: ECRRepository): ECRRepository {
|
|
204
|
+
if (!repository.Properties) {
|
|
205
|
+
repository.Properties = {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
repository.Properties.ImageScanningConfiguration = {
|
|
209
|
+
ScanOnPush: true,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return repository
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set lifecycle policy on an existing repository
|
|
217
|
+
*/
|
|
218
|
+
static setLifecyclePolicy(
|
|
219
|
+
repository: ECRRepository,
|
|
220
|
+
config: LifecyclePolicyConfig,
|
|
221
|
+
): ECRRepository {
|
|
222
|
+
if (!repository.Properties) {
|
|
223
|
+
repository.Properties = {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
repository.Properties.LifecyclePolicy = {
|
|
227
|
+
LifecyclePolicyText: JSON.stringify(Registry.generateLifecyclePolicy(config)),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return repository
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Add repository policy for cross-account access
|
|
235
|
+
*/
|
|
236
|
+
static addCrossAccountAccess(
|
|
237
|
+
repository: ECRRepository,
|
|
238
|
+
accountIds: string[],
|
|
239
|
+
): ECRRepository {
|
|
240
|
+
if (!repository.Properties) {
|
|
241
|
+
repository.Properties = {}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
repository.Properties.RepositoryPolicyText = {
|
|
245
|
+
Version: '2012-10-17',
|
|
246
|
+
Statement: [
|
|
247
|
+
{
|
|
248
|
+
Sid: 'CrossAccountPull',
|
|
249
|
+
Effect: 'Allow',
|
|
250
|
+
Principal: {
|
|
251
|
+
AWS: accountIds.map(id => `arn:aws:iam::${id}:root`),
|
|
252
|
+
},
|
|
253
|
+
Action: [
|
|
254
|
+
'ecr:GetDownloadUrlForLayer',
|
|
255
|
+
'ecr:BatchGetImage',
|
|
256
|
+
'ecr:BatchCheckLayerAvailability',
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return repository
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Add repository policy for Lambda service access
|
|
267
|
+
*/
|
|
268
|
+
static addLambdaAccess(repository: ECRRepository): ECRRepository {
|
|
269
|
+
if (!repository.Properties) {
|
|
270
|
+
repository.Properties = {}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
repository.Properties.RepositoryPolicyText = {
|
|
274
|
+
Version: '2012-10-17',
|
|
275
|
+
Statement: [
|
|
276
|
+
{
|
|
277
|
+
Sid: 'LambdaECRImageRetrievalPolicy',
|
|
278
|
+
Effect: 'Allow',
|
|
279
|
+
Principal: {
|
|
280
|
+
Service: 'lambda.amazonaws.com',
|
|
281
|
+
},
|
|
282
|
+
Action: [
|
|
283
|
+
'ecr:GetDownloadUrlForLayer',
|
|
284
|
+
'ecr:BatchGetImage',
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return repository
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generate a Dockerfile for Bun-based applications
|
|
295
|
+
*/
|
|
296
|
+
static generateBunDockerfile(options: {
|
|
297
|
+
baseImage?: string
|
|
298
|
+
serverPath: string
|
|
299
|
+
port?: number
|
|
300
|
+
additionalDirs?: string[]
|
|
301
|
+
healthCheckEndpoint?: string
|
|
302
|
+
nodeCompatible?: boolean
|
|
303
|
+
envVars?: Record<string, string>
|
|
304
|
+
buildCommands?: string[]
|
|
305
|
+
runCommand?: string
|
|
306
|
+
}): string {
|
|
307
|
+
const {
|
|
308
|
+
baseImage = 'oven/bun:1-debian',
|
|
309
|
+
serverPath,
|
|
310
|
+
port = 3000,
|
|
311
|
+
additionalDirs = [],
|
|
312
|
+
healthCheckEndpoint = '/health',
|
|
313
|
+
nodeCompatible = false,
|
|
314
|
+
envVars = {},
|
|
315
|
+
buildCommands = [],
|
|
316
|
+
runCommand,
|
|
317
|
+
} = options
|
|
318
|
+
|
|
319
|
+
const copyDirs = ['app', 'config', ...additionalDirs]
|
|
320
|
+
const entrypoint = runCommand || `bun run ${serverPath}`
|
|
321
|
+
|
|
322
|
+
let dockerfile = `# Multi-stage build for Bun application
|
|
323
|
+
# Generated by ts-cloud
|
|
324
|
+
|
|
325
|
+
# Builder stage
|
|
326
|
+
FROM ${baseImage} AS builder
|
|
327
|
+
|
|
328
|
+
WORKDIR /app
|
|
329
|
+
|
|
330
|
+
# Copy package files first for better caching
|
|
331
|
+
COPY package.json bun.lockb* ./
|
|
332
|
+
|
|
333
|
+
# Install dependencies
|
|
334
|
+
RUN bun install --frozen-lockfile --production
|
|
335
|
+
|
|
336
|
+
# Production stage
|
|
337
|
+
FROM ${baseImage} AS release
|
|
338
|
+
|
|
339
|
+
WORKDIR /app
|
|
340
|
+
|
|
341
|
+
# Create non-root user
|
|
342
|
+
RUN groupadd -r app && useradd -r -g app app
|
|
343
|
+
|
|
344
|
+
# Copy node_modules from builder
|
|
345
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
346
|
+
|
|
347
|
+
# Copy application code
|
|
348
|
+
COPY package.json ./
|
|
349
|
+
`
|
|
350
|
+
|
|
351
|
+
// Copy each directory
|
|
352
|
+
for (const dir of copyDirs) {
|
|
353
|
+
dockerfile += `COPY ${dir} ./${dir}\n`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Copy the server file
|
|
357
|
+
dockerfile += `COPY ${serverPath} ./${serverPath}\n`
|
|
358
|
+
|
|
359
|
+
// Add build commands if any
|
|
360
|
+
if (buildCommands.length > 0) {
|
|
361
|
+
dockerfile += '\n# Build commands\n'
|
|
362
|
+
for (const cmd of buildCommands) {
|
|
363
|
+
dockerfile += `RUN ${cmd}\n`
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Environment variables
|
|
368
|
+
dockerfile += '\n# Environment variables\n'
|
|
369
|
+
dockerfile += `ENV NODE_ENV=production\n`
|
|
370
|
+
dockerfile += `ENV PORT=${port}\n`
|
|
371
|
+
|
|
372
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
373
|
+
dockerfile += `ENV ${key}=${value}\n`
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Node compatibility mode
|
|
377
|
+
if (nodeCompatible) {
|
|
378
|
+
dockerfile += 'ENV BUN_INSTALL_FORCE_NODE_API=1\n'
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Storage directory
|
|
382
|
+
dockerfile += `
|
|
383
|
+
# Create storage directory
|
|
384
|
+
RUN mkdir -p /app/storage && chown -R app:app /app/storage
|
|
385
|
+
|
|
386
|
+
# Switch to non-root user
|
|
387
|
+
USER app
|
|
388
|
+
|
|
389
|
+
# Expose port
|
|
390
|
+
EXPOSE ${port}
|
|
391
|
+
|
|
392
|
+
# Health check
|
|
393
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
|
394
|
+
CMD curl -f http://localhost:${port}${healthCheckEndpoint} || exit 1
|
|
395
|
+
|
|
396
|
+
# Start application
|
|
397
|
+
CMD ["bun", "run", "${serverPath}"]
|
|
398
|
+
`
|
|
399
|
+
|
|
400
|
+
return dockerfile
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Generate Docker build commands
|
|
405
|
+
*/
|
|
406
|
+
static generateDockerBuildCommands(options: {
|
|
407
|
+
repositoryUri: string
|
|
408
|
+
tag?: string
|
|
409
|
+
dockerfilePath?: string
|
|
410
|
+
context?: string
|
|
411
|
+
additionalTags?: string[]
|
|
412
|
+
buildArgs?: Record<string, string>
|
|
413
|
+
platform?: string
|
|
414
|
+
noCache?: boolean
|
|
415
|
+
}): {
|
|
416
|
+
build: string
|
|
417
|
+
tag: string[]
|
|
418
|
+
push: string[]
|
|
419
|
+
all: string[]
|
|
420
|
+
} {
|
|
421
|
+
const {
|
|
422
|
+
repositoryUri,
|
|
423
|
+
tag = 'latest',
|
|
424
|
+
dockerfilePath = 'Dockerfile',
|
|
425
|
+
context = '.',
|
|
426
|
+
additionalTags = [],
|
|
427
|
+
buildArgs = {},
|
|
428
|
+
platform = 'linux/amd64',
|
|
429
|
+
noCache = false,
|
|
430
|
+
} = options
|
|
431
|
+
|
|
432
|
+
const imageUri = `${repositoryUri}:${tag}`
|
|
433
|
+
const allTags = [tag, ...additionalTags]
|
|
434
|
+
|
|
435
|
+
// Build command
|
|
436
|
+
let buildCmd = `docker build -f ${dockerfilePath} -t ${imageUri}`
|
|
437
|
+
|
|
438
|
+
if (platform) {
|
|
439
|
+
buildCmd += ` --platform ${platform}`
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (noCache) {
|
|
443
|
+
buildCmd += ' --no-cache'
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const [key, value] of Object.entries(buildArgs)) {
|
|
447
|
+
buildCmd += ` --build-arg ${key}=${value}`
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
buildCmd += ` ${context}`
|
|
451
|
+
|
|
452
|
+
// Tag commands
|
|
453
|
+
const tagCommands = allTags.slice(1).map(t =>
|
|
454
|
+
`docker tag ${imageUri} ${repositoryUri}:${t}`,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
// Push commands
|
|
458
|
+
const pushCommands = allTags.map(t =>
|
|
459
|
+
`docker push ${repositoryUri}:${t}`,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
build: buildCmd,
|
|
464
|
+
tag: tagCommands,
|
|
465
|
+
push: pushCommands,
|
|
466
|
+
all: [buildCmd, ...tagCommands, ...pushCommands],
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Generate ECR login command
|
|
472
|
+
*/
|
|
473
|
+
static generateEcrLoginCommand(region: string, accountId: string): string {
|
|
474
|
+
return `aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${accountId}.dkr.ecr.${region}.amazonaws.com`
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Build ECR repository URI
|
|
479
|
+
*/
|
|
480
|
+
static buildRepositoryUri(options: {
|
|
481
|
+
accountId: string
|
|
482
|
+
region: string
|
|
483
|
+
repositoryName: string
|
|
484
|
+
}): string {
|
|
485
|
+
return `${options.accountId}.dkr.ecr.${options.region}.amazonaws.com/${options.repositoryName}`
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Generate image tags based on deployment info
|
|
490
|
+
*/
|
|
491
|
+
static generateImageTags(options: {
|
|
492
|
+
version?: string
|
|
493
|
+
gitSha?: string
|
|
494
|
+
gitBranch?: string
|
|
495
|
+
environment?: string
|
|
496
|
+
timestamp?: boolean
|
|
497
|
+
}): string[] {
|
|
498
|
+
const tags: string[] = ['latest']
|
|
499
|
+
|
|
500
|
+
if (options.version) {
|
|
501
|
+
tags.push(options.version)
|
|
502
|
+
// Also add semantic version components
|
|
503
|
+
const parts = options.version.split('.')
|
|
504
|
+
if (parts.length >= 2) {
|
|
505
|
+
tags.push(`${parts[0]}.${parts[1]}`) // major.minor
|
|
506
|
+
}
|
|
507
|
+
if (parts.length >= 1) {
|
|
508
|
+
tags.push(parts[0]) // major
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (options.gitSha) {
|
|
513
|
+
tags.push(options.gitSha.substring(0, 7)) // Short SHA
|
|
514
|
+
tags.push(options.gitSha) // Full SHA
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (options.gitBranch) {
|
|
518
|
+
// Sanitize branch name for Docker tag
|
|
519
|
+
const sanitizedBranch = options.gitBranch
|
|
520
|
+
.replace(/[^a-zA-Z0-9.-]/g, '-')
|
|
521
|
+
.replace(/^-+|-+$/g, '')
|
|
522
|
+
tags.push(sanitizedBranch)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (options.environment) {
|
|
526
|
+
tags.push(options.environment)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (options.timestamp) {
|
|
530
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19)
|
|
531
|
+
tags.push(ts)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return [...new Set(tags)] // Remove duplicates
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Docker deployment workflow steps
|
|
539
|
+
*/
|
|
540
|
+
static readonly DeploymentWorkflow = {
|
|
541
|
+
/**
|
|
542
|
+
* Generate a complete deployment script
|
|
543
|
+
*/
|
|
544
|
+
generateDeployScript: (options: {
|
|
545
|
+
region: string
|
|
546
|
+
accountId: string
|
|
547
|
+
repositoryName: string
|
|
548
|
+
dockerfilePath?: string
|
|
549
|
+
serverPath: string
|
|
550
|
+
tags?: string[]
|
|
551
|
+
}): string => {
|
|
552
|
+
const repositoryUri = Registry.buildRepositoryUri({
|
|
553
|
+
accountId: options.accountId,
|
|
554
|
+
region: options.region,
|
|
555
|
+
repositoryName: options.repositoryName,
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
const tags = options.tags || ['latest']
|
|
559
|
+
const primaryTag = tags[0]
|
|
560
|
+
|
|
561
|
+
return `#!/bin/bash
|
|
562
|
+
set -e
|
|
563
|
+
|
|
564
|
+
# Configuration
|
|
565
|
+
REGION="${options.region}"
|
|
566
|
+
ACCOUNT_ID="${options.accountId}"
|
|
567
|
+
REPOSITORY="${options.repositoryName}"
|
|
568
|
+
IMAGE_URI="${repositoryUri}"
|
|
569
|
+
DOCKERFILE="${options.dockerfilePath || 'Dockerfile'}"
|
|
570
|
+
|
|
571
|
+
echo "=== ECR Login ==="
|
|
572
|
+
${Registry.generateEcrLoginCommand(options.region, options.accountId)}
|
|
573
|
+
|
|
574
|
+
echo "=== Building Image ==="
|
|
575
|
+
docker build -f $DOCKERFILE -t $IMAGE_URI:${primaryTag} --platform linux/amd64 .
|
|
576
|
+
|
|
577
|
+
${tags.slice(1).map(t => `echo "=== Tagging: ${t} ===" && docker tag $IMAGE_URI:${primaryTag} $IMAGE_URI:${t}`).join('\n')}
|
|
578
|
+
|
|
579
|
+
echo "=== Pushing Images ==="
|
|
580
|
+
${tags.map(t => `docker push $IMAGE_URI:${t}`).join('\n')}
|
|
581
|
+
|
|
582
|
+
echo "=== Deployment Complete ==="
|
|
583
|
+
echo "Image: $IMAGE_URI:${primaryTag}"
|
|
584
|
+
`
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate GitHub Actions workflow for ECR deployment
|
|
589
|
+
*/
|
|
590
|
+
generateGitHubActionsWorkflow: (options: {
|
|
591
|
+
region: string
|
|
592
|
+
repositoryName: string
|
|
593
|
+
dockerfilePath?: string
|
|
594
|
+
ecsCluster?: string
|
|
595
|
+
ecsService?: string
|
|
596
|
+
}): string => {
|
|
597
|
+
const workflow = {
|
|
598
|
+
name: 'Deploy to ECR',
|
|
599
|
+
on: {
|
|
600
|
+
push: {
|
|
601
|
+
branches: ['main'],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
env: {
|
|
605
|
+
AWS_REGION: options.region,
|
|
606
|
+
ECR_REPOSITORY: options.repositoryName,
|
|
607
|
+
},
|
|
608
|
+
jobs: {
|
|
609
|
+
deploy: {
|
|
610
|
+
'runs-on': 'ubuntu-latest',
|
|
611
|
+
permissions: {
|
|
612
|
+
'id-token': 'write',
|
|
613
|
+
'contents': 'read',
|
|
614
|
+
},
|
|
615
|
+
steps: [
|
|
616
|
+
{
|
|
617
|
+
name: 'Checkout',
|
|
618
|
+
uses: 'actions/checkout@v4',
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: 'Configure AWS credentials',
|
|
622
|
+
uses: 'aws-actions/configure-aws-credentials@v4',
|
|
623
|
+
with: {
|
|
624
|
+
'role-to-assume': '${{ secrets.AWS_ROLE_ARN }}',
|
|
625
|
+
'aws-region': '${{ env.AWS_REGION }}',
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'Login to Amazon ECR',
|
|
630
|
+
id: 'login-ecr',
|
|
631
|
+
uses: 'aws-actions/amazon-ecr-login@v2',
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'Build, tag, and push image to Amazon ECR',
|
|
635
|
+
env: {
|
|
636
|
+
ECR_REGISTRY: '${{ steps.login-ecr.outputs.registry }}',
|
|
637
|
+
IMAGE_TAG: '${{ github.sha }}',
|
|
638
|
+
},
|
|
639
|
+
run: `docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest ${options.dockerfilePath ? `-f ${options.dockerfilePath}` : ''} .
|
|
640
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
|
|
641
|
+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest`,
|
|
642
|
+
},
|
|
643
|
+
...(options.ecsCluster && options.ecsService
|
|
644
|
+
? [{
|
|
645
|
+
name: 'Deploy to ECS',
|
|
646
|
+
run: `aws ecs update-service --cluster ${options.ecsCluster} --service ${options.ecsService} --force-new-deployment`,
|
|
647
|
+
}]
|
|
648
|
+
: []),
|
|
649
|
+
],
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return `# Generated by ts-cloud
|
|
655
|
+
${JSON.stringify(workflow, null, 2).replace(/"/g, '').replace(/,\n/g, '\n')}`
|
|
656
|
+
},
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Common Dockerfile templates
|
|
661
|
+
*/
|
|
662
|
+
static readonly DockerfileTemplates = {
|
|
663
|
+
/**
|
|
664
|
+
* Minimal Bun server
|
|
665
|
+
*/
|
|
666
|
+
bunServer: (serverPath: string, port = 3000): string => Registry.generateBunDockerfile({
|
|
667
|
+
serverPath,
|
|
668
|
+
port,
|
|
669
|
+
}),
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Bun with build step
|
|
673
|
+
*/
|
|
674
|
+
bunWithBuild: (serverPath: string, buildCommand: string, port = 3000): string => Registry.generateBunDockerfile({
|
|
675
|
+
serverPath,
|
|
676
|
+
port,
|
|
677
|
+
buildCommands: [buildCommand],
|
|
678
|
+
}),
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Full-stack Bun app with static files
|
|
682
|
+
*/
|
|
683
|
+
bunFullStack: (serverPath: string, port = 3000): string => Registry.generateBunDockerfile({
|
|
684
|
+
serverPath,
|
|
685
|
+
port,
|
|
686
|
+
additionalDirs: ['public', 'views', 'dist'],
|
|
687
|
+
buildCommands: ['bun run build'],
|
|
688
|
+
}),
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* API-only Bun server
|
|
692
|
+
*/
|
|
693
|
+
bunApi: (serverPath: string, port = 3000): string => Registry.generateBunDockerfile({
|
|
694
|
+
serverPath,
|
|
695
|
+
port,
|
|
696
|
+
healthCheckEndpoint: '/api/health',
|
|
697
|
+
}),
|
|
698
|
+
}
|
|
699
|
+
}
|