Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add environment checker #13

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
33 changes: 33 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const Injector = require("./lib/injector");
const Component = require("./lib/component");
const Logger = require("./lib/logger");
const utils = require("./lib/utils");
const EnvValidator = require("./lib/env_validator");

function merapi(options) {
return new Container(options);
Expand Down Expand Up @@ -150,6 +151,10 @@ class Container extends Component.mixin(AsyncEmitter) {
}

*_initialize() {
yield this.emit("beforeValidateConfig", this);
this.validateConfig();
yield this.emit("afterValidateConfig", this);

yield this.emit("beforeInit", this);
yield this.emit("beforeConfigResolve", this);
this.config.resolve();
Expand Down Expand Up @@ -315,6 +320,34 @@ class Container extends Component.mixin(AsyncEmitter) {
}
}
}

validateConfig() {
const systemEnv = () => {
const result = {};
const env = process.env;
for(const key of Object.keys(env))
result["$"+key] = env[key]; // system env, append $ to key
return result;
};
const combinedEnv = Object.assign(
{},
this.options.envConfig && this.options.envConfig[this.config.env],
this.options.extConfig,
systemEnv()
);
const { config, delimiters } = this.options;
const result = EnvValidator.validateEnvironment(combinedEnv, config, delimiters);

if (result.empty.length > 0) {
this.logger.warn("WARNING! These configurations are empty string: ", result.empty);
}

if (result.undefined.length > 0) {
this.logger.error("These configurations are not set on env variables: ", result.undefined);
throw new Error("Configuration error, some env variables are not set");
}
return true;
}
}

merapi.Container = Container;
Expand Down
23 changes: 11 additions & 12 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function trimify(str) {

/**
* Config creator
*
*
* @param {Object} data
* Config data
*/
Expand Down Expand Up @@ -60,7 +60,6 @@ class Config {
}
data = val;
}

return data;
}
/**
Expand Down Expand Up @@ -109,7 +108,7 @@ class Config {

/**
* Check if config path exist
*
*
* @param {String} path
* config path
* @returns {Boolean}
Expand All @@ -118,7 +117,7 @@ class Config {
has(path) {
return this.get(path, true) !== undefined;
}

/**
* Get or use default value
* @param {String} path
Expand All @@ -128,7 +127,7 @@ class Config {
default(path, def) {
return this.has(path) ? this.get(path) : def;
}

/**
* Internal flatten function
* @param {Object} data
Expand All @@ -149,7 +148,7 @@ class Config {
}
});
}

return res;
}

Expand Down Expand Up @@ -192,7 +191,6 @@ class Config {
}
return tpl(params);
}

return tpl(params);
}

Expand Down Expand Up @@ -226,12 +224,13 @@ class Config {
for (let n in flat) {
ret[n] = this.set(n, this.resolve(n, false));
}

return ret;
}

/**
* Create subconfig by path
*
*
* @method path
* @param {String} path
* config path
Expand All @@ -241,7 +240,7 @@ class Config {
path(path, opts) {
return this.create(this.get(path), opts);
}

/**
* Extend config with data
* @param {Object} data
Expand All @@ -255,7 +254,7 @@ class Config {
}
return this;
}

/**
* Create new config
* @param {Object} data
Expand All @@ -278,7 +277,7 @@ class Config {

return config;
}

/**
* Create new config
* @param {Object} data
Expand Down
73 changes: 73 additions & 0 deletions lib/env_validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use strict";
const Config = require("./config");
const isNil = require("lodash/isNil");
const isEmpty = require("lodash/isEmpty");

/**
* Make sure environment variables needed in configuration exists.
*
* Return environment variables needed to be defined.
*
* @param {object} environment Environment variables to be validated, intended to be filled with process.env object
* @param {object} configuration Configuration used for merapi
* @param {object} delimiters Delimiters object, used to parse variables. Examples:
* formatted in {
* left: "${"
* right: "}"
* }
* for entry ${$SOME_ENV}
*
*/
exports.validateEnvironment = (environment, configuration, delimiters = { left: "{", right: "}" }) => {
if (isNil(environment)) throw new Error("No environment variable set in this system");
if (isNil(configuration) || isEmpty(environment)) throw new Error("No configuration is set");

const config = new Config();
const flattenConfiguration = config._flatten(configuration);
const neededValues = {
undefined: [],
empty: []
};

for (const key of Object.keys(flattenConfiguration)) {
const value = flattenConfiguration[key];
if (isNil(value)) {
throw new Error(`Error on Config, '${key}' is needed, but the value is ${value}`);
}
if (containDelimiters(value, delimiters)) {
const envKey = value.substring(delimiters.left.length, value.length - delimiters.right.length);
const envValue = environment[envKey];
const sanitisedEnvKey = envKey.replace(/\$/,""); // remove $

if (envValue === "") {
neededValues["empty"].push(sanitisedEnvKey);
} else if (isNil(envValue) && !isNestedValue(envKey, configuration)) {
neededValues["undefined"].push(sanitisedEnvKey);
}
}
}
return neededValues;
};

