diff --git a/CHANGELOG.md b/CHANGELOG.md index 55127b5..484a99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.5.0 (October 17, 2020) + +- Add support for multiple AWS Secrets Manager secrets in the same region + ## 1.4.2 (October 17, 2020) - Warn if a config is undefined, null, 'undefined' or an empty string diff --git a/README.md b/README.md index 8bbf5eb..b650aa1 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ console.log(config.API_ENDPOINT); ### Using AWS Secrets Manager -In order to use AWS Secrets Manager you have to add a `AWS_SECRETS_MANAGER_NAME` or `awsSecretsManagerName` setting to your config that specifies the name of the secret to look up: +In order to use AWS Secrets Manager you have to add a `AWS_SECRETS_MANAGER_NAME` or `awsSecretsManagerName` setting to your config that specifies the names of the secrets to look up: ```ts // config.default.ts @@ -81,9 +81,20 @@ export default { }; ``` +`AWS_SECRETS_MANAGER_NAME` can also be a comma separated list to allow connection to multiple secrets in AWS Secrets Manager. Each secret from the list is evaluated in order mean that if a specific key appears in two secrets the value will be overwritten by the last secret in the list. + +```ts +// config.default.ts +export default { + AWS_SECRETS_MANAGER_NAME: 'production/myapp/config, production/myapp/another-config', + API_ENDPOINT: 'https://api.kanye.rest/' +}; +``` + In addition to specifying the secret name you can also provide a region using the `AWS_SECRETS_MANAGER_REGION` or `awsSecretsManagerRegion` setting. The connection timeout in milliseconds can also be specified using the `AWS_SECRETS_MANAGER_TIMEOUT` or `awsSecretsManagerTimeout` setting: ```ts + // config.default.ts export default { AWS_SECRETS_MANAGER_NAME: 'production/myapp/config', diff --git a/package.json b/package.json index 48a78db..8b2502d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "config-dug", - "version": "1.4.2", + "version": "1.5.0", "description": "Config loader with support for AWS Secrets Manager", "author": "Neo Financial Engineering ", "main": "build/index.js", diff --git a/src/__mocks__/get-secret.ts b/src/__mocks__/get-secret.ts index 78d0762..c613a28 100644 --- a/src/__mocks__/get-secret.ts +++ b/src/__mocks__/get-secret.ts @@ -2,8 +2,19 @@ import awsSecretsManagerResponse from '../../test/fixtures/secrets/aws-secrets-manager-response.json'; -const getSecret = (_: string, __: string): object => { - return JSON.parse(awsSecretsManagerResponse.Value); +import multipleAwsSecretsManagerResponse1 from '../../test/fixtures/multiple-secrets/aws-secrets-manager-1-response.json'; +import multipleAwsSecretsManagerResponse2 from '../../test/fixtures/multiple-secrets/aws-secrets-manager-2-response.json'; + +const getSecret = (secretName: string, __: string): object => { + if (secretName === 'development/config-dug') { + return JSON.parse(awsSecretsManagerResponse.Value); + } else if (secretName === 'development/config-dug-1') { + return JSON.parse(multipleAwsSecretsManagerResponse1.Value); + } else if (secretName === 'development/config-dug-2') { + return JSON.parse(multipleAwsSecretsManagerResponse2.Value); + } + + return {}; }; export default getSecret; diff --git a/src/get-secret.ts b/src/get-secret.ts index 3e93cbe..7e2cdb6 100644 --- a/src/get-secret.ts +++ b/src/get-secret.ts @@ -2,7 +2,9 @@ import awsParamStore from 'aws-param-store'; -const getSecret = (secretName: string, region: string, timeout: number): object => { +import { SecretObject } from '.'; + +const getSecret = (secretName: string, region: string, timeout: number): SecretObject => { try { const secret = awsParamStore.getParameterSync(`/aws/reference/secretsmanager/${secretName}`, { region, diff --git a/src/index.ts b/src/index.ts index 1af19ac..2b046f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,10 @@ interface LoadSecretsArgs { awsSecretsManagerTimeout?: number; } +export interface SecretObject { + [key: string]: string; +} + const resolveFile = (appDirectory: string, configPath: string, fileName: string): string => { if (fs.existsSync(path.resolve(appDirectory, configPath, `${fileName}.ts`))) { debug( @@ -72,21 +76,39 @@ const convertString = (value: string): string | number | boolean => { return value; }; +const convertToArray = (value: string): string[] => { + return value + .split(',') + .map(entry => entry.trim()) + .filter(entry => !!entry); +}; + const loadSecrets = (config: LoadSecretsArgs): object => { const secretName = config.AWS_SECRETS_MANAGER_NAME || config.awsSecretsManagerName; const region = config.AWS_SECRETS_MANAGER_REGION || config.awsSecretsManagerRegion || 'us-east-1'; const timeout = config.AWS_SECRETS_MANAGER_TIMEOUT || config.awsSecretsManagerTimeout || 5000; if (secretName) { - debug('loading config from AWS Secrets Manager', secretName, region); + const secrets = convertToArray(secretName).map(name => { + debug('loading config from AWS Secrets Manager', name, region); - const secret = getSecret(secretName, region, timeout); + return getSecret(name, region, timeout); + }); - return Object.entries(secret).reduce((result: ConfigObject, [key, value]): ConfigObject => { - result[key] = convertString(value); + const mergedSecrets: SecretObject = {}; - return result; - }, {}); + secrets.forEach(secret => { + Object.assign(mergedSecrets, secret); + }); + + return Object.entries(mergedSecrets).reduce( + (result: ConfigObject, [key, value]): ConfigObject => { + result[key] = convertString(value); + + return result; + }, + {} + ); } else { return {}; } diff --git a/test/fixtures/multiple-secrets/aws-secrets-manager-1-response.json b/test/fixtures/multiple-secrets/aws-secrets-manager-1-response.json new file mode 100644 index 0000000..25879e6 --- /dev/null +++ b/test/fixtures/multiple-secrets/aws-secrets-manager-1-response.json @@ -0,0 +1,9 @@ +{ + "Name": "/aws/reference/secretsmanager/development/config-dug-1", + "Type": "SecureString", + "Value": "{\"DB_USERNAME\":\"config-dug\",\"DB_PASSWORD\":\"secret\",\"TEST_BOOLEAN\":\"true\",\"TEST_INTEGER\":\"42\",\"TEST_FLOAT\":\"4.2\",\"TEST_NUMBER_LIST\":\"123456,123456\"}", + "Version": 0, + "SourceResult": "{\"ARN\":\"arn:aws:secretsmanager:us-east-1:999999999999:secret:development/config-dug-qH33bS\",\"name\":\"development/config-dug\",\"versionId\":\"8439a2e1-9a24-49ff-b9e7-5e8ba5d6d5a6\",\"secretString\":\"{\\\"DB_USERNAME\\\":\\\"config-dug\\\",\\\"DB_PASSWORD\\\":\\\"secret\\\"}\",\"versionStages\":[\"AWSCURRENT\"],\"createdDate\":\"Apr 10, 2019 10:49:20 PM\"}", + "LastModifiedDate": "2019-04-10T22:49:20.589Z", + "ARN": "arn:aws:secretsmanager:us-east-1:999999999999:secret:development/config-dug-qH33bS" +} diff --git a/test/fixtures/multiple-secrets/aws-secrets-manager-2-response.json b/test/fixtures/multiple-secrets/aws-secrets-manager-2-response.json new file mode 100644 index 0000000..2468230 --- /dev/null +++ b/test/fixtures/multiple-secrets/aws-secrets-manager-2-response.json @@ -0,0 +1,9 @@ +{ + "Name": "/aws/reference/secretsmanager/development/config-dug-2", + "Type": "SecureString", + "Value": "{\"TEST_INTEGER\":\"22\",\"TEST_ANOTHER_INTEGER\":\"23\"}", + "Version": 0, + "SourceResult": "{\"ARN\":\"arn:aws:secretsmanager:us-east-1:999999999999:secret:development/config-dug-qH33bS\",\"name\":\"development/config-dug\",\"versionId\":\"8439a2e1-9a24-49ff-b9e7-5e8ba5d6d5a6\",\"secretString\":\"{\\\"DB_USERNAME\\\":\\\"config-dug\\\",\\\"DB_PASSWORD\\\":\\\"secret\\\"}\",\"versionStages\":[\"AWSCURRENT\"],\"createdDate\":\"Apr 10, 2019 10:49:20 PM\"}", + "LastModifiedDate": "2019-04-10T22:49:20.589Z", + "ARN": "arn:aws:secretsmanager:us-east-1:999999999999:secret:development/config-dug-qH33bS" +} diff --git a/test/fixtures/multiple-secrets/config.default.ts b/test/fixtures/multiple-secrets/config.default.ts new file mode 100644 index 0000000..a217d70 --- /dev/null +++ b/test/fixtures/multiple-secrets/config.default.ts @@ -0,0 +1,3 @@ +export default { + AWS_SECRETS_MANAGER_NAME: 'development/config-dug-1, development/config-dug-2' +}; diff --git a/test/secrets.test.ts b/test/secrets.test.ts index e66e19f..a2b1afb 100644 --- a/test/secrets.test.ts +++ b/test/secrets.test.ts @@ -15,3 +15,16 @@ test('loading secrets from AWS Secrets Manager works', (): void => { TEST_NUMBER_LIST: '123456,123456' }); }); + +test('loading multiple AWS Secrets Manager secrets works', (): void => { + const testConfig = loadConfig('test/fixtures/multiple-secrets'); + + expect(testConfig).toMatchObject({ + AWS_SECRETS_MANAGER_NAME: 'development/config-dug-1, development/config-dug-2', + DB_USERNAME: 'config-dug', + DB_PASSWORD: 'secret', + TEST_BOOLEAN: true, + TEST_INTEGER: 22, + TEST_ANOTHER_INTEGER: 23 + }); +});