diff --git a/README.md b/README.md index 1bc5772..7613d3b 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,21 @@ Note that stages are not required in your yaml file - you can also define it wit ## Configuration -Note that any AWS references (cred, cft, etc...) are currently hard-coded to us-east-1. +In order to override default configuration, you can supply a `env.js` located next to your `env.yml`. The format of this file is as follows: + +```javascript +{ + awsRegion: '', + resolvers: { + test: value => { + // transformation + return transformed-value; // or promise + } + } +} +``` +Resolvers specified in this file will allow you to expand on the current set of resolvers included in dotenvi (e.g., `env`, `cft`, etc...). In the above example, the resolver `test` will match all references that look like `${test:some-value}`. ## Discussion @@ -70,7 +83,6 @@ The reference syntax used in `env.yml` is inspired by [serverless](https://githu ## Possible Future Work -1. Support for user-defined resolvers (e.g., other than `cft` and `env`). -2. Allow for `dotenvi` to replace `dotenv`, if desired, by skipping the `.env`-generation step. -3. Support for references embedded within a configuration value (e.g., `foo-${env:BAR}` --> `foo-bar` if BAR=bar) -4. Support recursive reference calls (e.g., `${env:${env:FOO}}`) +1. Allow for `dotenvi` to replace `dotenv`, if desired, by skipping the `.env`-generation step. +2. Support for references embedded within a configuration value (e.g., `foo-${env:BAR}` --> `foo-bar` if BAR=bar) +3. Support recursive reference calls (e.g., `${env:${env:FOO}}`) diff --git a/example/env.js b/example/env.js new file mode 100644 index 0000000..f7b935b --- /dev/null +++ b/example/env.js @@ -0,0 +1,9 @@ +module.exports = { + awsRegion: 'us-east-1', + foo: 'ignore me', + resolvers: { + poop: arg => { + return 'poop!' + } + } +}; diff --git a/env.yml b/example/env.yml similarity index 93% rename from env.yml rename to example/env.yml index 0ff55a8..e02fee2 100644 --- a/env.yml +++ b/example/env.yml @@ -6,6 +6,7 @@ default_env: &default_env CRED_OPTIONAL: value: ${cred:IDSFJKLJ} optional: true + TEST: ${poop:hello} sandbox: <<: *default_env diff --git a/package.json b/package.json index f301d00..82d2b60 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "dotenvi": "dist/index.js" }, "jest": { - "moduleFileExtensions": ["ts", "js"], + "moduleFileExtensions": [ + "ts", + "js" + ], "transform": { "^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js" }, @@ -26,9 +29,7 @@ ], "testEnvironment": "node", "coverageDirectory": "./coverage", - "collectCoverage": true, - "testMatch": ["**/*.test.(ts|js)"], - "testEnvironment": "node" + "collectCoverage": true }, "dependencies": { "@types/argparse": "^1.0.33", @@ -54,9 +55,17 @@ "type": "git", "url": "https://github.com/b3ross/dotenvi.git" }, - "keywords": ["dotenv", "yaml", "config", "environment"], + "keywords": [ + "dotenv", + "yaml", + "config", + "environment" + ], "lint-staged": { - "*.{ts,json}": ["prettier --write", "git add"] + "*.{ts,json}": [ + "prettier --write", + "git add" + ] }, "prettier": { "printWidth": 120, diff --git a/src/index.ts b/src/index.ts index ec15066..a91c25f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,7 @@ import { ArgumentParser } from 'argparse'; import { Rewriter } from './rewriter'; import { ResolverMap, Document, InputDocument } from './types'; import { resolvers } from './resolvers'; -import { writeFile, validateOutput } from './utils'; +import { writeFile, validateOutput, loadConfig } from './utils'; import { parse } from './inputParser'; const parser = new ArgumentParser(); @@ -14,19 +14,20 @@ parser.addArgument(['-s', '--stage'], { help: 'Environment stage', dest: 'stage' }); -const args = parser.parseArgs(); +const args = parser.parseArgs(); +const config = loadConfig(); let document: InputDocument; try { - // TODO Load external resolvers const contents = fs.readFileSync('env.yml', 'utf8') document = parse(contents, args.stage); } catch (error) { console.error(`Could not load yaml ${error.stack}`); process.exit(1); } -const rewriter = new Rewriter(resolvers); + +const rewriter = new Rewriter(config); rewriter.rewrite(document).then(result => { const errors = validateOutput(document, result); if (errors.length) { diff --git a/src/resolvers.ts b/src/resolvers.ts index bfa9745..1026487 100644 --- a/src/resolvers.ts +++ b/src/resolvers.ts @@ -1,17 +1,17 @@ import * as AWS from 'aws-sdk'; -import { CloudFormation, Config } from 'aws-sdk'; +import { CloudFormation } from 'aws-sdk'; import { DescribeStacksOutput } from 'aws-sdk/clients/cloudformation'; import { promisify } from 'util'; +const Credstash = require('nodecredstash'); -import { ResolverMap } from './types'; +import { ResolverMap, Config } from './types'; -const Credstash = require('nodecredstash'); export const resolvers: ResolverMap = { - cft: async (argument: string) => { + cft: async (argument: string, config: Config) => { if (!AWS.config.region) { - AWS.config.update({ region: 'us-east-1' }); + AWS.config.update({ region: config.awsRegion }); } const cft = new CloudFormation(); const parsedArgument = argument.split('.', 2); @@ -45,8 +45,8 @@ export const resolvers: ResolverMap = { constant: async (argument: string) => { return argument; }, - cred: async (argument: string) => { - const credstash = new Credstash({ awsOpts: { region: 'us-east-1' } }); + cred: async (argument: string, config: Config) => { + const credstash = new Credstash({ awsOpts: { region: config.awsRegion } }); const promisified = promisify(credstash.getSecret); return promisified({ name: argument }) .catch((error: Error): string => { diff --git a/src/rewriter.test.js b/src/rewriter.test.js index 990c940..58b9c6f 100644 --- a/src/rewriter.test.js +++ b/src/rewriter.test.js @@ -13,7 +13,7 @@ describe('Rewriter', () => { } }; - const rewriter = new Rewriter(resolvers); + const rewriter = new Rewriter({ resolvers: resolvers }); return rewriter.rewrite(document).then((output) => { expect(output['explicit']).toBe(output['implicit']); }); @@ -27,7 +27,7 @@ describe('Rewriter', () => { } }; - const rewriter = new Rewriter(resolvers); + const rewriter = new Rewriter({ resolvers: resolvers }); return rewriter.rewrite(document).then((output) => { expect(output['test']).toBe('hello'); }); diff --git a/src/rewriter.ts b/src/rewriter.ts index f362ef5..bb52a25 100644 --- a/src/rewriter.ts +++ b/src/rewriter.ts @@ -1,7 +1,7 @@ -import { ResolverMap, Document, InputDocument } from './types'; +import { Document, InputDocument, Config } from './types'; export class Rewriter { - constructor(private resolvers: ResolverMap) { } + constructor(private config: Config) { } async rewrite(document: InputDocument): Promise { const variables = Object.keys(document); @@ -22,14 +22,14 @@ export class Rewriter { if (!resolver) { throw new Error(`Could not locate resolver for value ${value}`); } - const result = await resolver(innerValue); + const result = await resolver(innerValue, this.config); return result; } private getResolver(name: string) { if (!name) { - return this.resolvers['constant']; + return this.config.resolvers['constant']; } - return this.resolvers[name]; + return this.config.resolvers[name]; } } diff --git a/src/types.ts b/src/types.ts index 85d43f4..b8019a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,21 @@ -export type ResolverMap = { [name: string]: (arg: string) => Promise }; +export type ResolverFunction = (arg: string, config: Config) => Promise; +export type ResolverMap = { + [name: string]: ResolverFunction +}; export type Document = { [name: string]: string } - export class InputDocument { [name: string]: { - value: string, + value: string; optional?: boolean } } + +export class Config { + constructor() { + this.resolvers = {}; + this.awsRegion = 'us-east-1'; + } + awsRegion: string; + resolvers: ResolverMap +} diff --git a/src/utils.test.js b/src/utils.test.js index 6efe6da..1635126 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -33,7 +33,7 @@ describe('validateOutput', () => { it('Does not error if output null and optional', () => { const input = { 'environment': { - value: '${env:this_should_not_be_defined', + value: '${env:this_should_not_be_defined}', optional: true } }; diff --git a/src/utils.ts b/src/utils.ts index 6120029..70efcf5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import * as fs from 'fs'; -import { Document, InputDocument } from './types'; + +import { Document, InputDocument, Config } from './types'; +import { resolvers } from './resolvers'; export function writeFile(document: { [name: string]: string }) { let output = ''; @@ -25,3 +27,17 @@ export function validateOutput(input: InputDocument, output: Document): string[] } return errors; } + + +export function loadConfig(): Config { + let config: Config = new Config(); + if (fs.existsSync(`./env.js`)) { + console.info(`Loading configuration from ${process.cwd()}/env.js`); + config = require(`${process.cwd()}/env`) as Config; + if (!config.resolvers) { + config.resolvers = {}; + } + } + config.resolvers = Object.assign({}, config.resolvers, resolvers); + return config; +}