From d62bd6977ca01154200985fb1925ecde31952df3 Mon Sep 17 00:00:00 2001 From: Ben Ross Date: Fri, 20 Apr 2018 11:36:53 -0400 Subject: [PATCH] feat(credstash): Add credstash support (#14) * feat(credstash): Add credstash support * feat(credstash): Add credstash support * feat(credstash): Fix error validation formatting * feat(credstash): Add credstash support * feat(credstash): Fix .vscode --- .vscode/launch.json | 42 ++++++++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 18 ++++++++++++++++++ README.md | 8 +++++++- env.yml | 1 + package.json | 1 + src/index.ts | 36 +++++++++++++++++++----------------- src/resolvers.ts | 11 +++++++++++ src/utils.ts | 21 +++++++++++++++++++++ yarn.lock | 27 +++++++++++++++++++++++++++ 9 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 src/utils.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9334cb7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Jest", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", + "stopOnEntry": false, + "args": [ + "--runInBand", + "${file}" + ], + "cwd": "${workspaceRoot}", + "protocol": "legacy", + "runtimeArgs": [ + "--nolazy" + ], + "sourceMaps": true, + "outFiles": [ + "${workspaceRoot}/dist/**/*.js" + ] + }, + { + "name": "TS-Node", + "type": "node", + "request": "launch", + "args": [ + "${relativeFile}" + ], + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register" + ], + "sourceMaps": true, + "cwd": "${workspaceRoot}", + "protocol": "inspector", + "runtimeVersion": "8.10.0" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..efc6858 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "prettier.printWidth": 100, + "prettier.singleQuote": true, + "typescript.tsdk": "./node_modules/typescript/lib", + "search.exclude": { + "dist/": true + }, + "[javascript]": { + "editor.formatOnSave": true + }, + "[json]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + } +} + diff --git a/README.md b/README.md index 2e79e62..859528b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Define your configuration in a yaml file at the root of your application: default_env: &default_env SERVICE_URL: ${cft:my-stack.ServiceURL} ## Reference to an AWS CFT stack output SOME_ENV_VARIABLE: ${env:SOME_ENV_VARIABLE} ## Reference to an external environment variable + SOME_CREDSTASH_VARIABLE: ${cred:SOME_CREDSTASH_VARIABLE} ## Reference to a credstash key SOME_CONSTANT: SOME_CONSTANT development: @@ -36,8 +37,8 @@ staging: <<: *default_env production: - SOME_CONSTANT: OVERRIDE_FOR_PRODUCTION <<: *default_env + SOME_CONSTANT: OVERRIDE_FOR_PRODUCTION ``` Then, run `yarn dotenvi -s ` to generate a `.env` file for the stage desired (e.g., development, staging, production, etc...). Use the generated `.env` file in your normal processes using [dotenv](https://github.com/motdotla/dotenv). @@ -45,6 +46,11 @@ Then, run `yarn dotenvi -s ` to generate a `.env` file for the stage desi Note that stages are not required in your yaml file - you can also define it without stages, in which case you should not specify a stage with the `-s` option when you run `dotenvi`. +## Configuration + +Note that any AWS references (cred, cft, etc...) are currently hard-coded to us-east-1. + + ## Discussion The main design goals of dotenvi are as follows: diff --git a/env.yml b/env.yml index ad66543..fb466e6 100644 --- a/env.yml +++ b/env.yml @@ -2,6 +2,7 @@ default_env: &default_env IA_FARMAPI_ENV: ${env:IA_FARMAPI_ENV} FOO: ${cft:forms-api-fargate-stack.ServiceURL} BAZ: BAR + CRED: ${cred:IA_VPC_PRIVATE_SUBNET1_ID} sandbox: <<: *default_env diff --git a/package.json b/package.json index ec585bd..02510e0 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "aws-sdk": "^2.224.1", "jest": "^22.4.3", "js-yaml": "^3.11.0", + "nodecredstash": "^2.0.2", "ts-jest": "^22.4.4", "typescript": "^2.8.1" }, diff --git a/src/index.ts b/src/index.ts index 91a838b..d80b443 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { ArgumentParser } from 'argparse'; import { Rewriter } from './rewriter'; import { ResolverMap, Document } from './types'; import { resolvers } from './resolvers'; +import { validateDocument, writeFile } from './utils'; const parser = new ArgumentParser(); parser.addArgument(['-s', '--stage'], { @@ -15,30 +16,31 @@ parser.addArgument(['-s', '--stage'], { }); const args = parser.parseArgs(); -function writeFile(document: { [name: string]: string }) { - let output = ''; - const variables = Object.keys(document); - for (const variable of variables) { - output += `${variable}=${document[variable]}\n`; - } - fs.writeFileSync('.env', output); -} +let document; try { // TODO Load external resolvers - - let document = yaml.safeLoad(fs.readFileSync('./env.yml', 'utf8')) as Document; + document = yaml.safeLoad(fs.readFileSync('./env.yml', 'utf8')) as Document; if (args.stage) { document = (document as any)[args.stage]; if (!document) { throw new Error(`Could not locate stage ${args.stage} in file ${args.file}`); } } - const rewriter = new Rewriter(resolvers); - rewriter.rewrite(document).then(result => { - console.info(`Writing .env file to ${process.cwd()}/.env`); - writeFile(result); - }); -} catch (e) { - console.error(`Could not load yaml ${e}`); + + const errors = validateDocument(document); + if (errors.length) { + throw new Error(`Validation errors found while loading document. Did you forget to specify -s?: \n${errors.join("\n")}`); + } +} catch (error) { + console.error(`Could not load yaml ${error.stack}`); + process.exit(1); } +const rewriter = new Rewriter(resolvers); +rewriter.rewrite(document).then(result => { + console.info(`Writing .env file to ${process.cwd()}/.env`); + writeFile(result); +}).catch((error: Error) => { + console.error(`Could not write .env file: ${error.stack}`); + process.exit(1); +}); diff --git a/src/resolvers.ts b/src/resolvers.ts index da24282..8596736 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -1,12 +1,15 @@ import * as AWS from 'aws-sdk'; import { CloudFormation, Config } from 'aws-sdk'; import { DescribeStacksOutput } from 'aws-sdk/clients/cloudformation'; +import { promisify } from 'util'; import { ResolverMap } from './types'; +const Credstash = require('nodecredstash'); export const resolvers: ResolverMap = { cft: async (argument: string) => { + if (!AWS.config.region) { AWS.config.update({ region: 'us-east-1' }); } @@ -37,5 +40,13 @@ export const resolvers: ResolverMap = { }, constant: async (argument: string) => { return argument; + }, + cred: async (argument: string) => { + const credstash = new Credstash({ awsOpts: { region: 'us-east-1' } }); + const promisified = promisify(credstash.getSecret); + return promisified({ name: argument }) + .catch((error: Error) => { + throw new Error(`Could not load value ${argument} from credstash: ${error.stack}`); + }); } }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..1433f7e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,21 @@ +import * as fs from 'fs'; + +export function writeFile(document: { [name: string]: string }) { + let output = ''; + const keys = Object.keys(document); + for (const key of keys) { + output += `${key}=${document[key]}\n`; + } + fs.writeFileSync('.env', output); +} + +export function validateDocument(document: any): string[] { + const errors = []; + const keys = Object.keys(document); + for (const key of keys) { + if (typeof document[key] !== 'string') { + errors.push(`${key} has an invalid value ${JSON.stringify(document[key])}`); + } + } + return errors; +} diff --git a/yarn.lock b/yarn.lock index dca555b..71da5d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,10 @@ acorn@^5.0.0, acorn@^5.3.0: version "5.5.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" +aes-js@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072" + agent-base@4, agent-base@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.0.tgz#9838b5c3392b962bad031e6a4c5e1024abec45ce" @@ -417,6 +421,21 @@ aws-sdk@*, aws-sdk@^2.224.1: xml2js "0.4.17" xmlbuilder "4.2.1" +aws-sdk@^2.171.0: + version "2.225.1" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.225.1.tgz#6ec6994a6337f2b553b16f6576df34ea1ac314d8" + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.8" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.17" + xmlbuilder "4.2.1" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -4300,6 +4319,14 @@ node-uuid@~1.4.7: version "1.4.8" resolved "https://registry.yarnpkg.com/node-uuid/-/node-uuid-1.4.8.tgz#b040eb0923968afabf8d32fb1f17f1167fdab907" +nodecredstash@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/nodecredstash/-/nodecredstash-2.0.2.tgz#2a36ae12534c4e975aafde1deafdd85773eb5dcb" + dependencies: + aes-js "^3.1.0" + aws-sdk "^2.171.0" + debug "^3.1.0" + noop-logger@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"