env-secrets 0.5.2 → 0.5.3
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/.github/workflows/deploy-docs.yml +1 -1
- package/__e2e__/aws-exec-args.test.ts +97 -1
- package/__e2e__/utils/test-utils.ts +78 -0
- package/__tests__/index.test.ts +208 -58
- package/dist/index.js +15 -4
- package/docker-compose.yaml +1 -1
- package/package.json +5 -5
- package/src/index.ts +20 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cliWithEnv } from './utils/test-utils';
|
|
1
|
+
import { cliWithEnv, cliWithRealSpawn } from './utils/test-utils';
|
|
2
2
|
import { registerAwsE2eContext } from './utils/aws-e2e-context';
|
|
3
3
|
|
|
4
4
|
describe('AWS Program Execution CLI Args', () => {
|
|
@@ -42,3 +42,99 @@ describe('AWS Program Execution CLI Args', () => {
|
|
|
42
42
|
expect(envVars.API_KEY).toBe('secret123');
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
describe('AWS Real Spawn Execution (no NODE_ENV=test)', () => {
|
|
47
|
+
const { createTestSecret, getLocalStackEnv } = registerAwsE2eContext();
|
|
48
|
+
|
|
49
|
+
test('injected env vars are visible to the spawned child process (shell mode)', async () => {
|
|
50
|
+
const secret = await createTestSecret({
|
|
51
|
+
name: `test-secret-realspawn-${Date.now()}`,
|
|
52
|
+
value: '{"INJECTED_KEY": "injected_value"}',
|
|
53
|
+
description: 'Real spawn env injection test'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// printenv avoids any shell-quoting complexity in the test command string
|
|
57
|
+
const result = await cliWithRealSpawn(
|
|
58
|
+
['aws', '-s', secret.prefixedName, '--', 'printenv', 'INJECTED_KEY'],
|
|
59
|
+
getLocalStackEnv()
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(result.code).toBe(0);
|
|
63
|
+
expect(result.stdout.trim()).toBe('injected_value');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('exit code of child process is propagated on success', async () => {
|
|
67
|
+
const secret = await createTestSecret({
|
|
68
|
+
name: `test-secret-exitcode-ok-${Date.now()}`,
|
|
69
|
+
value: '{"DUMMY": "1"}',
|
|
70
|
+
description: 'Exit code propagation test (success)'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Use --no-shell so node -e args are passed directly without shell re-parsing
|
|
74
|
+
const result = await cliWithRealSpawn(
|
|
75
|
+
[
|
|
76
|
+
'aws',
|
|
77
|
+
'-s',
|
|
78
|
+
secret.prefixedName,
|
|
79
|
+
'--no-shell',
|
|
80
|
+
'--',
|
|
81
|
+
'node',
|
|
82
|
+
'-e',
|
|
83
|
+
'process.exit(0)'
|
|
84
|
+
],
|
|
85
|
+
getLocalStackEnv()
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result.code).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('exit code of child process is propagated on failure', async () => {
|
|
92
|
+
const secret = await createTestSecret({
|
|
93
|
+
name: `test-secret-exitcode-fail-${Date.now()}`,
|
|
94
|
+
value: '{"DUMMY": "1"}',
|
|
95
|
+
description: 'Exit code propagation test (failure)'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Use --no-shell so node -e args are passed directly without shell re-parsing
|
|
99
|
+
const result = await cliWithRealSpawn(
|
|
100
|
+
[
|
|
101
|
+
'aws',
|
|
102
|
+
'-s',
|
|
103
|
+
secret.prefixedName,
|
|
104
|
+
'--no-shell',
|
|
105
|
+
'--',
|
|
106
|
+
'node',
|
|
107
|
+
'-e',
|
|
108
|
+
'process.exit(42)'
|
|
109
|
+
],
|
|
110
|
+
getLocalStackEnv()
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(result.code).toBe(42);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('--no-shell passes args directly and env is injected', async () => {
|
|
117
|
+
const secret = await createTestSecret({
|
|
118
|
+
name: `test-secret-noshell-${Date.now()}`,
|
|
119
|
+
value: '{"INJECTED_KEY": "direct_value"}',
|
|
120
|
+
description: 'No-shell spawn test'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// printenv avoids any shell-quoting complexity in the test command string
|
|
124
|
+
const result = await cliWithRealSpawn(
|
|
125
|
+
[
|
|
126
|
+
'aws',
|
|
127
|
+
'-s',
|
|
128
|
+
secret.prefixedName,
|
|
129
|
+
'--no-shell',
|
|
130
|
+
'--',
|
|
131
|
+
'printenv',
|
|
132
|
+
'INJECTED_KEY'
|
|
133
|
+
],
|
|
134
|
+
getLocalStackEnv()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(result.code).toBe(0);
|
|
138
|
+
expect(result.stdout.trim()).toBe('direct_value');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -654,6 +654,84 @@ export function restoreTestProfile(
|
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
/**
|
|
658
|
+
* Like cliWithEnv but does NOT set NODE_ENV=test, so the real spawn path in
|
|
659
|
+
* src/index.ts is exercised instead of the early-return test branch.
|
|
660
|
+
*/
|
|
661
|
+
export async function cliWithRealSpawn(
|
|
662
|
+
args: string[],
|
|
663
|
+
env: Record<string, string>,
|
|
664
|
+
cwd = '.'
|
|
665
|
+
): Promise<CliResult> {
|
|
666
|
+
return new Promise((resolve) => {
|
|
667
|
+
const cleanEnv = { ...process.env };
|
|
668
|
+
delete cleanEnv.AWS_PROFILE;
|
|
669
|
+
delete cleanEnv.AWS_DEFAULT_PROFILE;
|
|
670
|
+
delete cleanEnv.AWS_SESSION_TOKEN;
|
|
671
|
+
delete cleanEnv.AWS_SECURITY_TOKEN;
|
|
672
|
+
delete cleanEnv.AWS_ROLE_ARN;
|
|
673
|
+
delete cleanEnv.AWS_ROLE_SESSION_NAME;
|
|
674
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
|
|
675
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
|
|
676
|
+
// Deliberately omit NODE_ENV=test so real spawn is used
|
|
677
|
+
delete cleanEnv.NODE_ENV;
|
|
678
|
+
|
|
679
|
+
const defaultEnv = {
|
|
680
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
681
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
682
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
683
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
684
|
+
AWS_REGION: 'us-east-1'
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const envVars = { ...cleanEnv, ...defaultEnv, ...env };
|
|
688
|
+
const cliPath = path.resolve('./dist/index');
|
|
689
|
+
const spawnArgs = [cliPath, ...args];
|
|
690
|
+
const command = `node ${cliPath} ${args.join(' ')}`;
|
|
691
|
+
|
|
692
|
+
debugLog(`Running CLI command (real spawn): ${command}`);
|
|
693
|
+
|
|
694
|
+
const child = spawn('node', spawnArgs, { cwd, env: envVars });
|
|
695
|
+
|
|
696
|
+
let stdout = '';
|
|
697
|
+
let stderr = '';
|
|
698
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
699
|
+
stdout += chunk.toString();
|
|
700
|
+
});
|
|
701
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
702
|
+
stderr += chunk.toString();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
|
|
706
|
+
const exitCode = code ?? (signal ? 1 : 0);
|
|
707
|
+
const errorMessage = signal
|
|
708
|
+
? `Process terminated by signal ${signal}`
|
|
709
|
+
: exitCode !== 0
|
|
710
|
+
? `Process exited with code ${exitCode}`
|
|
711
|
+
: null;
|
|
712
|
+
const result = {
|
|
713
|
+
code: exitCode,
|
|
714
|
+
error: errorMessage ? new Error(errorMessage) : null,
|
|
715
|
+
stdout,
|
|
716
|
+
stderr
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
if (exitCode !== 0) {
|
|
720
|
+
debugError(
|
|
721
|
+
signal
|
|
722
|
+
? `CLI command failed: terminated by signal ${signal}`
|
|
723
|
+
: `CLI command failed with code ${exitCode}`
|
|
724
|
+
);
|
|
725
|
+
debugError(`Command: ${command}`);
|
|
726
|
+
debugError(`Stdout: ${result.stdout}`);
|
|
727
|
+
debugError(`Stderr: ${result.stderr}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
resolve(result);
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
657
735
|
export async function checkAwslocalInstalled(): Promise<void> {
|
|
658
736
|
try {
|
|
659
737
|
await execAwslocalCommand('awslocal --version', {});
|
package/__tests__/index.test.ts
CHANGED
|
@@ -39,6 +39,24 @@ const mockObjectToExport = objectToExport as jest.MockedFunction<
|
|
|
39
39
|
typeof objectToExport
|
|
40
40
|
>;
|
|
41
41
|
|
|
42
|
+
// Build a ChildProcess-like mock that records event handlers
|
|
43
|
+
function makeChildMock() {
|
|
44
|
+
const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
|
|
45
|
+
const child = {
|
|
46
|
+
stdio: 'inherit',
|
|
47
|
+
on: jest.fn((event: string, cb: (...args: unknown[]) => void) => {
|
|
48
|
+
if (!handlers[event]) handlers[event] = [];
|
|
49
|
+
handlers[event].push(cb);
|
|
50
|
+
return child;
|
|
51
|
+
}),
|
|
52
|
+
emit(event: string, ...args: unknown[]) {
|
|
53
|
+
(handlers[event] ?? []).forEach((cb) => cb(...args));
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
} as any;
|
|
57
|
+
return child;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
describe('index.ts CLI functionality', () => {
|
|
43
61
|
beforeEach(() => {
|
|
44
62
|
jest.clearAllMocks();
|
|
@@ -94,43 +112,67 @@ describe('index.ts CLI functionality', () => {
|
|
|
94
112
|
);
|
|
95
113
|
});
|
|
96
114
|
|
|
97
|
-
it('should spawn
|
|
115
|
+
it('should spawn using shell (default) with joined command string', async () => {
|
|
98
116
|
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
99
117
|
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
stdio: 'inherit'
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
-
} as any;
|
|
106
|
-
mockSpawn.mockReturnValue(mockChildProcess);
|
|
119
|
+
const child = makeChildMock();
|
|
120
|
+
mockSpawn.mockReturnValue(child);
|
|
107
121
|
|
|
108
122
|
const program = ['node', 'script.js', 'arg1', 'arg2'];
|
|
109
|
-
const options = { secret: 'my-secret' };
|
|
123
|
+
const options = { secret: 'my-secret', shell: true };
|
|
110
124
|
|
|
111
|
-
// Simulate the action logic
|
|
112
125
|
let env = await mockSecretsmanager(options);
|
|
113
126
|
env = Object.assign({}, process.env, env);
|
|
114
127
|
|
|
115
128
|
if (program && program.length > 0) {
|
|
116
|
-
mockSpawn(program
|
|
129
|
+
mockSpawn(program.join(' '), [], {
|
|
117
130
|
stdio: 'inherit',
|
|
118
131
|
shell: true,
|
|
119
132
|
env
|
|
120
133
|
});
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
expect(mockSpawn).toHaveBeenCalledWith(
|
|
124
|
-
'
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
expect(mockSpawn).toHaveBeenCalledWith('node script.js arg1 arg2', [], {
|
|
137
|
+
stdio: 'inherit',
|
|
138
|
+
shell: true,
|
|
139
|
+
env: expect.objectContaining({
|
|
140
|
+
SECRET_KEY: 'secret_value'
|
|
141
|
+
})
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should spawn without shell when --no-shell is passed', async () => {
|
|
146
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
147
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
148
|
+
|
|
149
|
+
const child = makeChildMock();
|
|
150
|
+
mockSpawn.mockReturnValue(child);
|
|
151
|
+
|
|
152
|
+
const program = ['node', 'script.js', 'arg1'];
|
|
153
|
+
const options = { secret: 'my-secret', shell: false };
|
|
154
|
+
|
|
155
|
+
let env = await mockSecretsmanager(options);
|
|
156
|
+
env = Object.assign({}, process.env, env);
|
|
157
|
+
|
|
158
|
+
if (program && program.length > 0) {
|
|
159
|
+
if (options.shell) {
|
|
160
|
+
mockSpawn(program.join(' '), [], {
|
|
161
|
+
stdio: 'inherit',
|
|
162
|
+
shell: true,
|
|
163
|
+
env
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
mockSpawn(program[0], program.slice(1), { stdio: 'inherit', env });
|
|
132
167
|
}
|
|
133
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
expect(mockSpawn).toHaveBeenCalledWith('node', ['script.js', 'arg1'], {
|
|
171
|
+
stdio: 'inherit',
|
|
172
|
+
env: expect.objectContaining({
|
|
173
|
+
SECRET_KEY: 'secret_value'
|
|
174
|
+
})
|
|
175
|
+
});
|
|
134
176
|
});
|
|
135
177
|
|
|
136
178
|
it('should not spawn a program when no program is provided', async () => {
|
|
@@ -143,7 +185,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
143
185
|
|
|
144
186
|
const program: string[] = [];
|
|
145
187
|
if (program && program.length > 0) {
|
|
146
|
-
mockSpawn(program
|
|
188
|
+
mockSpawn(program.join(' '), [], {
|
|
147
189
|
stdio: 'inherit',
|
|
148
190
|
shell: true,
|
|
149
191
|
env
|
|
@@ -163,7 +205,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
163
205
|
|
|
164
206
|
const program: string[] = [];
|
|
165
207
|
if (program && program.length > 0) {
|
|
166
|
-
mockSpawn(program
|
|
208
|
+
mockSpawn(program.join(' '), [], {
|
|
167
209
|
stdio: 'inherit',
|
|
168
210
|
shell: true,
|
|
169
211
|
env
|
|
@@ -173,26 +215,21 @@ describe('index.ts CLI functionality', () => {
|
|
|
173
215
|
expect(mockSpawn).not.toHaveBeenCalled();
|
|
174
216
|
});
|
|
175
217
|
|
|
176
|
-
it('should handle single program argument', async () => {
|
|
218
|
+
it('should handle single program argument (shell mode)', async () => {
|
|
177
219
|
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
178
220
|
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
179
221
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
stdio: 'inherit'
|
|
183
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
-
} as any;
|
|
185
|
-
mockSpawn.mockReturnValue(mockChildProcess);
|
|
222
|
+
const child = makeChildMock();
|
|
223
|
+
mockSpawn.mockReturnValue(child);
|
|
186
224
|
|
|
187
225
|
const program = ['echo'];
|
|
188
|
-
const options = { secret: 'my-secret' };
|
|
226
|
+
const options = { secret: 'my-secret', shell: true };
|
|
189
227
|
|
|
190
|
-
// Simulate the action logic
|
|
191
228
|
let env = await mockSecretsmanager(options);
|
|
192
229
|
env = Object.assign({}, process.env, env);
|
|
193
230
|
|
|
194
231
|
if (program && program.length > 0) {
|
|
195
|
-
mockSpawn(program
|
|
232
|
+
mockSpawn(program.join(' '), [], {
|
|
196
233
|
stdio: 'inherit',
|
|
197
234
|
shell: true,
|
|
198
235
|
env
|
|
@@ -219,13 +256,12 @@ describe('index.ts CLI functionality', () => {
|
|
|
219
256
|
|
|
220
257
|
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
221
258
|
|
|
222
|
-
// Simulate the action logic
|
|
223
259
|
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
224
260
|
env = Object.assign({}, process.env, env);
|
|
225
261
|
|
|
226
262
|
const program = ['echo'];
|
|
227
263
|
if (program && program.length > 0) {
|
|
228
|
-
mockSpawn(program
|
|
264
|
+
mockSpawn(program.join(' '), [], {
|
|
229
265
|
stdio: 'inherit',
|
|
230
266
|
shell: true,
|
|
231
267
|
env
|
|
@@ -247,13 +283,12 @@ describe('index.ts CLI functionality', () => {
|
|
|
247
283
|
it('should handle secretsmanager returning empty object', async () => {
|
|
248
284
|
mockSecretsmanager.mockResolvedValue({});
|
|
249
285
|
|
|
250
|
-
// Simulate the action logic
|
|
251
286
|
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
252
287
|
env = Object.assign({}, process.env, env);
|
|
253
288
|
|
|
254
289
|
const program = ['echo'];
|
|
255
290
|
if (program && program.length > 0) {
|
|
256
|
-
mockSpawn(program
|
|
291
|
+
mockSpawn(program.join(' '), [], {
|
|
257
292
|
stdio: 'inherit',
|
|
258
293
|
shell: true,
|
|
259
294
|
env
|
|
@@ -276,6 +311,128 @@ describe('index.ts CLI functionality', () => {
|
|
|
276
311
|
'AWS connection failed'
|
|
277
312
|
);
|
|
278
313
|
});
|
|
314
|
+
|
|
315
|
+
describe('ChildProcess error and exit handling', () => {
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
318
|
+
throw new Error('process.exit called');
|
|
319
|
+
});
|
|
320
|
+
jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
afterEach(() => {
|
|
324
|
+
jest.restoreAllMocks();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should print error message and exit 1 on child process error', async () => {
|
|
328
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
329
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
330
|
+
|
|
331
|
+
const child = makeChildMock();
|
|
332
|
+
mockSpawn.mockReturnValue(child);
|
|
333
|
+
|
|
334
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
335
|
+
env = Object.assign({}, process.env, env);
|
|
336
|
+
|
|
337
|
+
const program = ['nonexistent-cmd'];
|
|
338
|
+
mockSpawn(program.join(' '), [], {
|
|
339
|
+
stdio: 'inherit',
|
|
340
|
+
shell: true,
|
|
341
|
+
env
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Simulate attaching handlers as src/index.ts does
|
|
345
|
+
child.on('error', (err: Error) => {
|
|
346
|
+
// eslint-disable-next-line no-console
|
|
347
|
+
console.error(`Failed to start process: ${err.message}`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(() => child.emit('error', new Error('spawn ENOENT'))).toThrow(
|
|
352
|
+
'process.exit called'
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// eslint-disable-next-line no-console
|
|
356
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
357
|
+
'Failed to start process: spawn ENOENT'
|
|
358
|
+
);
|
|
359
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should propagate child exit code 0', async () => {
|
|
363
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
364
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
365
|
+
|
|
366
|
+
const child = makeChildMock();
|
|
367
|
+
mockSpawn.mockReturnValue(child);
|
|
368
|
+
|
|
369
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
370
|
+
env = Object.assign({}, process.env, env);
|
|
371
|
+
|
|
372
|
+
mockSpawn(['node', '-e', '"process.exit(0)"'].join(' '), [], {
|
|
373
|
+
stdio: 'inherit',
|
|
374
|
+
shell: true,
|
|
375
|
+
env
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
child.on('exit', (code: number | null, signal: string | null) => {
|
|
379
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(() => child.emit('exit', 0, null)).toThrow(
|
|
383
|
+
'process.exit called'
|
|
384
|
+
);
|
|
385
|
+
expect(process.exit).toHaveBeenCalledWith(0);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should propagate non-zero child exit code', async () => {
|
|
389
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
390
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
391
|
+
|
|
392
|
+
const child = makeChildMock();
|
|
393
|
+
mockSpawn.mockReturnValue(child);
|
|
394
|
+
|
|
395
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
396
|
+
env = Object.assign({}, process.env, env);
|
|
397
|
+
|
|
398
|
+
mockSpawn(['node', '-e', '"process.exit(42)"'].join(' '), [], {
|
|
399
|
+
stdio: 'inherit',
|
|
400
|
+
shell: true,
|
|
401
|
+
env
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
child.on('exit', (code: number | null, signal: string | null) => {
|
|
405
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(() => child.emit('exit', 42, null)).toThrow(
|
|
409
|
+
'process.exit called'
|
|
410
|
+
);
|
|
411
|
+
expect(process.exit).toHaveBeenCalledWith(42);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should exit 1 when child is killed by a signal', async () => {
|
|
415
|
+
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
416
|
+
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
417
|
+
|
|
418
|
+
const child = makeChildMock();
|
|
419
|
+
mockSpawn.mockReturnValue(child);
|
|
420
|
+
|
|
421
|
+
let env = await mockSecretsmanager({ secret: 'my-secret' });
|
|
422
|
+
env = Object.assign({}, process.env, env);
|
|
423
|
+
|
|
424
|
+
mockSpawn('sleep 10', [], { stdio: 'inherit', shell: true, env });
|
|
425
|
+
|
|
426
|
+
child.on('exit', (code: number | null, signal: string | null) => {
|
|
427
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(() => child.emit('exit', null, 'SIGTERM')).toThrow(
|
|
431
|
+
'process.exit called'
|
|
432
|
+
);
|
|
433
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
279
436
|
});
|
|
280
437
|
|
|
281
438
|
describe('Debug logging', () => {
|
|
@@ -314,12 +471,8 @@ describe('index.ts CLI functionality', () => {
|
|
|
314
471
|
const mockEnv = { SECRET_KEY: 'secret_value' };
|
|
315
472
|
mockSecretsmanager.mockResolvedValue(mockEnv);
|
|
316
473
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
stdio: 'inherit'
|
|
320
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
-
} as any;
|
|
322
|
-
mockSpawn.mockReturnValue(mockChildProcess);
|
|
474
|
+
const child = makeChildMock();
|
|
475
|
+
mockSpawn.mockReturnValue(child);
|
|
323
476
|
|
|
324
477
|
const program = ['node', 'script.js', 'arg1'];
|
|
325
478
|
|
|
@@ -331,7 +484,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
331
484
|
// Simulate debug logging
|
|
332
485
|
mockDebugInstance(`${program[0]} ${program.slice(1).join(' ')}`);
|
|
333
486
|
|
|
334
|
-
mockSpawn(program
|
|
487
|
+
mockSpawn(program.join(' '), [], {
|
|
335
488
|
stdio: 'inherit',
|
|
336
489
|
shell: true,
|
|
337
490
|
env
|
|
@@ -438,16 +591,12 @@ describe('index.ts CLI functionality', () => {
|
|
|
438
591
|
const mockSecrets = { SECRET_KEY: 'secret_value' };
|
|
439
592
|
mockSecretsmanager.mockResolvedValue(mockSecrets);
|
|
440
593
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const options: { secret: string; output?: string } = {
|
|
449
|
-
secret: 'my-secret'
|
|
450
|
-
// No output option
|
|
594
|
+
const child = makeChildMock();
|
|
595
|
+
mockSpawn.mockReturnValue(child);
|
|
596
|
+
|
|
597
|
+
const options: { secret: string; output?: string; shell: boolean } = {
|
|
598
|
+
secret: 'my-secret',
|
|
599
|
+
shell: true
|
|
451
600
|
};
|
|
452
601
|
|
|
453
602
|
const program = ['echo', 'hello'];
|
|
@@ -474,7 +623,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
474
623
|
const env = Object.assign({}, process.env, secrets);
|
|
475
624
|
|
|
476
625
|
if (program && program.length > 0) {
|
|
477
|
-
mockSpawn(program
|
|
626
|
+
mockSpawn(program.join(' '), [], {
|
|
478
627
|
stdio: 'inherit',
|
|
479
628
|
shell: true,
|
|
480
629
|
env
|
|
@@ -483,7 +632,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
483
632
|
}
|
|
484
633
|
|
|
485
634
|
expect(mockSecretsmanager).toHaveBeenCalledWith(options);
|
|
486
|
-
expect(mockSpawn).toHaveBeenCalledWith('echo', [
|
|
635
|
+
expect(mockSpawn).toHaveBeenCalledWith('echo hello', [], {
|
|
487
636
|
stdio: 'inherit',
|
|
488
637
|
shell: true,
|
|
489
638
|
env: expect.objectContaining({
|
|
@@ -538,9 +687,10 @@ describe('index.ts CLI functionality', () => {
|
|
|
538
687
|
mockObjectToExport.mockReturnValue(mockEnvContent);
|
|
539
688
|
mockExistsSync.mockReturnValue(false);
|
|
540
689
|
|
|
541
|
-
const options: { secret: string; output: string } = {
|
|
690
|
+
const options: { secret: string; output: string; shell: boolean } = {
|
|
542
691
|
secret: 'my-secret',
|
|
543
|
-
output: '/tmp/secrets.env'
|
|
692
|
+
output: '/tmp/secrets.env',
|
|
693
|
+
shell: true
|
|
544
694
|
};
|
|
545
695
|
|
|
546
696
|
const program = ['echo', 'hello'];
|
|
@@ -566,7 +716,7 @@ describe('index.ts CLI functionality', () => {
|
|
|
566
716
|
const env = Object.assign({}, process.env, secrets);
|
|
567
717
|
|
|
568
718
|
if (program && program.length > 0) {
|
|
569
|
-
mockSpawn(program
|
|
719
|
+
mockSpawn(program.join(' '), [], {
|
|
570
720
|
stdio: 'inherit',
|
|
571
721
|
shell: true,
|
|
572
722
|
env
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,7 @@ const awsCommand = program
|
|
|
91
91
|
.option('-p, --profile <profile>', 'profile to use')
|
|
92
92
|
.option('-r, --region <region>', 'region to use')
|
|
93
93
|
.option('-o, --output <file>', 'output secrets to file instead of environment variables')
|
|
94
|
+
.option('--no-shell', 'run program directly without a shell (disables shell expansion)')
|
|
94
95
|
.action((program, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
96
|
if (!options.secret) {
|
|
96
97
|
exitWithError(new Error('Missing required option --secret for this command.'));
|
|
@@ -123,10 +124,20 @@ const awsCommand = program
|
|
|
123
124
|
console.log(JSON.stringify(env));
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
const child = options.shell
|
|
128
|
+
? (0, node_child_process_1.spawn)(program.join(' '), [], {
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
shell: true,
|
|
131
|
+
env
|
|
132
|
+
})
|
|
133
|
+
: (0, node_child_process_1.spawn)(program[0], program.slice(1), { stdio: 'inherit', env });
|
|
134
|
+
child.on('error', (err) => {
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.error(`Failed to start process: ${err.message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
child.on('exit', (code, signal) => {
|
|
140
|
+
process.exit(signal ? 1 : code !== null && code !== void 0 ? code : 0);
|
|
130
141
|
});
|
|
131
142
|
}
|
|
132
143
|
}
|
package/docker-compose.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "env-secrets",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "get secrets from a secrets vault and inject them into the running environment",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Mark C Allen (@markcallen)",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@everydaydevopsio/ballast": "^3.2.1",
|
|
34
|
-
"@types/debug": "^4.1.
|
|
34
|
+
"@types/debug": "^4.1.13",
|
|
35
35
|
"@types/jest": "^29.5.14",
|
|
36
36
|
"@types/node": "^18.19.130",
|
|
37
37
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"typescript": "^4.9.5"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@aws-sdk/client-secrets-manager": "^3.
|
|
57
|
-
"@aws-sdk/client-sts": "^3.
|
|
58
|
-
"@aws-sdk/credential-providers": "^3.
|
|
56
|
+
"@aws-sdk/client-secrets-manager": "^3.1030.0",
|
|
57
|
+
"@aws-sdk/client-sts": "^3.1030.0",
|
|
58
|
+
"@aws-sdk/credential-providers": "^3.1030.0",
|
|
59
59
|
"commander": "^9.5.0",
|
|
60
60
|
"debug": "^4.4.3"
|
|
61
61
|
},
|
package/src/index.ts
CHANGED
|
@@ -121,6 +121,10 @@ const awsCommand = program
|
|
|
121
121
|
'-o, --output <file>',
|
|
122
122
|
'output secrets to file instead of environment variables'
|
|
123
123
|
)
|
|
124
|
+
.option(
|
|
125
|
+
'--no-shell',
|
|
126
|
+
'run program directly without a shell (disables shell expansion)'
|
|
127
|
+
)
|
|
124
128
|
.action(async (program, options) => {
|
|
125
129
|
if (!options.secret) {
|
|
126
130
|
exitWithError(
|
|
@@ -163,10 +167,22 @@ const awsCommand = program
|
|
|
163
167
|
return;
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const child = options.shell
|
|
171
|
+
? spawn(program.join(' '), [], {
|
|
172
|
+
stdio: 'inherit',
|
|
173
|
+
shell: true,
|
|
174
|
+
env
|
|
175
|
+
})
|
|
176
|
+
: spawn(program[0], program.slice(1), { stdio: 'inherit', env });
|
|
177
|
+
|
|
178
|
+
child.on('error', (err) => {
|
|
179
|
+
// eslint-disable-next-line no-console
|
|
180
|
+
console.error(`Failed to start process: ${err.message}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
child.on('exit', (code, signal) => {
|
|
185
|
+
process.exit(signal ? 1 : code ?? 0);
|
|
170
186
|
});
|
|
171
187
|
}
|
|
172
188
|
}
|