@friggframework/devtools 2.0.0-next.100 → 2.0.0-next.102
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 +43 -7
- package/infrastructure/domains/shared/environment-builder.test.js +97 -1
- 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
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
* 3. Generated resource references
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const { isSsmOffloadActive, getOffloadedKeys } = require('../parameters/offload-utils');
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Get environment variables from AppDefinition
|
|
14
|
-
*
|
|
16
|
+
*
|
|
15
17
|
* Extracts environment variable definitions where value is true,
|
|
16
18
|
* and creates Serverless variable references.
|
|
17
|
-
*
|
|
19
|
+
*
|
|
20
|
+
* A value of 'ssm' offloads the variable to Parameter Store when offload is
|
|
21
|
+
* active (see SsmBuilder), keeping it out of the Lambda env map. When offload
|
|
22
|
+
* is not active (local mode / ssm disabled) it falls back to the same
|
|
23
|
+
* `${env:KEY, ''}` reference as `true` so `frigg start` + dotenv keeps working.
|
|
24
|
+
*
|
|
18
25
|
* @param {Object} appDefinition - Application definition
|
|
19
26
|
* @returns {Object} Environment variable mappings
|
|
20
27
|
*/
|
|
@@ -38,16 +45,38 @@ function getAppEnvironmentVars(appDefinition) {
|
|
|
38
45
|
'AWS_SESSION_TOKEN',
|
|
39
46
|
]);
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
return envVars;
|
|
43
|
-
}
|
|
48
|
+
const environment = appDefinition.environment || {};
|
|
44
49
|
|
|
45
50
|
console.log('📋 Loading environment variables from appDefinition...');
|
|
46
51
|
const envKeys = [];
|
|
47
52
|
const skippedKeys = [];
|
|
53
|
+
const offloadedKeys = [];
|
|
54
|
+
const offloadActive = isSsmOffloadActive(appDefinition);
|
|
48
55
|
|
|
49
|
-
for (const [key, value] of Object.entries(
|
|
50
|
-
if (value
|
|
56
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
57
|
+
if (value === 'ssm' && offloadActive) {
|
|
58
|
+
offloadedKeys.push(key);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (value !== true && value !== 'ssm') continue;
|
|
62
|
+
if (reservedVars.has(key)) {
|
|
63
|
+
skippedKeys.push(key);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
envVars[key] = `\${env:${key}, ''}`;
|
|
67
|
+
envKeys.push(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Keys declared only in ssm.parameters (no matching `environment` entry)
|
|
71
|
+
// get the same local-fallback treatment as `environment`-valued 'ssm' keys.
|
|
72
|
+
const ssmOnlyKeys = getOffloadedKeys(appDefinition).filter(
|
|
73
|
+
(key) => !(key in environment)
|
|
74
|
+
);
|
|
75
|
+
for (const key of ssmOnlyKeys) {
|
|
76
|
+
if (offloadActive) {
|
|
77
|
+
offloadedKeys.push(key);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
51
80
|
if (reservedVars.has(key)) {
|
|
52
81
|
skippedKeys.push(key);
|
|
53
82
|
continue;
|
|
@@ -69,6 +98,13 @@ function getAppEnvironmentVars(appDefinition) {
|
|
|
69
98
|
} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
|
|
70
99
|
);
|
|
71
100
|
}
|
|
101
|
+
if (offloadedKeys.length > 0) {
|
|
102
|
+
console.log(
|
|
103
|
+
` 🔒 Offloaded ${offloadedKeys.length} variables to SSM: ${offloadedKeys.join(
|
|
104
|
+
', '
|
|
105
|
+
)}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
72
108
|
|
|
73
109
|
return envVars;
|
|
74
110
|
}
|
|
@@ -38,7 +38,7 @@ describe('Environment Builder', () => {
|
|
|
38
38
|
expect(result.DISABLED_VAR).toBeUndefined();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it(
|
|
41
|
+
it("ignores non-boolean values other than the meaningful 'ssm' marker", () => {
|
|
42
42
|
const appDefinition = {
|
|
43
43
|
environment: {
|
|
44
44
|
VALID: true,
|
|
@@ -108,6 +108,102 @@ describe('Environment Builder', () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
describe("getAppEnvironmentVars() - 'ssm' offload", () => {
|
|
112
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
if (originalSkipDiscovery === undefined) {
|
|
116
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
117
|
+
} else {
|
|
118
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("excludes 'ssm' keys when offload is active", () => {
|
|
123
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
124
|
+
const appDefinition = {
|
|
125
|
+
ssm: { enable: true },
|
|
126
|
+
environment: { FOO: 'ssm', BAR: true },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
130
|
+
|
|
131
|
+
expect(result.FOO).toBeUndefined();
|
|
132
|
+
expect(result.BAR).toBe("${env:BAR, ''}");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("falls back to env reference for 'ssm' keys when FRIGG_SKIP_AWS_DISCOVERY is set", () => {
|
|
136
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
137
|
+
const appDefinition = {
|
|
138
|
+
ssm: { enable: true },
|
|
139
|
+
environment: { FOO: 'ssm' },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
143
|
+
|
|
144
|
+
expect(result.FOO).toBe("${env:FOO, ''}");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("falls back to env reference for 'ssm' keys when ssm is disabled", () => {
|
|
148
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
149
|
+
const appDefinition = {
|
|
150
|
+
ssm: { enable: false },
|
|
151
|
+
environment: { FOO: 'ssm' },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
155
|
+
|
|
156
|
+
expect(result.FOO).toBe("${env:FOO, ''}");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('falls back to env reference for a key declared only in ssm.parameters when FRIGG_SKIP_AWS_DISCOVERY is set', () => {
|
|
160
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
161
|
+
const appDefinition = {
|
|
162
|
+
ssm: {
|
|
163
|
+
enable: true,
|
|
164
|
+
parameters: { HUBSPOT_CLIENT_SECRET: { type: 'SecureString' } },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
169
|
+
|
|
170
|
+
expect(result.HUBSPOT_CLIENT_SECRET).toBe("${env:HUBSPOT_CLIENT_SECRET, ''}");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('excludes a key declared only in ssm.parameters when offload is active', () => {
|
|
174
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
175
|
+
const appDefinition = {
|
|
176
|
+
ssm: {
|
|
177
|
+
enable: true,
|
|
178
|
+
parameters: { HUBSPOT_CLIENT_SECRET: { type: 'SecureString' } },
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
183
|
+
|
|
184
|
+
expect(result.HUBSPOT_CLIENT_SECRET).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does not double-process a key present in both environment:'ssm' and ssm.parameters", () => {
|
|
188
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
189
|
+
const appDefinition = {
|
|
190
|
+
ssm: {
|
|
191
|
+
enable: true,
|
|
192
|
+
parameters: { FOO: { type: 'SecureString' } },
|
|
193
|
+
},
|
|
194
|
+
environment: { FOO: 'ssm' },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = getAppEnvironmentVars(appDefinition);
|
|
198
|
+
|
|
199
|
+
expect(result.FOO).toBeUndefined();
|
|
200
|
+
|
|
201
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
202
|
+
const fallbackResult = getAppEnvironmentVars(appDefinition);
|
|
203
|
+
expect(fallbackResult.FOO).toBe("${env:FOO, ''}");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
111
207
|
describe('buildEnvironment()', () => {
|
|
112
208
|
it('should combine app vars with standard Frigg variables', () => {
|
|
113
209
|
const appEnvironmentVars = {
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const {
|
|
2
|
+
isScopedEnvironmentActive,
|
|
3
|
+
applyFunctionEnvironments,
|
|
4
|
+
} = require('./function-environments');
|
|
5
|
+
|
|
6
|
+
describe('function-environments', () => {
|
|
7
|
+
const originalSkipDiscovery = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
if (originalSkipDiscovery === undefined) {
|
|
11
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
12
|
+
} else {
|
|
13
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = originalSkipDiscovery;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('isScopedEnvironmentActive', () => {
|
|
18
|
+
it('is true only when lambda.scopedEnvironment is set', () => {
|
|
19
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
20
|
+
expect(
|
|
21
|
+
isScopedEnvironmentActive({
|
|
22
|
+
lambda: { scopedEnvironment: true },
|
|
23
|
+
})
|
|
24
|
+
).toBe(true);
|
|
25
|
+
expect(isScopedEnvironmentActive({})).toBe(false);
|
|
26
|
+
expect(
|
|
27
|
+
isScopedEnvironmentActive({
|
|
28
|
+
lambda: { scopedEnvironment: false },
|
|
29
|
+
})
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('is false in local mode: the serverless-plugin injects LocalStack queue URLs at provider level only', () => {
|
|
34
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
|
|
35
|
+
expect(
|
|
36
|
+
isScopedEnvironmentActive({
|
|
37
|
+
lambda: { scopedEnvironment: true },
|
|
38
|
+
})
|
|
39
|
+
).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('getIntegrationFunctionNames', () => {
|
|
44
|
+
const { getIntegrationFunctionNames } = require('./function-environments');
|
|
45
|
+
|
|
46
|
+
it('always includes the router and queue worker', () => {
|
|
47
|
+
expect(
|
|
48
|
+
getIntegrationFunctionNames({ Definition: { name: 'hubspot' } })
|
|
49
|
+
).toEqual(['hubspot', 'hubspotQueueWorker']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('includes the webhook handler when webhooks are enabled', () => {
|
|
53
|
+
expect(
|
|
54
|
+
getIntegrationFunctionNames({
|
|
55
|
+
Definition: { name: 'hubspot', webhooks: true },
|
|
56
|
+
})
|
|
57
|
+
).toContain('hubspotWebhook');
|
|
58
|
+
expect(
|
|
59
|
+
getIntegrationFunctionNames({
|
|
60
|
+
Definition: { name: 'hubspot', webhooks: { enabled: true } },
|
|
61
|
+
})
|
|
62
|
+
).toContain('hubspotWebhook');
|
|
63
|
+
expect(
|
|
64
|
+
getIntegrationFunctionNames({
|
|
65
|
+
Definition: { name: 'hubspot', webhooks: { enabled: false } },
|
|
66
|
+
})
|
|
67
|
+
).not.toContain('hubspotWebhook');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('includes one function per routed extension binding, sanitized', () => {
|
|
71
|
+
const names = getIntegrationFunctionNames({
|
|
72
|
+
Definition: {
|
|
73
|
+
name: 'hubspot',
|
|
74
|
+
extensions: {
|
|
75
|
+
'my-ext': { extension: { routes: [{ path: '/x', method: 'GET' }] } },
|
|
76
|
+
routeless: { extension: { routes: [] } },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expect(names).toContain('hubspot__myext');
|
|
81
|
+
expect(names).not.toContain('hubspot__routeless');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('getAdminFunctionNames', () => {
|
|
86
|
+
const { getAdminFunctionNames } = require('./function-environments');
|
|
87
|
+
|
|
88
|
+
it('returns the admin functions only when admin scripts are configured', () => {
|
|
89
|
+
expect(
|
|
90
|
+
getAdminFunctionNames({ adminScripts: [{ Definition: { name: 'x' } }] })
|
|
91
|
+
).toEqual(['adminScriptRouter', 'adminScriptExecutor']);
|
|
92
|
+
expect(getAdminFunctionNames({})).toEqual([]);
|
|
93
|
+
expect(getAdminFunctionNames({ adminScripts: [] })).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('applyFunctionEnvironments', () => {
|
|
98
|
+
const makeFunctions = () => ({
|
|
99
|
+
auth: { handler: 'auth.handler' },
|
|
100
|
+
hubspot: {
|
|
101
|
+
handler: 'hubspot.handler',
|
|
102
|
+
environment: { EXISTING: 'builder-set' },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('assigns scoped env vars onto the target functions', () => {
|
|
107
|
+
const functions = makeFunctions();
|
|
108
|
+
applyFunctionEnvironments(functions, {
|
|
109
|
+
auth: { HUBSPOT_QUEUE_URL: 'url-1' },
|
|
110
|
+
hubspot: { HUBSPOT_QUEUE_URL: 'url-1' },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(functions.auth.environment).toEqual({
|
|
114
|
+
HUBSPOT_QUEUE_URL: 'url-1',
|
|
115
|
+
});
|
|
116
|
+
expect(functions.hubspot.environment).toEqual({
|
|
117
|
+
EXISTING: 'builder-set',
|
|
118
|
+
HUBSPOT_QUEUE_URL: 'url-1',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('never clobbers a key a builder already set directly on the function', () => {
|
|
123
|
+
const functions = makeFunctions();
|
|
124
|
+
applyFunctionEnvironments(functions, {
|
|
125
|
+
hubspot: { EXISTING: 'scoped-value', OTHER: 'x' },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(functions.hubspot.environment.EXISTING).toBe('builder-set');
|
|
129
|
+
expect(functions.hubspot.environment.OTHER).toBe('x');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('throws on an unknown function name instead of silently dropping vars', () => {
|
|
133
|
+
expect(() =>
|
|
134
|
+
applyFunctionEnvironments(makeFunctions(), {
|
|
135
|
+
typoFunction: { A: '1' },
|
|
136
|
+
})
|
|
137
|
+
).toThrow(/typoFunction/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('is a no-op for an empty map', () => {
|
|
141
|
+
const functions = makeFunctions();
|
|
142
|
+
applyFunctionEnvironments(functions, {});
|
|
143
|
+
expect(functions.auth.environment).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -20,6 +20,44 @@ const { SchedulerBuilder } = require('./domains/scheduler/scheduler-builder');
|
|
|
20
20
|
const { AdminScriptBuilder } = require('./domains/admin-scripts/admin-script-builder');
|
|
21
21
|
|
|
22
22
|
// Utilities
|
|
23
|
+
const { applyFunctionEnvironments } = require('./domains/shared/function-environments');
|
|
24
|
+
const {
|
|
25
|
+
isSsmOffloadActive,
|
|
26
|
+
SSM_PRELOAD_NODE_OPTIONS,
|
|
27
|
+
} = require('./domains/parameters/offload-utils');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load the SSM INIT preload (NODE_OPTIONS=--import) on skipEsbuild handlers
|
|
31
|
+
* only. Those package the full node_modules tree, so the preload .mjs is
|
|
32
|
+
* present at /var/task. esbuild-bundled functions (e.g. defaultWebsocket,
|
|
33
|
+
* adopter custom functions) do NOT ship it — and a missing --import target is a
|
|
34
|
+
* fatal Node startup error — so they are left with the handler-time loader
|
|
35
|
+
* fallback instead.
|
|
36
|
+
*
|
|
37
|
+
* Set at function scope (function env wins over provider env), APPENDED to any
|
|
38
|
+
* NODE_OPTIONS already on the function or provider — so an app's own flags
|
|
39
|
+
* (OTel auto-instrumentation, source maps, memory tuning) survive instead of
|
|
40
|
+
* being clobbered. A function-level assignment shadows provider env in Lambda,
|
|
41
|
+
* so the provider value must be folded in here. Only a value already in the
|
|
42
|
+
* definition is appended — never a synthesized ${env:NODE_OPTIONS}, which would
|
|
43
|
+
* leak the deploy host's shell into every Lambda.
|
|
44
|
+
*/
|
|
45
|
+
function applySsmPreloadNodeOptions(appDefinition, functions, providerEnvironment = {}) {
|
|
46
|
+
if (!isSsmOffloadActive(appDefinition)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const fn of Object.values(functions)) {
|
|
50
|
+
if (!fn.skipEsbuild) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
fn.environment = fn.environment || {};
|
|
54
|
+
const existing =
|
|
55
|
+
fn.environment.NODE_OPTIONS ?? providerEnvironment.NODE_OPTIONS;
|
|
56
|
+
fn.environment.NODE_OPTIONS = existing
|
|
57
|
+
? `${existing} ${SSM_PRELOAD_NODE_OPTIONS}`
|
|
58
|
+
: SSM_PRELOAD_NODE_OPTIONS;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
23
61
|
const { modifyHandlerPaths } = require('./domains/shared/utilities/handler-path-resolver');
|
|
24
62
|
const { createBaseDefinition } = require('./domains/shared/utilities/base-definition-factory');
|
|
25
63
|
const { ensurePrismaLayerExists } = require('./domains/shared/utilities/prisma-layer-manager');
|
|
@@ -75,6 +113,15 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
75
113
|
definition.provider.iamRoleStatements.push(...merged.iamStatements);
|
|
76
114
|
Object.assign(definition.provider.environment, merged.environment);
|
|
77
115
|
Object.assign(definition.functions, merged.functions);
|
|
116
|
+
applyFunctionEnvironments(
|
|
117
|
+
definition.functions,
|
|
118
|
+
merged.functionEnvironments
|
|
119
|
+
);
|
|
120
|
+
applySsmPreloadNodeOptions(
|
|
121
|
+
AppDefinition,
|
|
122
|
+
definition.functions,
|
|
123
|
+
definition.provider.environment
|
|
124
|
+
);
|
|
78
125
|
|
|
79
126
|
if (merged.vpcConfig) {
|
|
80
127
|
definition.provider.vpc = merged.vpcConfig;
|
|
@@ -117,5 +164,5 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
117
164
|
return definition;
|
|
118
165
|
};
|
|
119
166
|
|
|
120
|
-
module.exports = { composeServerlessDefinition };
|
|
167
|
+
module.exports = { composeServerlessDefinition, applySsmPreloadNodeOptions };
|
|
121
168
|
|