@friggframework/devtools 2.0.0--canary.461.ec909cf.0 → 2.0.0--canary.461.9483dbe.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frigg-cli/__tests__/unit/commands/build.test.js +6 -6
- package/frigg-cli/build-command/index.js +1 -1
- package/frigg-cli/deploy-command/index.js +6 -6
- package/frigg-cli/generate-command/index.js +2 -2
- package/frigg-cli/generate-iam-command.js +10 -10
- package/frigg-cli/start-command/index.js +1 -1
- package/frigg-cli/start-command/start-command.test.js +3 -3
- package/frigg-cli/utils/database-validator.js +14 -21
- package/infrastructure/REFACTOR.md +532 -0
- package/infrastructure/TRANSFORMATION-VISUAL.md +239 -0
- package/infrastructure/__tests__/postgres-config.test.js +1 -1
- package/infrastructure/create-frigg-infrastructure.js +1 -1
- 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-discovery.js +81 -0
- package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
- package/infrastructure/domains/integration/integration-builder.js +178 -0
- package/infrastructure/domains/integration/integration-builder.test.js +362 -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-discovery.test.js +257 -0
- package/infrastructure/domains/parameters/ssm-builder.js +79 -0
- package/infrastructure/domains/parameters/ssm-builder.test.js +188 -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 +169 -0
- package/infrastructure/domains/security/kms-builder.test.js +354 -0
- package/infrastructure/domains/security/kms-discovery.js +80 -0
- package/infrastructure/domains/security/kms-discovery.test.js +176 -0
- package/infrastructure/domains/shared/base-builder.js +112 -0
- package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
- package/infrastructure/domains/shared/environment-builder.js +118 -0
- package/infrastructure/domains/shared/environment-builder.test.js +246 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +366 -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 +132 -0
- package/infrastructure/domains/shared/resource-discovery.test.js +410 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +2 -3
- 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.test.js +259 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
- package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +134 -0
- package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
- package/infrastructure/esbuild.config.js +53 -0
- package/infrastructure/infrastructure-composer.js +85 -0
- package/infrastructure/scripts/build-prisma-layer.js +60 -47
- 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/environment/awsParameterStore.js +29 -18
- package/package.json +8 -8
- package/infrastructure/aws-discovery.js +0 -1704
- package/infrastructure/aws-discovery.test.js +0 -1666
- package/infrastructure/serverless-template.js +0 -2804
- package/infrastructure/serverless-template.test.js +0 -1897
- /package/infrastructure/{POSTGRES-CONFIGURATION.md → docs/POSTGRES-CONFIGURATION.md} +0 -0
- /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/{env-validator.js → domains/shared/validation/env-validator.js} +0 -0
- /package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +0 -0
- /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Infrastructure Builder Interface
|
|
3
|
+
*
|
|
4
|
+
* Domain Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* This abstract class defines the contract for all infrastructure builders.
|
|
7
|
+
* Each infrastructure domain (VPC, KMS, Database, etc.) implements this interface.
|
|
8
|
+
*
|
|
9
|
+
* Benefits of Hexagonal Architecture:
|
|
10
|
+
* - Domain logic separated from infrastructure concerns
|
|
11
|
+
* - Easy to test in isolation
|
|
12
|
+
* - Dependency injection for cross-cutting concerns
|
|
13
|
+
* - Clear boundaries between domains
|
|
14
|
+
* - Enables parallel execution where dependencies allow
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class InfrastructureBuilder {
|
|
18
|
+
/**
|
|
19
|
+
* Build infrastructure resources
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} appDefinition - Application definition from user
|
|
22
|
+
* @param {Object} discoveredResources - Resources discovered from AWS
|
|
23
|
+
* @returns {Object} CloudFormation resources to add to template
|
|
24
|
+
* @throws {Error} If validation fails or build encounters errors
|
|
25
|
+
*/
|
|
26
|
+
async build(appDefinition, discoveredResources) {
|
|
27
|
+
throw new Error('InfrastructureBuilder.build() must be implemented by subclass');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate configuration before building
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} config - Configuration to validate
|
|
34
|
+
* @returns {Object} Validation result { valid: boolean, errors: string[] }
|
|
35
|
+
*/
|
|
36
|
+
validate(config) {
|
|
37
|
+
throw new Error('InfrastructureBuilder.validate() must be implemented by subclass');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if this builder should execute
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} appDefinition - Application definition
|
|
44
|
+
* @returns {boolean} True if builder should execute
|
|
45
|
+
*/
|
|
46
|
+
shouldExecute(appDefinition) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get dependencies (other builders that must execute first)
|
|
52
|
+
*
|
|
53
|
+
* @returns {Array<string>} Array of builder names this depends on
|
|
54
|
+
*/
|
|
55
|
+
getDependencies() {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get builder name for logging and dependency resolution
|
|
61
|
+
*
|
|
62
|
+
* @returns {string} Builder name
|
|
63
|
+
*/
|
|
64
|
+
getName() {
|
|
65
|
+
return this.constructor.name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Value Object for validation results
|
|
71
|
+
*/
|
|
72
|
+
class ValidationResult {
|
|
73
|
+
constructor(valid = true, errors = [], warnings = []) {
|
|
74
|
+
this.valid = valid;
|
|
75
|
+
this.errors = errors;
|
|
76
|
+
this.warnings = warnings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
addError(error) {
|
|
80
|
+
this.errors.push(error);
|
|
81
|
+
this.valid = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addWarning(warning) {
|
|
85
|
+
this.warnings.push(warning);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hasErrors() {
|
|
89
|
+
return this.errors.length > 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
hasWarnings() {
|
|
93
|
+
return this.warnings.length > 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
toString() {
|
|
97
|
+
let result = `Valid: ${this.valid}\n`;
|
|
98
|
+
if (this.errors.length > 0) {
|
|
99
|
+
result += `Errors:\n - ${this.errors.join('\n - ')}\n`;
|
|
100
|
+
}
|
|
101
|
+
if (this.warnings.length > 0) {
|
|
102
|
+
result += `Warnings:\n - ${this.warnings.join('\n - ')}\n`;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
InfrastructureBuilder,
|
|
110
|
+
ValidationResult,
|
|
111
|
+
};
|
|
112
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Builder Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Application Layer - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Orchestrates the execution of all infrastructure builders with:
|
|
7
|
+
* - Dependency resolution
|
|
8
|
+
* - Parallel execution where possible
|
|
9
|
+
* - Error handling and validation
|
|
10
|
+
* - Progress reporting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { gatherDiscoveredResources } = require('./resource-discovery');
|
|
14
|
+
const { getAppEnvironmentVars, buildEnvironment } = require('./environment-builder');
|
|
15
|
+
|
|
16
|
+
class BuilderOrchestrator {
|
|
17
|
+
constructor(builders = []) {
|
|
18
|
+
this.builders = new Map();
|
|
19
|
+
builders.forEach(builder => this.registerBuilder(builder));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register a builder
|
|
24
|
+
*/
|
|
25
|
+
registerBuilder(builder) {
|
|
26
|
+
this.builders.set(builder.getName(), builder);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Validate all applicable builders
|
|
31
|
+
*/
|
|
32
|
+
async validateAll(appDefinition) {
|
|
33
|
+
console.log('\n🔍 Validating infrastructure configuration...');
|
|
34
|
+
|
|
35
|
+
const validationResults = [];
|
|
36
|
+
let hasErrors = false;
|
|
37
|
+
|
|
38
|
+
for (const [name, builder] of this.builders) {
|
|
39
|
+
if (builder.shouldExecute(appDefinition)) {
|
|
40
|
+
const result = builder.validate(appDefinition);
|
|
41
|
+
validationResults.push({ builder: name, result });
|
|
42
|
+
|
|
43
|
+
if (result.hasErrors()) {
|
|
44
|
+
hasErrors = true;
|
|
45
|
+
console.log(`❌ ${name} validation failed:`);
|
|
46
|
+
result.errors.forEach(error => console.log(` - ${error}`));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (result.hasWarnings()) {
|
|
50
|
+
console.log(`⚠️ ${name} warnings:`);
|
|
51
|
+
result.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (hasErrors) {
|
|
57
|
+
throw new Error('Infrastructure validation failed. See errors above.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('✅ All infrastructure validation passed\n');
|
|
61
|
+
return validationResults;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve builder execution order based on dependencies
|
|
66
|
+
*/
|
|
67
|
+
resolveBuildOrder(appDefinition) {
|
|
68
|
+
const executionOrder = [];
|
|
69
|
+
const visited = new Set();
|
|
70
|
+
const visiting = new Set();
|
|
71
|
+
|
|
72
|
+
const visit = (builderName) => {
|
|
73
|
+
if (visited.has(builderName)) return;
|
|
74
|
+
if (visiting.has(builderName)) {
|
|
75
|
+
throw new Error(`Circular dependency detected: ${builderName}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const builder = this.builders.get(builderName);
|
|
79
|
+
if (!builder || !builder.shouldExecute(appDefinition)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
visiting.add(builderName);
|
|
84
|
+
|
|
85
|
+
// Visit dependencies first
|
|
86
|
+
const dependencies = builder.getDependencies() || [];
|
|
87
|
+
dependencies.forEach(dep => visit(dep));
|
|
88
|
+
|
|
89
|
+
visiting.delete(builderName);
|
|
90
|
+
visited.add(builderName);
|
|
91
|
+
executionOrder.push(builderName);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Visit all builders
|
|
95
|
+
for (const [builderName] of this.builders) {
|
|
96
|
+
visit(builderName);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return executionOrder;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build all infrastructure
|
|
104
|
+
*/
|
|
105
|
+
async buildAll(appDefinition) {
|
|
106
|
+
console.log('\n🏗️ Building infrastructure...');
|
|
107
|
+
|
|
108
|
+
// Step 1: Validate configuration
|
|
109
|
+
await this.validateAll(appDefinition);
|
|
110
|
+
|
|
111
|
+
// Step 2: Discover AWS resources
|
|
112
|
+
const discoveredResources = await gatherDiscoveredResources(appDefinition);
|
|
113
|
+
|
|
114
|
+
// Step 3: Resolve build order
|
|
115
|
+
const buildOrder = this.resolveBuildOrder(appDefinition);
|
|
116
|
+
console.log(`📋 Build order: ${buildOrder.join(' → ')}\n`);
|
|
117
|
+
|
|
118
|
+
// Step 4: Execute builders in order
|
|
119
|
+
const buildResults = {};
|
|
120
|
+
|
|
121
|
+
for (const builderName of buildOrder) {
|
|
122
|
+
const builder = this.builders.get(builderName);
|
|
123
|
+
try {
|
|
124
|
+
const result = await builder.build(appDefinition, discoveredResources);
|
|
125
|
+
buildResults[builderName] = result;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`❌ ${builderName} build failed:`, error.message);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Step 5: Merge results
|
|
133
|
+
return this.mergeResults(buildResults, appDefinition, discoveredResources);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Merge builder results into cohesive definition
|
|
138
|
+
*/
|
|
139
|
+
mergeResults(buildResults, appDefinition, discoveredResources) {
|
|
140
|
+
console.log('\n🔗 Merging infrastructure results...');
|
|
141
|
+
|
|
142
|
+
const merged = {
|
|
143
|
+
resources: {},
|
|
144
|
+
iamStatements: [],
|
|
145
|
+
environment: {},
|
|
146
|
+
functions: {},
|
|
147
|
+
layers: {},
|
|
148
|
+
plugins: [],
|
|
149
|
+
custom: {},
|
|
150
|
+
vpcConfig: null,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Merge results from each builder
|
|
154
|
+
for (const [builderName, result] of Object.entries(buildResults)) {
|
|
155
|
+
// Merge resources
|
|
156
|
+
if (result.resources) {
|
|
157
|
+
Object.assign(merged.resources, result.resources);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Merge IAM statements
|
|
161
|
+
if (result.iamStatements) {
|
|
162
|
+
merged.iamStatements.push(...result.iamStatements);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Merge environment variables
|
|
166
|
+
if (result.environment) {
|
|
167
|
+
Object.assign(merged.environment, result.environment);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Merge functions
|
|
171
|
+
if (result.functions) {
|
|
172
|
+
Object.assign(merged.functions, result.functions);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Merge layers
|
|
176
|
+
if (result.layers) {
|
|
177
|
+
Object.assign(merged.layers, result.layers);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Merge plugins
|
|
181
|
+
if (result.plugins) {
|
|
182
|
+
merged.plugins.push(...result.plugins);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Merge custom configuration
|
|
186
|
+
if (result.pluginConfig) {
|
|
187
|
+
Object.assign(merged.custom, result.pluginConfig);
|
|
188
|
+
}
|
|
189
|
+
if (result.custom) {
|
|
190
|
+
Object.assign(merged.custom, result.custom);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Capture VPC config (from VpcBuilder)
|
|
194
|
+
if (result.vpcConfig) {
|
|
195
|
+
merged.vpcConfig = result.vpcConfig;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(` ✓ Merged ${builderName} results`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log('✅ Infrastructure build completed successfully\n');
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
merged,
|
|
205
|
+
discoveredResources,
|
|
206
|
+
appEnvironmentVars: getAppEnvironmentVars(appDefinition),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { BuilderOrchestrator };
|
|
212
|
+
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for BuilderOrchestrator
|
|
3
|
+
*
|
|
4
|
+
* Tests orchestration, dependency resolution, and parallel execution
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { BuilderOrchestrator } = require('./builder-orchestrator');
|
|
8
|
+
const { InfrastructureBuilder, ValidationResult } = require('./base-builder');
|
|
9
|
+
|
|
10
|
+
// Mock builders for testing
|
|
11
|
+
class MockBuilderA extends InfrastructureBuilder {
|
|
12
|
+
getName() { return 'MockBuilderA'; }
|
|
13
|
+
shouldExecute(appDef) { return appDef.featureA === true; }
|
|
14
|
+
validate(appDef) { return new ValidationResult(); }
|
|
15
|
+
async build(appDef, discovered) {
|
|
16
|
+
return {
|
|
17
|
+
resources: { ResourceA: { Type: 'Test' } },
|
|
18
|
+
iamStatements: [{ Effect: 'Allow', Action: 'test:A' }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class MockBuilderB extends InfrastructureBuilder {
|
|
24
|
+
getName() { return 'MockBuilderB'; }
|
|
25
|
+
shouldExecute(appDef) { return appDef.featureB === true; }
|
|
26
|
+
validate(appDef) { return new ValidationResult(); }
|
|
27
|
+
getDependencies() { return ['MockBuilderA']; } // Depends on A
|
|
28
|
+
async build(appDef, discovered) {
|
|
29
|
+
return {
|
|
30
|
+
resources: { ResourceB: { Type: 'Test' } },
|
|
31
|
+
environment: { VAR_B: 'value-b' },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class MockBuilderC extends InfrastructureBuilder {
|
|
37
|
+
getName() { return 'MockBuilderC'; }
|
|
38
|
+
shouldExecute() { return false; } // Never executes
|
|
39
|
+
validate() { return new ValidationResult(); }
|
|
40
|
+
async build() {
|
|
41
|
+
throw new Error('Should not be called');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
jest.mock('./resource-discovery', () => ({
|
|
46
|
+
gatherDiscoveredResources: jest.fn().mockResolvedValue({ discovered: true }),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
jest.mock('./environment-builder', () => ({
|
|
50
|
+
getAppEnvironmentVars: jest.fn().mockReturnValue({ ENV_VAR: 'value' }),
|
|
51
|
+
buildEnvironment: jest.fn().mockReturnValue({ ENV_VAR: 'value' }),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
describe('BuilderOrchestrator', () => {
|
|
55
|
+
let orchestrator;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('registerBuilder()', () => {
|
|
62
|
+
it('should register builders', () => {
|
|
63
|
+
orchestrator = new BuilderOrchestrator();
|
|
64
|
+
const builder = new MockBuilderA();
|
|
65
|
+
|
|
66
|
+
orchestrator.registerBuilder(builder);
|
|
67
|
+
|
|
68
|
+
expect(orchestrator.builders.has('MockBuilderA')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should register multiple builders via constructor', () => {
|
|
72
|
+
orchestrator = new BuilderOrchestrator([
|
|
73
|
+
new MockBuilderA(),
|
|
74
|
+
new MockBuilderB(),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
expect(orchestrator.builders.size).toBe(2);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('validateAll()', () => {
|
|
82
|
+
it('should validate all applicable builders', async () => {
|
|
83
|
+
orchestrator = new BuilderOrchestrator([
|
|
84
|
+
new MockBuilderA(),
|
|
85
|
+
new MockBuilderB(),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const appDef = { featureA: true, featureB: true };
|
|
89
|
+
|
|
90
|
+
await expect(orchestrator.validateAll(appDef)).resolves.toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should skip builders that shouldNotExecute', async () => {
|
|
94
|
+
orchestrator = new BuilderOrchestrator([
|
|
95
|
+
new MockBuilderA(),
|
|
96
|
+
new MockBuilderC(), // Should not execute
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const appDef = { featureA: true };
|
|
100
|
+
|
|
101
|
+
const results = await orchestrator.validateAll(appDef);
|
|
102
|
+
|
|
103
|
+
// Should only validate MockBuilderA
|
|
104
|
+
expect(results).toHaveLength(1);
|
|
105
|
+
expect(results[0].builder).toBe('MockBuilderA');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should throw error if validation fails', async () => {
|
|
109
|
+
class FailingBuilder extends InfrastructureBuilder {
|
|
110
|
+
getName() { return 'FailingBuilder'; }
|
|
111
|
+
shouldExecute() { return true; }
|
|
112
|
+
validate() {
|
|
113
|
+
const result = new ValidationResult();
|
|
114
|
+
result.addError('Test error');
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
orchestrator = new BuilderOrchestrator([new FailingBuilder()]);
|
|
120
|
+
|
|
121
|
+
await expect(orchestrator.validateAll({})).rejects.toThrow('Infrastructure validation failed');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('resolveBuildOrder()', () => {
|
|
126
|
+
it('should resolve dependencies correctly', () => {
|
|
127
|
+
orchestrator = new BuilderOrchestrator([
|
|
128
|
+
new MockBuilderB(), // Depends on A
|
|
129
|
+
new MockBuilderA(), // No dependencies
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const appDef = { featureA: true, featureB: true };
|
|
133
|
+
const order = orchestrator.resolveBuildOrder(appDef);
|
|
134
|
+
|
|
135
|
+
// A should come before B
|
|
136
|
+
expect(order).toEqual(['MockBuilderA', 'MockBuilderB']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should handle builders with no dependencies', () => {
|
|
140
|
+
orchestrator = new BuilderOrchestrator([
|
|
141
|
+
new MockBuilderA(),
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const appDef = { featureA: true };
|
|
145
|
+
const order = orchestrator.resolveBuildOrder(appDef);
|
|
146
|
+
|
|
147
|
+
expect(order).toEqual(['MockBuilderA']);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should skip builders that should not execute', () => {
|
|
151
|
+
orchestrator = new BuilderOrchestrator([
|
|
152
|
+
new MockBuilderA(),
|
|
153
|
+
new MockBuilderC(), // Should not execute
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
const appDef = { featureA: true };
|
|
157
|
+
const order = orchestrator.resolveBuildOrder(appDef);
|
|
158
|
+
|
|
159
|
+
expect(order).toEqual(['MockBuilderA']);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('buildAll()', () => {
|
|
164
|
+
it('should build all infrastructure and merge results', async () => {
|
|
165
|
+
orchestrator = new BuilderOrchestrator([
|
|
166
|
+
new MockBuilderA(),
|
|
167
|
+
new MockBuilderB(),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const appDef = { featureA: true, featureB: true };
|
|
171
|
+
|
|
172
|
+
const result = await orchestrator.buildAll(appDef);
|
|
173
|
+
|
|
174
|
+
expect(result.merged).toBeDefined();
|
|
175
|
+
expect(result.merged.resources).toMatchObject({
|
|
176
|
+
ResourceA: { Type: 'Test' },
|
|
177
|
+
ResourceB: { Type: 'Test' },
|
|
178
|
+
});
|
|
179
|
+
expect(result.merged.iamStatements).toHaveLength(1);
|
|
180
|
+
expect(result.merged.environment).toMatchObject({ VAR_B: 'value-b' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should skip builders that should not execute', async () => {
|
|
184
|
+
orchestrator = new BuilderOrchestrator([
|
|
185
|
+
new MockBuilderA(),
|
|
186
|
+
new MockBuilderC(), // Should not execute
|
|
187
|
+
]);
|
|
188
|
+
|
|
189
|
+
const appDef = { featureA: true };
|
|
190
|
+
|
|
191
|
+
const result = await orchestrator.buildAll(appDef);
|
|
192
|
+
|
|
193
|
+
// Should only have results from A
|
|
194
|
+
expect(Object.keys(result.merged.resources)).toEqual(['ResourceA']);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should throw error if builder fails', async () => {
|
|
198
|
+
class FailingBuilder extends InfrastructureBuilder {
|
|
199
|
+
getName() { return 'FailingBuilder'; }
|
|
200
|
+
shouldExecute() { return true; }
|
|
201
|
+
validate() { return new ValidationResult(); }
|
|
202
|
+
async build() {
|
|
203
|
+
throw new Error('Build failed');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
orchestrator = new BuilderOrchestrator([new FailingBuilder()]);
|
|
208
|
+
|
|
209
|
+
await expect(orchestrator.buildAll({})).rejects.toThrow('Build failed');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Builder Service
|
|
3
|
+
*
|
|
4
|
+
* Domain Service - Hexagonal Architecture
|
|
5
|
+
*
|
|
6
|
+
* Builds Lambda environment variable configuration from:
|
|
7
|
+
* 1. AppDefinition environment flags
|
|
8
|
+
* 2. Discovered AWS resources (VPC IDs, KMS keys, etc.)
|
|
9
|
+
* 3. Generated resource references
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get environment variables from AppDefinition
|
|
14
|
+
*
|
|
15
|
+
* Extracts environment variable definitions where value is true,
|
|
16
|
+
* and creates Serverless variable references.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} appDefinition - Application definition
|
|
19
|
+
* @returns {Object} Environment variable mappings
|
|
20
|
+
*/
|
|
21
|
+
function getAppEnvironmentVars(appDefinition) {
|
|
22
|
+
const envVars = {};
|
|
23
|
+
const reservedVars = new Set([
|
|
24
|
+
'_HANDLER',
|
|
25
|
+
'_X_AMZN_TRACE_ID',
|
|
26
|
+
'AWS_DEFAULT_REGION',
|
|
27
|
+
'AWS_EXECUTION_ENV',
|
|
28
|
+
'AWS_REGION',
|
|
29
|
+
'AWS_LAMBDA_FUNCTION_NAME',
|
|
30
|
+
'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
|
|
31
|
+
'AWS_LAMBDA_FUNCTION_VERSION',
|
|
32
|
+
'AWS_LAMBDA_INITIALIZATION_TYPE',
|
|
33
|
+
'AWS_LAMBDA_LOG_GROUP_NAME',
|
|
34
|
+
'AWS_LAMBDA_LOG_STREAM_NAME',
|
|
35
|
+
'AWS_ACCESS_KEY',
|
|
36
|
+
'AWS_ACCESS_KEY_ID',
|
|
37
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
38
|
+
'AWS_SESSION_TOKEN',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
if (!appDefinition.environment) {
|
|
42
|
+
return envVars;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('📋 Loading environment variables from appDefinition...');
|
|
46
|
+
const envKeys = [];
|
|
47
|
+
const skippedKeys = [];
|
|
48
|
+
|
|
49
|
+
for (const [key, value] of Object.entries(appDefinition.environment)) {
|
|
50
|
+
if (value !== true) continue;
|
|
51
|
+
if (reservedVars.has(key)) {
|
|
52
|
+
skippedKeys.push(key);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
envVars[key] = `\${env:${key}, ''}`;
|
|
56
|
+
envKeys.push(key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (envKeys.length > 0) {
|
|
60
|
+
console.log(
|
|
61
|
+
` Found ${envKeys.length} environment variables: ${envKeys.join(
|
|
62
|
+
', '
|
|
63
|
+
)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (skippedKeys.length > 0) {
|
|
67
|
+
console.log(
|
|
68
|
+
` ⚠️ Skipped ${skippedKeys.length
|
|
69
|
+
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return envVars;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build complete environment configuration for Lambda functions
|
|
78
|
+
*
|
|
79
|
+
* Combines app environment vars with discovered AWS resource references
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} appEnvironmentVars - Environment vars from AppDefinition
|
|
82
|
+
* @param {Object} discoveredResources - Discovered AWS resources
|
|
83
|
+
* @returns {Object} Complete environment configuration
|
|
84
|
+
*/
|
|
85
|
+
function buildEnvironment(appEnvironmentVars, discoveredResources) {
|
|
86
|
+
const environment = {
|
|
87
|
+
...appEnvironmentVars,
|
|
88
|
+
FRIGG_STACK: '${self:service}',
|
|
89
|
+
FRIGG_STAGE: '${self:provider.stage}',
|
|
90
|
+
FRIGG_REGION: '${self:provider.region}',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Add KMS key if discovered or created
|
|
94
|
+
if (discoveredResources.kmsKeyId) {
|
|
95
|
+
environment.KMS_KEY_ARN = discoveredResources.kmsKeyId;
|
|
96
|
+
} else if (discoveredResources.kmsKeyArn) {
|
|
97
|
+
environment.KMS_KEY_ARN = discoveredResources.kmsKeyArn;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add database connection info if discovered
|
|
101
|
+
if (discoveredResources.auroraClusterEndpoint) {
|
|
102
|
+
environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
103
|
+
environment.DATABASE_PORT = String(discoveredResources.auroraPort || 5432);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add secrets manager secret ARN if discovered
|
|
107
|
+
if (discoveredResources.databaseSecretArn) {
|
|
108
|
+
environment.DATABASE_SECRET_ARN = discoveredResources.databaseSecretArn;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return environment;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
getAppEnvironmentVars,
|
|
116
|
+
buildEnvironment,
|
|
117
|
+
};
|
|
118
|
+
|