Skip to content

Commit 3878a54

Browse files
Merge pull request #49 from recursivefunk/jra/aws-secrets-manager-extension
Jra/aws secrets manager extension
2 parents 3311be7 + 761aba7 commit 3878a54

File tree

7 files changed

+109
-22
lines changed

7 files changed

+109
-22
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
steps:
1111
- uses: actions/checkout@v2
1212
- name: Set up NodeJS
13-
uses: actions/setup-node@v2
13+
uses: actions/setup-node@v4
1414
with:
1515
node-version: 18.20.4
1616
cache: 'npm'

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# good-env
32

43
<p align="center">
@@ -157,6 +156,29 @@ const {
157156
const credentials = env.getAWS({ region: 'us-west-2' });
158157
```
159158

159+
### AWS Secrets Manager Integration
160+
161+
Some folks like to store secrets in AWS secrets manager in the form of a JSON object as opposed (or in addition) to environment variables. It's me, I'm some folks. Good Env now supports this pattern. To avoid introducing a dependency you'll have to bring your own instance of AWS Secrets Manager though. Be sure to specify your AWS region as an environment variable, otherwise, it'll default to `us-east-1`.
162+
163+
Note, if something goes wrong, this function _will_ throw an error.
164+
165+
```javascript
166+
const awsSecretsManager = require('@aws-sdk/client-secrets-manager');
167+
168+
(async function() {
169+
// Load secrets from AWS Secrets Manager
170+
await env.use(awsSecretsManager, 'my-secret-id');
171+
172+
// The secret ID can also be specified via environment variables
173+
// AWS_SECRET_ID or SECRET_ID
174+
await env.use(awsSecretsManager);
175+
176+
// Secrets are automatically merged with existing environment variables
177+
// and can be accessed using any of the standard methods
178+
const secretValue = env.get('someSecretFromAWSSecretsManager');
179+
}());
180+
```
181+
160182
## Important Behavior Notes
161183

162184
### Boolean Existence vs Value

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "good-env",
3-
"version": "7.4.0",
3+
"version": "7.5.0",
44
"description": "Better environment variable handling for Twelve-Factor node apps",
55
"main": "src/index.js",
66
"scripts": {
77
"test": "nyc --check-coverage --lines 100 node test/test.js",
88
"lint": "semistandard test/ src/index.js",
9-
"ci": "semistandard test/ src/index.js && nyc --check-coverage --lines 100 node test/test.js",
10-
"format": "semistandard --fix test/ src/index.js"
9+
"ci": "semistandard test/test.js src/index.js && nyc --check-coverage --lines 100 node test/test.js",
10+
"format": "semistandard --fix test/test.js src/*.js"
1111
},
1212
"keywords": [
1313
"environment",

src/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
declare module "good-env" {
2+
/**
3+
* @description Tell Good Env to go to secrets manager, grab the object under the specified secretId and merge it with the
4+
* environment.
5+
* @param {any} awsSecretsManager - An instance of AWS Secrets Manager is imported from the SDK
6+
* @param {string} awsSecretsManager - The secret ID to use to fetch the secrets object. If not supplied, the function will
7+
* check environment variables AWS_SECRET_ID and SECRET_ID. If neither of which are defined, the function will throw an error
8+
*/
9+
export const use: (awsSecretsManager: any, secretId?: string) => Promise<void>;
210
/**
311
* @description Fetches an IP address from the environment. If the value found under the specified key is not a valid IPv4
412
* or IPv6 IP and there's no default value, null is returned. If a default value is provided and it is a valid IPv4 or IPv6

src/index.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,29 @@ let store = { ...process.env };
1717

1818
module.exports = Object
1919
.create({
20-
async _mergeSecrets_ ({ fetcherFunc }) {
21-
const secret = await fetcherFunc();
20+
async use (awsSecretsManager, secretId) {
21+
const { SecretsManagerClient, GetSecretValueCommand } = awsSecretsManager;
22+
const client = new SecretsManagerClient({
23+
region: process.env.AWS_REGION || 'us-east-1'
24+
});
25+
26+
if (!secretId) {
27+
secretId = this.get(['AWS_SECRET_ID', 'SECRET_ID']);
28+
}
29+
30+
if (!secretId) {
31+
throw new Error('\'secretId\' was not specified, and it wasn\'t found as \'AWS_SECRET_ID\' or \'SECRET_ID\' in the environment.');
32+
}
33+
34+
const response = await client.send(
35+
new GetSecretValueCommand({
36+
SecretId: secretId,
37+
VersionStage: 'AWSCURRENT' // VersionStage defaults to AWSCURRENT if unspecified
38+
})
39+
);
40+
const secretStr = response.SecretString;
41+
const secret = JSON.parse(secretStr);
2242
store = { ...store, ...secret };
23-
return this;
2443
},
2544
/**
2645
* @description Fetches an IP address from the environment. If the value found under the specified key is not a valid IPv4

test/mocks.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
class GetSecretValueCommand {
3+
constructor ({}) {}
4+
}
5+
6+
class SecretsManagerClientHappy {
7+
constructor ({}) {}
8+
send () {
9+
return Promise.resolve({ SecretString: JSON.stringify({ secretVal1: 'val1', secretVal2: 'val2' }) });
10+
}
11+
}
12+
13+
class SecretsManagerClientNotHappy {
14+
constructor ({}) {}
15+
send () {
16+
return Promise.reject('Something went wrong');
17+
}
18+
}
19+
20+
module.exports = {
21+
GetSecretValueCommand,
22+
SecretsManagerClientHappy,
23+
SecretsManagerClientNotHappy,
24+
};

test/test.js

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@ require('dotenv').config({ path: 'test/test.env' });
44

55
const test = require('tape');
66
const env = require('../src/index');
7+
const {
8+
GetSecretValueCommand,
9+
SecretsManagerClientHappy
10+
} = require('./mocks');
11+
12+
test('it throws when no secretId is given when attempting to use secretsManager', async (t) => {
13+
const awsSecretsManager = { SecretsManagerClient: SecretsManagerClientHappy, GetSecretValueCommand };
14+
15+
try {
16+
await env.use(awsSecretsManager);
17+
t.fail('We should not be here. An error should have been thrown');
18+
} catch (e) {
19+
console.log(e.message);
20+
t.equals(e.message, '\'secretId\' was not specified, and it wasn\'t found as \'AWS_SECRET_ID\' or \'SECRET_ID\' in the environment.');
21+
t.end();
22+
}
23+
});
24+
25+
test('it uses secrets manager (happy path)', async (t) => {
26+
const awsSecretsManager = { SecretsManagerClient: SecretsManagerClientHappy, GetSecretValueCommand };
27+
await env.use(awsSecretsManager, 'my-secret');
28+
const foo = env.get('FOO');
29+
const secretVal1 = env.get('secretVal1');
30+
const secretVal2 = env.get('secretVal2');
31+
32+
t.equals(foo, 'bar');
33+
t.equals(secretVal1, 'val1');
34+
t.equals(secretVal2, 'val2');
735

8-
test('it merges secrets', async (t) => {
9-
const fetcherFunc = () => {
10-
return new Promise((resolve) => {
11-
setTimeout(() => {
12-
resolve({
13-
FOOB: 'bar',
14-
BARZ: 'baz'
15-
});
16-
}, 10);
17-
});
18-
};
19-
const env2 = await env._mergeSecrets_({ fetcherFunc });
20-
t.equals(env2.get('FOOB'), 'bar');
21-
t.equals(env2.get('BARZ'), 'baz');
2236
t.end();
2337
});
2438

0 commit comments

Comments
 (0)