const containDelimiters = (string, delimiters) => {
if (isNil(string)) return false;
if (isNil(delimiters)) return false;
return typeof string === "string" &&
string.includes(delimiters.left) &&
string.includes(delimiters.right);
};
exports.containDelimiters = containDelimiters;

const isNestedValue = (value, config) => {
let data = config;
const parts = value.split(".").map(val => /^\d+$/.test(val) ? parseInt(val) : val);
if (parts.length === 1) return false;
for(let i = 0; i < parts.length; i++) {
let value = data[parts[i]];
if (data === undefined) {
return false;
}
data = value;
}
return true;
};
118 changes: 118 additions & 0 deletions test/env_validator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"use strict";
const assert = require("assert");
const envValidator = require("../lib/env_validator");

/* eslint-env mocha */

describe("Env validator", () => {
let config;
let env = {
"$GEIST_URI": "https://example.com",
"$GEIST_TOKEN": "asasaklns12io1u31oi2u3"
};

const delimiters = {
left: "${",
right: "}"
};

beforeEach(() => {
config = {
geist: {
type: "proxy",
uri: "${$GEIST_URI}",
version: "v1",
secret: "${$GEIST_TOKEN}"
}
};
});

it("should return object of empty and undefined env variables, if not set", () => {
env["$GEIST_EMPTY"] = "";
config.geist.lala = "${$LALA}";
config.geist.empty = "${$GEIST_EMPTY}";
config.diaenne = {
type: "proxy",
uri: "${$DIAENNE_URI}",
version: "${$VERSION}"
};
config.auth = "${$SECRET}";

const result = {
undefined: ["LALA", "DIAENNE_URI", "VERSION", "SECRET"],
empty: ["GEIST_EMPTY"]
};
const actualResult = envValidator.validateEnvironment(env, config, delimiters);
assert.deepEqual(actualResult, result);
});

it("should return empty list of undefined and empty if env needed is set already", () => {
const result = envValidator.validateEnvironment(env, config, delimiters);
assert.deepStrictEqual(result, {
undefined: [],
empty: []
});
});

it("should throw error if one of the variable contains null", () => {
config.diaenne = {
type: null,
uri: "${$DIAENNE_URI}",
version: "${$VERSION}"
};
try {
envValidator.validateEnvironment(env, config, delimiters);
} catch(e) {
assert.equal(e.message, "Error on Config, 'diaenne.type' is needed, but the value is null");
}
});

it("should throw error if no environment variables is not installed in this system", () => {
try {
envValidator.validateEnvironment(null, config, delimiters);
} catch(e) {
assert.equal(e.message, "No environment variable set in this system");
}
});

it("should throw error if no configuration is set", () => {
try {
envValidator.validateEnvironment({}, null, delimiters);
} catch(e) {
assert.equal(e.message, "No configuration is set");
}
});
});

describe("containDelimiters", () => {
let delimiters;
before(() => {
delimiters = {left: "{", right: "}"};
});

it("should return false if string contains NO delimiters", () => {
const res = envValidator.containDelimiters("LALAJO", delimiters);
assert.deepStrictEqual(res, false);
});

it("should return true if string contains delimiters", () => {
const res = envValidator.containDelimiters("{LALAJO}", delimiters);
assert.deepStrictEqual(res, true);
});

it("should return false if string is null / undefined", () => {
let result = envValidator.containDelimiters(null, { left: "{", right: "}" });
assert.deepStrictEqual(result, false);

result = envValidator.containDelimiters(undefined, delimiters);
assert.deepStrictEqual(result, false);
});

it("should return false if delimiters is null / undefined", () => {
let res = envValidator.containDelimiters("{LALAJO}", null);
assert.deepStrictEqual(res, false);

res = envValidator.containDelimiters("{LALAJO}", undefined);
assert.deepStrictEqual(res, false);
});
});
Loading