env-secrets 0.5.1 → 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/README.md +6 -0
- package/__e2e__/aws-exec-args.test.ts +97 -1
- package/__e2e__/aws-secret-lifecycle.test.ts +60 -1
- package/__e2e__/utils/test-utils.ts +78 -0
- package/__tests__/index.test.ts +208 -58
- package/dist/index.js +51 -5
- package/docker-compose.yaml +1 -1
- package/docs/AWS.md +7 -0
- package/package.json +5 -5
- package/src/index.ts +65 -5
package/README.md
CHANGED
|
@@ -246,6 +246,12 @@ env-secrets aws secret upsert --file .env --name app/dev --output json
|
|
|
246
246
|
# {"NAME":"secret1"}
|
|
247
247
|
```
|
|
248
248
|
|
|
249
|
+
`env-secrets aws secret create` also always stores `SecretString` as a JSON object:
|
|
250
|
+
|
|
251
|
+
- object JSON input is preserved semantically as an object (formatting and key order may change)
|
|
252
|
+
- dotenv-style input is converted into key/value pairs
|
|
253
|
+
- scalar input is wrapped as `{"value":"..."}`.
|
|
254
|
+
|
|
249
255
|
12. **Append/remove keys on an existing JSON secret:**
|
|
250
256
|
|
|
251
257
|
```bash
|
|
@@ -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
|
+
});
|
|
@@ -5,7 +5,8 @@ import * as path from 'path';
|
|
|
5
5
|
import {
|
|
6
6
|
cliWithEnv,
|
|
7
7
|
cliWithEnvAndStdin,
|
|
8
|
-
cleanupTempFile
|
|
8
|
+
cleanupTempFile,
|
|
9
|
+
execAwslocalCommand
|
|
9
10
|
} from './utils/test-utils';
|
|
10
11
|
import { registerAwsE2eContext } from './utils/aws-e2e-context';
|
|
11
12
|
|
|
@@ -38,6 +39,14 @@ describe('AWS Secret Subcommand Lifecycle Args', () => {
|
|
|
38
39
|
expect(getResult.stdout).toContain(secretName);
|
|
39
40
|
expect(getResult.stdout).not.toContain('initial-value');
|
|
40
41
|
|
|
42
|
+
const createdSecret = await execAwslocalCommand(
|
|
43
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
44
|
+
getLocalStackEnv()
|
|
45
|
+
);
|
|
46
|
+
expect(JSON.parse(createdSecret.stdout.trim())).toEqual({
|
|
47
|
+
value: 'initial-value'
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
const deleteResult = await cliWithEnv(
|
|
42
51
|
[
|
|
43
52
|
'aws',
|
|
@@ -84,6 +93,39 @@ describe('AWS Secret Subcommand Lifecycle Args', () => {
|
|
|
84
93
|
expect(deleteResult.code).toBe(0);
|
|
85
94
|
});
|
|
86
95
|
|
|
96
|
+
test('should create a json secret object from stdin', async () => {
|
|
97
|
+
const secretName = `managed-secret-create-stdin-${Date.now()}`;
|
|
98
|
+
|
|
99
|
+
const createResult = await cliWithEnvAndStdin(
|
|
100
|
+
['aws', 'secret', 'create', '-n', secretName, '--value-stdin'],
|
|
101
|
+
getLocalStackEnv(),
|
|
102
|
+
'stdin-create-value'
|
|
103
|
+
);
|
|
104
|
+
expect(createResult.code).toBe(0);
|
|
105
|
+
|
|
106
|
+
const createdSecret = await execAwslocalCommand(
|
|
107
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
108
|
+
getLocalStackEnv()
|
|
109
|
+
);
|
|
110
|
+
expect(JSON.parse(createdSecret.stdout.trim())).toEqual({
|
|
111
|
+
value: 'stdin-create-value'
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const deleteResult = await cliWithEnv(
|
|
115
|
+
[
|
|
116
|
+
'aws',
|
|
117
|
+
'secret',
|
|
118
|
+
'delete',
|
|
119
|
+
'-n',
|
|
120
|
+
secretName,
|
|
121
|
+
'--force-delete-without-recovery',
|
|
122
|
+
'--yes'
|
|
123
|
+
],
|
|
124
|
+
getLocalStackEnv()
|
|
125
|
+
);
|
|
126
|
+
expect(deleteResult.code).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
87
129
|
test('should create from a single-line env file and retrieve via aws -s', async () => {
|
|
88
130
|
const secretName = `managed-secret-create-single-file-${Date.now()}`;
|
|
89
131
|
const tempFile = path.join(
|
|
@@ -119,6 +161,14 @@ describe('AWS Secret Subcommand Lifecycle Args', () => {
|
|
|
119
161
|
>;
|
|
120
162
|
expect(envVars.GITHUB_PAT).toBe('github_pat_single_line');
|
|
121
163
|
|
|
164
|
+
const createdSecret = await execAwslocalCommand(
|
|
165
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
166
|
+
getLocalStackEnv()
|
|
167
|
+
);
|
|
168
|
+
expect(JSON.parse(createdSecret.stdout.trim())).toEqual({
|
|
169
|
+
GITHUB_PAT: 'github_pat_single_line'
|
|
170
|
+
});
|
|
171
|
+
|
|
122
172
|
const deleteResult = await cliWithEnv(
|
|
123
173
|
[
|
|
124
174
|
'aws',
|
|
@@ -176,6 +226,15 @@ describe('AWS Secret Subcommand Lifecycle Args', () => {
|
|
|
176
226
|
expect(envVars.GITHUB_PAT).toBe('github_pat_multi_line');
|
|
177
227
|
expect(envVars.API_URL).toBe('https://example.com');
|
|
178
228
|
|
|
229
|
+
const createdSecret = await execAwslocalCommand(
|
|
230
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
231
|
+
getLocalStackEnv()
|
|
232
|
+
);
|
|
233
|
+
expect(JSON.parse(createdSecret.stdout.trim())).toEqual({
|
|
234
|
+
GITHUB_PAT: 'github_pat_multi_line',
|
|
235
|
+
API_URL: 'https://example.com'
|
|
236
|
+
});
|
|
237
|
+
|
|
179
238
|
const deleteResult = await cliWithEnv(
|
|
180
239
|
[
|
|
181
240
|
'aws',
|
|
@@ -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
|
@@ -43,6 +43,40 @@ const parseSecretJsonObject = (secretName, value) => {
|
|
|
43
43
|
}
|
|
44
44
|
return parsed;
|
|
45
45
|
};
|
|
46
|
+
const parseEnvToObject = (value) => {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = (0, helpers_1.parseEnvSecrets)(value);
|
|
49
|
+
if (parsed.entries.length === 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return Object.fromEntries(parsed.entries.map((entry) => [entry.key, entry.value]));
|
|
53
|
+
}
|
|
54
|
+
catch (_a) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const toSecretJsonObject = (value) => {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(value);
|
|
61
|
+
if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
if (typeof parsed === 'string') {
|
|
65
|
+
const envPayload = parseEnvToObject(parsed);
|
|
66
|
+
if (envPayload) {
|
|
67
|
+
return envPayload;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { value: parsed };
|
|
71
|
+
}
|
|
72
|
+
catch (_a) {
|
|
73
|
+
const envPayload = parseEnvToObject(value);
|
|
74
|
+
if (envPayload) {
|
|
75
|
+
return envPayload;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { value };
|
|
79
|
+
};
|
|
46
80
|
// main program
|
|
47
81
|
program
|
|
48
82
|
.name('env-secrets')
|
|
@@ -57,6 +91,7 @@ const awsCommand = program
|
|
|
57
91
|
.option('-p, --profile <profile>', 'profile to use')
|
|
58
92
|
.option('-r, --region <region>', 'region to use')
|
|
59
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)')
|
|
60
95
|
.action((program, options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
96
|
if (!options.secret) {
|
|
62
97
|
exitWithError(new Error('Missing required option --secret for this command.'));
|
|
@@ -89,10 +124,20 @@ const awsCommand = program
|
|
|
89
124
|
console.log(JSON.stringify(env));
|
|
90
125
|
return;
|
|
91
126
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
96
141
|
});
|
|
97
142
|
}
|
|
98
143
|
}
|
|
@@ -125,9 +170,10 @@ secretCommand
|
|
|
125
170
|
if (!value) {
|
|
126
171
|
throw new Error('Secret value is required. Provide --value, --value-stdin, or --file.');
|
|
127
172
|
}
|
|
173
|
+
const payload = toSecretJsonObject(value);
|
|
128
174
|
const result = yield (0, secretsmanager_admin_1.createSecret)({
|
|
129
175
|
name: options.name,
|
|
130
|
-
value,
|
|
176
|
+
value: JSON.stringify(payload),
|
|
131
177
|
description: options.description,
|
|
132
178
|
kmsKeyId: options.kmsKeyId,
|
|
133
179
|
tags: options.tag,
|
package/docker-compose.yaml
CHANGED
package/docs/AWS.md
CHANGED
|
@@ -200,6 +200,12 @@ source secrets.env
|
|
|
200
200
|
--output json
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
`create` always writes a JSON object:
|
|
204
|
+
|
|
205
|
+
- object JSON input is preserved semantically as an object (formatting and key order may change) (`{"API_KEY":"abc123"}`)
|
|
206
|
+
- dotenv-style input is converted (`KEY=value` -> `{"KEY":"value"}`)
|
|
207
|
+
- non-object/scalar input is wrapped (`super-secret-value` -> `{"value":"super-secret-value"}`)
|
|
208
|
+
|
|
203
209
|
2. **Create from stdin (recommended for sensitive values):**
|
|
204
210
|
|
|
205
211
|
```bash
|
|
@@ -262,6 +268,7 @@ source secrets.env
|
|
|
262
268
|
|
|
263
269
|
- `delete` requires `--yes`.
|
|
264
270
|
- `create`/`update` accept `--value`, `--value-stdin`, or `--file` (use only one).
|
|
271
|
+
- `create` always stores `SecretString` as a JSON object.
|
|
265
272
|
- `append` and `remove` require the secret value to be a JSON object.
|
|
266
273
|
- `upsert/import --file --name` parses `export KEY=value` and `KEY=value`, stores them as one JSON secret object, ignores blank lines/comments, and reports `created`, `updated`, `skipped`, and `failed`.
|
|
267
274
|
- Use `--value-stdin` to avoid shell history leakage for sensitive values.
|
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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './vaults/secretsmanager-admin';
|
|
19
19
|
import {
|
|
20
20
|
asOutputFormat,
|
|
21
|
+
parseEnvSecrets,
|
|
21
22
|
parseEnvSecretsFile,
|
|
22
23
|
printData,
|
|
23
24
|
parseRecoveryDays,
|
|
@@ -58,6 +59,48 @@ const parseSecretJsonObject = (
|
|
|
58
59
|
return parsed as Record<string, unknown>;
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
const parseEnvToObject = (
|
|
63
|
+
value: string
|
|
64
|
+
): Record<string, unknown> | undefined => {
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseEnvSecrets(value);
|
|
67
|
+
if (parsed.entries.length === 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Object.fromEntries(
|
|
72
|
+
parsed.entries.map((entry) => [entry.key, entry.value])
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const toSecretJsonObject = (value: string): Record<string, unknown> => {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(value) as unknown;
|
|
82
|
+
if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
|
|
83
|
+
return parsed as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof parsed === 'string') {
|
|
87
|
+
const envPayload = parseEnvToObject(parsed);
|
|
88
|
+
if (envPayload) {
|
|
89
|
+
return envPayload;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { value: parsed };
|
|
94
|
+
} catch {
|
|
95
|
+
const envPayload = parseEnvToObject(value);
|
|
96
|
+
if (envPayload) {
|
|
97
|
+
return envPayload;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { value };
|
|
102
|
+
};
|
|
103
|
+
|
|
61
104
|
// main program
|
|
62
105
|
program
|
|
63
106
|
.name('env-secrets')
|
|
@@ -78,6 +121,10 @@ const awsCommand = program
|
|
|
78
121
|
'-o, --output <file>',
|
|
79
122
|
'output secrets to file instead of environment variables'
|
|
80
123
|
)
|
|
124
|
+
.option(
|
|
125
|
+
'--no-shell',
|
|
126
|
+
'run program directly without a shell (disables shell expansion)'
|
|
127
|
+
)
|
|
81
128
|
.action(async (program, options) => {
|
|
82
129
|
if (!options.secret) {
|
|
83
130
|
exitWithError(
|
|
@@ -120,10 +167,22 @@ const awsCommand = program
|
|
|
120
167
|
return;
|
|
121
168
|
}
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
127
186
|
});
|
|
128
187
|
}
|
|
129
188
|
}
|
|
@@ -166,9 +225,10 @@ secretCommand
|
|
|
166
225
|
);
|
|
167
226
|
}
|
|
168
227
|
|
|
228
|
+
const payload = toSecretJsonObject(value);
|
|
169
229
|
const result = await createSecret({
|
|
170
230
|
name: options.name,
|
|
171
|
-
value,
|
|
231
|
+
value: JSON.stringify(payload),
|
|
172
232
|
description: options.description,
|
|
173
233
|
kmsKeyId: options.kmsKeyId,
|
|
174
234
|
tags: options.tag,
|