diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..bb6abc1 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "rules": { + "indent": [ + 2, + 2, + {"SwitchCase": 1} + ], + "quotes": [ + 2, + "single" + ], + "linebreak-style": [ + 2, + "unix" + ], + "semi": [ + 2, + "always" + ] + }, + "env": { + "node": true + }, + "extends": "eslint:recommended" +} diff --git a/consul-kv-sync.js b/consul-kv-sync.js new file mode 100755 index 0000000..2c7b47c --- /dev/null +++ b/consul-kv-sync.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +var fs = require('fs'); +var Promise = require('bluebird'); +var program = require('commander'); +var jptr = require('json-ptr'); +var _ = require('lodash'); +var request = require('request-promise'); +var base64 = require('js-base64').Base64; + +var pkg = require('./package.json'); + +var readFile = Promise.promisify(fs.readFile); + +function readFragments(fileName) { + return readFile(fileName, "utf8").then(function(contents){ + return jptr.list(JSON.parse(contents)); + }); +} + +request.defaults({json:true}); + +program.version(pkg.version) + .description("Synchronizes one or more JSON manifests with consul's key value store") + .option('-H, --host ', "Consul API url, default: http://consul.service.consul:8500"); + +program.parse(process.argv); + +var host = program.host || process.env.CONSUL_HOST || 'http://consul.service.consul:8500/'; + +Promise.all(_.map(program.args, readFragments)).then(function (files){ + var flattened = _.flatten(files); + var prefix = flattened[1].fragmentId.substring(2); + var reduced = _.reduce(_.filter(flattened, function(x){ return _.isString(x.value) || _.isFinite(x.value)}), function(acc, item){ + acc[item.fragmentId.substring(2)] = item.value; + return acc; + }, {}); + + var existing = {}; + request.get(host + 'v1/kv/' + prefix + '?recurse=1', {json: true}) + .then(function (res) { + _.each(res, function(item){ + existing[item.Key] = base64.decode(item.Value); + }) + if (res.statusCode == 200) { + existing = res.body; + } + return; + }).catch(function(err){ + console.log(err); + //ignore errors, jus + }).then(function(){ + return Promise.all(_.map(reduced, function(value, key){ + delete existing[key]; + return request.put(host + 'v1/kv/' + key, {body:''+value}); + })); + }).then(function(){ + return Promise.all(_.map(existing, function(value, key){ + return request(host + 'v1/kv/' + key, {method:'DELETE'}); + })); + }).then(function(){ + console.log(reduced); + console.log('Synced'); + }); +}); + +if (!program.args.length) program.help(); + diff --git a/package.json b/package.json index 35a9358..a9a5746 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,21 @@ } ], "scripts": { - "test": "mocha test" + "test": "mocha", + "test:watch": "onchange 'test/**/*' '*.js' '*.json' -- npm run test", + "lint": "esw --quiet", + "lint:watch": "esw --quiet --watch", + "ci": "npm run lint && npm run test" }, "dependencies": { "assert-plus": "^0.1.5", "bluebird": "^3.0.5", + "commander": "^2.9.0", + "js-base64": "^2.1.9", + "json-ptr": "^0.2.0", "lodash": "^3.10.1", - "request": "^2.65.0" + "request": "^2.65.0", + "request-promise": "^1.0.2" }, "config": { "title": "Consul KV Sync", @@ -35,6 +43,8 @@ }, "devDependencies": { "chai": "^3.3.0", + "eslint": "^1.9.0", + "eslint-watch": "^2.1.3", "istanbul": "^0.4.0", "mocha": "^2.2.5", "onchange": "^2.0.0" diff --git a/test/one.json b/test/one.json new file mode 100644 index 0000000..2c4be00 --- /dev/null +++ b/test/one.json @@ -0,0 +1,6 @@ +{ + "service": { + "one": "value 1", + "two": "value 2" + } +} diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..7616f21 --- /dev/null +++ b/test/test.js @@ -0,0 +1,70 @@ + +var exec = require('child_process').exec; +var expect = require('chai').expect; +var request = require('request-promise'); +var base64 = require('js-base64').Base64; +var _ = require('lodash'); + +var host = process.env.CONSUL_HOST || 'http://consul.service.consul:8500/'; + +describe('consul-kv-sync', function(){ + + describe('#run', function(){ + var response; + before(function(done){ + request.put(host + 'v1/kv/service/four', { + body: 'value for removal', + json: true + }).then(function(){ + + var proc = exec('node ../consul-kv-sync.js --host "' + host + '" ./one.json ./two.json', {cwd:__dirname}); + proc.on('exit', function(){ + request.get(host + 'v1/kv/service?recurse=1', {json: true}).then(function(result){ + response = result; + done(); + }).catch(done); + }); + }); + }); + + it ('should set value to correct value', function(){ + var item = response.find(function(item){ + return item.Key == 'service/two'; + }); + + expect(item).to.be.ok; + expect(base64.decode(item.Value)).to.eql('value 2'); + }); + + it ('should set overridden value to correct value', function(){ + var item = response.find(function(item){ + return item.Key == 'service/one'; + }); + + expect(item).to.be.ok; + expect(base64.decode(item.Value)).to.eql('value from file two'); + }); + + it ('should set array parameters correctly', function(){ + var items = _.filter(response, function(item){ + return /^service\/arrayparam/.test(item.Key); + }); + + expect(items.length).to.eql(4); + expect(items[0].Key).to.eql('service/arrayparam/0'); + expect(base64.decode(items[0].Value)).to.eql('1'); + expect(base64.decode(items[1].Value)).to.eql('2'); + expect(base64.decode(items[2].Value)).to.eql('3'); + expect(base64.decode(items[3].Value)).to.eql('4'); + }); + + it ('should set remove existing keys that are not in config file', function(){ + var items = _.filter(response, function(item){ + return item.Key == 'service/four'; + }); + + expect(items.length).to.eql(0); + }); + + }); +}); diff --git a/test/two.json b/test/two.json new file mode 100644 index 0000000..df1cab7 --- /dev/null +++ b/test/two.json @@ -0,0 +1,7 @@ +{ + "service": { + "one": "value from file two", + "three": "value 3", + "arrayparam": [1,2,3,4] + } +}