@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.47
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/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/HEALTH.md +468 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/__tests__/template-generation.test.js +687 -0
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
- package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
- package/infrastructure/domains/database/aurora-builder.js +809 -0
- package/infrastructure/domains/database/aurora-builder.test.js +950 -0
- package/infrastructure/domains/database/aurora-discovery.js +87 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +695 -0
- package/infrastructure/domains/database/migration-builder.test.js +294 -0
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
- package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
- package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
- package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
- package/infrastructure/domains/health/application/ports/index.js +26 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
- package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
- package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
- package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
- package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
- package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
- package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
- package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
- package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
- package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
- package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
- package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
- package/infrastructure/domains/health/domain/entities/issue.js +299 -0
- package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
- package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
- package/infrastructure/domains/health/domain/entities/resource.js +159 -0
- package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
- package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
- package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
- package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
- package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
- package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
- package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
- package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
- package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
- package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
- package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
- package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
- package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
- package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
- package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
- package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
- package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
- package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
- package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
- package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
- package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
- package/infrastructure/domains/integration/integration-builder.js +397 -0
- package/infrastructure/domains/integration/integration-builder.test.js +593 -0
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/integration/websocket-builder.js +69 -0
- package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
- package/infrastructure/domains/networking/vpc-builder.js +1829 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
- package/infrastructure/domains/networking/vpc-discovery.js +177 -0
- package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
- package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
- package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
- package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
- package/infrastructure/domains/security/kms-builder.js +366 -0
- package/infrastructure/domains/security/kms-builder.test.js +374 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +177 -0
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
- package/infrastructure/domains/shared/environment-builder.js +119 -0
- package/infrastructure/domains/shared/environment-builder.test.js +247 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
- package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
- package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
- package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
- package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
- package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
- package/infrastructure/domains/shared/resource-discovery.js +192 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
- package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +87 -0
- package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
- package/infrastructure/scripts/build-prisma-layer.js +553 -0
- package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
- package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
- package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
- package/layers/prisma/nodejs/package.json +8 -0
- package/management-ui/server/utils/cliIntegration.js +1 -1
- package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
- package/package.json +11 -11
- package/frigg-cli/.eslintrc.js +0 -141
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
- package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
- package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
- package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
- package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
- package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
- package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
- package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
- package/frigg-cli/__tests__/utils/test-setup.js +0 -287
- package/frigg-cli/build-command/index.js +0 -65
- package/frigg-cli/db-setup-command/index.js +0 -193
- package/frigg-cli/deploy-command/index.js +0 -175
- package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
- package/frigg-cli/generate-command/azure-generator.js +0 -43
- package/frigg-cli/generate-command/gcp-generator.js +0 -47
- package/frigg-cli/generate-command/index.js +0 -332
- package/frigg-cli/generate-command/terraform-generator.js +0 -555
- package/frigg-cli/generate-iam-command.js +0 -118
- package/frigg-cli/index.js +0 -75
- package/frigg-cli/index.test.js +0 -158
- package/frigg-cli/init-command/backend-first-handler.js +0 -756
- package/frigg-cli/init-command/index.js +0 -93
- package/frigg-cli/init-command/template-handler.js +0 -143
- package/frigg-cli/install-command/backend-js.js +0 -33
- package/frigg-cli/install-command/commit-changes.js +0 -16
- package/frigg-cli/install-command/environment-variables.js +0 -127
- package/frigg-cli/install-command/environment-variables.test.js +0 -136
- package/frigg-cli/install-command/index.js +0 -54
- package/frigg-cli/install-command/install-package.js +0 -13
- package/frigg-cli/install-command/integration-file.js +0 -30
- package/frigg-cli/install-command/logger.js +0 -12
- package/frigg-cli/install-command/template.js +0 -90
- package/frigg-cli/install-command/validate-package.js +0 -75
- package/frigg-cli/jest.config.js +0 -124
- package/frigg-cli/package.json +0 -54
- package/frigg-cli/start-command/index.js +0 -149
- package/frigg-cli/start-command/start-command.test.js +0 -297
- package/frigg-cli/test/init-command.test.js +0 -180
- package/frigg-cli/test/npm-registry.test.js +0 -319
- package/frigg-cli/ui-command/index.js +0 -154
- package/frigg-cli/utils/app-resolver.js +0 -319
- package/frigg-cli/utils/backend-path.js +0 -25
- package/frigg-cli/utils/database-validator.js +0 -161
- package/frigg-cli/utils/error-messages.js +0 -257
- package/frigg-cli/utils/npm-registry.js +0 -167
- package/frigg-cli/utils/prisma-runner.js +0 -280
- package/frigg-cli/utils/process-manager.js +0 -199
- package/frigg-cli/utils/repo-detection.js +0 -405
- package/infrastructure/aws-discovery.js +0 -1176
- package/infrastructure/aws-discovery.test.js +0 -1220
- package/infrastructure/serverless-template.js +0 -2094
- /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
- /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
- /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
- /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
- /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
- /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aurora PostgreSQL Builder
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Responsible for:
|
|
7
|
+
* - Aurora Serverless v2 cluster creation or discovery
|
|
8
|
+
* - Database subnet groups
|
|
9
|
+
* - Database security groups
|
|
10
|
+
* - Secrets Manager integration for credentials
|
|
11
|
+
* - Database connection environment variables
|
|
12
|
+
*
|
|
13
|
+
* Uses ownership-based architecture:
|
|
14
|
+
* - STACK: Resources in our CloudFormation template (definitions + Refs)
|
|
15
|
+
* - EXTERNAL: Resources outside our stack (reference by physical ID)
|
|
16
|
+
* - AUTO: System decides based on discovery
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
|
|
20
|
+
const AuroraResourceResolver = require('./aurora-resolver');
|
|
21
|
+
const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
|
|
22
|
+
const { ResourceOwnership } = require('../shared/types/resource-ownership');
|
|
23
|
+
|
|
24
|
+
class AuroraBuilder extends InfrastructureBuilder {
|
|
25
|
+
constructor() {
|
|
26
|
+
super();
|
|
27
|
+
this.name = 'AuroraBuilder';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
shouldExecute(appDefinition) {
|
|
31
|
+
// Skip Aurora in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
|
|
32
|
+
// Aurora is an AWS-specific service that should only be created in production
|
|
33
|
+
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return appDefinition.database?.postgres?.enable === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getDependencies() {
|
|
41
|
+
return ['VpcBuilder']; // Aurora requires VPC to be configured first
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
validate(appDefinition) {
|
|
45
|
+
const result = new ValidationResult();
|
|
46
|
+
|
|
47
|
+
if (!appDefinition.database?.postgres) {
|
|
48
|
+
result.addError('PostgreSQL database configuration is missing');
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const dbConfig = appDefinition.database.postgres;
|
|
53
|
+
|
|
54
|
+
// Validate management mode
|
|
55
|
+
const validModes = ['discover', 'managed', 'use-existing'];
|
|
56
|
+
const management = dbConfig.management || 'discover';
|
|
57
|
+
if (!validModes.includes(management)) {
|
|
58
|
+
result.addError(`Invalid database.postgres.management: "${management}"`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate use-existing requirements
|
|
62
|
+
if (management === 'use-existing' && !dbConfig.endpoint) {
|
|
63
|
+
result.addError('database.postgres.endpoint is required when management="use-existing"');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate capacity settings
|
|
67
|
+
if (dbConfig.minCapacity !== undefined && (dbConfig.minCapacity < 0.5 || dbConfig.minCapacity > 128)) {
|
|
68
|
+
result.addError('database.postgres.minCapacity must be between 0.5 and 128');
|
|
69
|
+
}
|
|
70
|
+
if (dbConfig.maxCapacity !== undefined && (dbConfig.maxCapacity < 0.5 || dbConfig.maxCapacity > 128)) {
|
|
71
|
+
result.addError('database.postgres.maxCapacity must be between 0.5 and 128');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Warn about public accessibility in production
|
|
75
|
+
if (dbConfig.publiclyAccessible === true) {
|
|
76
|
+
result.addWarning('database.postgres.publiclyAccessible=true is not recommended for production');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build Aurora infrastructure using ownership-based architecture
|
|
84
|
+
*/
|
|
85
|
+
async build(appDefinition, discoveredResources) {
|
|
86
|
+
console.log(`\n[${this.name}] Configuring Aurora PostgreSQL...`);
|
|
87
|
+
|
|
88
|
+
// Backwards compatibility: Translate old schema to new ownership schema
|
|
89
|
+
appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
|
|
90
|
+
|
|
91
|
+
// Initialize result
|
|
92
|
+
const result = {
|
|
93
|
+
resources: {},
|
|
94
|
+
iamStatements: [],
|
|
95
|
+
environment: {},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Special case: use-existing with endpoint (bypass resolver)
|
|
99
|
+
if (appDefinition.database?.postgres?._useExistingEndpoint) {
|
|
100
|
+
console.log(' Using provided database endpoint (use-existing mode)');
|
|
101
|
+
await this.useExistingAurora(appDefinition, discoveredResources, result);
|
|
102
|
+
console.log(`\n[${this.name}] ✅ Aurora PostgreSQL configuration completed`);
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get structured discovery result
|
|
107
|
+
const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
|
|
108
|
+
|
|
109
|
+
// Use AuroraResourceResolver to make ownership decisions
|
|
110
|
+
const resolver = new AuroraResourceResolver();
|
|
111
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
112
|
+
|
|
113
|
+
console.log('\n 📋 Resource Ownership Decisions:');
|
|
114
|
+
console.log(` Cluster: ${decisions.cluster.ownership} - ${decisions.cluster.reason}`);
|
|
115
|
+
console.log(` Instance: ${decisions.instance.ownership} - ${decisions.instance.reason}`);
|
|
116
|
+
console.log(` Subnet Group: ${decisions.subnetGroup.ownership} - ${decisions.subnetGroup.reason}`);
|
|
117
|
+
console.log(` Secret: ${decisions.secret.ownership} - ${decisions.secret.reason}`);
|
|
118
|
+
|
|
119
|
+
// Build resources based on ownership decisions
|
|
120
|
+
await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
|
|
121
|
+
|
|
122
|
+
console.log(`\n[${this.name}] ✅ Aurora PostgreSQL configuration completed`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Convert flat discovery to structured discovery
|
|
128
|
+
* Provides backwards compatibility for tests
|
|
129
|
+
*/
|
|
130
|
+
convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
|
|
131
|
+
const discovery = createEmptyDiscoveryResult();
|
|
132
|
+
|
|
133
|
+
if (!flatDiscovery) {
|
|
134
|
+
return discovery;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if resources are from CloudFormation stack
|
|
138
|
+
const isManagedIsolated = appDefinition.managementMode === 'managed' &&
|
|
139
|
+
(appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
|
|
140
|
+
const hasExistingStackResources = isManagedIsolated && flatDiscovery.auroraClusterId &&
|
|
141
|
+
typeof flatDiscovery.auroraClusterId === 'string';
|
|
142
|
+
|
|
143
|
+
if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
|
|
144
|
+
discovery.fromCloudFormation = true;
|
|
145
|
+
discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
|
|
146
|
+
|
|
147
|
+
// Add stack-managed resources
|
|
148
|
+
let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
|
|
149
|
+
|
|
150
|
+
// Infer logical IDs from physical IDs if needed
|
|
151
|
+
if (hasExistingStackResources && existingLogicalIds.length === 0) {
|
|
152
|
+
if (flatDiscovery.auroraClusterId) existingLogicalIds.push('FriggAuroraCluster');
|
|
153
|
+
if (flatDiscovery.auroraInstanceId) existingLogicalIds.push('FriggAuroraInstance');
|
|
154
|
+
if (flatDiscovery.dbSubnetGroupName) existingLogicalIds.push('FriggDBSubnetGroup');
|
|
155
|
+
if (flatDiscovery.dbSecretArn) existingLogicalIds.push('FriggDBSecret');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
existingLogicalIds.forEach(logicalId => {
|
|
159
|
+
let resourceType = '';
|
|
160
|
+
let physicalId = '';
|
|
161
|
+
|
|
162
|
+
if (logicalId === 'FriggAuroraCluster') {
|
|
163
|
+
resourceType = 'AWS::RDS::DBCluster';
|
|
164
|
+
physicalId = flatDiscovery.auroraClusterId;
|
|
165
|
+
} else if (logicalId === 'FriggAuroraInstance') {
|
|
166
|
+
resourceType = 'AWS::RDS::DBInstance';
|
|
167
|
+
physicalId = flatDiscovery.auroraInstanceId;
|
|
168
|
+
} else if (logicalId === 'FriggDBSubnetGroup') {
|
|
169
|
+
resourceType = 'AWS::RDS::DBSubnetGroup';
|
|
170
|
+
physicalId = flatDiscovery.dbSubnetGroupName;
|
|
171
|
+
} else if (logicalId === 'FriggDBSecret') {
|
|
172
|
+
resourceType = 'AWS::SecretsManager::Secret';
|
|
173
|
+
physicalId = flatDiscovery.dbSecretArn;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (physicalId && typeof physicalId === 'string') {
|
|
177
|
+
discovery.stackManaged.push({
|
|
178
|
+
logicalId,
|
|
179
|
+
physicalId,
|
|
180
|
+
resourceType
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
// Resources discovered from AWS API (external)
|
|
186
|
+
// Handle both cluster ID and endpoint
|
|
187
|
+
if (flatDiscovery.auroraClusterId && typeof flatDiscovery.auroraClusterId === 'string') {
|
|
188
|
+
discovery.external.push({
|
|
189
|
+
physicalId: flatDiscovery.auroraClusterId,
|
|
190
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
191
|
+
source: 'aws-discovery'
|
|
192
|
+
});
|
|
193
|
+
} else if (flatDiscovery.auroraClusterEndpoint && typeof flatDiscovery.auroraClusterEndpoint === 'string') {
|
|
194
|
+
// Endpoint provided (discover mode) - treat as external
|
|
195
|
+
discovery.external.push({
|
|
196
|
+
physicalId: flatDiscovery.auroraClusterEndpoint,
|
|
197
|
+
resourceType: 'AWS::RDS::DBCluster',
|
|
198
|
+
source: 'aws-discovery',
|
|
199
|
+
properties: { Endpoint: flatDiscovery.auroraClusterEndpoint }
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (flatDiscovery.auroraInstanceId && typeof flatDiscovery.auroraInstanceId === 'string') {
|
|
204
|
+
discovery.external.push({
|
|
205
|
+
physicalId: flatDiscovery.auroraInstanceId,
|
|
206
|
+
resourceType: 'AWS::RDS::DBInstance',
|
|
207
|
+
source: 'aws-discovery'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return discovery;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Translate legacy configuration to ownership-based configuration
|
|
217
|
+
* Provides backwards compatibility
|
|
218
|
+
*/
|
|
219
|
+
translateLegacyConfig(appDefinition, discoveredResources) {
|
|
220
|
+
// If already using ownership schema, return as-is
|
|
221
|
+
if (appDefinition.database?.postgres?.ownership) {
|
|
222
|
+
return appDefinition;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const translated = JSON.parse(JSON.stringify(appDefinition));
|
|
226
|
+
|
|
227
|
+
// Initialize ownership sections
|
|
228
|
+
if (!translated.database) translated.database = {};
|
|
229
|
+
if (!translated.database.postgres) translated.database.postgres = {};
|
|
230
|
+
if (!translated.database.postgres.ownership) {
|
|
231
|
+
translated.database.postgres.ownership = {};
|
|
232
|
+
}
|
|
233
|
+
if (!translated.database.postgres.external) {
|
|
234
|
+
translated.database.postgres.external = {};
|
|
235
|
+
}
|
|
236
|
+
if (!translated.database.postgres.config) {
|
|
237
|
+
translated.database.postgres.config = {};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle top-level managementMode
|
|
241
|
+
const globalMode = appDefinition.managementMode || 'discover';
|
|
242
|
+
const vpcIsolation = appDefinition.vpcIsolation || 'shared';
|
|
243
|
+
|
|
244
|
+
if (globalMode === 'managed') {
|
|
245
|
+
if (appDefinition.database?.postgres?.management) {
|
|
246
|
+
console.log(` ⚠️ managementMode='managed' ignoring: database.postgres.management`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (vpcIsolation === 'isolated') {
|
|
250
|
+
const hasStackAurora = discoveredResources?.auroraClusterId &&
|
|
251
|
+
typeof discoveredResources.auroraClusterId === 'string';
|
|
252
|
+
|
|
253
|
+
if (hasStackAurora) {
|
|
254
|
+
translated.database.postgres.ownership.cluster = 'auto';
|
|
255
|
+
translated.database.postgres.ownership.instance = 'auto';
|
|
256
|
+
translated.database.postgres.ownership.subnetGroup = 'auto';
|
|
257
|
+
translated.database.postgres.ownership.secret = 'auto';
|
|
258
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has Aurora, reusing`);
|
|
259
|
+
} else {
|
|
260
|
+
translated.database.postgres.ownership.cluster = 'stack';
|
|
261
|
+
translated.database.postgres.ownership.instance = 'stack';
|
|
262
|
+
translated.database.postgres.ownership.subnetGroup = 'stack';
|
|
263
|
+
translated.database.postgres.ownership.secret = 'stack';
|
|
264
|
+
console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack Aurora, creating new`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
translated.database.postgres.ownership.cluster = 'auto';
|
|
268
|
+
translated.database.postgres.ownership.instance = 'auto';
|
|
269
|
+
translated.database.postgres.ownership.subnetGroup = 'auto';
|
|
270
|
+
translated.database.postgres.ownership.secret = 'auto';
|
|
271
|
+
console.log(` managementMode='managed' + vpcIsolation='shared' → discovering Aurora`);
|
|
272
|
+
}
|
|
273
|
+
} else if (globalMode === 'existing') {
|
|
274
|
+
translated.database.postgres.ownership.cluster = 'external';
|
|
275
|
+
translated.database.postgres.ownership.instance = 'external';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle legacy database.postgres.management
|
|
279
|
+
// BUT: if managementMode (top-level) is set, it takes precedence
|
|
280
|
+
const dbManagement = appDefinition.database?.postgres?.management;
|
|
281
|
+
if (dbManagement && globalMode !== 'managed' && globalMode !== 'existing') {
|
|
282
|
+
if (dbManagement === 'managed') {
|
|
283
|
+
translated.database.postgres.ownership.cluster = 'stack';
|
|
284
|
+
translated.database.postgres.ownership.instance = 'stack';
|
|
285
|
+
translated.database.postgres.ownership.subnetGroup = 'stack';
|
|
286
|
+
translated.database.postgres.ownership.secret = 'stack';
|
|
287
|
+
} else if (dbManagement === 'use-existing') {
|
|
288
|
+
// For use-existing with endpoint, we bypass resolver entirely
|
|
289
|
+
// Mark this with a special flag
|
|
290
|
+
translated.database.postgres._useExistingEndpoint = true;
|
|
291
|
+
if (appDefinition.database.postgres.endpoint) {
|
|
292
|
+
translated.database.postgres.external.endpoint = appDefinition.database.postgres.endpoint;
|
|
293
|
+
}
|
|
294
|
+
} else if (dbManagement === 'discover') {
|
|
295
|
+
translated.database.postgres.ownership.cluster = 'auto';
|
|
296
|
+
translated.database.postgres.ownership.instance = 'auto';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Preserve other database config
|
|
301
|
+
if (appDefinition.database?.postgres?.minCapacity) {
|
|
302
|
+
translated.database.postgres.config.minCapacity = appDefinition.database.postgres.minCapacity;
|
|
303
|
+
}
|
|
304
|
+
if (appDefinition.database?.postgres?.maxCapacity) {
|
|
305
|
+
translated.database.postgres.config.maxCapacity = appDefinition.database.postgres.maxCapacity;
|
|
306
|
+
}
|
|
307
|
+
if (appDefinition.database?.postgres?.publiclyAccessible !== undefined) {
|
|
308
|
+
translated.database.postgres.config.publiclyAccessible = appDefinition.database.postgres.publiclyAccessible;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return translated;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build all Aurora resources based on ownership decisions
|
|
316
|
+
*/
|
|
317
|
+
async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
|
|
318
|
+
// Determine build strategy from ownership decisions
|
|
319
|
+
|
|
320
|
+
if (decisions.cluster.ownership === ResourceOwnership.EXTERNAL) {
|
|
321
|
+
// External cluster discovered - reference it without creating infrastructure
|
|
322
|
+
console.log(' → Discovering and referencing external Aurora cluster');
|
|
323
|
+
await this.discoverAurora(appDefinition, discoveredResources, result);
|
|
324
|
+
} else if (decisions.cluster.ownership === ResourceOwnership.STACK && decisions.cluster.physicalId) {
|
|
325
|
+
// Cluster exists in stack - add definitions (CloudFormation idempotency)
|
|
326
|
+
console.log(' → Adding Aurora definitions to template (existing in stack)');
|
|
327
|
+
await this.createNewAurora(appDefinition, discoveredResources, result);
|
|
328
|
+
} else if (decisions.cluster.ownership === ResourceOwnership.STACK && !decisions.cluster.physicalId) {
|
|
329
|
+
// Create new cluster (stack, no existing)
|
|
330
|
+
console.log(' → Creating new Aurora cluster in stack');
|
|
331
|
+
await this.createNewAurora(appDefinition, discoveredResources, result);
|
|
332
|
+
} else {
|
|
333
|
+
// Fallback: discover mode
|
|
334
|
+
console.log(' → Discovering Aurora resources');
|
|
335
|
+
await this.discoverAurora(appDefinition, discoveredResources, result);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Create new Aurora cluster
|
|
341
|
+
*/
|
|
342
|
+
async createNewAurora(appDefinition, discoveredResources, result) {
|
|
343
|
+
console.log(' Creating new Aurora Serverless v2 cluster...');
|
|
344
|
+
|
|
345
|
+
const dbConfig = appDefinition.database.postgres;
|
|
346
|
+
const publiclyAccessible = dbConfig.publiclyAccessible === true;
|
|
347
|
+
|
|
348
|
+
// Get subnet IDs for DB Subnet Group
|
|
349
|
+
const subnetIds = publiclyAccessible
|
|
350
|
+
? [discoveredResources.publicSubnetId1, discoveredResources.publicSubnetId2]
|
|
351
|
+
: [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2];
|
|
352
|
+
|
|
353
|
+
if (!subnetIds[0] || !subnetIds[1]) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Aurora requires 2 ${publiclyAccessible ? 'public' : 'private'} subnets in different AZs. ` +
|
|
356
|
+
'Ensure VPC is configured correctly.'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Database Subnet Group
|
|
361
|
+
result.resources.FriggDBSubnetGroup = {
|
|
362
|
+
Type: 'AWS::RDS::DBSubnetGroup',
|
|
363
|
+
Properties: {
|
|
364
|
+
DBSubnetGroupName: '${self:service}-${self:provider.stage}-db-subnet-group',
|
|
365
|
+
DBSubnetGroupDescription: 'Subnet group for Frigg Aurora cluster',
|
|
366
|
+
SubnetIds: subnetIds,
|
|
367
|
+
Tags: [
|
|
368
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet' },
|
|
369
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Database Credentials Secret
|
|
375
|
+
result.resources.FriggDBSecret = {
|
|
376
|
+
Type: 'AWS::SecretsManager::Secret',
|
|
377
|
+
Properties: {
|
|
378
|
+
Name: '${self:service}-${self:provider.stage}-db-credentials',
|
|
379
|
+
Description: 'Aurora database credentials',
|
|
380
|
+
GenerateSecretString: {
|
|
381
|
+
SecretStringTemplate: JSON.stringify({ username: dbConfig.username || 'postgres' }),
|
|
382
|
+
GenerateStringKey: 'password',
|
|
383
|
+
PasswordLength: 32,
|
|
384
|
+
// Exclude URL-special characters for Prisma connection string compatibility
|
|
385
|
+
// Prisma docs: https://www.prisma.io/docs/reference/database-reference/connection-urls#special-characters
|
|
386
|
+
// Exclude: " @ : / ? # [ ] % \ (all have special meaning in URLs or need escaping)
|
|
387
|
+
ExcludeCharacters: '"@:/?#[]%\\\\',
|
|
388
|
+
},
|
|
389
|
+
Tags: [
|
|
390
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-secret' },
|
|
391
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Aurora Cluster
|
|
397
|
+
result.resources.FriggAuroraCluster = {
|
|
398
|
+
Type: 'AWS::RDS::DBCluster',
|
|
399
|
+
DeletionPolicy: 'Snapshot',
|
|
400
|
+
Properties: {
|
|
401
|
+
Engine: 'aurora-postgresql',
|
|
402
|
+
EngineMode: 'provisioned',
|
|
403
|
+
EngineVersion: dbConfig.engineVersion || '15.13', // Configurable, defaults to 15.13 (latest as of Oct 2025)
|
|
404
|
+
Port: 5432, // Explicitly set PostgreSQL port (AWS may not auto-detect)
|
|
405
|
+
DatabaseName: dbConfig.database || 'frigg',
|
|
406
|
+
MasterUsername: {
|
|
407
|
+
'Fn::Sub': '{{resolve:secretsmanager:${FriggDBSecret}:SecretString:username}}',
|
|
408
|
+
},
|
|
409
|
+
MasterUserPassword: {
|
|
410
|
+
'Fn::Sub': '{{resolve:secretsmanager:${FriggDBSecret}:SecretString:password}}',
|
|
411
|
+
},
|
|
412
|
+
DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
|
|
413
|
+
VpcSecurityGroupIds: discoveredResources.vpcSecurityGroupIds || [
|
|
414
|
+
{ Ref: 'FriggLambdaSecurityGroup' },
|
|
415
|
+
],
|
|
416
|
+
// Note: PubliclyAccessible is NOT supported on Aurora clusters
|
|
417
|
+
// It should only be set on DB instances (see FriggAuroraInstance below)
|
|
418
|
+
ServerlessV2ScalingConfiguration: {
|
|
419
|
+
MinCapacity: dbConfig.minCapacity || 0.5,
|
|
420
|
+
MaxCapacity: dbConfig.maxCapacity || 1,
|
|
421
|
+
},
|
|
422
|
+
EnableHttpEndpoint: false,
|
|
423
|
+
BackupRetentionPeriod: 7,
|
|
424
|
+
PreferredBackupWindow: '03:00-04:00',
|
|
425
|
+
PreferredMaintenanceWindow: 'sun:04:00-sun:05:00',
|
|
426
|
+
Tags: [
|
|
427
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora' },
|
|
428
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Aurora Instance
|
|
434
|
+
result.resources.FriggAuroraInstance = {
|
|
435
|
+
Type: 'AWS::RDS::DBInstance',
|
|
436
|
+
Properties: {
|
|
437
|
+
Engine: 'aurora-postgresql',
|
|
438
|
+
DBInstanceClass: 'db.serverless',
|
|
439
|
+
DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
|
|
440
|
+
PubliclyAccessible: publiclyAccessible,
|
|
441
|
+
Tags: [
|
|
442
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
|
|
443
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// Environment variables
|
|
449
|
+
result.environment.DATABASE_URL = this.buildDatabaseUrl(
|
|
450
|
+
{ 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint.Address'] },
|
|
451
|
+
{ 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint.Port'] },
|
|
452
|
+
dbConfig.database || 'frigg',
|
|
453
|
+
{ Ref: 'FriggDBSecret' }
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// IAM permissions for Secrets Manager
|
|
457
|
+
result.iamStatements.push({
|
|
458
|
+
Effect: 'Allow',
|
|
459
|
+
Action: ['secretsmanager:GetSecretValue'],
|
|
460
|
+
Resource: { Ref: 'FriggDBSecret' },
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Add self-referencing security group ingress rule to allow Lambda to connect to Aurora
|
|
464
|
+
// Since both Lambda and Aurora share the same security group, we need to allow the SG to accept traffic from itself
|
|
465
|
+
result.resources.FriggAuroraIngressRule = {
|
|
466
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
467
|
+
Properties: {
|
|
468
|
+
GroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
469
|
+
IpProtocol: 'tcp',
|
|
470
|
+
FromPort: 5432,
|
|
471
|
+
ToPort: 5432,
|
|
472
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
473
|
+
Description: 'Allow Lambda functions to connect to Aurora PostgreSQL (self-referencing rule)',
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
console.log(' ✅ Aurora Serverless v2 cluster resources created');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Use existing Aurora cluster
|
|
482
|
+
*/
|
|
483
|
+
async useExistingAurora(appDefinition, discoveredResources, result) {
|
|
484
|
+
console.log(' Using existing Aurora cluster...');
|
|
485
|
+
|
|
486
|
+
const dbConfig = appDefinition.database.postgres;
|
|
487
|
+
|
|
488
|
+
if (!dbConfig.endpoint) {
|
|
489
|
+
throw new Error('database.postgres.endpoint is required when management="use-existing"');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Set environment variables for existing cluster
|
|
493
|
+
result.environment.DATABASE_HOST = dbConfig.endpoint;
|
|
494
|
+
result.environment.DATABASE_PORT = String(dbConfig.port || 5432);
|
|
495
|
+
result.environment.DATABASE_NAME = dbConfig.database || 'frigg';
|
|
496
|
+
result.environment.DATABASE_USER = dbConfig.username || 'postgres';
|
|
497
|
+
|
|
498
|
+
console.log(` ✅ Using existing cluster: ${dbConfig.endpoint}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Discover existing Aurora cluster
|
|
503
|
+
*/
|
|
504
|
+
async discoverAurora(appDefinition, discoveredResources, result) {
|
|
505
|
+
console.log(' Discovering Aurora cluster...');
|
|
506
|
+
|
|
507
|
+
if (!discoveredResources.auroraClusterEndpoint) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
'No Aurora cluster found in discovery mode. Set management to "managed" or provide endpoint with "use-existing".'
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
console.log(` ✅ Using discovered Aurora cluster: ${discoveredResources.auroraClusterEndpoint}`);
|
|
514
|
+
|
|
515
|
+
const dbConfig = appDefinition.database.postgres;
|
|
516
|
+
|
|
517
|
+
// Use discovered cluster details
|
|
518
|
+
result.environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
519
|
+
result.environment.DATABASE_PORT = String(discoveredResources.auroraPort || 5432);
|
|
520
|
+
|
|
521
|
+
// Check if we should auto-create credentials
|
|
522
|
+
if (dbConfig.autoCreateCredentials && !discoveredResources.databaseSecretArn) {
|
|
523
|
+
console.log(' Creating Secrets Manager secret and rotating Aurora password...');
|
|
524
|
+
|
|
525
|
+
// Create Secrets Manager secret with auto-generated password
|
|
526
|
+
result.resources.FriggDBSecret = {
|
|
527
|
+
Type: 'AWS::SecretsManager::Secret',
|
|
528
|
+
Properties: {
|
|
529
|
+
Name: '${self:service}-${self:provider.stage}-db-credentials',
|
|
530
|
+
Description: 'Aurora database credentials (auto-created for discovered cluster)',
|
|
531
|
+
GenerateSecretString: {
|
|
532
|
+
SecretStringTemplate: JSON.stringify({ username: dbConfig.username || 'postgres' }),
|
|
533
|
+
GenerateStringKey: 'password',
|
|
534
|
+
PasswordLength: 32,
|
|
535
|
+
// Exclude URL-special characters for Prisma connection string compatibility
|
|
536
|
+
// Prisma docs: https://www.prisma.io/docs/reference/database-reference/connection-urls#special-characters
|
|
537
|
+
// Exclude: " @ : / ? # [ ] % \ (all have special meaning in URLs or need escaping)
|
|
538
|
+
ExcludeCharacters: '"@:/?#[]%\\\\',
|
|
539
|
+
},
|
|
540
|
+
Tags: [
|
|
541
|
+
{ Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-secret' },
|
|
542
|
+
{ Key: 'ManagedBy', Value: 'Frigg' },
|
|
543
|
+
{ Key: 'Purpose', Value: 'DiscoveredClusterCredentials' },
|
|
544
|
+
],
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// Get the cluster identifier from the endpoint
|
|
549
|
+
// Format: cluster-name.cluster-xyz.region.rds.amazonaws.com
|
|
550
|
+
const clusterIdentifier = discoveredResources.auroraClusterEndpoint.split('.')[0];
|
|
551
|
+
|
|
552
|
+
// Create custom resource to rotate the Aurora master password
|
|
553
|
+
// This uses a Lambda-backed CloudFormation custom resource
|
|
554
|
+
result.resources.FriggAuroraPasswordRotator = {
|
|
555
|
+
Type: 'Custom::AuroraPasswordRotator',
|
|
556
|
+
Properties: {
|
|
557
|
+
ServiceToken: { 'Fn::GetAtt': ['PasswordRotatorLambda', 'Arn'] },
|
|
558
|
+
ClusterIdentifier: clusterIdentifier,
|
|
559
|
+
SecretArn: { Ref: 'FriggDBSecret' },
|
|
560
|
+
Region: '${self:provider.region}',
|
|
561
|
+
},
|
|
562
|
+
DependsOn: ['FriggDBSecret', 'PasswordRotatorLambda'],
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Lambda function to rotate the password
|
|
566
|
+
result.resources.PasswordRotatorLambda = {
|
|
567
|
+
Type: 'AWS::Lambda::Function',
|
|
568
|
+
Properties: {
|
|
569
|
+
FunctionName: '${self:service}-${self:provider.stage}-password-rotator',
|
|
570
|
+
Runtime: 'nodejs22.x',
|
|
571
|
+
Handler: 'index.handler',
|
|
572
|
+
Role: { 'Fn::GetAtt': ['PasswordRotatorRole', 'Arn'] },
|
|
573
|
+
Timeout: 60,
|
|
574
|
+
Code: {
|
|
575
|
+
ZipFile: `
|
|
576
|
+
const { RDSClient, ModifyDBClusterCommand } = require('@aws-sdk/client-rds');
|
|
577
|
+
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
|
|
578
|
+
|
|
579
|
+
exports.handler = async (event, context) => {
|
|
580
|
+
console.log('Event:', JSON.stringify(event, null, 2));
|
|
581
|
+
|
|
582
|
+
const { RequestType, ResourceProperties } = event;
|
|
583
|
+
const { ClusterIdentifier, SecretArn, Region } = ResourceProperties;
|
|
584
|
+
|
|
585
|
+
const sendResponse = async (status, data = {}) => {
|
|
586
|
+
const responseBody = JSON.stringify({
|
|
587
|
+
Status: status,
|
|
588
|
+
Reason: data.Reason || 'See CloudWatch logs',
|
|
589
|
+
PhysicalResourceId: context.logStreamName,
|
|
590
|
+
StackId: event.StackId,
|
|
591
|
+
RequestId: event.RequestId,
|
|
592
|
+
LogicalResourceId: event.LogicalResourceId,
|
|
593
|
+
Data: data,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
await fetch(event.ResponseURL, {
|
|
597
|
+
method: 'PUT',
|
|
598
|
+
body: responseBody,
|
|
599
|
+
headers: { 'Content-Type': '' },
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
if (RequestType === 'Delete') {
|
|
605
|
+
await sendResponse('SUCCESS', { Message: 'Delete not required' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Get the new password from Secrets Manager
|
|
610
|
+
const smClient = new SecretsManagerClient({ region: Region });
|
|
611
|
+
const secretResponse = await smClient.send(
|
|
612
|
+
new GetSecretValueCommand({ SecretId: SecretArn })
|
|
613
|
+
);
|
|
614
|
+
const secret = JSON.parse(secretResponse.SecretString);
|
|
615
|
+
const newPassword = secret.password;
|
|
616
|
+
|
|
617
|
+
// Rotate the Aurora cluster master password
|
|
618
|
+
const rdsClient = new RDSClient({ region: Region });
|
|
619
|
+
await rdsClient.send(
|
|
620
|
+
new ModifyDBClusterCommand({
|
|
621
|
+
DBClusterIdentifier: ClusterIdentifier,
|
|
622
|
+
MasterUserPassword: newPassword,
|
|
623
|
+
ApplyImmediately: true,
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
console.log('Successfully rotated password for cluster: ' + ClusterIdentifier);
|
|
628
|
+
await sendResponse('SUCCESS', {
|
|
629
|
+
Message: 'Password rotated successfully',
|
|
630
|
+
ClusterIdentifier,
|
|
631
|
+
});
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error('Error rotating password:', error);
|
|
634
|
+
await sendResponse('FAILED', { Reason: error.message });
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
`,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// IAM role for the password rotator Lambda
|
|
643
|
+
result.resources.PasswordRotatorRole = {
|
|
644
|
+
Type: 'AWS::IAM::Role',
|
|
645
|
+
Properties: {
|
|
646
|
+
AssumeRolePolicyDocument: {
|
|
647
|
+
Version: '2012-10-17',
|
|
648
|
+
Statement: [
|
|
649
|
+
{
|
|
650
|
+
Effect: 'Allow',
|
|
651
|
+
Principal: { Service: 'lambda.amazonaws.com' },
|
|
652
|
+
Action: 'sts:AssumeRole',
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
},
|
|
656
|
+
ManagedPolicyArns: [
|
|
657
|
+
'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',
|
|
658
|
+
],
|
|
659
|
+
Policies: [
|
|
660
|
+
{
|
|
661
|
+
PolicyName: 'PasswordRotatorPolicy',
|
|
662
|
+
PolicyDocument: {
|
|
663
|
+
Version: '2012-10-17',
|
|
664
|
+
Statement: [
|
|
665
|
+
{
|
|
666
|
+
Effect: 'Allow',
|
|
667
|
+
Action: [
|
|
668
|
+
'rds:ModifyDBCluster',
|
|
669
|
+
'rds:DescribeDBClusters',
|
|
670
|
+
],
|
|
671
|
+
Resource: '*',
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
Effect: 'Allow',
|
|
675
|
+
Action: ['secretsmanager:GetSecretValue'],
|
|
676
|
+
Resource: { Ref: 'FriggDBSecret' },
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// Use the secret for DATABASE_URL
|
|
686
|
+
result.environment.DATABASE_SECRET_ARN = { Ref: 'FriggDBSecret' };
|
|
687
|
+
result.environment.DATABASE_URL = this.buildDatabaseUrl(
|
|
688
|
+
discoveredResources.auroraClusterEndpoint,
|
|
689
|
+
discoveredResources.auroraPort || 5432,
|
|
690
|
+
dbConfig.database || 'frigg',
|
|
691
|
+
{ Ref: 'FriggDBSecret' }
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
// Grant Lambda functions permission to read the secret
|
|
695
|
+
result.iamStatements.push({
|
|
696
|
+
Effect: 'Allow',
|
|
697
|
+
Action: ['secretsmanager:GetSecretValue'],
|
|
698
|
+
Resource: { Ref: 'FriggDBSecret' },
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
console.log(' ✅ Credentials auto-creation configured');
|
|
702
|
+
} else if (discoveredResources.databaseSecretArn) {
|
|
703
|
+
// Use existing discovered secret
|
|
704
|
+
result.environment.DATABASE_SECRET_ARN = discoveredResources.databaseSecretArn;
|
|
705
|
+
result.environment.DATABASE_URL = this.buildDatabaseUrl(
|
|
706
|
+
discoveredResources.auroraClusterEndpoint,
|
|
707
|
+
discoveredResources.auroraPort || 5432,
|
|
708
|
+
dbConfig.database || 'frigg',
|
|
709
|
+
discoveredResources.databaseSecretArn
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
result.iamStatements.push({
|
|
713
|
+
Effect: 'Allow',
|
|
714
|
+
Action: ['secretsmanager:GetSecretValue'],
|
|
715
|
+
Resource: discoveredResources.databaseSecretArn,
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
console.log(' ✅ Using discovered Secrets Manager credentials');
|
|
719
|
+
} else {
|
|
720
|
+
// No secret and no auto-create - set individual DB connection components
|
|
721
|
+
// The application will construct DATABASE_URL at runtime from these components + DATABASE_USER + DATABASE_PASSWORD
|
|
722
|
+
const dbName = dbConfig.database || 'frigg';
|
|
723
|
+
|
|
724
|
+
result.environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
725
|
+
result.environment.DATABASE_PORT = String(discoveredResources.auroraPort || 5432);
|
|
726
|
+
result.environment.DATABASE_NAME = dbName;
|
|
727
|
+
|
|
728
|
+
// Note: DATABASE_URL is NOT set here to avoid Serverless variable resolution errors
|
|
729
|
+
// The application (Frigg Core) should construct it at runtime from:
|
|
730
|
+
// DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD
|
|
731
|
+
|
|
732
|
+
console.log(' ℹ️ No Secrets Manager secret found - set DATABASE_USER and DATABASE_PASSWORD in Lambda environment');
|
|
733
|
+
console.log(' ℹ️ Application will construct DATABASE_URL at runtime from DATABASE_HOST, DATABASE_PORT, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD');
|
|
734
|
+
console.log(' ℹ️ Or enable autoCreateCredentials=true to automatically create and rotate credentials');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Add security group ingress rule to allow Lambda to connect to Aurora
|
|
738
|
+
if (discoveredResources.auroraSecurityGroupId) {
|
|
739
|
+
result.resources.FriggAuroraIngressRule = {
|
|
740
|
+
Type: 'AWS::EC2::SecurityGroupIngress',
|
|
741
|
+
Properties: {
|
|
742
|
+
GroupId: discoveredResources.auroraSecurityGroupId,
|
|
743
|
+
IpProtocol: 'tcp',
|
|
744
|
+
FromPort: discoveredResources.auroraPort || 5432,
|
|
745
|
+
ToPort: discoveredResources.auroraPort || 5432,
|
|
746
|
+
SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
|
|
747
|
+
Description: 'Allow Lambda functions to connect to Aurora PostgreSQL',
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
console.log(` ✅ Added security group ingress rule for Lambda → Aurora connectivity`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
console.log(` ✅ Discovered cluster configuration complete`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Build DATABASE_URL connection string
|
|
758
|
+
* @param {string|object} host - Database host (string or CloudFormation intrinsic function)
|
|
759
|
+
* @param {string|number|object} port - Database port (string/number or CloudFormation intrinsic function)
|
|
760
|
+
* @param {string} database - Database name
|
|
761
|
+
* @param {string|object} secretRef - Secret ARN (string) or CloudFormation Ref object
|
|
762
|
+
*/
|
|
763
|
+
buildDatabaseUrl(host, port, database, secretRef) {
|
|
764
|
+
// Handle secretRef as either a string ARN or CloudFormation Ref object
|
|
765
|
+
const resolveSecretRef = (secretRefValue) => {
|
|
766
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
767
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
768
|
+
return {
|
|
769
|
+
'Fn::Sub': [
|
|
770
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:username}}',
|
|
771
|
+
{ SecretArn: secretRefValue },
|
|
772
|
+
],
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
// String ARN - use directly
|
|
776
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:username}}`;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
const resolveSecretPassword = (secretRefValue) => {
|
|
780
|
+
if (typeof secretRefValue === 'object' && secretRefValue.Ref) {
|
|
781
|
+
// CloudFormation Ref - use nested Fn::Sub to resolve it
|
|
782
|
+
return {
|
|
783
|
+
'Fn::Sub': [
|
|
784
|
+
'{{resolve:secretsmanager:${SecretArn}:SecretString:password}}',
|
|
785
|
+
{ SecretArn: secretRefValue },
|
|
786
|
+
],
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
// String ARN - use directly
|
|
790
|
+
return `{{resolve:secretsmanager:${secretRefValue}:SecretString:password}}`;
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
'Fn::Sub': [
|
|
795
|
+
`postgresql://\${Username}:\${Password}@\${Host}:\${Port}/\${Database}`,
|
|
796
|
+
{
|
|
797
|
+
Username: resolveSecretRef(secretRef),
|
|
798
|
+
Password: resolveSecretPassword(secretRef),
|
|
799
|
+
Host: host,
|
|
800
|
+
Port: port,
|
|
801
|
+
Database: database,
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
module.exports = { AuroraBuilder };
|
|
809
|
+
|