@friggframework/devtools 2.0.0--canary.625.0de0db4.0 → 2.0.0--canary.625.eaeb79b.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.
|
@@ -163,21 +163,59 @@ async function pushOffloadedParameters(appDefinition, stage, options = {}) {
|
|
|
163
163
|
options
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
console.warn(
|
|
168
|
-
`⚠️ Skipping ${key}: no value in the environment (--allow-empty)`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (specs.length === 0) {
|
|
166
|
+
if (specs.length === 0 && skipped.length === 0) {
|
|
173
167
|
return { pushed: [], skipped };
|
|
174
168
|
}
|
|
175
169
|
|
|
176
|
-
const {
|
|
170
|
+
const {
|
|
171
|
+
SSMClient,
|
|
172
|
+
PutParameterCommand,
|
|
173
|
+
GetParametersCommand,
|
|
174
|
+
} = require('@aws-sdk/client-ssm');
|
|
177
175
|
const client = new SSMClient({
|
|
178
176
|
region: resolvePushRegion(options),
|
|
179
177
|
});
|
|
180
178
|
|
|
179
|
+
// --allow-empty only rotates keys that already exist. A skipped key still
|
|
180
|
+
// ships in FRIGG_SSM_OFFLOADED_KEYS, so if its parameter does not exist the
|
|
181
|
+
// runtime fails fast at cold start on every function — a green deploy that
|
|
182
|
+
// bricks the fleet. Verify existence and abort instead.
|
|
183
|
+
if (skipped.length > 0) {
|
|
184
|
+
const prefix = resolveParameterPrefix(appDefinition, stage);
|
|
185
|
+
const names = skipped.map((key) => `${prefix}/${key}`);
|
|
186
|
+
const existing = new Set();
|
|
187
|
+
for (let i = 0; i < names.length; i += 10) {
|
|
188
|
+
const { Parameters = [] } = await client.send(
|
|
189
|
+
new GetParametersCommand({ Names: names.slice(i, i + 10) })
|
|
190
|
+
);
|
|
191
|
+
for (const param of Parameters) {
|
|
192
|
+
existing.add(param.Name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const orphaned = skipped.filter(
|
|
196
|
+
(key) => !existing.has(`${prefix}/${key}`)
|
|
197
|
+
);
|
|
198
|
+
if (orphaned.length > 0) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`--allow-empty skipped ${orphaned.join(
|
|
201
|
+
', '
|
|
202
|
+
)}, but no such parameter exists in Parameter Store. --allow-empty ` +
|
|
203
|
+
`only rotates keys that already have a value; a skipped key with no ` +
|
|
204
|
+
`parameter stays in FRIGG_SSM_OFFLOADED_KEYS and fails every function ` +
|
|
205
|
+
`at cold start. Set a value and push without --allow-empty.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
for (const key of skipped) {
|
|
209
|
+
console.warn(
|
|
210
|
+
`⚠️ Skipping ${key}: no value in the environment; keeping the existing Parameter Store value (--allow-empty)`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (specs.length === 0) {
|
|
216
|
+
return { pushed: [], skipped };
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
const pushed = [];
|
|
182
220
|
for (const spec of specs) {
|
|
183
221
|
const input = {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const { mockClient } = require('aws-sdk-client-mock');
|
|
2
|
-
const {
|
|
2
|
+
const {
|
|
3
|
+
SSMClient,
|
|
4
|
+
PutParameterCommand,
|
|
5
|
+
GetParametersCommand,
|
|
6
|
+
} = require('@aws-sdk/client-ssm');
|
|
3
7
|
const { pushOffloadedParameters, resolvePushRegion } = require('./index');
|
|
4
8
|
|
|
5
9
|
describe('ssm-command pushOffloadedParameters', () => {
|
|
@@ -95,8 +99,11 @@ describe('ssm-command pushOffloadedParameters', () => {
|
|
|
95
99
|
expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
|
|
96
100
|
});
|
|
97
101
|
|
|
98
|
-
it('skips missing values with allowEmpty
|
|
102
|
+
it('skips missing values with allowEmpty when the parameter already exists', async () => {
|
|
99
103
|
delete process.env.MY_SECRET;
|
|
104
|
+
ssmMock.on(GetParametersCommand).resolves({
|
|
105
|
+
Parameters: [{ Name: '/frigg/my-app/dev/MY_SECRET' }],
|
|
106
|
+
});
|
|
100
107
|
|
|
101
108
|
const result = await pushOffloadedParameters(appDefinition, 'dev', {
|
|
102
109
|
allowEmpty: true,
|
|
@@ -105,6 +112,16 @@ describe('ssm-command pushOffloadedParameters', () => {
|
|
|
105
112
|
expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(1);
|
|
106
113
|
});
|
|
107
114
|
|
|
115
|
+
it('aborts when allowEmpty skips a key that does not exist in Parameter Store', async () => {
|
|
116
|
+
delete process.env.MY_SECRET;
|
|
117
|
+
ssmMock.on(GetParametersCommand).resolves({ Parameters: [] });
|
|
118
|
+
|
|
119
|
+
await expect(
|
|
120
|
+
pushOffloadedParameters(appDefinition, 'dev', { allowEmpty: true })
|
|
121
|
+
).rejects.toThrow(/MY_SECRET.*no such parameter|--allow-empty/s);
|
|
122
|
+
expect(ssmMock.commandCalls(PutParameterCommand)).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
108
125
|
it('rejects values over the standard tier limit and suggests advanced', async () => {
|
|
109
126
|
process.env.MY_CONFIG = 'x'.repeat(4097);
|
|
110
127
|
|
|
@@ -32,17 +32,30 @@ const {
|
|
|
32
32
|
* present at /var/task. esbuild-bundled functions (e.g. defaultWebsocket,
|
|
33
33
|
* adopter custom functions) do NOT ship it — and a missing --import target is a
|
|
34
34
|
* fatal Node startup error — so they are left with the handler-time loader
|
|
35
|
-
* fallback instead.
|
|
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.
|
|
36
44
|
*/
|
|
37
|
-
function applySsmPreloadNodeOptions(appDefinition, functions) {
|
|
45
|
+
function applySsmPreloadNodeOptions(appDefinition, functions, providerEnvironment = {}) {
|
|
38
46
|
if (!isSsmOffloadActive(appDefinition)) {
|
|
39
47
|
return;
|
|
40
48
|
}
|
|
41
49
|
for (const fn of Object.values(functions)) {
|
|
42
|
-
if (fn.skipEsbuild) {
|
|
43
|
-
|
|
44
|
-
fn.environment.NODE_OPTIONS = SSM_PRELOAD_NODE_OPTIONS;
|
|
50
|
+
if (!fn.skipEsbuild) {
|
|
51
|
+
continue;
|
|
45
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;
|
|
46
59
|
}
|
|
47
60
|
}
|
|
48
61
|
const { modifyHandlerPaths } = require('./domains/shared/utilities/handler-path-resolver');
|
|
@@ -104,7 +117,11 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
104
117
|
definition.functions,
|
|
105
118
|
merged.functionEnvironments
|
|
106
119
|
);
|
|
107
|
-
applySsmPreloadNodeOptions(
|
|
120
|
+
applySsmPreloadNodeOptions(
|
|
121
|
+
AppDefinition,
|
|
122
|
+
definition.functions,
|
|
123
|
+
definition.provider.environment
|
|
124
|
+
);
|
|
108
125
|
|
|
109
126
|
if (merged.vpcConfig) {
|
|
110
127
|
definition.provider.vpc = merged.vpcConfig;
|
|
@@ -147,5 +164,5 @@ const composeServerlessDefinition = async (AppDefinition) => {
|
|
|
147
164
|
return definition;
|
|
148
165
|
};
|
|
149
166
|
|
|
150
|
-
module.exports = { composeServerlessDefinition };
|
|
167
|
+
module.exports = { composeServerlessDefinition, applySsmPreloadNodeOptions };
|
|
151
168
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
composeServerlessDefinition,
|
|
3
|
+
applySsmPreloadNodeOptions,
|
|
4
|
+
} = require('./infrastructure-composer');
|
|
5
|
+
const {
|
|
6
|
+
SSM_PRELOAD_NODE_OPTIONS,
|
|
7
|
+
} = require('./domains/parameters/offload-utils');
|
|
2
8
|
|
|
3
9
|
// Helper to build discovery responses with overridable fields
|
|
4
10
|
const createDiscoveryResponse = (overrides = {}) => ({
|
|
@@ -1949,3 +1955,88 @@ describe('composeServerlessDefinition', () => {
|
|
|
1949
1955
|
});
|
|
1950
1956
|
});
|
|
1951
1957
|
});
|
|
1958
|
+
|
|
1959
|
+
describe('applySsmPreloadNodeOptions', () => {
|
|
1960
|
+
const appDefinition = {
|
|
1961
|
+
ssm: { enable: true },
|
|
1962
|
+
environment: { FOO: 'ssm' },
|
|
1963
|
+
};
|
|
1964
|
+
const savedSkip = process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
1965
|
+
|
|
1966
|
+
beforeEach(() => {
|
|
1967
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
afterEach(() => {
|
|
1971
|
+
if (savedSkip === undefined) {
|
|
1972
|
+
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
1973
|
+
} else {
|
|
1974
|
+
process.env.FRIGG_SKIP_AWS_DISCOVERY = savedSkip;
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
it('sets the preload NODE_OPTIONS on a skipEsbuild function with no existing value', () => {
|
|
1979
|
+
const functions = { auth: { skipEsbuild: true } };
|
|
1980
|
+
applySsmPreloadNodeOptions(appDefinition, functions);
|
|
1981
|
+
expect(functions.auth.environment.NODE_OPTIONS).toBe(
|
|
1982
|
+
SSM_PRELOAD_NODE_OPTIONS
|
|
1983
|
+
);
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
it('appends to an existing function-level NODE_OPTIONS instead of clobbering it', () => {
|
|
1987
|
+
const functions = {
|
|
1988
|
+
auth: {
|
|
1989
|
+
skipEsbuild: true,
|
|
1990
|
+
environment: { NODE_OPTIONS: '--enable-source-maps' },
|
|
1991
|
+
},
|
|
1992
|
+
};
|
|
1993
|
+
applySsmPreloadNodeOptions(appDefinition, functions);
|
|
1994
|
+
expect(functions.auth.environment.NODE_OPTIONS).toBe(
|
|
1995
|
+
`--enable-source-maps ${SSM_PRELOAD_NODE_OPTIONS}`
|
|
1996
|
+
);
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
it('folds in a provider-level NODE_OPTIONS (which the function env would otherwise shadow)', () => {
|
|
2000
|
+
const functions = { auth: { skipEsbuild: true } };
|
|
2001
|
+
applySsmPreloadNodeOptions(appDefinition, functions, {
|
|
2002
|
+
NODE_OPTIONS: '--require ./otel.js',
|
|
2003
|
+
});
|
|
2004
|
+
expect(functions.auth.environment.NODE_OPTIONS).toBe(
|
|
2005
|
+
`--require ./otel.js ${SSM_PRELOAD_NODE_OPTIONS}`
|
|
2006
|
+
);
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
it('prefers a function-level value over the provider-level one', () => {
|
|
2010
|
+
const functions = {
|
|
2011
|
+
auth: {
|
|
2012
|
+
skipEsbuild: true,
|
|
2013
|
+
environment: { NODE_OPTIONS: '--fn-flag' },
|
|
2014
|
+
},
|
|
2015
|
+
};
|
|
2016
|
+
applySsmPreloadNodeOptions(appDefinition, functions, {
|
|
2017
|
+
NODE_OPTIONS: '--provider-flag',
|
|
2018
|
+
});
|
|
2019
|
+
expect(functions.auth.environment.NODE_OPTIONS).toBe(
|
|
2020
|
+
`--fn-flag ${SSM_PRELOAD_NODE_OPTIONS}`
|
|
2021
|
+
);
|
|
2022
|
+
});
|
|
2023
|
+
|
|
2024
|
+
it('never touches esbuild-bundled functions', () => {
|
|
2025
|
+
const functions = {
|
|
2026
|
+
websocket: { environment: { NODE_OPTIONS: '--keep-me' } },
|
|
2027
|
+
};
|
|
2028
|
+
applySsmPreloadNodeOptions(appDefinition, functions, {
|
|
2029
|
+
NODE_OPTIONS: '--provider',
|
|
2030
|
+
});
|
|
2031
|
+
expect(functions.websocket.environment.NODE_OPTIONS).toBe('--keep-me');
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
it('is a no-op when offload is inactive', () => {
|
|
2035
|
+
const functions = { auth: { skipEsbuild: true } };
|
|
2036
|
+
applySsmPreloadNodeOptions(
|
|
2037
|
+
{ ssm: { enable: true } }, // no offloaded keys → inactive
|
|
2038
|
+
functions
|
|
2039
|
+
);
|
|
2040
|
+
expect(functions.auth.environment).toBeUndefined();
|
|
2041
|
+
});
|
|
2042
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/devtools",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.625.
|
|
4
|
+
"version": "2.0.0--canary.625.eaeb79b.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"frigg": "./frigg-cli/index.js"
|
|
7
7
|
},
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"@babel/eslint-parser": "^7.18.9",
|
|
27
27
|
"@babel/parser": "^7.25.3",
|
|
28
28
|
"@babel/traverse": "^7.25.3",
|
|
29
|
-
"@friggframework/core": "2.0.0--canary.625.
|
|
30
|
-
"@friggframework/schemas": "2.0.0--canary.625.
|
|
31
|
-
"@friggframework/test": "2.0.0--canary.625.
|
|
29
|
+
"@friggframework/core": "2.0.0--canary.625.eaeb79b.0",
|
|
30
|
+
"@friggframework/schemas": "2.0.0--canary.625.eaeb79b.0",
|
|
31
|
+
"@friggframework/test": "2.0.0--canary.625.eaeb79b.0",
|
|
32
32
|
"@hapi/boom": "^10.0.1",
|
|
33
33
|
"@inquirer/prompts": "^5.3.8",
|
|
34
34
|
"axios": "^1.18.0",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"validate-npm-package-name": "^5.0.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@friggframework/eslint-config": "2.0.0--canary.625.
|
|
60
|
-
"@friggframework/prettier-config": "2.0.0--canary.625.
|
|
59
|
+
"@friggframework/eslint-config": "2.0.0--canary.625.eaeb79b.0",
|
|
60
|
+
"@friggframework/prettier-config": "2.0.0--canary.625.eaeb79b.0",
|
|
61
61
|
"aws-sdk-client-mock": "^4.1.0",
|
|
62
62
|
"aws-sdk-client-mock-jest": "^4.1.0",
|
|
63
63
|
"jest": "^30.1.3",
|
|
@@ -89,5 +89,5 @@
|
|
|
89
89
|
"publishConfig": {
|
|
90
90
|
"access": "public"
|
|
91
91
|
},
|
|
92
|
-
"gitHead": "
|
|
92
|
+
"gitHead": "eaeb79b0e8f8bbe466ac40f389d603e86bd806b7"
|
|
93
93
|
}
|