Skip to content

Commit

Permalink
feat(config): Add config support (#17)
Browse files Browse the repository at this point in the history
* feat(config): Add config support

* feat(config): Add config support

* feat(config): Move examples into example directory

* feat(config): Move examples into example directory

* feat(config): Make config members non-optional

* feat(config): Fix tests

* feat(config): Fix README

* feat(config): Fix README

* feat(config): Fix README

* feat(config): Fix README
  • Loading branch information
Ben Ross authored Apr 23, 2018
1 parent de2a19c commit 1520ebc
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 34 deletions.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<aws-region>',
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

Expand All @@ -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}}`)
9 changes: 9 additions & 0 deletions example/env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = {
awsRegion: 'us-east-1',
foo: 'ignore me',
resolvers: {
poop: arg => {
return 'poop!'
}
}
};
1 change: 1 addition & 0 deletions env.yml → example/env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ default_env: &default_env
CRED_OPTIONAL:
value: ${cred:IDSFJKLJ}
optional: true
TEST: ${poop:hello}

sandbox:
<<: *default_env
Expand Down
21 changes: 15 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
"dotenvi": "dist/index.js"
},
"jest": {
"moduleFileExtensions": ["ts", "js"],
"moduleFileExtensions": [
"ts",
"js"
],
"transform": {
"^.+\\.(ts|tsx)$": "./node_modules/ts-jest/preprocessor.js"
},
Expand All @@ -26,9 +29,7 @@
],
"testEnvironment": "node",
"coverageDirectory": "./coverage",
"collectCoverage": true,
"testMatch": ["**/*.test.(ts|js)"],
"testEnvironment": "node"
"collectCoverage": true
},
"dependencies": {
"@types/argparse": "^1.0.33",
Expand All @@ -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,
Expand Down
9 changes: 5 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ 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();
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) {
Expand Down
14 changes: 7 additions & 7 deletions src/resolvers.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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 => {
Expand Down
4 changes: 2 additions & 2 deletions src/rewriter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
});
Expand All @@ -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');
});
Expand Down
10 changes: 5 additions & 5 deletions src/rewriter.ts
Original file line number Diff line number Diff line change
@@ -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<Document> {
const variables = Object.keys(document);
Expand All @@ -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];
}
}
17 changes: 14 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
export type ResolverMap = { [name: string]: (arg: string) => Promise<string> };
export type ResolverFunction = (arg: string, config: Config) => Promise<string>;
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
}
2 changes: 1 addition & 1 deletion src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};
Expand Down
18 changes: 17 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -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 = '';
Expand All @@ -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;
}

0 comments on commit 1520ebc

Please sign in to comment.