From 810abbc7c573bb643fbc9a841a57884583cdd0b1 Mon Sep 17 00:00:00 2001 From: Kostas Kapetanakis Date: Sun, 5 Feb 2017 01:54:23 +0000 Subject: [PATCH 01/12] Parsing YAML file with more complex hierarchy --- lib/consul/index.js | 17 +++++- package.json | 1 + test/git2consul_parsing_yml_test.js | 91 +++++++++++++++++++++++++++++ test/resources/complex_sample.yaml | 26 +++++++++ test/utils/consul_utils.js | 1 + 5 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 test/git2consul_parsing_yml_test.js create mode 100644 test/resources/complex_sample.yaml diff --git a/lib/consul/index.js b/lib/consul/index.js index b1fbdad..9adaeee 100644 --- a/lib/consul/index.js +++ b/lib/consul/index.js @@ -84,12 +84,23 @@ var create_key_name = function(branch, file, ref) { var render_obj = function(parts, prefix, obj) { _.mapObject(obj, function(val, key) { - if (_.isArray(val)) return; - + if (_.isArray(val)) { + var thingToReturn = []; + if(val[0].constructor !== {}.constructor){ + thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),val)); + parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(val) }); + }else{ + val.forEach(function(value){ + thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),value)); + parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(value) }); + }); + } + return thingToReturn; + } if (_.isObject(val)) return render_obj(parts, prefix + '/' + encodeURIComponent(key), val) - parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': val}); }); + logger.debug("PARTS found: " , parts); } /** diff --git a/package.json b/package.json index 01afb9c..65eeaeb 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "js-yaml": "^3.6.1" }, "devDependencies": { + "fs": "0.0.1-security", "grunt": "^0.4.5", "grunt-debian-package": "^0.1.11", "istanbul": "0.2.11", diff --git a/test/git2consul_parsing_yml_test.js b/test/git2consul_parsing_yml_test.js new file mode 100644 index 0000000..a731e89 --- /dev/null +++ b/test/git2consul_parsing_yml_test.js @@ -0,0 +1,91 @@ +var should = require('should'); +var _ = require('underscore'); +var fs = require('fs'); + +var mkdirp = require('mkdirp'); + +// We want this above any git2consul module to make sure logging gets configured +require('./git2consul_bootstrap_test.js'); + +var repo = require('../lib/git/repo.js'); +var git_utils = require('./utils/git_utils.js'); +var consul_utils = require('./utils/consul_utils.js'); + +var git_commands = require('../lib/git/commands.js'); + +describe('Parse YAML', function() { + + // The current copy of the git master branch. This is initialized before each test in the suite. + var branch; + beforeEach(function(done) { + + // Each of these tests needs a working repo instance, so create it here and expose it to the suite + // namespace. These are all tests of expand_keys mode, so set that here. + git_utils.initRepo(_.extend(git_utils.createRepoConfig(), {'expand_keys': true}), function(err, repo) { + if (err) return done(err); + + // The default repo created by initRepo has a single branch, master. + branch = repo.branches['master']; + + done(); + }); + }); + + /*YAML*/ + + it ('should handle complex YAML files', function(done) { + var sample_key = 'complex_sample.yaml'; + // from: js-yaml / test / samples-load-errors / forbidden-value.yml + var sample_file = "./test/resources/complex_sample.yaml"; + + fs.readFile(sample_file, "utf-8", function(err, new_file_content){ + if (err) return done(err); + + // Add the sample file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, new_file_content, "Add a file.", function(err) { + if (err) return done(err); + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + var values_to_test = [ + { + key:'name', + value:'Kostas D\'vloper' + },{ + key:'job', + value:'Developer' + },{ + key:'skill', + value:'Elite' + },{ + key:'employed', + value:'true' + },{ + key:'foods', + value:'["Apple","Orange","Strawberry","Mango"]' + },{ + key:'languages/perl/certified', + value:'true' + },{ + key:'languages/perl/level/scored', + value:'["high","medium"]' + },{ + key:'languages/perl/level/scored/0', + value:'high' + }, + ]; + values_to_test.forEach(function(test_value){ + consul_utils.validateValue("test_repo/master/complex_sample.yaml/"+test_value['key'], test_value['value'], function(err, value) { + if (err) return done(err); + }); + }); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/complex_sample.yaml/my.server.path/KEY-ENV-VAR', 'true', function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + }); +}); diff --git a/test/resources/complex_sample.yaml b/test/resources/complex_sample.yaml new file mode 100644 index 0000000..31be23d --- /dev/null +++ b/test/resources/complex_sample.yaml @@ -0,0 +1,26 @@ +name: Kostas D'vloper +job: Developer +skill: Elite +employed: True +foods: + - Apple + - Orange + - Strawberry + - Mango +languages: + perl: + certified: true + level: + scored: + - high + - medium + python: + certified: true + level: + scored: + - high + - medium + pascal: Lame +my.server.path: + - KEY-ENV-VAR: "true" + - OTHER-VAR: "59900" \ No newline at end of file diff --git a/test/utils/consul_utils.js b/test/utils/consul_utils.js index c77e4c5..b5a57fd 100644 --- a/test/utils/consul_utils.js +++ b/test/utils/consul_utils.js @@ -33,6 +33,7 @@ exports.getKeyIndices = function(key, cb) { exports.validateValue = function(key, expected_value, cb) { logger.trace('Looking for key %s with value %s', key, expected_value); exports.getValue(key, function(err, value) { + logger.trace("found key: ",key, value); if (err) return cb(err); if (!expected_value) { (value == undefined).should.equal(true); From 2658e1ecd01caddde225569537cef26f2086017e Mon Sep 17 00:00:00 2001 From: Kostas Kapetanakis Date: Sun, 5 Feb 2017 01:56:28 +0000 Subject: [PATCH 02/12] try to fix, and skip *previously* broken test (not broken by these commits) --- test/git2consul_ignore_repo_name_test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/git2consul_ignore_repo_name_test.js b/test/git2consul_ignore_repo_name_test.js index 7b5c2a9..70b39c7 100644 --- a/test/git2consul_ignore_repo_name_test.js +++ b/test/git2consul_ignore_repo_name_test.js @@ -17,8 +17,8 @@ var git_commands = require('../lib/git/commands.js'); describe('ignore_repo_name', function() { - it ('should create folders on consul without the repo name prefix', function(done) { - + it.skip('should create folders on consul without the repo name prefix', function(done) { + // Create a remote git repo. Then, init a Repo object with property file and validate // that keys are in the appropriate place in the Consul KV store without the repo name prefix. git_commands.init(git_utils.TEST_REMOTE_REPO, function(err) { @@ -33,7 +33,7 @@ describe('ignore_repo_name', function() { var repo_config = git_utils.createRepoConfig(); repo_config.source_root = "src/main/resources"; repo_config.expand_keys = true; - repo.include_branch_name = false; + repo_config.include_branch_name = false; repo_config.ignore_repo_name = true; var repo = new Repo(repo_config); repo.init(function(err) { From 639f3f69e9df3d63af57ba8a84050b087fc2a522 Mon Sep 17 00:00:00 2001 From: Kostas Kapetanakis Date: Sun, 5 Feb 2017 02:11:40 +0000 Subject: [PATCH 03/12] Re-enable failing test to check on Travis --- test/git2consul_ignore_repo_name_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git2consul_ignore_repo_name_test.js b/test/git2consul_ignore_repo_name_test.js index 70b39c7..01edfca 100644 --- a/test/git2consul_ignore_repo_name_test.js +++ b/test/git2consul_ignore_repo_name_test.js @@ -17,7 +17,7 @@ var git_commands = require('../lib/git/commands.js'); describe('ignore_repo_name', function() { - it.skip('should create folders on consul without the repo name prefix', function(done) { + it ('should create folders on consul without the repo name prefix', function(done) { // Create a remote git repo. Then, init a Repo object with property file and validate // that keys are in the appropriate place in the Consul KV store without the repo name prefix. From 83084e7a515a515f62a08d3529fd9be08b480607 Mon Sep 17 00:00:00 2001 From: Kostas Kapetanakis Date: Tue, 7 Feb 2017 18:25:03 +0000 Subject: [PATCH 04/12] less noisy output (skipping middle values that were stringified) --- lib/consul/index.js | 13 +++++++++---- test/git2consul_parsing_yml_test.js | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/consul/index.js b/lib/consul/index.js index 9adaeee..cd974a2 100644 --- a/lib/consul/index.js +++ b/lib/consul/index.js @@ -84,15 +84,20 @@ var create_key_name = function(branch, file, ref) { var render_obj = function(parts, prefix, obj) { _.mapObject(obj, function(val, key) { - if (_.isArray(val)) { + if (_.isArray(val)) { var thingToReturn = []; if(val[0].constructor !== {}.constructor){ thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),val)); - parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(val) }); + parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(val)}); }else{ val.forEach(function(value){ - thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),value)); - parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(value) }); + if(value.constructor === {}.constructor){ + thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key)+"/"+Object.keys(value),value[Object.keys(value)[0]])); + parts.push({'key': prefix + '/' + encodeURIComponent(key)+"/"+Object.keys(value), 'value': value[Object.keys(value)[0]]}); + }else{ + thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),value)); + parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(value) }); + } }); } return thingToReturn; diff --git a/test/git2consul_parsing_yml_test.js b/test/git2consul_parsing_yml_test.js index a731e89..151172d 100644 --- a/test/git2consul_parsing_yml_test.js +++ b/test/git2consul_parsing_yml_test.js @@ -33,7 +33,7 @@ describe('Parse YAML', function() { /*YAML*/ - it ('should handle complex YAML files', function(done) { + it.only('should handle complex YAML files', function(done) { var sample_key = 'complex_sample.yaml'; // from: js-yaml / test / samples-load-errors / forbidden-value.yml var sample_file = "./test/resources/complex_sample.yaml"; @@ -72,7 +72,7 @@ describe('Parse YAML', function() { },{ key:'languages/perl/level/scored/0', value:'high' - }, + } ]; values_to_test.forEach(function(test_value){ consul_utils.validateValue("test_repo/master/complex_sample.yaml/"+test_value['key'], test_value['value'], function(err, value) { From 5626a11aeb6a2d554cf9f5f18575f95d9efd0328 Mon Sep 17 00:00:00 2001 From: "Reser, Ben" Date: Fri, 12 May 2017 05:32:01 -0500 Subject: [PATCH 05/12] Make test suite actually exit with proper exit code. Current test suite always exits with zero because it's piped through bunyan. Since bunyan is always successful and it's the last command on the pipe chain we get it's exit code. By moving the command into a shell script and setting the pipefail option for bash we will exit with the proper exit code. This means that Travis CI will correctly determine test failures. --- package.json | 2 +- test/test.sh | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100755 test/test.sh diff --git a/package.json b/package.json index 65eeaeb..5d800f1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "preferGlobal": true, "bin": "./index.js", "scripts": { - "test": "./node_modules/.bin/_mocha --reporter spec -t 10000 test | bunyan", + "test": "./test/test.sh", "cov": "`npm bin`/istanbul cover --root . -x node_modules -x test --dir ./reports `npm bin`/_mocha -- --reporter spec -t 10000 test | bunyan", "coveralls": "npm run cov && node_modules/coveralls/bin/coveralls.js < reports/lcov.info" }, diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..9b845f7 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -o pipefail + +./node_modules/.bin/_mocha --reporter spec -t 10000 test | bunyan From 67c1efded0d52e9202b03b38604f8ddcea545e35 Mon Sep 17 00:00:00 2001 From: "Reser, Ben" Date: Fri, 12 May 2017 09:20:32 -0500 Subject: [PATCH 06/12] Fix the failing ignore_repo_name test. Was trying to set the include_branch_name property on a variable that didn't exist. Which caused the test to fail with a syntax error. Was using a property file but wasn't expecting the file extension to be in the key path. --- test/git2consul_ignore_repo_name_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/git2consul_ignore_repo_name_test.js b/test/git2consul_ignore_repo_name_test.js index 01edfca..de96306 100644 --- a/test/git2consul_ignore_repo_name_test.js +++ b/test/git2consul_ignore_repo_name_test.js @@ -38,7 +38,7 @@ describe('ignore_repo_name', function() { var repo = new Repo(repo_config); repo.init(function(err) { if (err) return done(err); - consul_utils.validateValue('user-service-dev/default.connection.pool.db.url', "jdbc:mysql://db-host:3306/user", function(err, value) { + consul_utils.validateValue('user-service-dev.properties/default.connection.pool.db.url', "jdbc:mysql://db-host:3306/user", function(err, value) { if (err) return done(err); done(); }); From e6f1e584cd071f46016b436a61e0a6d7d6cfc00c Mon Sep 17 00:00:00 2001 From: "Reser, Ben" Date: Fri, 11 Aug 2017 16:29:24 -0700 Subject: [PATCH 07/12] Update links for build and test coverage. Part of moving from Cimpress to breser. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 499ac4d..ee68dcf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # git2consul -[![Build Status](https://travis-ci.org/Cimpress-MCP/git2consul.svg?branch=master)](https://travis-ci.org/Cimpress-MCP/git2consul) -[![Coverage Status](https://img.shields.io/coveralls/Cimpress-MCP/git2consul.svg)](https://coveralls.io/r/Cimpress-MCP/git2consul?branch=master) +[![Build Status](https://travis-ci.org/breser/git2consul.svg?branch=master)](https://travis-ci.org/breser/git2consul) +[![Coverage Status](https://img.shields.io/coveralls/breser/git2consul.svg)](https://coveralls.io/r/breser/git2consul?branch=master) [![Join the chat at https://gitter.im/Cimpress-MCP/git2consul](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Cimpress-MCP/git2consul?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Google Groups](https://img.shields.io/badge/google--group-git2consul-green.svg)](https://groups.google.com/group/git2consul-tool/) From 3c262bb8cabf993cadd2363003848d7e357201e2 Mon Sep 17 00:00:00 2001 From: "Reser, Ben" Date: Fri, 11 Aug 2017 20:02:06 -0700 Subject: [PATCH 08/12] Use --no-renames with doing git diff. git 2.x seems to automatically have renames on and git2consul doesn't know how to handle the R status. Might be better to handle all the statuses, but this appears to make it work with newer versions of git without further changes so going with this. Tested this with 1.9.1 and 2.14.1. Noticed this since Travis CI now has 2.13.0 --- lib/git/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/git/commands.js b/lib/git/commands.js index 2df8890..895de8a 100644 --- a/lib/git/commands.js +++ b/lib/git/commands.js @@ -83,7 +83,7 @@ var fixup_path = function(path) { }; exports.listChangedFiles = function(from_ref, to_ref, cwd, cb) { - run_command('git diff --name-status ' + from_ref + ' ' + to_ref, cwd, function(err, output) { + run_command('git diff --no-renames --name-status ' + from_ref + ' ' + to_ref, cwd, function(err, output) { /* istanbul ignore if */ if (err) return cb(err); From fa1002d80b33a370f712d8ce73b81c18f4c69601 Mon Sep 17 00:00:00 2001 From: "Reser, Ben" Date: Fri, 11 Aug 2017 20:32:45 -0700 Subject: [PATCH 09/12] Re-enable test coverage reporting Fixed the cov command just like I'd done for the test command, so that it will properly exit with an non-zero exit code on test failure. Switched back to using the cov script for doing the tests. --- .travis.yml | 2 +- package.json | 2 +- test/test.sh | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 93fa054..e78b58e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_script: - git config --global user.email "ryan@ryanbreen.com" - git config --global user.name "Ryan Breen" script: -- npm run test +- npm run cov deploy: provider: npm email: ryan@ryanbreen.com diff --git a/package.json b/package.json index 5d800f1..79663af 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "bin": "./index.js", "scripts": { "test": "./test/test.sh", - "cov": "`npm bin`/istanbul cover --root . -x node_modules -x test --dir ./reports `npm bin`/_mocha -- --reporter spec -t 10000 test | bunyan", + "cov": "./test/test.sh cov", "coveralls": "npm run cov && node_modules/coveralls/bin/coveralls.js < reports/lcov.info" }, "licenses": [ diff --git a/test/test.sh b/test/test.sh index 9b845f7..d0bdc6e 100755 --- a/test/test.sh +++ b/test/test.sh @@ -2,4 +2,12 @@ set -o pipefail -./node_modules/.bin/_mocha --reporter spec -t 10000 test | bunyan +bin=$(npm bin) +mocha_args='--reporter spec -t 10000 test' + +if [ "$1" == "cov" ]; then + "$bin/istanbul" cover --root . -x node_modules -x test --dir ./reports \ + "$bin/_mocha" -- $mocha_args | bunyan +else + "$bin/_mocha" $mocha_args | bunyan +fi From 9177187648b5d7adfc318db69fe33d9d250ecf62 Mon Sep 17 00:00:00 2001 From: Kostas Kapetanakis Date: Sat, 14 Oct 2017 11:14:02 +0100 Subject: [PATCH 10/12] change single const to var, remove .only --- lib/consul/index.js | 2 +- test/git2consul_parsing_yml_test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/consul/index.js b/lib/consul/index.js index fd9c4a9..855da5c 100644 --- a/lib/consul/index.js +++ b/lib/consul/index.js @@ -10,7 +10,7 @@ var consul = require('consul')({'host': global.endpoint, 'port': global.port, 's var token = undefined; -const EXPAND_EXTENSIONS = ['json', 'yaml', 'yml', 'properties']; +var EXPAND_EXTENSIONS = ['json', 'yaml', 'yml', 'properties']; // This makes life a bit easier for expand_keys mode, allowing us to check for a .json // extension with less code per line. diff --git a/test/git2consul_parsing_yml_test.js b/test/git2consul_parsing_yml_test.js index 151172d..9c0513e 100644 --- a/test/git2consul_parsing_yml_test.js +++ b/test/git2consul_parsing_yml_test.js @@ -33,7 +33,7 @@ describe('Parse YAML', function() { /*YAML*/ - it.only('should handle complex YAML files', function(done) { + it('should handle complex YAML files', function(done) { var sample_key = 'complex_sample.yaml'; // from: js-yaml / test / samples-load-errors / forbidden-value.yml var sample_file = "./test/resources/complex_sample.yaml"; From b2ca7a4af408f17c34ba99394b7ed1528010f32b Mon Sep 17 00:00:00 2001 From: "Michael K. Werle" Date: Fri, 15 Dec 2017 11:29:01 -0600 Subject: [PATCH 11/12] Remove fs dev depdencency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 18e07a9..5feb215 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "js-yaml": "^3.6.1" }, "devDependencies": { - "fs": "0.0.1-security", "grunt": "^1.0.1", "grunt-debian-package": "^0.1.11", "istanbul": "0.4.5", From 34da17eaaf282a0fa5c6bb424266dee81442a712 Mon Sep 17 00:00:00 2001 From: "Michael K. Werle" Date: Mon, 18 Dec 2017 09:37:31 -0600 Subject: [PATCH 12/12] Flexible array and array key formatting. --- README.md | 99 +++++++++++++++++++++++- lib/array_handler.js | 104 ++++++++++++++++++++++++++ lib/consul/index.js | 60 +++++++++------ lib/git/branch.js | 6 ++ test/git2consul_array_handler_test.js | 73 ++++++++++++++++++ test/git2consul_parsing_yml_test.js | 12 +-- test/resources/complex_sample.yaml | 4 +- 7 files changed, 326 insertions(+), 32 deletions(-) create mode 100644 lib/array_handler.js create mode 100644 test/git2consul_array_handler_test.js diff --git a/README.md b/README.md index ee68dcf..b608cb4 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,6 @@ A few notes on how this behaves: Similarly to JSON, git2consul can treat YAML documents in your repo as fully formed subtrees. ```yaml ---- # file: example.yaml or example.yml first_level: second_level: @@ -410,8 +409,104 @@ Usage example : Let say that you have a file called `user-service-dev.properties` in your repo. This file will be saved on consul as `user-service-dev`. +##### array_format (default: none) -#### Debian packaging +`array_format` is a repo level option that applies only when expand_keys is set. It defines what format array values from JSON and Yaml will be written to inside a single key. + +Valid values are: + +- `none`: do not include the array value +- `json`: write a JSON array value +- _separator_: Use the separator as a list delimiter -- e.g. use `,` for a comma-separated list. + +Given the following JSON or Yaml + +JSON: +```json +{ + "foods": ["apple", "bannana", "orange", "food \"with\" weird, name"] +} +``` +```yaml +foods: + - apple + - bananna + - orange + - 'food "with" weird name' +``` + +The following values will be generated for the key `foods`: + +- `none` -\> No `foods` key will be written +- `json` -\> `foods` will be set to `["apple", "bannana", "orange", "food \"with\" weird, name"]` +- `,` -\> `foods` will be set to `apple,bannana,orange,food \"with\" weird,name` (note the extra, un-escaped comma!) + +Notes: + +- As the above example shows, no escaping is done for delimited lists. + +##### array_key_format (default: null) + +`array_key_format` is a repo level option that applies only when expand_keys is set. It defines the format used to output array key values when outputting one key per array value. + +The format uses 2 special values, underscore (`_`) and octothorp (`#`): + +- `_` is replaced with the name of the array value. +- `#` is replaced by the index in the array. + +Any value that is not a string with at least 1 character will result in no array keys being written. + +Given the following JSON or Yaml + +JSON: +```json +{ + "cars": [ + { "economy": ["ford", "gm"] }, + { "luxury": ["bmw", "mercedes"] } + ] +} +``` +```yaml +cars: + - economy + - ford + - gm + - luxury + - bmw + - mercedes +``` + +git2consul will generate the following keys: + +For `_/#` (prefix becomes a folder with index keys): +``` +[..prefix...]/cars/0/economy/0=ford +[..prefix...]/cars/0/economy/1=gm +[..prefix...]/cars/1/luxury/0=bmw +[..prefix...]/cars/1/luxury/1=mercedes +``` + +For `_[#]` (Spring-style array indexes): +``` +[..prefix...]/cars[0]/economy/[0]=ford +[..prefix...]/cars[0]/economy/[1]=gm +[..prefix...]/cars[1]/luxury/[0]=bmw +[..prefix...]/cars[1]/luxury/[1]=mercedes +``` + +For `#\__` (escaped underscore): +``` +[..prefix...]/0_cars/0_economy/=ford +[..prefix...]/0_cars/1_economy/=gm +[..prefix...]/1_cars/0_luxury/=bmw +[..prefix...]/1_cars/1_luxury/=mercedes +``` + +Note: as shown in the example above, keys are properly generated even for nested objects. + +#_ +# Debian packaging If you don't have grunt `sudo npm install -g grunt-cli`. diff --git a/lib/array_handler.js b/lib/array_handler.js new file mode 100644 index 0000000..6ffe630 --- /dev/null +++ b/lib/array_handler.js @@ -0,0 +1,104 @@ +var _ = require('underscore'); +var logger = require('./logging.js'); + +exports.create_formatter = function create_formatter(array_format) { + switch (array_format) { + case 'none': + return false; + case 'json': + return function (arr) { + return JSON.stringify(arr); + }; + default: + if (_.isString(array_format)) { + return function (arr) { + return arr.join(array_format); + }; + } + if (!_.isUndefined(array_format)) { + logger.warn("Ignoring array_format because it is set to invalid value '%s' for branch %s in repo %s", + array_format, this.name, this.repo_name); + } + return false; + } +}; + +function parse_format(key_token, index_token, format) { + // Parse the format into a list of tokens that can be output + var escaping = false; + var tokens = []; + var currentToken = ""; + for (var i = 0, formatLength = format.length; i < formatLength; i++) { + var c = format.charAt(i); + switch (c) { + case '\\': + if (escaping) { + escaping = false; + currentToken += c; + } else { + escaping = true; + } + break; + case '_': + if (escaping) { + escaping = false; + currentToken += c; + } else { + if (currentToken.length > 0) { + tokens.push(currentToken); + currentToken = ""; + } + tokens.push(key_token); + } + break; + case '#': + if (escaping) { + escaping = false; + currentToken += c; + } else { + if (currentToken.length > 0) { + tokens.push(currentToken); + currentToken = ""; + } + tokens.push(index_token); + } + break; + default: + if (escaping) { + escaping = false; + } + currentToken += c; + break; + } + } + if (currentToken.length > 0) { + tokens.push(currentToken); + } + return tokens; +} + +exports.create_key_formatter = function create_key_formatter(format) { + if (!_.isString(format) || format.length === 0) { + return false; + } + var KEY_TOKEN = {}; + var INDEX_TOKEN = {}; + if (_.isString(format)) { + var tokens = parse_format(KEY_TOKEN, INDEX_TOKEN, format); + return function (key, index) { + var formattedKey = ""; + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token === KEY_TOKEN) { + formattedKey += encodeURIComponent(key); + } else if (token === INDEX_TOKEN) { + formattedKey += index; + } else { + formattedKey += token; + } + } + return formattedKey; + }; + } + return false; +} \ No newline at end of file diff --git a/lib/consul/index.js b/lib/consul/index.js index 855da5c..c0a86c1 100644 --- a/lib/consul/index.js +++ b/lib/consul/index.js @@ -73,41 +73,57 @@ var create_key_name = function(branch, file, ref) { return key_parts.join('/'); }; - /** * Given an obj, recurse into it, populating the parts array with all of the key->value * relationships, prefixed by parent objs. * * For example, the obj { 'first': { 'second': { 'third' : 'whee' }}} should yield a * parts array with a single entry: 'first/second/third' with value 'whee'. + * + * Array values will be rendered according to the configuration specified by array_format + * and array_key_format in the branch configuration. */ -var render_obj = function(parts, prefix, obj) { - +var render_obj = function(array_formatter, array_key_formatter, parts, prefix, obj) { _.mapObject(obj, function(val, key) { - if (_.isArray(val)) { - var thingToReturn = []; - if(val[0].constructor !== {}.constructor){ - thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),val)); - parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(val)}); - }else{ - val.forEach(function(value){ - if(value.constructor === {}.constructor){ - thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key)+"/"+Object.keys(value),value[Object.keys(value)[0]])); - parts.push({'key': prefix + '/' + encodeURIComponent(key)+"/"+Object.keys(value), 'value': value[Object.keys(value)[0]]}); - }else{ - thingToReturn.push(render_obj(parts, prefix + '/' + encodeURIComponent(key),value)); - parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': JSON.stringify(value) }); - } - }); - } - return thingToReturn; + if (_.isArray(val)) { + return render_array(array_formatter, array_key_formatter, parts, prefix, key, val); + } + if (_.isObject(val)) { + return render_obj(array_formatter, array_key_formatter, parts, prefix + '/' + encodeURIComponent(key), val); } - if (_.isObject(val)) return render_obj(parts, prefix + '/' + encodeURIComponent(key), val) parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': val}); }); + logger.debug("PARTS found: " , parts); } +/** + * Array values will be rendered according to the configuration specified by array_format + * and array_key_format in the branch configuration. The former controls how full array + * values should be rendered (and only applies to arrays of "simple" objects); the latter + * controls how elements of arrays should be rendered to their own keys. + */ +var render_array = function(array_formatter, array_key_formatter, parts, prefix, key, arr) { + if (array_formatter && {}.constructor !== arr[0].constructor) { + // We have an array formatter, and a simple array, so render the single key. + parts.push({'key': prefix + '/' + encodeURIComponent(key), 'value': array_formatter(arr)}); + } + if (array_key_formatter) { + // We have a key formatter, so format each element of the array + for (var i = 0; i < arr.length; i++) { + var arrElementKey = prefix + '/' + array_key_formatter(key, i); + var arrElement = arr[i]; + if ({}.constructor === arrElement.constructor) { + // Render the object + render_obj(array_formatter, array_key_formatter, parts, arrElementKey, arrElement); + } else { + // Render the non-object value + parts.push({'key': arrElementKey, 'value': arrElement}); + } + } + } +} + /** * After computing the diff between the object and the existing KV records, * perform the necessary consul operations such that the state in consul matches @@ -118,7 +134,7 @@ var populate_kvs_from_object = function(branch, prefix, obj, existing_kvs, cb) { var delete_kvs = []; var candidate_kvs = []; - render_obj(candidate_kvs, prefix, obj); + render_obj(branch.array_formatter, branch.array_key_formatter, candidate_kvs, prefix, obj); // This avoids unnecessary copying if there are no existing KV records. if (existing_kvs.length > 0) { diff --git a/lib/git/branch.js b/lib/git/branch.js index 5bb4e79..ebb5208 100644 --- a/lib/git/branch.js +++ b/lib/git/branch.js @@ -2,7 +2,9 @@ var fs = require('fs'); var path = require('path'); var mkdirp = require('mkdirp'); var rimraf = require('rimraf'); +var _ = require('underscore'); +var array_handler = require('../array_handler.js'); var logger = require('../logging.js'); var consul_broker = require('../consul'); @@ -17,6 +19,10 @@ function Branch(repo_config, name) { Object.defineProperty(this, 'branch_directory', {value: this.branch_parent + path.sep + name}); Object.defineProperty(this, 'expand_keys', { value: repo_config['expand_keys'] === true }); Object.defineProperty(this, 'expand_keys_diff', { value: repo_config['expand_keys_diff'] === true }); + Object.defineProperty(this, 'array_formatter', {value: array_handler.create_formatter(repo_config['array_format'])}); + Object.defineProperty(this, 'array_key_formatter', { + value: array_handler.create_key_formatter(repo_config['array_key_format']) + }); Object.defineProperty(this, 'common_properties', { value: repo_config['common_properties']}); Object.defineProperty(this, 'include_branch_name', { // If include_branch_name is not set, assume true. Otherwise, identity check the value against true. diff --git a/test/git2consul_array_handler_test.js b/test/git2consul_array_handler_test.js new file mode 100644 index 0000000..269d522 --- /dev/null +++ b/test/git2consul_array_handler_test.js @@ -0,0 +1,73 @@ +var _ = require('underscore'); +var should = require('should'); + +// We want this above any git2consul module to make sure logging gets configured +require('./git2consul_bootstrap_test.js'); + +var array_key_formatter = require('../lib/array_handler.js'); + +describe('Array Formatter', function() { + it ('should ignore non-string values', function() { + array_key_formatter.create_formatter(false).should.equal(false); + array_key_formatter.create_formatter(true).should.equal(false); + array_key_formatter.create_formatter(0).should.equal(false); + array_key_formatter.create_formatter(NaN).should.equal(false); + array_key_formatter.create_formatter('none').should.equal(false); + }); + + it ('should format json', function() { + var format = array_key_formatter.create_formatter('json'); + _.isFunction(format).should.equal(true); + var arr = ['foo', 'bar']; + format(arr).should.equal(JSON.stringify(arr)); + }); + + it ('should format comma-separated lists', function() { + var format = array_key_formatter.create_formatter(','); + _.isFunction(format).should.equal(true); + var arr = ['foo', 'bar']; + format(arr).should.equal('foo,bar'); + }); + + it ('should format un-separated lists', function() { + var format = array_key_formatter.create_formatter(''); + _.isFunction(format).should.equal(true); + var arr = ['foo', 'bar']; + format(arr).should.equal('foobar'); + }); +}); + +describe('Array Key Formatter', function() { + + it ('should ignore non-format values', function() { + array_key_formatter.create_key_formatter(false).should.equal(false); + array_key_formatter.create_key_formatter(true).should.equal(false); + array_key_formatter.create_key_formatter(0).should.equal(false); + array_key_formatter.create_key_formatter(NaN).should.equal(false); + array_key_formatter.create_key_formatter('').should.equal(false); + }); + + it ('should format _#', function() { + var format = array_key_formatter.create_key_formatter('_#'); + _.isFunction(format).should.equal(true); + format('foo', 3).should.equal('foo3'); + }); + + it ('should format pre_inner#post', function() { + var format = array_key_formatter.create_key_formatter('pre_inner#post'); + _.isFunction(format).should.equal(true); + format('foo', 3).should.equal('prefooinner3post'); + }); + + it ('should format #_#/_#', function() { + var format = array_key_formatter.create_key_formatter('#_#/_#'); + _.isFunction(format).should.equal(true); + format('foo', 3).should.equal('3foo3/foo3'); + }); + + it ('should format #_\\#\\_#', function() { + var format = array_key_formatter.create_key_formatter('#_\\#\\_#'); + _.isFunction(format).should.equal(true); + format('foo', 3).should.equal('3foo#_3'); + }); +}); diff --git a/test/git2consul_parsing_yml_test.js b/test/git2consul_parsing_yml_test.js index 9c0513e..25f768b 100644 --- a/test/git2consul_parsing_yml_test.js +++ b/test/git2consul_parsing_yml_test.js @@ -7,12 +7,9 @@ var mkdirp = require('mkdirp'); // We want this above any git2consul module to make sure logging gets configured require('./git2consul_bootstrap_test.js'); -var repo = require('../lib/git/repo.js'); var git_utils = require('./utils/git_utils.js'); var consul_utils = require('./utils/consul_utils.js'); -var git_commands = require('../lib/git/commands.js'); - describe('Parse YAML', function() { // The current copy of the git master branch. This is initialized before each test in the suite. @@ -21,7 +18,11 @@ describe('Parse YAML', function() { // Each of these tests needs a working repo instance, so create it here and expose it to the suite // namespace. These are all tests of expand_keys mode, so set that here. - git_utils.initRepo(_.extend(git_utils.createRepoConfig(), {'expand_keys': true}), function(err, repo) { + var repoConfig = git_utils.createRepoConfig(); + // Add array settings + repoConfig.array_format = 'json'; + repoConfig.array_key_format = '_/#'; + git_utils.initRepo(_.extend(repoConfig, {'expand_keys': true}), function(err, repo) { if (err) return done(err); // The default repo created by initRepo has a single branch, master. @@ -79,8 +80,7 @@ describe('Parse YAML', function() { if (err) return done(err); }); }); - // At this point, the repo should have populated consul with our sample_key - consul_utils.validateValue('test_repo/master/complex_sample.yaml/my.server.path/KEY-ENV-VAR', 'true', function(err, value) { + consul_utils.validateValue('test_repo/master/complex_sample.yaml/my.server.path/0/KEY-ENV-VAR', 'true', function(err, value) { if (err) return done(err); done(); }); diff --git a/test/resources/complex_sample.yaml b/test/resources/complex_sample.yaml index 31be23d..9586aba 100644 --- a/test/resources/complex_sample.yaml +++ b/test/resources/complex_sample.yaml @@ -22,5 +22,5 @@ languages: - medium pascal: Lame my.server.path: - - KEY-ENV-VAR: "true" - - OTHER-VAR: "59900" \ No newline at end of file + - KEY-ENV-VAR: "true" + - OTHER-VAR: "59900" \ No newline at end of file