env-secrets 0.3.3 → 0.5.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/AGENTS.md +12 -3
- package/README.md +52 -8
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +341 -2
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +217 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +355 -0
- package/dist/cli/helpers.js +172 -0
- package/dist/index.js +456 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +273 -0
- package/dist/vaults/secretsmanager.js +8 -16
- package/docs/AWS.md +129 -2
- package/jest.e2e.config.js +1 -0
- package/package.json +5 -5
- package/src/cli/helpers.ts +239 -0
- package/src/index.ts +595 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +397 -0
- package/src/vaults/secretsmanager.ts +8 -21
- package/website/docs/cli-reference.mdx +101 -0
- package/website/docs/providers/aws-secrets-manager.mdx +59 -0
package/AGENTS.md
CHANGED
|
@@ -51,7 +51,7 @@ Always run quaity checks after creating or modifing files
|
|
|
51
51
|
### Testing Strategy
|
|
52
52
|
|
|
53
53
|
Always run unit tests after creating or modifying files.
|
|
54
|
-
Always run end to end tests before pushing code to a remote git repository.
|
|
54
|
+
Always start Docker Compose LocalStack and run end to end tests before pushing code to a remote git repository.
|
|
55
55
|
|
|
56
56
|
- **Unit Tests**: Jest framework, located in `__tests__/`
|
|
57
57
|
- **E2E Tests**: Located in `__e2e__/`
|
|
@@ -60,6 +60,7 @@ Always run end to end tests before pushing code to a remote git repository.
|
|
|
60
60
|
- `yarn test` - runs all tests
|
|
61
61
|
- `yarn test:unit` - runs unit tests only
|
|
62
62
|
- `yarn test:e2e` - builds and runs e2e tests
|
|
63
|
+
- `docker compose up -d localstack` - start LocalStack for e2e tests
|
|
63
64
|
|
|
64
65
|
## Project Structure
|
|
65
66
|
|
|
@@ -120,8 +121,9 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
120
121
|
|
|
121
122
|
1. Run `yarn prettier:fix && yarn lint` to ensure code quality
|
|
122
123
|
2. Run `yarn test` to ensure all tests pass
|
|
123
|
-
3.
|
|
124
|
-
4. Update
|
|
124
|
+
3. Run `docker compose up -d localstack` and then `yarn test:e2e` before pushing
|
|
125
|
+
4. Update tests for new features or bug fixes
|
|
126
|
+
5. Update documentation if needed
|
|
125
127
|
|
|
126
128
|
### Pull Request Process
|
|
127
129
|
|
|
@@ -130,6 +132,10 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
130
132
|
3. Add tests for new functionality
|
|
131
133
|
4. Ensure all CI checks pass
|
|
132
134
|
5. Submit a pull request with a clear description
|
|
135
|
+
6. Always request a GitHub Copilot review on every new pull request
|
|
136
|
+
7. After requesting Copilot review, wait 5 minutes and check for review comments
|
|
137
|
+
8. If no Copilot review is present yet, wait another 5 minutes and check again
|
|
138
|
+
9. Create a plan to address Copilot feedback, but evaluate each suggestion critically and do not accept recommendations blindly
|
|
133
139
|
|
|
134
140
|
## Development Environment
|
|
135
141
|
|
|
@@ -138,6 +144,8 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
138
144
|
- Node.js 20.0.0 or higher (see .nvmrc)
|
|
139
145
|
- Yarn package manager
|
|
140
146
|
- AWS CLI (for testing AWS integration)
|
|
147
|
+
- Homebrew (macOS/Linux) with `awscli-local` installed:
|
|
148
|
+
- `brew install awscli-local`
|
|
141
149
|
|
|
142
150
|
### Setup
|
|
143
151
|
|
|
@@ -145,5 +153,6 @@ yarn test:unit:coverage # Run tests with coverage
|
|
|
145
153
|
git clone https://github.com/markcallen/env-secrets.git
|
|
146
154
|
cd env-secrets
|
|
147
155
|
yarn install
|
|
156
|
+
brew install awscli-local
|
|
148
157
|
yarn build
|
|
149
158
|
```
|
package/README.md
CHANGED
|
@@ -103,6 +103,17 @@ env-secrets aws -s my-app-secrets -r us-east-1 -- node app.js
|
|
|
103
103
|
- `-o, --output <file>` (optional): Output secrets to a file instead of injecting into environment variables. File will be created with 0400 permissions and will not overwrite existing files
|
|
104
104
|
- `-- <program-to-run>`: The program to run with the injected environment variables (only used when `-o` is not specified)
|
|
105
105
|
|
|
106
|
+
For `aws secret` management subcommands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`), use:
|
|
107
|
+
|
|
108
|
+
- `-r, --region <region>` to target a specific region
|
|
109
|
+
- `-p, --profile <profile>` to select credentials profile
|
|
110
|
+
- `--output <format>` for `json` or `table`
|
|
111
|
+
|
|
112
|
+
These options are honored consistently on `aws secret` subcommands.
|
|
113
|
+
|
|
114
|
+
`env-secrets aws -s` is for fetching/injecting secret values into a child process.
|
|
115
|
+
`env-secrets aws secret ...` is for lifecycle management commands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`).
|
|
116
|
+
|
|
106
117
|
#### Examples
|
|
107
118
|
|
|
108
119
|
1. **Create a secret using AWS CLI:**
|
|
@@ -209,6 +220,42 @@ env-secrets aws -s my-secret -r us-east-1 -o secrets.env
|
|
|
209
220
|
# Error: File secrets.env already exists and will not be overwritten
|
|
210
221
|
```
|
|
211
222
|
|
|
223
|
+
10. **Load secrets into your current shell session:**
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Write export statements to a file
|
|
227
|
+
env-secrets aws -s my-secret -r us-east-1 -o secrets.env
|
|
228
|
+
|
|
229
|
+
# Load into current shell
|
|
230
|
+
source secrets.env
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Note: `env-secrets aws -s ... -- <command>` injects secrets into the spawned child process only.
|
|
234
|
+
To affect your current shell, use file output and `source` it.
|
|
235
|
+
|
|
236
|
+
11. **Upsert secrets from a local env file:**
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Supported line formats:
|
|
240
|
+
# export NAME=secret1
|
|
241
|
+
# NAME=secret1
|
|
242
|
+
env-secrets aws secret upsert --file .env --name app/dev --output json
|
|
243
|
+
|
|
244
|
+
# Creates/updates a single secret named app/dev
|
|
245
|
+
# with SecretString like:
|
|
246
|
+
# {"NAME":"secret1"}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
12. **Append/remove keys on an existing JSON secret:**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Add or overwrite one key
|
|
253
|
+
env-secrets aws secret append -n app/dev --key JIRA_EMAIL_TOKEN -v blah --output json
|
|
254
|
+
|
|
255
|
+
# Remove one or more keys
|
|
256
|
+
env-secrets aws secret remove -n app/dev --key API_KEY --key OLD_TOKEN --output json
|
|
257
|
+
```
|
|
258
|
+
|
|
212
259
|
## Security Considerations
|
|
213
260
|
|
|
214
261
|
- 🔐 **Credential Management**: The tool respects AWS credential precedence (environment variables, IAM roles, profiles)
|
|
@@ -459,11 +506,8 @@ The end-to-end tests use LocalStack to emulate AWS Secrets Manager and test the
|
|
|
459
506
|
1. **Install awslocal** (required for e2e tests):
|
|
460
507
|
|
|
461
508
|
```bash
|
|
462
|
-
#
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
# Or using npm
|
|
466
|
-
npm install -g awscli-local
|
|
509
|
+
# macOS/Linux (recommended)
|
|
510
|
+
brew install awscli-local
|
|
467
511
|
```
|
|
468
512
|
|
|
469
513
|
2. **Start LocalStack**:
|
|
@@ -508,15 +552,15 @@ The end-to-end test suite includes:
|
|
|
508
552
|
- **Program Execution**: Tests for executing programs with injected environment variables
|
|
509
553
|
- **Error Handling**: Tests for various error scenarios and edge cases
|
|
510
554
|
- **AWS Profile Support**: Tests for both default and custom AWS profiles
|
|
511
|
-
- **Region Support**: Tests for different AWS regions
|
|
555
|
+
- **Region Support**: Tests for different AWS regions, including multi-region `aws secret list` isolation checks
|
|
512
556
|
|
|
513
557
|
#### Troubleshooting E2E Tests
|
|
514
558
|
|
|
515
559
|
**awslocal not found**:
|
|
516
560
|
|
|
517
561
|
```bash
|
|
518
|
-
# Install awslocal
|
|
519
|
-
|
|
562
|
+
# Install awslocal (macOS/Linux)
|
|
563
|
+
brew install awscli-local
|
|
520
564
|
|
|
521
565
|
# Verify installation
|
|
522
566
|
awslocal --version
|
package/__e2e__/README.md
CHANGED
|
@@ -33,11 +33,8 @@ localstack start
|
|
|
33
33
|
The tests require `awslocal` to be installed, which is a wrapper around AWS CLI that automatically points to LocalStack:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
# Install awslocal
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Or using npm
|
|
40
|
-
npm install -g awscli-local
|
|
36
|
+
# Install awslocal (macOS/Linux recommended)
|
|
37
|
+
brew install awscli-local
|
|
41
38
|
|
|
42
39
|
# Verify installation
|
|
43
40
|
awslocal --version
|
package/__e2e__/index.test.ts
CHANGED
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
LocalStackHelper,
|
|
3
3
|
cli,
|
|
4
4
|
cliWithEnv,
|
|
5
|
+
cliWithEnvAndStdin,
|
|
5
6
|
createTempFile,
|
|
6
7
|
cleanupTempFile,
|
|
7
8
|
createTestProfile,
|
|
8
9
|
restoreTestProfile,
|
|
9
10
|
TestSecret,
|
|
10
|
-
CreatedSecret
|
|
11
|
+
CreatedSecret,
|
|
12
|
+
execAwslocalCommand
|
|
11
13
|
} from './utils/test-utils';
|
|
12
14
|
import { debugLog } from './utils/debug-logger';
|
|
13
15
|
import * as fs from 'fs';
|
|
@@ -332,7 +334,344 @@ describe('End-to-End Tests', () => {
|
|
|
332
334
|
const result = await cliWithEnv(['aws'], getLocalStackEnv());
|
|
333
335
|
|
|
334
336
|
expect(result.code).toBe(1);
|
|
335
|
-
expect(result.stderr).toContain('required option');
|
|
337
|
+
expect(result.stderr).toContain('Missing required option --secret');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe('AWS Secret Management Commands', () => {
|
|
342
|
+
test('should create, list, get, and delete a secret', async () => {
|
|
343
|
+
const secretName = `e2e-managed-secret-${Date.now()}`;
|
|
344
|
+
|
|
345
|
+
const createResult = await cliWithEnv(
|
|
346
|
+
['aws', 'secret', 'create', '-n', secretName, '-v', 'initial-value'],
|
|
347
|
+
getLocalStackEnv()
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(createResult.code).toBe(0);
|
|
351
|
+
expect(createResult.stdout).toContain(secretName);
|
|
352
|
+
|
|
353
|
+
const listResult = await cliWithEnv(
|
|
354
|
+
['aws', 'secret', 'list', '--prefix', 'e2e-managed-secret-'],
|
|
355
|
+
getLocalStackEnv()
|
|
356
|
+
);
|
|
357
|
+
expect(listResult.code).toBe(0);
|
|
358
|
+
expect(listResult.stdout).toContain(secretName);
|
|
359
|
+
|
|
360
|
+
const getResult = await cliWithEnv(
|
|
361
|
+
['aws', 'secret', 'get', '-n', secretName],
|
|
362
|
+
getLocalStackEnv()
|
|
363
|
+
);
|
|
364
|
+
expect(getResult.code).toBe(0);
|
|
365
|
+
expect(getResult.stdout).toContain(secretName);
|
|
366
|
+
expect(getResult.stdout).not.toContain('initial-value');
|
|
367
|
+
|
|
368
|
+
const deleteResult = await cliWithEnv(
|
|
369
|
+
[
|
|
370
|
+
'aws',
|
|
371
|
+
'secret',
|
|
372
|
+
'delete',
|
|
373
|
+
'-n',
|
|
374
|
+
secretName,
|
|
375
|
+
'--force-delete-without-recovery',
|
|
376
|
+
'--yes'
|
|
377
|
+
],
|
|
378
|
+
getLocalStackEnv()
|
|
379
|
+
);
|
|
380
|
+
expect(deleteResult.code).toBe(0);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('should update secret value from stdin', async () => {
|
|
384
|
+
const secret = await createTestSecret({
|
|
385
|
+
name: `managed-secret-stdin-${Date.now()}`,
|
|
386
|
+
value: 'initial-value',
|
|
387
|
+
description: 'Secret for stdin update test'
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const updateResult = await cliWithEnvAndStdin(
|
|
391
|
+
[
|
|
392
|
+
'aws',
|
|
393
|
+
'secret',
|
|
394
|
+
'update',
|
|
395
|
+
'-n',
|
|
396
|
+
secret.prefixedName,
|
|
397
|
+
'--value-stdin'
|
|
398
|
+
],
|
|
399
|
+
getLocalStackEnv(),
|
|
400
|
+
'stdin-updated-value'
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(updateResult.code).toBe(0);
|
|
404
|
+
expect(updateResult.stderr).toBe('');
|
|
405
|
+
|
|
406
|
+
const deleteResult = await cliWithEnv(
|
|
407
|
+
[
|
|
408
|
+
'aws',
|
|
409
|
+
'secret',
|
|
410
|
+
'delete',
|
|
411
|
+
'-n',
|
|
412
|
+
secret.prefixedName,
|
|
413
|
+
'--force-delete-without-recovery',
|
|
414
|
+
'--yes'
|
|
415
|
+
],
|
|
416
|
+
getLocalStackEnv()
|
|
417
|
+
);
|
|
418
|
+
expect(deleteResult.code).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('should append and remove keys on a JSON secret', async () => {
|
|
422
|
+
const secretName = `managed-secret-append-remove-${Date.now()}`;
|
|
423
|
+
const tempFile = path.join(
|
|
424
|
+
os.tmpdir(),
|
|
425
|
+
`env-secrets-append-remove-${Date.now()}.env`
|
|
426
|
+
);
|
|
427
|
+
fs.writeFileSync(tempFile, 'API_KEY=first');
|
|
428
|
+
|
|
429
|
+
const createResult = await cliWithEnv(
|
|
430
|
+
[
|
|
431
|
+
'aws',
|
|
432
|
+
'secret',
|
|
433
|
+
'upsert',
|
|
434
|
+
'--file',
|
|
435
|
+
tempFile,
|
|
436
|
+
'--name',
|
|
437
|
+
secretName,
|
|
438
|
+
'--output',
|
|
439
|
+
'json'
|
|
440
|
+
],
|
|
441
|
+
getLocalStackEnv()
|
|
442
|
+
);
|
|
443
|
+
expect(createResult.code).toBe(0);
|
|
444
|
+
|
|
445
|
+
const appendResult = await cliWithEnv(
|
|
446
|
+
[
|
|
447
|
+
'aws',
|
|
448
|
+
'secret',
|
|
449
|
+
'append',
|
|
450
|
+
'-n',
|
|
451
|
+
secretName,
|
|
452
|
+
'--key',
|
|
453
|
+
'JIRA_EMAIL_TOKEN',
|
|
454
|
+
'-v',
|
|
455
|
+
'blah',
|
|
456
|
+
'--output',
|
|
457
|
+
'json'
|
|
458
|
+
],
|
|
459
|
+
getLocalStackEnv()
|
|
460
|
+
);
|
|
461
|
+
expect(appendResult.code).toBe(0);
|
|
462
|
+
|
|
463
|
+
const afterAppend = await execAwslocalCommand(
|
|
464
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
465
|
+
getLocalStackEnv()
|
|
466
|
+
);
|
|
467
|
+
expect(JSON.parse(afterAppend.stdout.trim())).toEqual({
|
|
468
|
+
API_KEY: 'first',
|
|
469
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const removeResult = await cliWithEnv(
|
|
473
|
+
[
|
|
474
|
+
'aws',
|
|
475
|
+
'secret',
|
|
476
|
+
'remove',
|
|
477
|
+
'-n',
|
|
478
|
+
secretName,
|
|
479
|
+
'--key',
|
|
480
|
+
'API_KEY',
|
|
481
|
+
'--output',
|
|
482
|
+
'json'
|
|
483
|
+
],
|
|
484
|
+
getLocalStackEnv()
|
|
485
|
+
);
|
|
486
|
+
expect(removeResult.code).toBe(0);
|
|
487
|
+
|
|
488
|
+
const afterRemove = await execAwslocalCommand(
|
|
489
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
490
|
+
getLocalStackEnv()
|
|
491
|
+
);
|
|
492
|
+
expect(JSON.parse(afterRemove.stdout.trim())).toEqual({
|
|
493
|
+
JIRA_EMAIL_TOKEN: 'blah'
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const deleteResult = await cliWithEnv(
|
|
497
|
+
[
|
|
498
|
+
'aws',
|
|
499
|
+
'secret',
|
|
500
|
+
'delete',
|
|
501
|
+
'-n',
|
|
502
|
+
secretName,
|
|
503
|
+
'--force-delete-without-recovery',
|
|
504
|
+
'--yes'
|
|
505
|
+
],
|
|
506
|
+
getLocalStackEnv()
|
|
507
|
+
);
|
|
508
|
+
expect(deleteResult.code).toBe(0);
|
|
509
|
+
cleanupTempFile(tempFile);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('should require confirmation for delete', async () => {
|
|
513
|
+
const secret = await createTestSecret({
|
|
514
|
+
name: `managed-secret-confirm-${Date.now()}`,
|
|
515
|
+
value: 'value',
|
|
516
|
+
description: 'Secret for delete confirmation test'
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const result = await cliWithEnv(
|
|
520
|
+
['aws', 'secret', 'delete', '-n', secret.prefixedName],
|
|
521
|
+
getLocalStackEnv()
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
expect(result.code).toBe(1);
|
|
525
|
+
expect(result.stderr).toContain('requires --yes confirmation');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('should honor region flag for secret list across multiple regions', async () => {
|
|
529
|
+
const secret = await createTestSecret(
|
|
530
|
+
{
|
|
531
|
+
name: `managed-secret-multi-region-${Date.now()}`,
|
|
532
|
+
value: '{"region":"us-west-2"}',
|
|
533
|
+
description: 'Secret used for region isolation test'
|
|
534
|
+
},
|
|
535
|
+
'us-west-2'
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const westResult = await cliWithEnv(
|
|
539
|
+
[
|
|
540
|
+
'aws',
|
|
541
|
+
'secret',
|
|
542
|
+
'list',
|
|
543
|
+
'--prefix',
|
|
544
|
+
secret.prefixedName,
|
|
545
|
+
'-r',
|
|
546
|
+
'us-west-2',
|
|
547
|
+
'--output',
|
|
548
|
+
'json'
|
|
549
|
+
],
|
|
550
|
+
getLocalStackEnv()
|
|
551
|
+
);
|
|
552
|
+
expect(westResult.code).toBe(0);
|
|
553
|
+
const westRows = JSON.parse(westResult.stdout) as Array<{
|
|
554
|
+
name: string;
|
|
555
|
+
}>;
|
|
556
|
+
expect(westRows.some((row) => row.name === secret.prefixedName)).toBe(
|
|
557
|
+
true
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const eastResult = await cliWithEnv(
|
|
561
|
+
[
|
|
562
|
+
'aws',
|
|
563
|
+
'secret',
|
|
564
|
+
'list',
|
|
565
|
+
'--prefix',
|
|
566
|
+
secret.prefixedName,
|
|
567
|
+
'-r',
|
|
568
|
+
'us-east-1',
|
|
569
|
+
'--output',
|
|
570
|
+
'json'
|
|
571
|
+
],
|
|
572
|
+
getLocalStackEnv()
|
|
573
|
+
);
|
|
574
|
+
expect(eastResult.code).toBe(0);
|
|
575
|
+
const eastRows = JSON.parse(eastResult.stdout) as Array<{
|
|
576
|
+
name: string;
|
|
577
|
+
}>;
|
|
578
|
+
expect(eastRows).toEqual([]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('should upsert secrets from env file', async () => {
|
|
582
|
+
const secretName = `e2e-upsert-${Date.now()}`;
|
|
583
|
+
const tempFile = path.join(
|
|
584
|
+
os.tmpdir(),
|
|
585
|
+
`env-secrets-upsert-${Date.now()}.env`
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
fs.writeFileSync(
|
|
589
|
+
tempFile,
|
|
590
|
+
['# sample', 'export API_KEY=first', 'DB_URL = postgres://one'].join(
|
|
591
|
+
'\n'
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const firstRun = await cliWithEnv(
|
|
596
|
+
[
|
|
597
|
+
'aws',
|
|
598
|
+
'secret',
|
|
599
|
+
'upsert',
|
|
600
|
+
'--file',
|
|
601
|
+
tempFile,
|
|
602
|
+
'--name',
|
|
603
|
+
secretName,
|
|
604
|
+
'--output',
|
|
605
|
+
'json'
|
|
606
|
+
],
|
|
607
|
+
getLocalStackEnv()
|
|
608
|
+
);
|
|
609
|
+
expect(firstRun.code).toBe(0);
|
|
610
|
+
const firstJson = JSON.parse(firstRun.stdout) as {
|
|
611
|
+
summary: { created: number; updated: number; skipped: number };
|
|
612
|
+
};
|
|
613
|
+
expect(firstJson.summary.created).toBe(1);
|
|
614
|
+
expect(firstJson.summary.updated).toBe(0);
|
|
615
|
+
|
|
616
|
+
const firstSecret = await execAwslocalCommand(
|
|
617
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
618
|
+
getLocalStackEnv()
|
|
619
|
+
);
|
|
620
|
+
expect(JSON.parse(firstSecret.stdout.trim())).toEqual({
|
|
621
|
+
API_KEY: 'first',
|
|
622
|
+
DB_URL: 'postgres://one'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
fs.writeFileSync(
|
|
626
|
+
tempFile,
|
|
627
|
+
['export API_KEY=second', 'DB_URL=postgres://two'].join('\n')
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
const secondRun = await cliWithEnv(
|
|
631
|
+
[
|
|
632
|
+
'aws',
|
|
633
|
+
'secret',
|
|
634
|
+
'import',
|
|
635
|
+
'--file',
|
|
636
|
+
tempFile,
|
|
637
|
+
'--name',
|
|
638
|
+
secretName,
|
|
639
|
+
'--output',
|
|
640
|
+
'json'
|
|
641
|
+
],
|
|
642
|
+
getLocalStackEnv()
|
|
643
|
+
);
|
|
644
|
+
expect(secondRun.code).toBe(0);
|
|
645
|
+
const secondJson = JSON.parse(secondRun.stdout) as {
|
|
646
|
+
summary: { created: number; updated: number; skipped: number };
|
|
647
|
+
};
|
|
648
|
+
expect(secondJson.summary.created).toBe(0);
|
|
649
|
+
expect(secondJson.summary.updated).toBe(1);
|
|
650
|
+
expect(secondJson.summary.skipped).toBe(0);
|
|
651
|
+
|
|
652
|
+
const secondSecret = await execAwslocalCommand(
|
|
653
|
+
`awslocal secretsmanager get-secret-value --secret-id "${secretName}" --region us-east-1 --query SecretString --output text`,
|
|
654
|
+
getLocalStackEnv()
|
|
655
|
+
);
|
|
656
|
+
expect(JSON.parse(secondSecret.stdout.trim())).toEqual({
|
|
657
|
+
API_KEY: 'second',
|
|
658
|
+
DB_URL: 'postgres://two'
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const deleteResult = await cliWithEnv(
|
|
662
|
+
[
|
|
663
|
+
'aws',
|
|
664
|
+
'secret',
|
|
665
|
+
'delete',
|
|
666
|
+
'-n',
|
|
667
|
+
secretName,
|
|
668
|
+
'--force-delete-without-recovery',
|
|
669
|
+
'--yes'
|
|
670
|
+
],
|
|
671
|
+
getLocalStackEnv()
|
|
672
|
+
);
|
|
673
|
+
expect(deleteResult.code).toBe(0);
|
|
674
|
+
cleanupTempFile(tempFile);
|
|
336
675
|
});
|
|
337
676
|
});
|
|
338
677
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as fs from 'fs';
|
|
@@ -506,6 +506,66 @@ export async function cliWithEnv(
|
|
|
506
506
|
});
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
export async function cliWithEnvAndStdin(
|
|
510
|
+
args: string[],
|
|
511
|
+
env: Record<string, string>,
|
|
512
|
+
stdin: string,
|
|
513
|
+
cwd = '.'
|
|
514
|
+
): Promise<CliResult> {
|
|
515
|
+
return await new Promise((resolve) => {
|
|
516
|
+
const cleanEnv = { ...process.env };
|
|
517
|
+
delete cleanEnv.AWS_PROFILE;
|
|
518
|
+
delete cleanEnv.AWS_DEFAULT_PROFILE;
|
|
519
|
+
delete cleanEnv.AWS_SESSION_TOKEN;
|
|
520
|
+
delete cleanEnv.AWS_SECURITY_TOKEN;
|
|
521
|
+
delete cleanEnv.AWS_ROLE_ARN;
|
|
522
|
+
delete cleanEnv.AWS_ROLE_SESSION_NAME;
|
|
523
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
|
|
524
|
+
delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
|
|
525
|
+
|
|
526
|
+
const defaultEnv = {
|
|
527
|
+
AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
|
|
528
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
529
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
530
|
+
AWS_DEFAULT_REGION: 'us-east-1',
|
|
531
|
+
AWS_REGION: 'us-east-1',
|
|
532
|
+
NODE_ENV: 'test'
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const envVars = { ...cleanEnv, ...defaultEnv, ...env };
|
|
536
|
+
const child = spawn('node', [path.resolve('./dist/index'), ...args], {
|
|
537
|
+
cwd,
|
|
538
|
+
env: envVars,
|
|
539
|
+
stdio: 'pipe'
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
let stdout = '';
|
|
543
|
+
let stderr = '';
|
|
544
|
+
let error: Error | null = null;
|
|
545
|
+
|
|
546
|
+
child.stdout.on('data', (chunk) => {
|
|
547
|
+
stdout += chunk.toString();
|
|
548
|
+
});
|
|
549
|
+
child.stderr.on('data', (chunk) => {
|
|
550
|
+
stderr += chunk.toString();
|
|
551
|
+
});
|
|
552
|
+
child.on('error', (err) => {
|
|
553
|
+
error = err;
|
|
554
|
+
});
|
|
555
|
+
child.on('close', (code) => {
|
|
556
|
+
resolve({
|
|
557
|
+
code: code ?? (error ? 1 : 0),
|
|
558
|
+
error,
|
|
559
|
+
stdout,
|
|
560
|
+
stderr
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
child.stdin.write(stdin);
|
|
565
|
+
child.stdin.end();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
509
569
|
export function createTempFile(content: string) {
|
|
510
570
|
const tempDir = os.tmpdir();
|
|
511
571
|
const tempFile = path.join(tempDir, `env-secrets-test-${Date.now()}.env`);
|