@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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching utilities for performance optimization
|
|
3
|
+
* Caches CloudFormation templates, credentials, and other data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
|
|
10
|
+
export interface CacheOptions {
|
|
11
|
+
ttl?: number // Time to live in milliseconds
|
|
12
|
+
maxSize?: number // Maximum cache size
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CacheEntry<T> {
|
|
16
|
+
value: T
|
|
17
|
+
timestamp: number
|
|
18
|
+
hash?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Simple in-memory cache with TTL support
|
|
23
|
+
*/
|
|
24
|
+
export class Cache<T = any> {
|
|
25
|
+
private cache: Map<string, CacheEntry<T>>
|
|
26
|
+
private ttl: number
|
|
27
|
+
private maxSize: number
|
|
28
|
+
|
|
29
|
+
constructor(options: CacheOptions = {}) {
|
|
30
|
+
this.cache = new Map()
|
|
31
|
+
this.ttl = options.ttl || 5 * 60 * 1000 // Default: 5 minutes
|
|
32
|
+
this.maxSize = options.maxSize || 100 // Default: 100 entries
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get value from cache
|
|
37
|
+
*/
|
|
38
|
+
get(key: string): T | undefined {
|
|
39
|
+
const entry = this.cache.get(key)
|
|
40
|
+
|
|
41
|
+
if (!entry) {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if entry has expired
|
|
46
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
47
|
+
this.cache.delete(key)
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return entry.value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set value in cache
|
|
56
|
+
*/
|
|
57
|
+
set(key: string, value: T, hash?: string): void {
|
|
58
|
+
// If cache is full, remove oldest entry
|
|
59
|
+
if (this.cache.size >= this.maxSize) {
|
|
60
|
+
const firstKey = this.cache.keys().next().value
|
|
61
|
+
if (firstKey !== undefined) {
|
|
62
|
+
this.cache.delete(firstKey)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.cache.set(key, {
|
|
67
|
+
value,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
hash,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if cache has key and it's not expired
|
|
75
|
+
*/
|
|
76
|
+
has(key: string): boolean {
|
|
77
|
+
return this.get(key) !== undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear cache
|
|
82
|
+
*/
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.cache.clear()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove expired entries
|
|
89
|
+
*/
|
|
90
|
+
prune(): void {
|
|
91
|
+
const now = Date.now()
|
|
92
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
93
|
+
if (now - entry.timestamp > this.ttl) {
|
|
94
|
+
this.cache.delete(key)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get cache stats
|
|
101
|
+
*/
|
|
102
|
+
stats(): { size: number, ttl: number, maxSize: number } {
|
|
103
|
+
return {
|
|
104
|
+
size: this.cache.size,
|
|
105
|
+
ttl: this.ttl,
|
|
106
|
+
maxSize: this.maxSize,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* File-based cache for persistent caching
|
|
113
|
+
*/
|
|
114
|
+
export class FileCache<T = any> {
|
|
115
|
+
private cacheDir: string
|
|
116
|
+
private ttl: number
|
|
117
|
+
|
|
118
|
+
constructor(cacheDir: string, options: CacheOptions = {}) {
|
|
119
|
+
this.cacheDir = cacheDir
|
|
120
|
+
this.ttl = options.ttl || 24 * 60 * 60 * 1000 // Default: 24 hours
|
|
121
|
+
|
|
122
|
+
// Create cache directory if it doesn't exist
|
|
123
|
+
if (!fs.existsSync(cacheDir)) {
|
|
124
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get cache file path for key
|
|
130
|
+
*/
|
|
131
|
+
private getCachePath(key: string): string {
|
|
132
|
+
const hash = createHash('sha256').update(key).digest('hex')
|
|
133
|
+
return path.join(this.cacheDir, `${hash}.json`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get value from cache
|
|
138
|
+
*/
|
|
139
|
+
get(key: string): T | undefined {
|
|
140
|
+
const cachePath = this.getCachePath(key)
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(cachePath)) {
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const data = fs.readFileSync(cachePath, 'utf-8')
|
|
148
|
+
const entry: CacheEntry<T> = JSON.parse(data)
|
|
149
|
+
|
|
150
|
+
// Check if entry has expired
|
|
151
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
152
|
+
fs.unlinkSync(cachePath)
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return entry.value
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// If cache file is corrupted, delete it
|
|
160
|
+
fs.unlinkSync(cachePath)
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Set value in cache
|
|
167
|
+
*/
|
|
168
|
+
set(key: string, value: T, hash?: string): void {
|
|
169
|
+
const cachePath = this.getCachePath(key)
|
|
170
|
+
|
|
171
|
+
const entry: CacheEntry<T> = {
|
|
172
|
+
value,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
hash,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.writeFileSync(cachePath, JSON.stringify(entry), 'utf-8')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if cache has key and it's not expired
|
|
182
|
+
*/
|
|
183
|
+
has(key: string): boolean {
|
|
184
|
+
return this.get(key) !== undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Clear all cache files
|
|
189
|
+
*/
|
|
190
|
+
clear(): void {
|
|
191
|
+
const files = fs.readdirSync(this.cacheDir)
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
fs.unlinkSync(path.join(this.cacheDir, file))
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove expired entries
|
|
199
|
+
*/
|
|
200
|
+
prune(): void {
|
|
201
|
+
const files = fs.readdirSync(this.cacheDir)
|
|
202
|
+
const now = Date.now()
|
|
203
|
+
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
const filePath = path.join(this.cacheDir, file)
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const data = fs.readFileSync(filePath, 'utf-8')
|
|
209
|
+
const entry: CacheEntry<any> = JSON.parse(data)
|
|
210
|
+
|
|
211
|
+
if (now - entry.timestamp > this.ttl) {
|
|
212
|
+
fs.unlinkSync(filePath)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// If file is corrupted, delete it
|
|
217
|
+
fs.unlinkSync(filePath)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Template cache for CloudFormation templates
|
|
225
|
+
*/
|
|
226
|
+
export class TemplateCache {
|
|
227
|
+
private cache: FileCache<string>
|
|
228
|
+
|
|
229
|
+
constructor(cacheDir: string = '.ts-cloud/cache/templates') {
|
|
230
|
+
this.cache = new FileCache<string>(cacheDir, {
|
|
231
|
+
ttl: 24 * 60 * 60 * 1000, // 24 hours
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get template from cache
|
|
237
|
+
*/
|
|
238
|
+
getTemplate(stackName: string): string | undefined {
|
|
239
|
+
return this.cache.get(`template:${stackName}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Save template to cache
|
|
244
|
+
*/
|
|
245
|
+
setTemplate(stackName: string, template: string): void {
|
|
246
|
+
const hash = this.hashTemplate(template)
|
|
247
|
+
this.cache.set(`template:${stackName}`, template, hash)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if template has changed
|
|
252
|
+
*/
|
|
253
|
+
hasChanged(stackName: string, newTemplate: string): boolean {
|
|
254
|
+
const cached = this.cache.get(`template:${stackName}`)
|
|
255
|
+
|
|
256
|
+
if (!cached) {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const cachedHash = this.hashTemplate(cached)
|
|
261
|
+
const newHash = this.hashTemplate(newTemplate)
|
|
262
|
+
|
|
263
|
+
return cachedHash !== newHash
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Hash template for comparison
|
|
268
|
+
*/
|
|
269
|
+
private hashTemplate(template: string): string {
|
|
270
|
+
return createHash('sha256').update(template).digest('hex')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Clear all templates
|
|
275
|
+
*/
|
|
276
|
+
clear(): void {
|
|
277
|
+
this.cache.clear()
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Prune expired templates
|
|
282
|
+
*/
|
|
283
|
+
prune(): void {
|
|
284
|
+
this.cache.prune()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Global template cache instance
|
|
290
|
+
*/
|
|
291
|
+
export const templateCache: TemplateCache = new TemplateCache()
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template diff utilities for incremental deployments
|
|
3
|
+
* Detect changes between CloudFormation templates to optimize deployments
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CloudFormationTemplate } from '../cloudformation/types'
|
|
7
|
+
|
|
8
|
+
export interface TemplateDiff {
|
|
9
|
+
added: string[] // Resource logical IDs
|
|
10
|
+
modified: string[] // Resource logical IDs
|
|
11
|
+
deleted: string[] // Resource logical IDs
|
|
12
|
+
unchanged: string[] // Resource logical IDs
|
|
13
|
+
parametersChanged: boolean
|
|
14
|
+
outputsChanged: boolean
|
|
15
|
+
hasChanges: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare two CloudFormation templates
|
|
20
|
+
*/
|
|
21
|
+
export function diffTemplates(
|
|
22
|
+
oldTemplate: CloudFormationTemplate,
|
|
23
|
+
newTemplate: CloudFormationTemplate,
|
|
24
|
+
): TemplateDiff {
|
|
25
|
+
const added: string[] = []
|
|
26
|
+
const modified: string[] = []
|
|
27
|
+
const deleted: string[] = []
|
|
28
|
+
const unchanged: string[] = []
|
|
29
|
+
|
|
30
|
+
const oldResources = oldTemplate.Resources || {}
|
|
31
|
+
const newResources = newTemplate.Resources || {}
|
|
32
|
+
|
|
33
|
+
const oldResourceIds = new Set(Object.keys(oldResources))
|
|
34
|
+
const newResourceIds = new Set(Object.keys(newResources))
|
|
35
|
+
|
|
36
|
+
// Find added resources
|
|
37
|
+
for (const id of newResourceIds) {
|
|
38
|
+
if (!oldResourceIds.has(id)) {
|
|
39
|
+
added.push(id)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find deleted resources
|
|
44
|
+
for (const id of oldResourceIds) {
|
|
45
|
+
if (!newResourceIds.has(id)) {
|
|
46
|
+
deleted.push(id)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Find modified resources
|
|
51
|
+
for (const id of newResourceIds) {
|
|
52
|
+
if (oldResourceIds.has(id)) {
|
|
53
|
+
const oldResource = oldResources[id]
|
|
54
|
+
const newResource = newResources[id]
|
|
55
|
+
|
|
56
|
+
if (JSON.stringify(oldResource) !== JSON.stringify(newResource)) {
|
|
57
|
+
modified.push(id)
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
unchanged.push(id)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if parameters changed
|
|
66
|
+
const parametersChanged = JSON.stringify(oldTemplate.Parameters || {})
|
|
67
|
+
!== JSON.stringify(newTemplate.Parameters || {})
|
|
68
|
+
|
|
69
|
+
// Check if outputs changed
|
|
70
|
+
const outputsChanged = JSON.stringify(oldTemplate.Outputs || {})
|
|
71
|
+
!== JSON.stringify(newTemplate.Outputs || {})
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
added,
|
|
75
|
+
modified,
|
|
76
|
+
deleted,
|
|
77
|
+
unchanged,
|
|
78
|
+
parametersChanged,
|
|
79
|
+
outputsChanged,
|
|
80
|
+
hasChanges: added.length > 0 || modified.length > 0 || deleted.length > 0 || parametersChanged || outputsChanged,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get diff summary string
|
|
86
|
+
*/
|
|
87
|
+
export function getDiffSummary(diff: TemplateDiff): string {
|
|
88
|
+
const lines: string[] = []
|
|
89
|
+
|
|
90
|
+
if (!diff.hasChanges) {
|
|
91
|
+
return 'No changes detected'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (diff.added.length > 0) {
|
|
95
|
+
lines.push(`Added resources (${diff.added.length}):`)
|
|
96
|
+
for (const id of diff.added) {
|
|
97
|
+
lines.push(` + ${id}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (diff.modified.length > 0) {
|
|
102
|
+
lines.push(`Modified resources (${diff.modified.length}):`)
|
|
103
|
+
for (const id of diff.modified) {
|
|
104
|
+
lines.push(` ~ ${id}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (diff.deleted.length > 0) {
|
|
109
|
+
lines.push(`Deleted resources (${diff.deleted.length}):`)
|
|
110
|
+
for (const id of diff.deleted) {
|
|
111
|
+
lines.push(` - ${id}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (diff.parametersChanged) {
|
|
116
|
+
lines.push('Parameters changed')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (diff.outputsChanged) {
|
|
120
|
+
lines.push('Outputs changed')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if diff requires replacement (destructive changes)
|
|
128
|
+
*/
|
|
129
|
+
export function requiresReplacement(
|
|
130
|
+
diff: TemplateDiff,
|
|
131
|
+
oldTemplate: CloudFormationTemplate,
|
|
132
|
+
newTemplate: CloudFormationTemplate,
|
|
133
|
+
): boolean {
|
|
134
|
+
// If any resources are deleted, it requires replacement
|
|
135
|
+
if (diff.deleted.length > 0) {
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check modified resources for replacement-requiring changes
|
|
140
|
+
for (const id of diff.modified) {
|
|
141
|
+
const oldResource = oldTemplate.Resources[id]
|
|
142
|
+
const newResource = newTemplate.Resources[id]
|
|
143
|
+
|
|
144
|
+
// If resource type changed, it requires replacement
|
|
145
|
+
if (oldResource.Type !== newResource.Type) {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if properties that require replacement changed
|
|
150
|
+
// This is a simplified check - in reality, each resource type has specific properties
|
|
151
|
+
const replacementProperties = getReplacementProperties(newResource.Type)
|
|
152
|
+
|
|
153
|
+
for (const prop of replacementProperties) {
|
|
154
|
+
const oldValue = oldResource.Properties?.[prop]
|
|
155
|
+
const newValue = newResource.Properties?.[prop]
|
|
156
|
+
|
|
157
|
+
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get properties that require replacement when changed
|
|
168
|
+
* This is a simplified version - production code should use CloudFormation property docs
|
|
169
|
+
*/
|
|
170
|
+
function getReplacementProperties(resourceType: string): string[] {
|
|
171
|
+
const replacementProps: Record<string, string[]> = {
|
|
172
|
+
'AWS::S3::Bucket': ['BucketName'],
|
|
173
|
+
'AWS::EC2::Instance': ['ImageId', 'InstanceType', 'KeyName'],
|
|
174
|
+
'AWS::RDS::DBInstance': ['DBInstanceIdentifier', 'Engine'],
|
|
175
|
+
'AWS::DynamoDB::Table': ['TableName', 'KeySchema'],
|
|
176
|
+
'AWS::Lambda::Function': ['FunctionName'],
|
|
177
|
+
'AWS::ECS::Service': ['ServiceName'],
|
|
178
|
+
'AWS::ElasticLoadBalancingV2::LoadBalancer': ['Name', 'Type'],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return replacementProps[resourceType] || []
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Categorize changes by risk level
|
|
186
|
+
*/
|
|
187
|
+
export function categorizeChanges(diff: TemplateDiff): {
|
|
188
|
+
safe: string[]
|
|
189
|
+
caution: string[]
|
|
190
|
+
dangerous: string[]
|
|
191
|
+
} {
|
|
192
|
+
const safe: string[] = []
|
|
193
|
+
const caution: string[] = []
|
|
194
|
+
const dangerous: string[] = []
|
|
195
|
+
|
|
196
|
+
// Additions are generally safe
|
|
197
|
+
safe.push(...diff.added)
|
|
198
|
+
|
|
199
|
+
// Modifications need to be categorized
|
|
200
|
+
// This is simplified - real implementation would analyze specific property changes
|
|
201
|
+
for (const id of diff.modified) {
|
|
202
|
+
// For now, mark all modifications as caution
|
|
203
|
+
caution.push(id)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Deletions are dangerous
|
|
207
|
+
dangerous.push(...diff.deleted)
|
|
208
|
+
|
|
209
|
+
return { safe, caution, dangerous }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get deployment strategy based on diff
|
|
214
|
+
*/
|
|
215
|
+
export function getDeploymentStrategy(diff: TemplateDiff): {
|
|
216
|
+
strategy: 'create' | 'update' | 'replace' | 'skip'
|
|
217
|
+
reason: string
|
|
218
|
+
} {
|
|
219
|
+
if (!diff.hasChanges) {
|
|
220
|
+
return {
|
|
221
|
+
strategy: 'skip',
|
|
222
|
+
reason: 'No changes detected',
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (diff.deleted.length > 0) {
|
|
227
|
+
return {
|
|
228
|
+
strategy: 'replace',
|
|
229
|
+
reason: 'Resources will be deleted',
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (diff.added.length > 0 && diff.modified.length === 0) {
|
|
234
|
+
return {
|
|
235
|
+
strategy: 'update',
|
|
236
|
+
reason: 'Only new resources added',
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
strategy: 'update',
|
|
242
|
+
reason: 'Resources will be updated',
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculate diff statistics
|
|
248
|
+
*/
|
|
249
|
+
export function getDiffStats(diff: TemplateDiff): {
|
|
250
|
+
total: number
|
|
251
|
+
added: number
|
|
252
|
+
modified: number
|
|
253
|
+
deleted: number
|
|
254
|
+
unchanged: number
|
|
255
|
+
changePercentage: number
|
|
256
|
+
} {
|
|
257
|
+
const total = diff.added.length + diff.modified.length + diff.deleted.length + diff.unchanged.length
|
|
258
|
+
const changes = diff.added.length + diff.modified.length + diff.deleted.length
|
|
259
|
+
const changePercentage = total > 0 ? (changes / total) * 100 : 0
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
total,
|
|
263
|
+
added: diff.added.length,
|
|
264
|
+
modified: diff.modified.length,
|
|
265
|
+
deleted: diff.deleted.length,
|
|
266
|
+
unchanged: diff.unchanged.length,
|
|
267
|
+
changePercentage,
|
|
268
|
+
}
|
|
269
|
+
}
|