@friggframework/devtools 2.0.0--canary.623.b39b42d.0 → 2.0.0--canary.625.950ba82.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/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 +270 -0
- package/frigg-cli/ssm-command/index.test.js +301 -0
- package/infrastructure/__tests__/helpers/test-utils.js +3 -5
- package/infrastructure/__tests__/scoped-environment.test.js +126 -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-resolver.js +12 -3
- package/infrastructure/domains/networking/vpc-resolver.test.js +40 -0
- package/infrastructure/domains/parameters/offload-utils.js +175 -0
- package/infrastructure/domains/parameters/offload-utils.test.js +193 -0
- package/infrastructure/domains/parameters/ssm-builder.js +60 -14
- package/infrastructure/domains/parameters/ssm-builder.test.js +145 -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 +33 -1
- package/infrastructure/domains/security/iam-generator.test.js +73 -0
- package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +15 -0
- package/infrastructure/domains/security/templates/iam-policy-full.json +7 -2
- 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 +5 -0
- package/infrastructure/infrastructure-composer.test.js +69 -14
- package/infrastructure/integration.test.js +3 -5
- package/package.json +8 -7
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test suite for generate-iam command
|
|
3
|
+
*
|
|
4
|
+
* Drives generateIamCommand end-to-end (real getFeatureSummary +
|
|
5
|
+
* generateIAMCloudFormation) to verify appDefinition.ssm.kmsKeyArn
|
|
6
|
+
* actually reaches the generated CloudFormation template. A unit test
|
|
7
|
+
* that hand-supplies ssmKmsKeyArn straight to generateIAMCloudFormation
|
|
8
|
+
* would not catch a caller that forgets to thread it through.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
jest.mock('fs-extra');
|
|
12
|
+
jest.mock('@friggframework/core', () => ({
|
|
13
|
+
findNearestBackendPackageJson: jest.fn()
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs-extra');
|
|
18
|
+
const { findNearestBackendPackageJson } = require('@friggframework/core');
|
|
19
|
+
const { generateIamCommand } = require('../../../generate-iam-command');
|
|
20
|
+
|
|
21
|
+
describe('CLI Command: generate-iam', () => {
|
|
22
|
+
const mockBackendPath = '/mock/backend/package.json';
|
|
23
|
+
const mockBackendDir = '/mock/backend';
|
|
24
|
+
const mockAppDefinitionPath = path.join(mockBackendDir, 'index.js');
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.resetModules();
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
|
|
30
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {});
|
|
31
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
32
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
33
|
+
|
|
34
|
+
findNearestBackendPackageJson.mockReturnValue(mockBackendPath);
|
|
35
|
+
|
|
36
|
+
fs.existsSync = jest.fn().mockReturnValue(true);
|
|
37
|
+
fs.ensureDir = jest.fn().mockResolvedValue();
|
|
38
|
+
fs.writeFile = jest.fn().mockResolvedValue();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
jest.restoreAllMocks();
|
|
43
|
+
jest.dontMock(mockAppDefinitionPath);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('scopes the SSM-mediated KMS grant to ssm.kmsKeyArn when configured', async () => {
|
|
47
|
+
const mockAppDefinition = {
|
|
48
|
+
name: 'test-app',
|
|
49
|
+
ssm: {
|
|
50
|
+
enable: true,
|
|
51
|
+
kmsKeyArn:
|
|
52
|
+
'arn:aws:kms:us-east-1:123456789012:key/abcd-1234'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
jest.doMock(
|
|
57
|
+
mockAppDefinitionPath,
|
|
58
|
+
() => ({ Definition: mockAppDefinition }),
|
|
59
|
+
{ virtual: true }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await generateIamCommand({});
|
|
63
|
+
|
|
64
|
+
expect(fs.writeFile).toHaveBeenCalled();
|
|
65
|
+
const [, generatedYaml] = fs.writeFile.mock.calls[0];
|
|
66
|
+
|
|
67
|
+
expect(generatedYaml).toContain('FriggSSMParameterKMSEncryption');
|
|
68
|
+
expect(generatedYaml).toContain(
|
|
69
|
+
'arn:aws:kms:us-east-1:123456789012:key/abcd-1234'
|
|
70
|
+
);
|
|
71
|
+
expect(generatedYaml).not.toContain(
|
|
72
|
+
'arn:aws:kms:*:${AWS::AccountId}:key/*'
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('falls back to the account-wide KMS wildcard when ssm.kmsKeyArn is not set', async () => {
|
|
77
|
+
const mockAppDefinition = {
|
|
78
|
+
name: 'test-app',
|
|
79
|
+
ssm: { enable: true }
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
jest.doMock(
|
|
83
|
+
mockAppDefinitionPath,
|
|
84
|
+
() => ({ Definition: mockAppDefinition }),
|
|
85
|
+
{ virtual: true }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await generateIamCommand({});
|
|
89
|
+
|
|
90
|
+
expect(fs.writeFile).toHaveBeenCalled();
|
|
91
|
+
const [, generatedYaml] = fs.writeFile.mock.calls[0];
|
|
92
|
+
|
|
93
|
+
expect(generatedYaml).toContain(
|
|
94
|
+
'arn:aws:kms:*:${AWS::AccountId}:key/*'
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -267,12 +267,56 @@ async function runPostDeploymentHealthCheck(stackName, options) {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Push SSM-offloaded parameters before deploying so they exist before new
|
|
272
|
+
* code cold-starts (ADR-027). A push failure aborts the deploy: shipping
|
|
273
|
+
* code whose parameters are missing would fail every cold start anyway.
|
|
274
|
+
*/
|
|
275
|
+
async function pushOffloadedParametersOrAbort(appDefinition, options) {
|
|
276
|
+
const {
|
|
277
|
+
getOffloadedKeys,
|
|
278
|
+
} = require('../../infrastructure/domains/parameters/offload-utils');
|
|
279
|
+
if (!appDefinition || getOffloadedKeys(appDefinition).length === 0) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
require('dotenv').config();
|
|
284
|
+
const { pushOffloadedParameters } = require('../ssm-command');
|
|
285
|
+
|
|
286
|
+
console.log('🔒 Pushing SSM-offloaded parameters before deploy...');
|
|
287
|
+
try {
|
|
288
|
+
const { pushed } = await pushOffloadedParameters(
|
|
289
|
+
appDefinition,
|
|
290
|
+
options.stage,
|
|
291
|
+
options
|
|
292
|
+
);
|
|
293
|
+
console.log(` ✓ ${pushed.length} parameter(s) up to date`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(`\n✗ SSM parameter push failed: ${error.message}`);
|
|
296
|
+
const pushedBeforeFailure = error.pushed || [];
|
|
297
|
+
if (pushedBeforeFailure.length === 0) {
|
|
298
|
+
console.error(' Deployment aborted — no parameters were changed.');
|
|
299
|
+
} else {
|
|
300
|
+
const names = pushedBeforeFailure.map((p) => p.name).join(', ');
|
|
301
|
+
console.error(
|
|
302
|
+
` Deployment aborted, but ${pushedBeforeFailure.length} parameter(s) were already pushed and are now live in Parameter Store: ${names}`
|
|
303
|
+
);
|
|
304
|
+
console.error(
|
|
305
|
+
' Those values will take effect on old Lambda code at its next SSM cache TTL refresh. Redeploy soon so the running code matches.'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
270
312
|
async function deployCommand(options) {
|
|
271
313
|
console.log('Deploying the serverless application...');
|
|
272
314
|
|
|
273
315
|
const appDefinition = loadAppDefinition();
|
|
274
316
|
const environment = validateAndBuildEnvironment(appDefinition, options);
|
|
275
317
|
|
|
318
|
+
await pushOffloadedParametersOrAbort(appDefinition, options);
|
|
319
|
+
|
|
276
320
|
// Execute deployment
|
|
277
321
|
const exitCode = await executeServerlessDeployment(environment, options);
|
|
278
322
|
|
|
@@ -120,7 +120,8 @@ async function generateCommand(options = {}) {
|
|
|
120
120
|
appName,
|
|
121
121
|
features,
|
|
122
122
|
userPrefix: options.user || 'frigg-deployment-user',
|
|
123
|
-
stackName: options.stackName || 'frigg-deployment-iam'
|
|
123
|
+
stackName: options.stackName || 'frigg-deployment-iam',
|
|
124
|
+
ssmKmsKeyArn: appDefinition.ssm?.kmsKeyArn
|
|
124
125
|
});
|
|
125
126
|
fileExtension = 'yaml';
|
|
126
127
|
deploymentInstructions = generateCloudFormationInstructions(options);
|
package/frigg-cli/index.js
CHANGED
|
@@ -86,6 +86,7 @@ const { dbSetupCommand } = require('./db-setup-command');
|
|
|
86
86
|
const { doctorCommand } = require('./doctor-command');
|
|
87
87
|
const { repairCommand } = require('./repair-command');
|
|
88
88
|
const { authCommand } = require('./auth-command');
|
|
89
|
+
const { ssmPushCommand } = require('./ssm-command');
|
|
89
90
|
|
|
90
91
|
const program = new Command();
|
|
91
92
|
|
|
@@ -169,6 +170,20 @@ program
|
|
|
169
170
|
.option('-v, --verbose', 'enable verbose output')
|
|
170
171
|
.action(repairCommand);
|
|
171
172
|
|
|
173
|
+
// SSM command group for the parameter offload feature (ADR-027)
|
|
174
|
+
const ssmProgram = program
|
|
175
|
+
.command('ssm')
|
|
176
|
+
.description('Manage SSM Parameter Store offloaded configuration');
|
|
177
|
+
|
|
178
|
+
ssmProgram
|
|
179
|
+
.command('push')
|
|
180
|
+
.description('Push SSM-offloaded environment values to Parameter Store')
|
|
181
|
+
.option('-s, --stage <stage>', 'deployment stage', 'dev')
|
|
182
|
+
.option('-r, --region <region>', 'AWS region (defaults to AWS_REGION env var or us-east-1)')
|
|
183
|
+
.option('--allow-empty', 'skip (instead of fail on) keys with missing or empty values')
|
|
184
|
+
.option('--tier <tier>', 'override tier for all keys: standard (4KB values) or advanced (8KB, billed)')
|
|
185
|
+
.action(ssmPushCommand);
|
|
186
|
+
|
|
172
187
|
// Auth command group for testing API module authentication
|
|
173
188
|
const authProgram = program
|
|
174
189
|
.command('auth')
|
|
@@ -205,4 +220,4 @@ authProgram
|
|
|
205
220
|
|
|
206
221
|
program.parse(process.argv);
|
|
207
222
|
|
|
208
|
-
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand, authCommand };
|
|
223
|
+
module.exports = { initCommand, installCommand, startCommand, buildCommand, deployCommand, generateIamCommand, uiCommand, dbSetupCommand, doctorCommand, repairCommand, authCommand, ssmPushCommand };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frigg ssm push
|
|
3
|
+
*
|
|
4
|
+
* Writes SSM-offloaded environment values (see ADR-027) from the CLI
|
|
5
|
+
* process environment into Parameter Store. Runs automatically before
|
|
6
|
+
* `frigg deploy` so parameters exist before new code cold-starts; also
|
|
7
|
+
* available standalone to rotate values without deploying.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const dotenv = require('dotenv');
|
|
12
|
+
const {
|
|
13
|
+
getOffloadedKeys,
|
|
14
|
+
resolveParameterPrefix,
|
|
15
|
+
validateOffloadConfig,
|
|
16
|
+
} = require('../../infrastructure/domains/parameters/offload-utils');
|
|
17
|
+
|
|
18
|
+
const TIER_LIMITS = {
|
|
19
|
+
standard: 4096,
|
|
20
|
+
advanced: 8192,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const THROTTLE_BACKOFF_MS = [100, 200, 400];
|
|
24
|
+
|
|
25
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Send a PutParameterCommand, retrying ThrottlingException with short
|
|
29
|
+
* exponential backoff (mirrors sendWithRetry in core/parameters-to-env.js).
|
|
30
|
+
*/
|
|
31
|
+
async function putParameterWithRetry(client, PutParameterCommand, input) {
|
|
32
|
+
let attempt = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
try {
|
|
35
|
+
return await client.send(new PutParameterCommand(input));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (
|
|
38
|
+
error.name === 'ThrottlingException' &&
|
|
39
|
+
attempt < THROTTLE_BACKOFF_MS.length
|
|
40
|
+
) {
|
|
41
|
+
await sleep(THROTTLE_BACKOFF_MS[attempt]);
|
|
42
|
+
attempt += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the tier for a single offloaded key. An explicit `--tier` CLI
|
|
52
|
+
* option overrides every key; otherwise each key uses its own
|
|
53
|
+
* ssm.parameters[KEY].tier, defaulting to 'standard'.
|
|
54
|
+
*/
|
|
55
|
+
function resolveKeyTier(appDefinition, key, options = {}) {
|
|
56
|
+
const raw =
|
|
57
|
+
options.tier ||
|
|
58
|
+
appDefinition.ssm?.parameters?.[key]?.tier ||
|
|
59
|
+
'standard';
|
|
60
|
+
const tier = String(raw).toLowerCase();
|
|
61
|
+
if (!TIER_LIMITS[tier]) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Unknown parameter tier '${raw}' for ${key}. Use 'standard' or 'advanced'.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return tier;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the PutParameter specs for every offloaded key, reading values
|
|
71
|
+
* from process.env. Throws on invalid config, missing values (unless
|
|
72
|
+
* allowEmpty), or values that exceed their own tier limit.
|
|
73
|
+
*/
|
|
74
|
+
function collectParameterSpecs(appDefinition, stage, options = {}) {
|
|
75
|
+
const { errors: configErrors } = validateOffloadConfig(appDefinition);
|
|
76
|
+
if (configErrors.length > 0) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Invalid SSM offload configuration:\n - ${configErrors.join(
|
|
79
|
+
'\n - '
|
|
80
|
+
)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const keys = getOffloadedKeys(appDefinition);
|
|
85
|
+
if (keys.length === 0) {
|
|
86
|
+
return { specs: [], skipped: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const prefix = resolveParameterPrefix(appDefinition, stage);
|
|
90
|
+
const specs = [];
|
|
91
|
+
const skipped = [];
|
|
92
|
+
const missing = [];
|
|
93
|
+
const oversized = [];
|
|
94
|
+
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
const value = process.env[key];
|
|
97
|
+
|
|
98
|
+
if (value === undefined || value === '') {
|
|
99
|
+
if (options.allowEmpty) {
|
|
100
|
+
skipped.push(key);
|
|
101
|
+
} else {
|
|
102
|
+
missing.push(key);
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const tier = resolveKeyTier(appDefinition, key, options);
|
|
108
|
+
const maxBytes = TIER_LIMITS[tier];
|
|
109
|
+
const bytes = Buffer.byteLength(value, 'utf8');
|
|
110
|
+
if (bytes > maxBytes) {
|
|
111
|
+
oversized.push(
|
|
112
|
+
`${key} (${bytes} bytes > ${tier} tier limit ${maxBytes})`
|
|
113
|
+
);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
specs.push({
|
|
118
|
+
key,
|
|
119
|
+
name: `${prefix}/${key}`,
|
|
120
|
+
value,
|
|
121
|
+
type: appDefinition.ssm?.parameters?.[key]?.type || 'String',
|
|
122
|
+
tier,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (missing.length > 0) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Missing or empty values for offloaded keys: ${missing.join(
|
|
129
|
+
', '
|
|
130
|
+
)}. Set them in the environment (or .env) before pushing, or pass --allow-empty to skip them.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (oversized.length > 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Offloaded values exceed their parameter tier limit: ${oversized.join(
|
|
137
|
+
', '
|
|
138
|
+
)}. Values over ${TIER_LIMITS.standard} bytes need the advanced tier (up to ${TIER_LIMITS.advanced} bytes, billed by AWS): set ssm.parameters.<KEY>.tier: 'advanced' in the app definition, or pass --tier advanced to 'frigg ssm push'.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { specs, skipped };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the region parameters are pushed to. An explicit `--region`
|
|
147
|
+
* option wins; otherwise fall back to AWS_REGION and finally to the same
|
|
148
|
+
* default the composed stack uses ('us-east-1'), so pushes and cold-start
|
|
149
|
+
* reads always target the same region.
|
|
150
|
+
*/
|
|
151
|
+
function resolvePushRegion(options = {}) {
|
|
152
|
+
return options.region || process.env.AWS_REGION || 'us-east-1';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Push all offloaded parameters for an app definition. Silent no-op when
|
|
157
|
+
* the offload set is empty. Returns { pushed, skipped }.
|
|
158
|
+
*/
|
|
159
|
+
async function pushOffloadedParameters(appDefinition, stage, options = {}) {
|
|
160
|
+
const { specs, skipped } = collectParameterSpecs(
|
|
161
|
+
appDefinition,
|
|
162
|
+
stage,
|
|
163
|
+
options
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
for (const key of skipped) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`⚠️ Skipping ${key}: no value in the environment (--allow-empty)`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (specs.length === 0) {
|
|
173
|
+
return { pushed: [], skipped };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { SSMClient, PutParameterCommand } = require('@aws-sdk/client-ssm');
|
|
177
|
+
const client = new SSMClient({
|
|
178
|
+
region: resolvePushRegion(options),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const pushed = [];
|
|
182
|
+
for (const spec of specs) {
|
|
183
|
+
const input = {
|
|
184
|
+
Name: spec.name,
|
|
185
|
+
Value: spec.value,
|
|
186
|
+
Type: spec.type,
|
|
187
|
+
Overwrite: true,
|
|
188
|
+
};
|
|
189
|
+
if (spec.tier === 'advanced') {
|
|
190
|
+
input.Tier = 'Advanced';
|
|
191
|
+
}
|
|
192
|
+
if (spec.type === 'SecureString' && appDefinition.ssm?.kmsKeyArn) {
|
|
193
|
+
input.KeyId = appDefinition.ssm.kmsKeyArn;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const result = await putParameterWithRetry(
|
|
198
|
+
client,
|
|
199
|
+
PutParameterCommand,
|
|
200
|
+
input
|
|
201
|
+
);
|
|
202
|
+
pushed.push({ name: spec.name, version: result.Version });
|
|
203
|
+
console.log(
|
|
204
|
+
` ✅ ${spec.name} (${spec.type}, v${result.Version})`
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const failure = new Error(
|
|
208
|
+
`Failed to push parameter ${spec.name} (${spec.key}): ${error.message}`
|
|
209
|
+
);
|
|
210
|
+
failure.pushed = pushed;
|
|
211
|
+
throw failure;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { pushed, skipped };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function loadAppDefinition() {
|
|
219
|
+
const appDefPath = path.join(process.cwd(), 'index.js');
|
|
220
|
+
const { Definition } = require(appDefPath);
|
|
221
|
+
return Definition;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* `frigg ssm push` command action.
|
|
226
|
+
*/
|
|
227
|
+
async function ssmPushCommand(options) {
|
|
228
|
+
dotenv.config();
|
|
229
|
+
|
|
230
|
+
let appDefinition;
|
|
231
|
+
try {
|
|
232
|
+
appDefinition = loadAppDefinition();
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(
|
|
235
|
+
`✗ Could not load the app definition from ${process.cwd()}/index.js: ${error.message}`
|
|
236
|
+
);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const keys = getOffloadedKeys(appDefinition);
|
|
241
|
+
if (keys.length === 0) {
|
|
242
|
+
console.log(
|
|
243
|
+
'No environment variables are marked for SSM offload — nothing to push.'
|
|
244
|
+
);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(
|
|
249
|
+
`🔒 Pushing ${keys.length} offloaded parameter(s) for stage '${options.stage}'...`
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const { pushed } = await pushOffloadedParameters(
|
|
254
|
+
appDefinition,
|
|
255
|
+
options.stage,
|
|
256
|
+
options
|
|
257
|
+
);
|
|
258
|
+
console.log(`✓ Pushed ${pushed.length} parameter(s)`);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.error(`✗ ${error.message}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
ssmPushCommand,
|
|
267
|
+
pushOffloadedParameters,
|
|
268
|
+
collectParameterSpecs,
|
|
269
|
+
resolvePushRegion,
|
|
270
|
+
};
|