@friggframework/devtools 2.0.0-next.101 → 2.0.0-next.103
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/generate-iam.test.js +97 -0
- package/frigg-cli/deploy-command/index.js +44 -0
- package/frigg-cli/generate-command/index.js +2 -1
- package/frigg-cli/generate-iam-command.js +2 -1
- package/frigg-cli/index.js +16 -1
- package/frigg-cli/ssm-command/index.js +308 -0
- package/frigg-cli/ssm-command/index.test.js +318 -0
- package/infrastructure/__tests__/helpers/test-utils.js +3 -5
- package/infrastructure/__tests__/scoped-environment.test.js +126 -0
- package/infrastructure/__tests__/ssm-preload-node-options.test.js +79 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +17 -3
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +45 -0
- package/infrastructure/domains/database/migration-builder.js +46 -9
- package/infrastructure/domains/database/migration-builder.test.js +101 -0
- package/infrastructure/domains/integration/integration-builder.js +65 -9
- package/infrastructure/domains/integration/integration-builder.test.js +95 -0
- package/infrastructure/domains/networking/vpc-builder.js +57 -7
- package/infrastructure/domains/networking/vpc-builder.test.js +41 -0
- package/infrastructure/domains/networking/vpc-discovery.js +9 -2
- package/infrastructure/domains/networking/vpc-discovery.test.js +19 -1
- package/infrastructure/domains/networking/vpc-resolver.js +12 -3
- package/infrastructure/domains/networking/vpc-resolver.test.js +40 -0
- package/infrastructure/domains/parameters/offload-utils.js +190 -0
- package/infrastructure/domains/parameters/offload-utils.test.js +193 -0
- package/infrastructure/domains/parameters/ssm-builder.js +67 -14
- package/infrastructure/domains/parameters/ssm-builder.test.js +158 -1
- package/infrastructure/domains/scheduler/scheduler-builder.js +44 -8
- package/infrastructure/domains/scheduler/scheduler-builder.test.js +118 -0
- package/infrastructure/domains/security/iam-generator.js +36 -1
- package/infrastructure/domains/security/iam-generator.test.js +83 -0
- package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +17 -0
- package/infrastructure/domains/security/templates/iam-policy-full.json +8 -3
- package/infrastructure/domains/shared/builder-orchestrator.js +14 -0
- package/infrastructure/domains/shared/builder-orchestrator.test.js +45 -0
- package/infrastructure/domains/shared/environment-builder.js +88 -15
- package/infrastructure/domains/shared/environment-builder.test.js +304 -22
- package/infrastructure/domains/shared/function-environments.js +97 -0
- package/infrastructure/domains/shared/function-environments.test.js +146 -0
- package/infrastructure/infrastructure-composer.js +48 -1
- package/infrastructure/infrastructure-composer.test.js +161 -15
- package/infrastructure/integration.test.js +3 -5
- package/package.json +8 -7
|
@@ -1,20 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment Builder Service
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Domain Service - Hexagonal Architecture
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Builds Lambda environment variable configuration from:
|
|
7
7
|
* 1. AppDefinition environment flags
|
|
8
8
|
* 2. Discovered AWS resources (VPC IDs, KMS keys, etc.)
|
|
9
9
|
* 3. Generated resource references
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const {
|
|
13
|
+
isSsmOffloadActive,
|
|
14
|
+
getOffloadedKeys,
|
|
15
|
+
} = require('../parameters/offload-utils');
|
|
16
|
+
|
|
17
|
+
// OTLP-family exporters read their endpoint/headers from these standard env
|
|
18
|
+
// vars (ADR-011). When such an exporter is configured we auto-register them as
|
|
19
|
+
// Serverless passthroughs so the deployed Lambda inherits them from the deploy
|
|
20
|
+
// environment — no need for the adopter to also list them under `environment`.
|
|
21
|
+
//
|
|
22
|
+
// NOTE (VPC egress): a Lambda in a private subnet needs a NAT gateway or a VPC
|
|
23
|
+
// endpoint to reach an external OTLP backend (Honeycomb/Datadog). Without egress
|
|
24
|
+
// the exporter fails silently within its flush timeout — see the deploy docs.
|
|
25
|
+
const OTLP_EXPORTER_TYPES = new Set(['otlp', 'honeycomb', 'datadog']);
|
|
26
|
+
const OTEL_PASSTHROUGH_VARS = [
|
|
27
|
+
'OTEL_EXPORTER_OTLP_ENDPOINT',
|
|
28
|
+
'OTEL_EXPORTER_OTLP_HEADERS',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function addTelemetryEnvPassthrough(appDefinition, envVars) {
|
|
32
|
+
const exporterType = appDefinition?.telemetry?.exporter?.type;
|
|
33
|
+
if (!OTLP_EXPORTER_TYPES.has(exporterType)) return;
|
|
34
|
+
|
|
35
|
+
// Skip offloaded keys here: baking '${env:KEY, ''}' resolves to '' at
|
|
36
|
+
// deploy (the value lives only in SSM), and '' !== undefined then blocks
|
|
37
|
+
// the SSM loader from ever fetching the real value.
|
|
38
|
+
const offloadedKeys = isSsmOffloadActive(appDefinition)
|
|
39
|
+
? new Set(getOffloadedKeys(appDefinition))
|
|
40
|
+
: null;
|
|
41
|
+
for (const key of OTEL_PASSTHROUGH_VARS) {
|
|
42
|
+
if (offloadedKeys?.has(key)) continue;
|
|
43
|
+
envVars[key] = `\${env:${key}, ''}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
12
47
|
/**
|
|
13
48
|
* Get environment variables from AppDefinition
|
|
14
|
-
*
|
|
49
|
+
*
|
|
15
50
|
* Extracts environment variable definitions where value is true,
|
|
16
51
|
* and creates Serverless variable references.
|
|
17
|
-
*
|
|
52
|
+
*
|
|
53
|
+
* A value of 'ssm' offloads the variable to Parameter Store when offload is
|
|
54
|
+
* active (see SsmBuilder), keeping it out of the Lambda env map. When offload
|
|
55
|
+
* is not active (local mode / ssm disabled) it falls back to the same
|
|
56
|
+
* `${env:KEY, ''}` reference as `true` so `frigg start` + dotenv keeps working.
|
|
57
|
+
*
|
|
18
58
|
* @param {Object} appDefinition - Application definition
|
|
19
59
|
* @returns {Object} Environment variable mappings
|
|
20
60
|
*/
|
|
@@ -38,16 +78,40 @@ function getAppEnvironmentVars(appDefinition) {
|
|
|
38
78
|
'AWS_SESSION_TOKEN',
|
|
39
79
|
]);
|
|
40
80
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
81
|
+
addTelemetryEnvPassthrough(appDefinition, envVars);
|
|
82
|
+
|
|
83
|
+
const environment = appDefinition.environment || {};
|
|
44
84
|
|
|
45
85
|
console.log('📋 Loading environment variables from appDefinition...');
|
|
46
86
|
const envKeys = [];
|
|
47
87
|
const skippedKeys = [];
|
|
88
|
+
const offloadedKeys = [];
|
|
89
|
+
const offloadActive = isSsmOffloadActive(appDefinition);
|
|
90
|
+
|
|
91
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
92
|
+
if (value === 'ssm' && offloadActive) {
|
|
93
|
+
offloadedKeys.push(key);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (value !== true && value !== 'ssm') continue;
|
|
97
|
+
if (reservedVars.has(key)) {
|
|
98
|
+
skippedKeys.push(key);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
envVars[key] = `\${env:${key}, ''}`;
|
|
102
|
+
envKeys.push(key);
|
|
103
|
+
}
|
|
48
104
|
|
|
49
|
-
|
|
50
|
-
|
|
105
|
+
// Keys declared only in ssm.parameters (no matching `environment` entry)
|
|
106
|
+
// get the same local-fallback treatment as `environment`-valued 'ssm' keys.
|
|
107
|
+
const ssmOnlyKeys = getOffloadedKeys(appDefinition).filter(
|
|
108
|
+
(key) => !(key in environment)
|
|
109
|
+
);
|
|
110
|
+
for (const key of ssmOnlyKeys) {
|
|
111
|
+
if (offloadActive) {
|
|
112
|
+
offloadedKeys.push(key);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
51
115
|
if (reservedVars.has(key)) {
|
|
52
116
|
skippedKeys.push(key);
|
|
53
117
|
continue;
|
|
@@ -65,19 +129,27 @@ function getAppEnvironmentVars(appDefinition) {
|
|
|
65
129
|
}
|
|
66
130
|
if (skippedKeys.length > 0) {
|
|
67
131
|
console.log(
|
|
68
|
-
` ⚠️ Skipped ${
|
|
132
|
+
` ⚠️ Skipped ${
|
|
133
|
+
skippedKeys.length
|
|
69
134
|
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
70
135
|
);
|
|
71
136
|
}
|
|
137
|
+
if (offloadedKeys.length > 0) {
|
|
138
|
+
console.log(
|
|
139
|
+
` 🔒 Offloaded ${
|
|
140
|
+
offloadedKeys.length
|
|
141
|
+
} variables to SSM: ${offloadedKeys.join(', ')}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
72
144
|
|
|
73
145
|
return envVars;
|
|
74
146
|
}
|
|
75
147
|
|
|
76
148
|
/**
|
|
77
149
|
* Build complete environment configuration for Lambda functions
|
|
78
|
-
*
|
|
150
|
+
*
|
|
79
151
|
* Combines app environment vars with discovered AWS resource references
|
|
80
|
-
*
|
|
152
|
+
*
|
|
81
153
|
* @param {Object} appEnvironmentVars - Environment vars from AppDefinition
|
|
82
154
|
* @param {Object} discoveredResources - Discovered AWS resources
|
|
83
155
|
* @returns {Object} Complete environment configuration
|
|
@@ -85,7 +157,7 @@ function getAppEnvironmentVars(appDefinition) {
|
|
|
85
157
|
function buildEnvironment(appEnvironmentVars, discoveredResources) {
|
|
86
158
|
const environment = {
|
|
87
159
|
...appEnvironmentVars,
|
|
88
|
-
STAGE: '${self:provider.stage}',
|
|
160
|
+
STAGE: '${self:provider.stage}', // Used by encryption bypass logic
|
|
89
161
|
FRIGG_STACK: '${self:service}',
|
|
90
162
|
FRIGG_STAGE: '${self:provider.stage}',
|
|
91
163
|
FRIGG_REGION: '${self:provider.region}',
|
|
@@ -101,7 +173,9 @@ function buildEnvironment(appEnvironmentVars, discoveredResources) {
|
|
|
101
173
|
// Add database connection info if discovered
|
|
102
174
|
if (discoveredResources.auroraClusterEndpoint) {
|
|
103
175
|
environment.DATABASE_HOST = discoveredResources.auroraClusterEndpoint;
|
|
104
|
-
environment.DATABASE_PORT = String(
|
|
176
|
+
environment.DATABASE_PORT = String(
|
|
177
|
+
discoveredResources.auroraPort || 5432
|
|
178
|
+
);
|
|
105
179
|
}
|
|
106
180
|
|
|
107
181
|
// Add secrets manager secret ARN if discovered
|
|
@@ -116,4 +190,3 @@ module.exports = {
|
|
|
116
190
|
getAppEnvironmentVars,
|
|
117
191
|
buildEnvironment,
|
|
118
192
|
};
|
|
119
|
-
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for Environment Builder Service
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tests environment variable extraction and building
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
getAppEnvironmentVars,
|
|
9
|
+
buildEnvironment,
|
|
10
|
+
} = require('./environment-builder');
|
|
8
11
|
|
|
9
12
|
describe('Environment Builder', () => {
|
|
10
13
|
describe('getAppEnvironmentVars()', () => {
|
|
@@ -38,7 +41,7 @@ describe('Environment Builder', () => {
|
|
|
38
41
|
expect(result.DISABLED_VAR).toBeUndefined();
|
|
39
42
|
});
|
|
40
43
|
|
|
41
|
-
it(
|
|
44
|
+
it("ignores non-boolean values other than the meaningful 'ssm' marker", () => {
|
|
42
45
|
const appDefinition = {
|
|
43
46
|
environment: {
|
|
44
47
|
VALID: true,
|
|
@@ -108,6 +111,244 @@ describe('Environment Builder', () => {
|
|
|
108
111
|
});
|
|
109
112
|
});
|
|
110
113
|
|
|
114
|
+
describe('telemetry env passthrough (ADR-011)', () => {
|
|
115
|
+
it('auto-adds OTLP env passthroughs when an OTLP-family exporter is configured', () => {
|
|
116
|
+
const result = getAppEnvironmentVars({
|
|
117
|
+
telemetry: { exporter: { type: 'otlp' } },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBe(
|
|
121
|
+
"${env:OTEL_EXPORTER_OTLP_ENDPOINT, ''}"
|
|
122
|
+
);
|
|
123
|
+
expect(result.OTEL_EXPORTER_OTLP_HEADERS).toBe(
|
|
124
|
+
"${env:OTEL_EXPORTER_OTLP_HEADERS, ''}"
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it.each(['honeycomb', 'datadog'])(
|
|
129
|
+
'adds OTLP passthroughs for the "%s" preset',
|
|
130
|
+
(type) => {
|
|
131
|
+
const result = getAppEnvironmentVars({
|
|
132
|
+
telemetry: { exporter: { type } },
|
|
133
|
+
});
|
|
134
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBeDefined();
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
it('adds no OTLP env vars for console/none exporters or absent telemetry', () => {
|
|
139
|
+
expect(
|
|
140
|
+
getAppEnvironmentVars({
|
|
141
|
+
telemetry: { exporter: { type: 'console' } },
|
|
142
|
+
}).OTEL_EXPORTER_OTLP_ENDPOINT
|
|
143
|
+
).toBeUndefined();
|
|
144
|
+
expect(
|
|
145
|
+
getAppEnvironmentVars({}).OTEL_EXPORTER_OTLP_ENDPOINT
|
|
146
|
+
).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("getAppEnvironmentVars() - 'ssm' offload", () => {
|
|
151
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
152
|
+
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
if (originalSkipDiscovery === undefined) {
|
|
155
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
156
|
+
} else {
|
|
157
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("excludes 'ssm' keys when offload is active", () => {
|
|
162
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
163
|
+
const appDefinition = {
|
|
164
|
+
ssm: { enable: true },
|
|
165
|
+
environment: { FOO: 'ssm', BAR: true },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
169
|
+
|
|
170
|
+
expect(result.FOO).toBeUndefined();
|
|
171
|
+
expect(result.BAR).toBe("${env:BAR, ''}");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("falls back to env reference for 'ssm' keys when FRIGG_SKIP_AWS_DISCOVERY is set", () => {
|
|
175
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
176
|
+
const appDefinition = {
|
|
177
|
+
ssm: { enable: true },
|
|
178
|
+
environment: { FOO: 'ssm' },
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
182
|
+
|
|
183
|
+
expect(result.FOO).toBe("${env:FOO, ''}");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("falls back to env reference for 'ssm' keys when ssm is disabled", () => {
|
|
187
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
188
|
+
const appDefinition = {
|
|
189
|
+
ssm: { enable: false },
|
|
190
|
+
environment: { FOO: 'ssm' },
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
194
|
+
|
|
195
|
+
expect(result.FOO).toBe("${env:FOO, ''}");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('falls back to env reference for a key declared only in ssm.parameters when FRIGG_SKIP_AWS_DISCOVERY is set', () => {
|
|
199
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
200
|
+
const appDefinition = {
|
|
201
|
+
ssm: {
|
|
202
|
+
enable: true,
|
|
203
|
+
parameters: {
|
|
204
|
+
HUBSPOT_CLIENT_SECRET: { type: 'SecureString' },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
210
|
+
|
|
211
|
+
expect(result.HUBSPOT_CLIENT_SECRET).toBe(
|
|
212
|
+
"${env:HUBSPOT_CLIENT_SECRET, ''}"
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('excludes a key declared only in ssm.parameters when offload is active', () => {
|
|
217
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
218
|
+
const appDefinition = {
|
|
219
|
+
ssm: {
|
|
220
|
+
enable: true,
|
|
221
|
+
parameters: {
|
|
222
|
+
HUBSPOT_CLIENT_SECRET: { type: 'SecureString' },
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
228
|
+
|
|
229
|
+
expect(result.HUBSPOT_CLIENT_SECRET).toBeUndefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("does not double-process a key present in both environment:'ssm' and ssm.parameters", () => {
|
|
233
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
234
|
+
const appDefinition = {
|
|
235
|
+
ssm: {
|
|
236
|
+
enable: true,
|
|
237
|
+
parameters: { FOO: { type: 'SecureString' } },
|
|
238
|
+
},
|
|
239
|
+
environment: { FOO: 'ssm' },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
243
|
+
|
|
244
|
+
expect(result.FOO).toBeUndefined();
|
|
245
|
+
|
|
246
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
247
|
+
const fallbackResult = getAppEnvironmentVars(appDefinition);
|
|
248
|
+
expect(fallbackResult.FOO).toBe("${env:FOO, ''}");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('telemetry env passthrough + SSM offload interaction', () => {
|
|
253
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
254
|
+
|
|
255
|
+
afterEach(() => {
|
|
256
|
+
if (originalSkipDiscovery === undefined) {
|
|
257
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
258
|
+
} else {
|
|
259
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not bake an OTEL passthrough var that is marked 'ssm' when offload is active", () => {
|
|
264
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
265
|
+
const appDefinition = {
|
|
266
|
+
telemetry: { exporter: { type: 'datadog' } },
|
|
267
|
+
ssm: { enable: true },
|
|
268
|
+
environment: {
|
|
269
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'ssm',
|
|
270
|
+
OTEL_EXPORTER_OTLP_HEADERS: 'ssm',
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
275
|
+
|
|
276
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBeUndefined();
|
|
277
|
+
expect(result.OTEL_EXPORTER_OTLP_HEADERS).toBeUndefined();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("keeps the direct passthrough for an OTEL var not marked 'ssm' while offloading its sibling", () => {
|
|
281
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
282
|
+
const appDefinition = {
|
|
283
|
+
telemetry: { exporter: { type: 'otlp' } },
|
|
284
|
+
ssm: { enable: true },
|
|
285
|
+
environment: {
|
|
286
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'ssm',
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
291
|
+
|
|
292
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBeUndefined();
|
|
293
|
+
expect(result.OTEL_EXPORTER_OTLP_HEADERS).toBe(
|
|
294
|
+
"${env:OTEL_EXPORTER_OTLP_HEADERS, ''}"
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("falls back to the env passthrough for an 'ssm'-marked OTEL var in local mode", () => {
|
|
299
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
300
|
+
const appDefinition = {
|
|
301
|
+
telemetry: { exporter: { type: 'datadog' } },
|
|
302
|
+
ssm: { enable: true },
|
|
303
|
+
environment: {
|
|
304
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'ssm',
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
309
|
+
|
|
310
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBe(
|
|
311
|
+
"${env:OTEL_EXPORTER_OTLP_ENDPOINT, ''}"
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("leaves a 'true'-marked OTEL var as a direct passthrough even when offload is active for another key", () => {
|
|
316
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
317
|
+
const appDefinition = {
|
|
318
|
+
telemetry: { exporter: { type: 'datadog' } },
|
|
319
|
+
ssm: { enable: true },
|
|
320
|
+
environment: {
|
|
321
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: true,
|
|
322
|
+
SOME_OFFLOADED_SECRET: 'ssm',
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
327
|
+
|
|
328
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBe(
|
|
329
|
+
"${env:OTEL_EXPORTER_OTLP_ENDPOINT, ''}"
|
|
330
|
+
);
|
|
331
|
+
expect(result.SOME_OFFLOADED_SECRET).toBeUndefined();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('does not bake an OTEL passthrough var offloaded only via ssm.parameters', () => {
|
|
335
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
336
|
+
const appDefinition = {
|
|
337
|
+
telemetry: { exporter: { type: 'datadog' } },
|
|
338
|
+
ssm: {
|
|
339
|
+
enable: true,
|
|
340
|
+
parameters: {
|
|
341
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: { type: 'SecureString' },
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
347
|
+
|
|
348
|
+
expect(result.OTEL_EXPORTER_OTLP_ENDPOINT).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
111
352
|
describe('buildEnvironment()', () => {
|
|
112
353
|
it('should combine app vars with standard Frigg variables', () => {
|
|
113
354
|
const appEnvironmentVars = {
|
|
@@ -115,7 +356,10 @@ describe('Environment Builder', () => {
|
|
|
115
356
|
};
|
|
116
357
|
const discoveredResources = {};
|
|
117
358
|
|
|
118
|
-
const result = buildEnvironment(
|
|
359
|
+
const result = buildEnvironment(
|
|
360
|
+
appEnvironmentVars,
|
|
361
|
+
discoveredResources
|
|
362
|
+
);
|
|
119
363
|
|
|
120
364
|
expect(result.API_KEY).toBe("${env:API_KEY, ''}");
|
|
121
365
|
expect(result.STAGE).toBe('${self:provider.stage}');
|
|
@@ -130,9 +374,14 @@ describe('Environment Builder', () => {
|
|
|
130
374
|
kmsKeyId: 'arn:aws:kms:us-east-1:123456:key/abc-123',
|
|
131
375
|
};
|
|
132
376
|
|
|
133
|
-
const result = buildEnvironment(
|
|
377
|
+
const result = buildEnvironment(
|
|
378
|
+
appEnvironmentVars,
|
|
379
|
+
discoveredResources
|
|
380
|
+
);
|
|
134
381
|
|
|
135
|
-
expect(result.KMS_KEY_ARN).toBe(
|
|
382
|
+
expect(result.KMS_KEY_ARN).toBe(
|
|
383
|
+
'arn:aws:kms:us-east-1:123456:key/abc-123'
|
|
384
|
+
);
|
|
136
385
|
});
|
|
137
386
|
|
|
138
387
|
it('should prefer kmsKeyId over kmsKeyArn if both present', () => {
|
|
@@ -142,22 +391,33 @@ describe('Environment Builder', () => {
|
|
|
142
391
|
kmsKeyArn: 'arn:aws:kms:us-east-1:123456:key/secondary',
|
|
143
392
|
};
|
|
144
393
|
|
|
145
|
-
const result = buildEnvironment(
|
|
394
|
+
const result = buildEnvironment(
|
|
395
|
+
appEnvironmentVars,
|
|
396
|
+
discoveredResources
|
|
397
|
+
);
|
|
146
398
|
|
|
147
399
|
// Implementation uses if/else-if, so kmsKeyId takes priority
|
|
148
|
-
expect(result.KMS_KEY_ARN).toBe(
|
|
400
|
+
expect(result.KMS_KEY_ARN).toBe(
|
|
401
|
+
'arn:aws:kms:us-east-1:123456:key/primary'
|
|
402
|
+
);
|
|
149
403
|
});
|
|
150
404
|
|
|
151
405
|
it('should add database connection info if discovered', () => {
|
|
152
406
|
const appEnvironmentVars = {};
|
|
153
407
|
const discoveredResources = {
|
|
154
|
-
auroraClusterEndpoint:
|
|
408
|
+
auroraClusterEndpoint:
|
|
409
|
+
'cluster.abc.us-east-1.rds.amazonaws.com',
|
|
155
410
|
auroraPort: 5432,
|
|
156
411
|
};
|
|
157
412
|
|
|
158
|
-
const result = buildEnvironment(
|
|
413
|
+
const result = buildEnvironment(
|
|
414
|
+
appEnvironmentVars,
|
|
415
|
+
discoveredResources
|
|
416
|
+
);
|
|
159
417
|
|
|
160
|
-
expect(result.DATABASE_HOST).toBe(
|
|
418
|
+
expect(result.DATABASE_HOST).toBe(
|
|
419
|
+
'cluster.abc.us-east-1.rds.amazonaws.com'
|
|
420
|
+
);
|
|
161
421
|
expect(result.DATABASE_PORT).toBe('5432');
|
|
162
422
|
});
|
|
163
423
|
|
|
@@ -167,7 +427,10 @@ describe('Environment Builder', () => {
|
|
|
167
427
|
auroraClusterEndpoint: 'cluster.example.com',
|
|
168
428
|
};
|
|
169
429
|
|
|
170
|
-
const result = buildEnvironment(
|
|
430
|
+
const result = buildEnvironment(
|
|
431
|
+
appEnvironmentVars,
|
|
432
|
+
discoveredResources
|
|
433
|
+
);
|
|
171
434
|
|
|
172
435
|
expect(result.DATABASE_HOST).toBe('cluster.example.com');
|
|
173
436
|
expect(result.DATABASE_PORT).toBe('5432');
|
|
@@ -176,12 +439,18 @@ describe('Environment Builder', () => {
|
|
|
176
439
|
it('should add database secret ARN if discovered', () => {
|
|
177
440
|
const appEnvironmentVars = {};
|
|
178
441
|
const discoveredResources = {
|
|
179
|
-
databaseSecretArn:
|
|
442
|
+
databaseSecretArn:
|
|
443
|
+
'arn:aws:secretsmanager:us-east-1:123456:secret:db-secret',
|
|
180
444
|
};
|
|
181
445
|
|
|
182
|
-
const result = buildEnvironment(
|
|
446
|
+
const result = buildEnvironment(
|
|
447
|
+
appEnvironmentVars,
|
|
448
|
+
discoveredResources
|
|
449
|
+
);
|
|
183
450
|
|
|
184
|
-
expect(result.DATABASE_SECRET_ARN).toBe(
|
|
451
|
+
expect(result.DATABASE_SECRET_ARN).toBe(
|
|
452
|
+
'arn:aws:secretsmanager:us-east-1:123456:secret:db-secret'
|
|
453
|
+
);
|
|
185
454
|
});
|
|
186
455
|
|
|
187
456
|
it('should combine all discovered resources', () => {
|
|
@@ -192,19 +461,27 @@ describe('Environment Builder', () => {
|
|
|
192
461
|
kmsKeyArn: 'arn:aws:kms:us-east-1:123456:key/abc',
|
|
193
462
|
auroraClusterEndpoint: 'db.example.com',
|
|
194
463
|
auroraPort: 3306,
|
|
195
|
-
databaseSecretArn:
|
|
464
|
+
databaseSecretArn:
|
|
465
|
+
'arn:aws:secretsmanager:us-east-1:123456:secret:db',
|
|
196
466
|
};
|
|
197
467
|
|
|
198
|
-
const result = buildEnvironment(
|
|
468
|
+
const result = buildEnvironment(
|
|
469
|
+
appEnvironmentVars,
|
|
470
|
+
discoveredResources
|
|
471
|
+
);
|
|
199
472
|
|
|
200
473
|
expect(result.CUSTOM_VAR).toBe("${env:CUSTOM_VAR, ''}");
|
|
201
474
|
expect(result.FRIGG_STACK).toBe('${self:service}');
|
|
202
475
|
expect(result.FRIGG_STAGE).toBe('${self:provider.stage}');
|
|
203
476
|
expect(result.FRIGG_REGION).toBe('${self:provider.region}');
|
|
204
|
-
expect(result.KMS_KEY_ARN).toBe(
|
|
477
|
+
expect(result.KMS_KEY_ARN).toBe(
|
|
478
|
+
'arn:aws:kms:us-east-1:123456:key/abc'
|
|
479
|
+
);
|
|
205
480
|
expect(result.DATABASE_HOST).toBe('db.example.com');
|
|
206
481
|
expect(result.DATABASE_PORT).toBe('3306');
|
|
207
|
-
expect(result.DATABASE_SECRET_ARN).toBe(
|
|
482
|
+
expect(result.DATABASE_SECRET_ARN).toBe(
|
|
483
|
+
'arn:aws:secretsmanager:us-east-1:123456:secret:db'
|
|
484
|
+
);
|
|
208
485
|
});
|
|
209
486
|
|
|
210
487
|
it('should handle empty discoveredResources', () => {
|
|
@@ -213,7 +490,10 @@ describe('Environment Builder', () => {
|
|
|
213
490
|
};
|
|
214
491
|
const discoveredResources = {};
|
|
215
492
|
|
|
216
|
-
const result = buildEnvironment(
|
|
493
|
+
const result = buildEnvironment(
|
|
494
|
+
appEnvironmentVars,
|
|
495
|
+
discoveredResources
|
|
496
|
+
);
|
|
217
497
|
|
|
218
498
|
expect(result.API_KEY).toBe("${env:API_KEY, ''}");
|
|
219
499
|
expect(result.FRIGG_STACK).toBe('${self:service}');
|
|
@@ -237,11 +517,13 @@ describe('Environment Builder', () => {
|
|
|
237
517
|
auroraPort: 3306, // Number
|
|
238
518
|
};
|
|
239
519
|
|
|
240
|
-
const result = buildEnvironment(
|
|
520
|
+
const result = buildEnvironment(
|
|
521
|
+
appEnvironmentVars,
|
|
522
|
+
discoveredResources
|
|
523
|
+
);
|
|
241
524
|
|
|
242
525
|
expect(result.DATABASE_PORT).toBe('3306'); // String
|
|
243
526
|
expect(typeof result.DATABASE_PORT).toBe('string');
|
|
244
527
|
});
|
|
245
528
|
});
|
|
246
529
|
});
|
|
247
|
-
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-function environment scoping (ADR-027)
|
|
3
|
+
*
|
|
4
|
+
* Builders emit `result.functionEnvironments` — a map of function name to
|
|
5
|
+
* env vars — instead of broadcasting framework vars app-wide through
|
|
6
|
+
* `result.environment`. The composer applies the merged map onto the final
|
|
7
|
+
* function definitions after every function (base + builder) exists.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether builders should scope framework env vars per function. Skipped in
|
|
12
|
+
* local mode: the serverless-plugin injects LocalStack queue URLs at
|
|
13
|
+
* provider level only, and function-level values would shadow them.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} appDefinition
|
|
16
|
+
* @returns {boolean}
|
|
17
|
+
*/
|
|
18
|
+
function isScopedEnvironmentActive(appDefinition = {}) {
|
|
19
|
+
if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') return false;
|
|
20
|
+
return appDefinition.lambda?.scopedEnvironment === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply a merged functionEnvironments map onto the composed functions.
|
|
25
|
+
* A key a builder already set directly on a function wins (e.g. the
|
|
26
|
+
* admin-script router's own SCHEDULER_ROLE_ARN must not be clobbered by
|
|
27
|
+
* the integration-scheduler value). An unknown function name is a hard
|
|
28
|
+
* error — silently dropping a var would surface as a runtime failure.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} functions - definition.functions (mutated)
|
|
31
|
+
* @param {Object} functionEnvironments - { fnName: { KEY: value } }
|
|
32
|
+
*/
|
|
33
|
+
function applyFunctionEnvironments(functions, functionEnvironments = {}) {
|
|
34
|
+
for (const [fnName, env] of Object.entries(functionEnvironments)) {
|
|
35
|
+
const fn = functions[fnName];
|
|
36
|
+
if (!fn) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`functionEnvironments targets unknown function '${fnName}' (known: ${Object.keys(
|
|
39
|
+
functions
|
|
40
|
+
).join(', ')})`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
fn.environment = { ...env, ...(fn.environment || {}) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Function names the integration builder creates for one integration.
|
|
49
|
+
* Wire contract: must stay in sync with
|
|
50
|
+
* IntegrationBuilder.createFunctionDefinitions — a drift here surfaces as a
|
|
51
|
+
* hard unknown-function error at compose time, not a silent var drop.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} integration - entry from appDefinition.integrations
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
function getIntegrationFunctionNames(integration) {
|
|
57
|
+
const name = integration.Definition.name;
|
|
58
|
+
const names = [name, `${name}QueueWorker`];
|
|
59
|
+
|
|
60
|
+
const webhooks = integration.Definition.webhooks;
|
|
61
|
+
if (webhooks === true || webhooks?.enabled === true) {
|
|
62
|
+
names.splice(1, 0, `${name}Webhook`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [bindingKey, binding] of Object.entries(
|
|
66
|
+
integration.Definition.extensions || {}
|
|
67
|
+
)) {
|
|
68
|
+
const routes = binding?.extension?.routes || [];
|
|
69
|
+
if (routes.length === 0) continue;
|
|
70
|
+
names.push(`${name}__${String(bindingKey).replace(/[^A-Za-z0-9]/g, '')}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return names;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Admin-script functions, when the feature is on (mirrors
|
|
78
|
+
* AdminScriptBuilder.shouldExecute). Both can instantiate arbitrary
|
|
79
|
+
* integrations, so they belong in every integration's queue-URL consumer
|
|
80
|
+
* set.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} appDefinition
|
|
83
|
+
* @returns {string[]}
|
|
84
|
+
*/
|
|
85
|
+
function getAdminFunctionNames(appDefinition = {}) {
|
|
86
|
+
return Array.isArray(appDefinition.adminScripts) &&
|
|
87
|
+
appDefinition.adminScripts.length > 0
|
|
88
|
+
? ['adminScriptRouter', 'adminScriptExecutor']
|
|
89
|
+
: [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
isScopedEnvironmentActive,
|
|
94
|
+
applyFunctionEnvironments,
|
|
95
|
+
getIntegrationFunctionNames,
|
|
96
|
+
getAdminFunctionNames,
|
|
97
|
+
};
|