@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,110 @@
|
|
|
1
|
+
import type { CloudFormationResource, CloudFormationTemplate } from '@stacksjs/ts-cloud-aws-types'
|
|
2
|
+
|
|
3
|
+
export class TemplateBuilder {
|
|
4
|
+
private template: CloudFormationTemplate
|
|
5
|
+
|
|
6
|
+
constructor(description?: string) {
|
|
7
|
+
this.template = {
|
|
8
|
+
AWSTemplateFormatVersion: '2010-09-09',
|
|
9
|
+
Description: description,
|
|
10
|
+
Resources: {},
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Add a resource to the template
|
|
16
|
+
*/
|
|
17
|
+
addResource(logicalId: string, resource: CloudFormationResource): this {
|
|
18
|
+
this.template.Resources[logicalId] = resource
|
|
19
|
+
return this
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add multiple resources to the template
|
|
24
|
+
*/
|
|
25
|
+
addResources(resources: Record<string, CloudFormationResource>): this {
|
|
26
|
+
Object.assign(this.template.Resources, resources)
|
|
27
|
+
return this
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add a parameter to the template
|
|
32
|
+
*/
|
|
33
|
+
addParameter(name: string, parameter: NonNullable<CloudFormationTemplate['Parameters']>[string]): this {
|
|
34
|
+
if (!this.template.Parameters) {
|
|
35
|
+
this.template.Parameters = {}
|
|
36
|
+
}
|
|
37
|
+
this.template.Parameters[name] = parameter
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add an output to the template
|
|
43
|
+
*/
|
|
44
|
+
addOutput(name: string, output: NonNullable<CloudFormationTemplate['Outputs']>[string]): this {
|
|
45
|
+
if (!this.template.Outputs) {
|
|
46
|
+
this.template.Outputs = {}
|
|
47
|
+
}
|
|
48
|
+
this.template.Outputs[name] = output
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get resources from the template
|
|
54
|
+
*/
|
|
55
|
+
getResources(): Record<string, CloudFormationResource> {
|
|
56
|
+
return this.template.Resources
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build and return the CloudFormation template
|
|
61
|
+
*/
|
|
62
|
+
build(): CloudFormationTemplate {
|
|
63
|
+
return this.template
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert template to JSON string
|
|
68
|
+
*/
|
|
69
|
+
toJSON(pretty = true): string {
|
|
70
|
+
return JSON.stringify(this.template, null, pretty ? 2 : 0)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert template to YAML string (simple implementation)
|
|
75
|
+
*/
|
|
76
|
+
toYAML(): string {
|
|
77
|
+
// Simple YAML conversion - for production, use a proper YAML library
|
|
78
|
+
return this.convertToYAML(this.template)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private convertToYAML(obj: any, indent = 0): string {
|
|
82
|
+
const spaces = ' '.repeat(indent)
|
|
83
|
+
let yaml = ''
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
86
|
+
if (value === null || value === undefined)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
90
|
+
yaml += `${spaces}${key}:\n${this.convertToYAML(value, indent + 1)}`
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(value)) {
|
|
93
|
+
yaml += `${spaces}${key}:\n`
|
|
94
|
+
for (const item of value) {
|
|
95
|
+
if (typeof item === 'object') {
|
|
96
|
+
yaml += `${spaces} -\n${this.convertToYAML(item, indent + 2)}`
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
yaml += `${spaces} - ${item}\n`
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
yaml += `${spaces}${key}: ${value}\n`
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return yaml
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFormation Template Validator
|
|
3
|
+
* Validates CloudFormation templates for correctness and best practices
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CloudFormationTemplate, CloudFormationResource } from '@stacksjs/ts-cloud-aws-types'
|
|
7
|
+
|
|
8
|
+
export interface ValidationError {
|
|
9
|
+
path: string
|
|
10
|
+
message: string
|
|
11
|
+
severity: 'error' | 'warning' | 'info'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean
|
|
16
|
+
errors: ValidationError[]
|
|
17
|
+
warnings: ValidationError[]
|
|
18
|
+
info: ValidationError[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate a CloudFormation template
|
|
23
|
+
*/
|
|
24
|
+
export function validateTemplate(template: CloudFormationTemplate): ValidationResult {
|
|
25
|
+
const errors: ValidationError[] = []
|
|
26
|
+
const warnings: ValidationError[] = []
|
|
27
|
+
const info: ValidationError[] = []
|
|
28
|
+
|
|
29
|
+
// 1. Validate template structure
|
|
30
|
+
validateTemplateStructure(template, errors)
|
|
31
|
+
|
|
32
|
+
// 2. Validate resources
|
|
33
|
+
if (template.Resources) {
|
|
34
|
+
validateResources(template.Resources, errors, warnings)
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
errors.push({
|
|
38
|
+
path: 'Resources',
|
|
39
|
+
message: 'Template must contain at least one resource',
|
|
40
|
+
severity: 'error',
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Validate parameters
|
|
45
|
+
if (template.Parameters) {
|
|
46
|
+
validateParameters(template.Parameters, errors, warnings)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 4. Validate outputs
|
|
50
|
+
if (template.Outputs) {
|
|
51
|
+
validateOutputs(template.Outputs, errors)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 5. Validate references (Ref, GetAtt, etc.)
|
|
55
|
+
validateReferences(template, errors)
|
|
56
|
+
|
|
57
|
+
// 6. Check for circular dependencies
|
|
58
|
+
const circularDeps = detectCircularDependencies(template)
|
|
59
|
+
if (circularDeps.length > 0) {
|
|
60
|
+
for (const cycle of circularDeps) {
|
|
61
|
+
errors.push({
|
|
62
|
+
path: 'Resources',
|
|
63
|
+
message: `Circular dependency detected: ${cycle.join(' → ')}`,
|
|
64
|
+
severity: 'error',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 7. Check for best practices
|
|
70
|
+
checkBestPractices(template, warnings, info)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
valid: errors.length === 0,
|
|
74
|
+
errors,
|
|
75
|
+
warnings,
|
|
76
|
+
info,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate template structure
|
|
82
|
+
*/
|
|
83
|
+
function validateTemplateStructure(
|
|
84
|
+
template: CloudFormationTemplate,
|
|
85
|
+
errors: ValidationError[],
|
|
86
|
+
): void {
|
|
87
|
+
if (!template.AWSTemplateFormatVersion) {
|
|
88
|
+
errors.push({
|
|
89
|
+
path: 'AWSTemplateFormatVersion',
|
|
90
|
+
message: 'Template should specify AWSTemplateFormatVersion',
|
|
91
|
+
severity: 'error',
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
else if (template.AWSTemplateFormatVersion !== '2010-09-09') {
|
|
95
|
+
errors.push({
|
|
96
|
+
path: 'AWSTemplateFormatVersion',
|
|
97
|
+
message: 'AWSTemplateFormatVersion must be "2010-09-09"',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate resources
|
|
105
|
+
*/
|
|
106
|
+
function validateResources(
|
|
107
|
+
resources: Record<string, CloudFormationResource>,
|
|
108
|
+
errors: ValidationError[],
|
|
109
|
+
warnings: ValidationError[],
|
|
110
|
+
): void {
|
|
111
|
+
const logicalIds = Object.keys(resources)
|
|
112
|
+
|
|
113
|
+
if (logicalIds.length === 0) {
|
|
114
|
+
errors.push({
|
|
115
|
+
path: 'Resources',
|
|
116
|
+
message: 'Template must contain at least one resource',
|
|
117
|
+
severity: 'error',
|
|
118
|
+
})
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (logicalIds.length > 500) {
|
|
123
|
+
warnings.push({
|
|
124
|
+
path: 'Resources',
|
|
125
|
+
message: `Template contains ${logicalIds.length} resources (limit is 500)`,
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const logicalId of logicalIds) {
|
|
131
|
+
const resource = resources[logicalId]
|
|
132
|
+
|
|
133
|
+
// Validate logical ID format
|
|
134
|
+
if (!/^[a-zA-Z0-9]+$/.test(logicalId)) {
|
|
135
|
+
errors.push({
|
|
136
|
+
path: `Resources.${logicalId}`,
|
|
137
|
+
message: 'Logical ID must contain only alphanumeric characters',
|
|
138
|
+
severity: 'error',
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate Type
|
|
143
|
+
if (!resource.Type) {
|
|
144
|
+
errors.push({
|
|
145
|
+
path: `Resources.${logicalId}.Type`,
|
|
146
|
+
message: 'Resource Type is required',
|
|
147
|
+
severity: 'error',
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
else if (!resource.Type.startsWith('AWS::') && !resource.Type.startsWith('Custom::')) {
|
|
151
|
+
errors.push({
|
|
152
|
+
path: `Resources.${logicalId}.Type`,
|
|
153
|
+
message: 'Resource Type must start with "AWS::" or "Custom::"',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate DeletionPolicy
|
|
159
|
+
if (resource.DeletionPolicy
|
|
160
|
+
&& !['Delete', 'Retain', 'Snapshot'].includes(resource.DeletionPolicy)) {
|
|
161
|
+
errors.push({
|
|
162
|
+
path: `Resources.${logicalId}.DeletionPolicy`,
|
|
163
|
+
message: 'DeletionPolicy must be "Delete", "Retain", or "Snapshot"',
|
|
164
|
+
severity: 'error',
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Warn about resources without DeletionPolicy (data services)
|
|
169
|
+
if (!resource.DeletionPolicy && isDataResource(resource.Type)) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
path: `Resources.${logicalId}.DeletionPolicy`,
|
|
172
|
+
message: `${resource.Type} should specify DeletionPolicy to prevent accidental data loss`,
|
|
173
|
+
severity: 'warning',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate parameters
|
|
181
|
+
*/
|
|
182
|
+
function validateParameters(
|
|
183
|
+
parameters: Record<string, any>,
|
|
184
|
+
errors: ValidationError[],
|
|
185
|
+
warnings: ValidationError[],
|
|
186
|
+
): void {
|
|
187
|
+
const paramNames = Object.keys(parameters)
|
|
188
|
+
|
|
189
|
+
if (paramNames.length > 200) {
|
|
190
|
+
warnings.push({
|
|
191
|
+
path: 'Parameters',
|
|
192
|
+
message: `Template contains ${paramNames.length} parameters (limit is 200)`,
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const paramName of paramNames) {
|
|
198
|
+
const param = parameters[paramName]
|
|
199
|
+
|
|
200
|
+
if (!param.Type) {
|
|
201
|
+
errors.push({
|
|
202
|
+
path: `Parameters.${paramName}.Type`,
|
|
203
|
+
message: 'Parameter Type is required',
|
|
204
|
+
severity: 'error',
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const validTypes = ['String', 'Number', 'List<Number>', 'CommaDelimitedList',
|
|
209
|
+
'AWS::EC2::AvailabilityZone::Name', 'AWS::EC2::Image::Id',
|
|
210
|
+
'AWS::EC2::Instance::Id', 'AWS::EC2::KeyPair::KeyName',
|
|
211
|
+
'AWS::EC2::SecurityGroup::GroupName', 'AWS::EC2::SecurityGroup::Id',
|
|
212
|
+
'AWS::EC2::Subnet::Id', 'AWS::EC2::Volume::Id', 'AWS::EC2::VPC::Id',
|
|
213
|
+
'AWS::Route53::HostedZone::Id', 'List<AWS::EC2::AvailabilityZone::Name>',
|
|
214
|
+
'List<AWS::EC2::Image::Id>', 'List<AWS::EC2::Instance::Id>',
|
|
215
|
+
'List<AWS::EC2::SecurityGroup::GroupName>', 'List<AWS::EC2::SecurityGroup::Id>',
|
|
216
|
+
'List<AWS::EC2::Subnet::Id>', 'List<AWS::EC2::Volume::Id>',
|
|
217
|
+
'List<AWS::EC2::VPC::Id>', 'List<AWS::Route53::HostedZone::Id>',
|
|
218
|
+
'AWS::SSM::Parameter::Name', 'AWS::SSM::Parameter::Value<String>',
|
|
219
|
+
'AWS::SSM::Parameter::Value<List<String>>',
|
|
220
|
+
'AWS::SSM::Parameter::Value<CommaDelimitedList>',
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
if (param.Type && !validTypes.includes(param.Type)) {
|
|
224
|
+
errors.push({
|
|
225
|
+
path: `Parameters.${paramName}.Type`,
|
|
226
|
+
message: `Invalid parameter type: ${param.Type}`,
|
|
227
|
+
severity: 'error',
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validate outputs
|
|
235
|
+
*/
|
|
236
|
+
function validateOutputs(
|
|
237
|
+
outputs: Record<string, any>,
|
|
238
|
+
errors: ValidationError[],
|
|
239
|
+
): void {
|
|
240
|
+
const outputNames = Object.keys(outputs)
|
|
241
|
+
|
|
242
|
+
for (const outputName of outputNames) {
|
|
243
|
+
const output = outputs[outputName]
|
|
244
|
+
|
|
245
|
+
if (!output.Value) {
|
|
246
|
+
errors.push({
|
|
247
|
+
path: `Outputs.${outputName}.Value`,
|
|
248
|
+
message: 'Output Value is required',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate references between resources
|
|
257
|
+
*/
|
|
258
|
+
function validateReferences(
|
|
259
|
+
template: CloudFormationTemplate,
|
|
260
|
+
errors: ValidationError[],
|
|
261
|
+
): void {
|
|
262
|
+
const resources = template.Resources || {}
|
|
263
|
+
const parameters = template.Parameters || {}
|
|
264
|
+
const resourceIds = new Set(Object.keys(resources))
|
|
265
|
+
const parameterNames = new Set(Object.keys(parameters))
|
|
266
|
+
|
|
267
|
+
function checkReferences(obj: any, path: string): void {
|
|
268
|
+
if (typeof obj !== 'object' || obj === null)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
if (obj.Ref) {
|
|
272
|
+
const ref = obj.Ref
|
|
273
|
+
if (!resourceIds.has(ref) && !parameterNames.has(ref) && ref !== 'AWS::Region' && ref !== 'AWS::AccountId' && ref !== 'AWS::StackName' && ref !== 'AWS::StackId' && ref !== 'AWS::URLSuffix' && ref !== 'AWS::Partition' && ref !== 'AWS::NoValue') {
|
|
274
|
+
errors.push({
|
|
275
|
+
path,
|
|
276
|
+
message: `Reference to non-existent resource or parameter: ${ref}`,
|
|
277
|
+
severity: 'error',
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (obj['Fn::GetAtt']) {
|
|
283
|
+
const getAtt = obj['Fn::GetAtt']
|
|
284
|
+
if (Array.isArray(getAtt) && getAtt.length >= 1) {
|
|
285
|
+
const resourceId = getAtt[0]
|
|
286
|
+
if (!resourceIds.has(resourceId)) {
|
|
287
|
+
errors.push({
|
|
288
|
+
path,
|
|
289
|
+
message: `GetAtt references non-existent resource: ${resourceId}`,
|
|
290
|
+
severity: 'error',
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Recurse into object properties
|
|
297
|
+
for (const key in obj) {
|
|
298
|
+
checkReferences(obj[key], `${path}.${key}`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check all resources
|
|
303
|
+
for (const logicalId in resources) {
|
|
304
|
+
checkReferences(resources[logicalId], `Resources.${logicalId}`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check outputs
|
|
308
|
+
if (template.Outputs) {
|
|
309
|
+
for (const outputName in template.Outputs) {
|
|
310
|
+
checkReferences(template.Outputs[outputName], `Outputs.${outputName}`)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Detect circular dependencies in the template
|
|
317
|
+
*/
|
|
318
|
+
function detectCircularDependencies(template: CloudFormationTemplate): string[][] {
|
|
319
|
+
const resources = template.Resources || {}
|
|
320
|
+
const graph = new Map<string, Set<string>>()
|
|
321
|
+
|
|
322
|
+
// Build dependency graph
|
|
323
|
+
for (const logicalId in resources) {
|
|
324
|
+
const deps = new Set<string>()
|
|
325
|
+
|
|
326
|
+
// Check DependsOn
|
|
327
|
+
const resource = resources[logicalId]
|
|
328
|
+
if (resource.DependsOn) {
|
|
329
|
+
if (Array.isArray(resource.DependsOn)) {
|
|
330
|
+
resource.DependsOn.forEach(dep => deps.add(dep))
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
deps.add(resource.DependsOn)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check Ref and GetAtt
|
|
338
|
+
const refDeps = extractDependencies(resource)
|
|
339
|
+
refDeps.forEach(dep => deps.add(dep))
|
|
340
|
+
|
|
341
|
+
graph.set(logicalId, deps)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Detect cycles using DFS
|
|
345
|
+
const cycles: string[][] = []
|
|
346
|
+
const visited = new Set<string>()
|
|
347
|
+
const recursionStack = new Set<string>()
|
|
348
|
+
|
|
349
|
+
function dfs(node: string, path: string[]): void {
|
|
350
|
+
visited.add(node)
|
|
351
|
+
recursionStack.add(node)
|
|
352
|
+
path.push(node)
|
|
353
|
+
|
|
354
|
+
const deps = graph.get(node) || new Set()
|
|
355
|
+
for (const dep of deps) {
|
|
356
|
+
if (!visited.has(dep)) {
|
|
357
|
+
dfs(dep, [...path])
|
|
358
|
+
}
|
|
359
|
+
else if (recursionStack.has(dep)) {
|
|
360
|
+
// Cycle detected
|
|
361
|
+
const cycleStart = path.indexOf(dep)
|
|
362
|
+
const cycle = [...path.slice(cycleStart), dep]
|
|
363
|
+
cycles.push(cycle)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
recursionStack.delete(node)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const node of graph.keys()) {
|
|
371
|
+
if (!visited.has(node)) {
|
|
372
|
+
dfs(node, [])
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return cycles
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Extract dependencies from a resource
|
|
381
|
+
*/
|
|
382
|
+
function extractDependencies(obj: any): Set<string> {
|
|
383
|
+
const deps = new Set<string>()
|
|
384
|
+
|
|
385
|
+
function traverse(value: any): void {
|
|
386
|
+
if (typeof value !== 'object' || value === null)
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
if (value.Ref && typeof value.Ref === 'string') {
|
|
390
|
+
// Skip pseudo-parameters
|
|
391
|
+
if (!value.Ref.startsWith('AWS::')) {
|
|
392
|
+
deps.add(value.Ref)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (value['Fn::GetAtt'] && Array.isArray(value['Fn::GetAtt'])) {
|
|
397
|
+
deps.add(value['Fn::GetAtt'][0])
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Recurse
|
|
401
|
+
for (const key in value) {
|
|
402
|
+
traverse(value[key])
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
traverse(obj)
|
|
407
|
+
return deps
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check if a resource type stores data
|
|
412
|
+
*/
|
|
413
|
+
function isDataResource(type: string): boolean {
|
|
414
|
+
const dataResourceTypes = [
|
|
415
|
+
'AWS::S3::Bucket',
|
|
416
|
+
'AWS::DynamoDB::Table',
|
|
417
|
+
'AWS::RDS::DBInstance',
|
|
418
|
+
'AWS::RDS::DBCluster',
|
|
419
|
+
'AWS::ElastiCache::CacheCluster',
|
|
420
|
+
'AWS::ElastiCache::ReplicationGroup',
|
|
421
|
+
'AWS::EFS::FileSystem',
|
|
422
|
+
'AWS::OpenSearchService::Domain',
|
|
423
|
+
]
|
|
424
|
+
return dataResourceTypes.includes(type)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Check for best practices
|
|
429
|
+
*/
|
|
430
|
+
function checkBestPractices(
|
|
431
|
+
template: CloudFormationTemplate,
|
|
432
|
+
warnings: ValidationError[],
|
|
433
|
+
info: ValidationError[],
|
|
434
|
+
): void {
|
|
435
|
+
const resources = template.Resources || {}
|
|
436
|
+
|
|
437
|
+
// Check for description
|
|
438
|
+
if (!template.Description) {
|
|
439
|
+
info.push({
|
|
440
|
+
path: 'Description',
|
|
441
|
+
message: 'Consider adding a Description to the template',
|
|
442
|
+
severity: 'info',
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check for tags
|
|
447
|
+
for (const logicalId in resources) {
|
|
448
|
+
const resource = resources[logicalId]
|
|
449
|
+
const props = resource.Properties as any
|
|
450
|
+
|
|
451
|
+
if (props && !props.Tags && supportsTagging(resource.Type)) {
|
|
452
|
+
info.push({
|
|
453
|
+
path: `Resources.${logicalId}`,
|
|
454
|
+
message: `Consider adding Tags to ${resource.Type}`,
|
|
455
|
+
severity: 'info',
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check for encryption on data resources
|
|
460
|
+
if (isDataResource(resource.Type)) {
|
|
461
|
+
if (resource.Type === 'AWS::S3::Bucket' && !props?.BucketEncryption) {
|
|
462
|
+
warnings.push({
|
|
463
|
+
path: `Resources.${logicalId}`,
|
|
464
|
+
message: 'S3 bucket should enable encryption',
|
|
465
|
+
severity: 'warning',
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (resource.Type === 'AWS::RDS::DBInstance' && !props?.StorageEncrypted) {
|
|
470
|
+
warnings.push({
|
|
471
|
+
path: `Resources.${logicalId}`,
|
|
472
|
+
message: 'RDS instance should enable storage encryption',
|
|
473
|
+
severity: 'warning',
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if resource type supports tagging
|
|
482
|
+
*/
|
|
483
|
+
function supportsTagging(type: string): boolean {
|
|
484
|
+
// Most AWS resources support tagging
|
|
485
|
+
const noTagSupport = [
|
|
486
|
+
'AWS::CloudFormation::Stack',
|
|
487
|
+
'AWS::CloudFormation::WaitCondition',
|
|
488
|
+
'AWS::CloudFormation::WaitConditionHandle',
|
|
489
|
+
]
|
|
490
|
+
return !noTagSupport.includes(type)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Validate template size
|
|
495
|
+
*/
|
|
496
|
+
export function validateTemplateSize(templateJson: string): ValidationResult {
|
|
497
|
+
const errors: ValidationError[] = []
|
|
498
|
+
const warnings: ValidationError[] = []
|
|
499
|
+
const info: ValidationError[] = []
|
|
500
|
+
|
|
501
|
+
const sizeInBytes = Buffer.byteLength(templateJson, 'utf8')
|
|
502
|
+
const sizeInKB = sizeInBytes / 1024
|
|
503
|
+
|
|
504
|
+
// CloudFormation limits
|
|
505
|
+
const maxSize = 51200 // 51,200 bytes (50 KB) for direct upload
|
|
506
|
+
const s3MaxSize = 460800 // 460,800 bytes (450 KB) for S3 upload
|
|
507
|
+
|
|
508
|
+
if (sizeInBytes > s3MaxSize) {
|
|
509
|
+
errors.push({
|
|
510
|
+
path: 'Template',
|
|
511
|
+
message: `Template size (${sizeInKB.toFixed(2)} KB) exceeds maximum size of 450 KB`,
|
|
512
|
+
severity: 'error',
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
else if (sizeInBytes > maxSize) {
|
|
516
|
+
warnings.push({
|
|
517
|
+
path: 'Template',
|
|
518
|
+
message: `Template size (${sizeInKB.toFixed(2)} KB) exceeds 50 KB. Must use S3 for deployment.`,
|
|
519
|
+
severity: 'warning',
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
valid: errors.length === 0,
|
|
525
|
+
errors,
|
|
526
|
+
warnings,
|
|
527
|
+
info,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Validate resource limits
|
|
533
|
+
*/
|
|
534
|
+
export function validateResourceLimits(template: CloudFormationTemplate): ValidationResult {
|
|
535
|
+
const errors: ValidationError[] = []
|
|
536
|
+
const warnings: ValidationError[] = []
|
|
537
|
+
const info: ValidationError[] = []
|
|
538
|
+
|
|
539
|
+
const resources = template.Resources || {}
|
|
540
|
+
const parameters = template.Parameters || {}
|
|
541
|
+
const outputs = template.Outputs || {}
|
|
542
|
+
|
|
543
|
+
// CloudFormation limits
|
|
544
|
+
if (Object.keys(resources).length > 500) {
|
|
545
|
+
errors.push({
|
|
546
|
+
path: 'Resources',
|
|
547
|
+
message: `Template has ${Object.keys(resources).length} resources (limit is 500)`,
|
|
548
|
+
severity: 'error',
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (Object.keys(parameters).length > 200) {
|
|
553
|
+
errors.push({
|
|
554
|
+
path: 'Parameters',
|
|
555
|
+
message: `Template has ${Object.keys(parameters).length} parameters (limit is 200)`,
|
|
556
|
+
severity: 'error',
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (Object.keys(outputs).length > 200) {
|
|
561
|
+
errors.push({
|
|
562
|
+
path: 'Outputs',
|
|
563
|
+
message: `Template has ${Object.keys(outputs).length} outputs (limit is 200)`,
|
|
564
|
+
severity: 'error',
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
valid: errors.length === 0,
|
|
570
|
+
errors,
|
|
571
|
+
warnings,
|
|
572
|
+
info,
|
|
573
|
+
}
|
|
574
|
+
}
|