diff --git a/lib/consul/index.js b/lib/consul/index.js index 3d5d743..6cd56bf 100644 --- a/lib/consul/index.js +++ b/lib/consul/index.js @@ -1,6 +1,7 @@ var _ = require('underscore'); var fs = require('fs'); var path = require('path'); +var utils = require('../utils.js'); var logger = require('../logging.js'); @@ -21,7 +22,7 @@ exports.setToken = function(tok) { var write_content_to_consul = function(key_name, content, cb) { logger.trace('Adding key %s, value:\n%s', key_name, content); - consul.kv.set({'key': key_name, value: content, token: token}, function(err) { + consul.kv.set({'key': key_name, value: content !== null ? String(content) : null, token: token}, function(err) { if (err) { return cb('Failed to write key ' + key_name + ' due to ' + err); } @@ -54,7 +55,7 @@ var create_key_name = function(branch, file, ref) { /** * 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'. */ @@ -74,7 +75,7 @@ var render_obj = function(parts, prefix, obj) { * as parents of subtrees. Arrays are ignored since they add annoying problems (multiple array * entries can have the same value, so what do we do then?). */ -var populate_kvs_from_json = function(branch, prefix, obj, cb) { +var populate_kvs_from_object = function(branch, prefix, obj, cb) { var writes = []; @@ -89,41 +90,94 @@ var populate_kvs_from_json = function(branch, prefix, obj, cb) { }); }; + /** * If a file was modified, read its new value and update consul's KV store. */ var file_modified = function(branch, file, cb) { - var fqf = branch.branch_directory + path.sep + file; - - logger.trace('Attempting to read "%s"', fqf); - - if (branch.expand_keys && file.endsWith('.json')) { - // Delete current tree. Yeah, I know this is kinda the coward's way out, but it's a hell of a lot - // easier to get provably correct than diffing the file against the contents of the KV store. - file_deleted(branch, file, function(err) { - if (err) return cb('Failed to delete key ' + key_name + ' due to ' + err); - - fs.readFile(fqf, {encoding:'utf8'}, function(err, body) { + var handle_json_kv_file = function(file_path, cb) { + fs.readFile(file_path, {encoding: 'utf8'}, function (err, body) { /* istanbul ignore if */ - if (err) return cb('Failed to read key ' + fqf + ' due to ' + err); + if (err) return cb('Failed to read key ' + file_path + ' due to ' + err); var body = body ? body.trim() : ''; try { var obj = JSON.parse(body); - populate_kvs_from_json(branch, create_key_name(branch, file), obj, cb); - } catch(e) { + populate_kvs_from_object(branch, create_key_name(branch, file), obj, cb); + } catch (e) { logger.warn("Failed to parse .json file. Using body string as a KV."); write_content_to_consul(create_key_name(branch, file), body, cb); } }); + }; + + var handle_properties_kv_file = function(file_path, common_properties_relative_path, cb) { + + function extract_and_populate_properties(file_body, common_body, cb) { + utils.load_properties(file_body, common_body, function (error, obj) { + if (error) { + logger.warn('Failed to load properties for : ' + file + ' due to : ' + error + '.' + ' Using body string as a KV.'); + handle_as_flat_file(file_path, branch, file, cb); + } else { + populate_kvs_from_object(branch, create_key_name(branch, file), obj, cb); + } + }); + } + + fs.readFile(file_path, {encoding: 'utf8'}, function (err, file_body) { + /* istanbul ignore if */ + if (err) return cb('Failed to read key ' + file_path + ' due to ' + err); + + if(common_properties_relative_path) { + var path_to_common_properties_file = branch.branch_directory + path.sep + common_properties_relative_path; + fs.readFile(path_to_common_properties_file, {encoding: 'utf8'}, function (err, common_body) { + if (err) { + logger.warn('Failed to read common variables for ' + path_to_common_properties_file + ' due to ' + err); + common_body = ''; + } + extract_and_populate_properties(file_body, common_body, cb); + }); + } + else { + extract_and_populate_properties(file_body, '', cb); + } }); - } else { - fs.readFile(fqf, {encoding:'utf8'}, function(err, body) { + }; + + var handle_as_flat_file = function(fqf, branch, file, cb) { + fs.readFile(fqf, {encoding: 'utf8'}, function (err, body) { /* istanbul ignore if */ if (err) return cb('Failed to read key ' + fqf + ' due to ' + err); var body = body ? body.trim() : ''; write_content_to_consul(create_key_name(branch, file), body, cb); }); + }; + + var handle_expanded_keys_with_different_file_types = function() { + if (file.endsWith('.json')) { + // Delete current tree. Yeah, I know this is kinda the coward's way out, but it's a hell of a lot + // easier to get provably correct than diffing the file against the contents of the KV store. + file_deleted(branch, file, function (err) { + if (err) return cb('Failed to delete key ' + key_name + ' due to ' + err); + handle_json_kv_file(fqf, cb); + }); + } else if (file.endsWith('.properties')) { + file_deleted(branch, file, function (err) { + if (err) return cb('Failed to delete key ' + key_name + ' due to ' + err); + handle_properties_kv_file(fqf, branch.common_properties, cb); + }); + } else { + handle_as_flat_file(fqf, branch, file, cb); + } + }; + + var fqf = branch.branch_directory + path.sep + file; + logger.trace('Attempting to read "%s"', fqf); + + if (branch.expand_keys) { + handle_expanded_keys_with_different_file_types(); + } else { + handle_as_flat_file(fqf, branch, file, cb); } }; diff --git a/lib/git/branch.js b/lib/git/branch.js index 418bf18..2f935b5 100644 --- a/lib/git/branch.js +++ b/lib/git/branch.js @@ -16,6 +16,7 @@ function Branch(repo_config, name) { Object.defineProperty(this, 'branch_parent', {value: repo_config.local_store + path.sep + 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, '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. value: repo_config['include_branch_name'] == undefined || repo_config['include_branch_name'] === true diff --git a/lib/utils.js b/lib/utils.js index 30eebdb..5f35785 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,5 @@ var fs = require('fs'); +var properties = require ("properties"); /** * Check to make sure the provided path is a writeable directory. Throw an exception if not. @@ -23,3 +24,19 @@ module.exports.validate_writeable_directory = function(dir) { throw new Error(dir + ' is not a directory'); } }; + +var options = { + path: false, + variables: true +}; + +module.exports.load_properties = function (specific_file, common_file, cb){ + properties.parse (common_file, options, + function (error, env){ + if (error) return cb (error); + //Pass the common properties as external variables + options.vars = env; + + properties.parse (specific_file, options, cb); + }); +}; \ No newline at end of file diff --git a/package.json b/package.json index 5ddca28..f8312f5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "express": "~4.6.1", "mkdirp": "0.5.0", "rimraf": "2.2.8", - "underscore": "^1.8.0" + "underscore": "^1.8.0", + "properties": "1.2.1" }, "devDependencies": { "istanbul": "0.2.11", diff --git a/readme.md b/readme.md index 55d56ea..6b9f87f 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,25 @@ -#### git2consul +### git2consul [![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) git2consul takes one or many git repositories and mirrors them into [Consul](http://www.consul.io/) KVs. The goal is for organizations of any size to use git as the backing store, audit trail, and access control mechanism for configuration changes and Consul as the delivery mechanism. -##### Installation +#### Installation `npm install git2consul` -##### Mailing List +#### Mailing List [Google Groups](https://groups.google.com/group/git2consul-tool/) -##### Requirements / Caveats +#### Requirements / Caveats * git2consul does most of its Git work by shelling out to git. Git must be installed and on your path. * git2consul does the rest of its work by calling Consul's REST API. A Consul agent must be running on localhost. * git2consul requires write access to the KV store of its Consul agent. * git2consul has only been tested on Unix. -##### Quick Start Guide +#### Quick Start Guide Let's start off with a simple example to show you how it works. You can use this as a starting point and then tailor it to your use-case. @@ -66,11 +66,11 @@ The Values of those Keys are the contents of the respective files. Changing the Once you are happy with your configuration, you can run git2consul as a daemon either in a `screen` session or via an init script of whatever type is appropriate on your platform. -##### Configuration +#### Configuration git2consul expects to be run on the same node as a Consul agent. git2consul expects its own configuration to be stored as a JSON object in '/git2consul/config' in your Consul KV. The utility `utils/config_seeder.js` will take a JSON file and set `/git2consul/config` to contain its contents. -###### Configuration Format +##### Configuration Format ```javascript { @@ -124,7 +124,7 @@ Note that multiple webhooks can share the same port. The only constraint is tha The above example also logs to stdout as well as to file. Logging is handled via [Bunyan](https://github.com/trentm/node-bunyan). The value of the `logger` property is passed to the Bunyan `createLogger()` method, so any configuration supported by vanilla Bunyan will work out of the box in git2consul. -##### How it works +#### How it works git2consul uses the name and branches of configured repos to namespace the created KVs. The goal is to allow multiple teams to use the same Consul agents and KV store to migrate configuration data around a network without needing to worry about data conflicts. In the above example, a settings file stored at `foo_service/settings.json` in the `development` branch of the repo `vp_config` would be persisted in Consul as `vp_config/development/foo_service/settings.json`. @@ -132,17 +132,19 @@ If you are using a more [Twelve-Factor](http://12factor.net/) approach, where yo As changes are detected in the specified Git repos, git2consul determines which files have been added, updated, or deleted and replicates those changes to the KV. Because only changed branches and files are analyzed, git2consul should have a very slim profile on hosting systems. -##### Alternative Modes of Operation +#### Alternative Modes of Operation -###### No Daemon +##### No Daemon If there are no webhooks or polling watchers configured, git2consul will terminate as soon as all tracked repos and branches have been synced with Consul. If you would like to force git2consul not to attach any webhooks or polling watchers, you can either pass the command-line switch `-n` or include the field `"no_daemon": true` at the top level of your config JSON. -###### Halt-on-change +##### Halt-on-change If you would like git2consul to shutdown every time its configuration changes, you can enable halt-on-change with the command-line switch `-h` or inclusion of the field `"halt_on_change": true` at the top level of your config JSON. If this switch is enabled, git2consul will wait for changes in the config (which is itself stored in Consul) and gracefully halt when a change is detected. It is expected that your git2consul process is configured to run as a service, so restarting git2consul is the responsibility of your service manager. -###### expand_keys +##### expand_keys + +###### JSON If you would like git2consul to treat JSON documents in your repo as fully formed subtrees, you can enable expand_keys mode via inclusion of the field `"expand_keys": true` at the top level of the repo's configuration. If this mode is enabled, git2consul will treat any valid JSON file (that is, any file with extension ".json" that parses to an object) as if it contains a subtree of Consul KVs. For example, if you have the file `root.json` in repo `expando_keys` with the following contents: @@ -174,25 +176,56 @@ A few notes on how this behaves: * Any non-JSON files, including files with the extension ".json" that contain invalid JSON, are stored in your KV as if expand_keys mode was not enabled. -##### Options +###### .properties + +Similarly to JSON, git2consul can also treat [Java .properties](http://docs.oracle.com/javase/7/docs/api/java/util/Properties.html#load%28java.io.Reader%29) as a simple k/v format. + +This is useful for teams willing to keep using legacy .properties files or don't want to use consul locally. + +Additionally, It has support for local variable : + +``` +bar=bar +foo=${bar} +``` + +Note: +- the tokens **#** and **!** are parsed as comment tokens. +- the tokens **=**, **whitespace** and **:** are parsed as separator tokens. + +Example, if you have a file `simple.properties` : + +`bar=foo` + +git2consul will generate -###### include_branch_name (default: true) +``` +/expand_keys/simple.json/foo +``` + +returning `bar` + +You can combine .properties files with the [common_properties option](#common_properties-default-undefined), if you need a way to inject shared/common properties into other files. + +#### Options + +##### include_branch_name (default: true) `include_branch_name` is a repo-level option instructing git2consul to use the branch name as part of the key prefix. Setting this option to false will omit the branch name. -###### mountpoint (default: undefined) +##### mountpoint (default: undefined) A `mountpoint` is a repo-level option instructing git2consul to prepend a string to the key name. By default, git2consul creates keys at the root of the KV store with the repo name being a top-level key. By setting a mountpoint, you define a prefix of arbitrary depth that will serve as the root for your key names. When building the key name, git2consul will concatenate mountpoint, repo name, branch name (assuming `include_branch_name` is true), and the path of the file in your git repo. *Note*: mountpoints can neither begin or end in with the character '/'. git2consul will reject your repo config if that's the case. -###### source_root (default: undefined) +##### source_root (default: undefined) A `source_root` is a repo-level option instructing git2consul to navigate to a subdirectory in the git repo before mapping files to KVs. By default, git2consul mirrors the entire repo into Consul KVs. If you have a repo configured with the source_root `config/for/this/datacenter`, the file `config/for/this/datacenter/web/config.json` would be mapped to the KV as `/web/config.json`. -###### support_tags (default: undefined) +##### support_tags (default: undefined) A `support_tags` is a repo-level option instructing git2consul to treat tags as if they were branches. Tags will be dynamically polled by the hook as "branches that don't change". @@ -223,15 +256,60 @@ usage example : } ``` -##### Clients +##### common_properties (default: undefined) + +a `common_properties` is a repo-level option instructing git2consul to inject common/shared properties as variables into other .properties files. +This option is active only if you use the [expand_keys mode with properties](#properties). + +Usage example : + +```javascript +{ + "version": "1.0", + "repos" : [{ + "name" : "sample_configuration", + "url" : "https://github.com/ryanbreen/git2consul_data.git", + "expand_keys": true, + "common_properties" : "common.properties", + "branches" : ["dev"], + "hooks": [{ + "type" : "polling", + "interval" : "1" + }] + }] +} +``` + +If you have a file `common.properties` : + +`foo=bar` + +and `simple.properties` : + +`foo=${foo}` + +git2consul will generate + +``` +/expand_keys/simple.json/foo +``` + +returning `bar`. + +Note : + +- If a variable is missing or unset, git2consul will store the file as a flat file without considering it as a k/v format. +- If the path to common_properties is incorrect or corrupted, git2consul will ignore it and won't inject any properties. + +#### Clients A client system should query Consul for the subset of the KV containing the data relevant to its operation. To extend the above example, our `foo_service` on the development network might subscribe to the KV root `vp_config/development/foo_service` and emit any changes to disk (via something like [fsconsul](https://github.com/ryanbreen/fsconsul)) or environment variables (via something like [envconsul](https://github.com/hashicorp/envconsul)). -##### Tokens +#### Tokens If you are using tokens for ACLs, you can pass a token to git2consul by specifying the `TOKEN` environment variable. git2consul requires read/write access to your KV. The purpose of git2consul is to treat git as the single source of truth for KVs, so it typically makes the most sense to give all other users a read-only token to the KV. -##### CI +#### CI Builds are automatically run by Travis on any push or pull request. @@ -241,6 +319,6 @@ Coverage is run automatically by Travis and uploaded to coveralls.io. [![Coverage Status](https://img.shields.io/coveralls/Cimpress-MCP/git2consul.svg)](https://coveralls.io/r/Cimpress-MCP/git2consul?branch=master) -##### License +#### License Apache 2.0 diff --git a/test/git2consul_expand_keys_test.js b/test/git2consul_expand_keys_test.js index a625e92..5bde029 100644 --- a/test/git2consul_expand_keys_test.js +++ b/test/git2consul_expand_keys_test.js @@ -30,6 +30,7 @@ describe('Expand keys', function() { }); }); + /*JSON*/ it ('should handle additions of new JSON files', function(done) { var sample_key = 'simple.json'; var sample_value = '{ "first_level" : { "second_level": "is_all_we_need" } }'; @@ -117,9 +118,132 @@ describe('Expand keys', function() { }); }); - it ('should handle JSON files comingled with non-JSON files', function(done) { + it ('should handle JSON files with special characters', function(done) { + var sample_key = 'special.json'; + var sample_value = { + "fuzzy" : { + "second level": "ain\'t no one got time for that", + "second/level": { + "ok?": "yes" + } + } + }; + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, JSON.stringify(sample_value), "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/special.json/fuzzy/second%20level', sample_value['fuzzy']['second level'], function(err, value) { + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/special.json/fuzzy/second%2Flevel/ok%3F', sample_value['fuzzy']['second/level']['ok?'], function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + }); + + /*properties*/ + + it ('should handle additions of new properties files', function(done) { + var sample_key = 'simple.properties'; + var sample_value = 'foo=bar'; + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/simple.properties/foo', 'bar', function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + + it ('should handle changing properties files', function(done) { + var sample_key = 'changeme.properties'; + var sample_value = 'foo=bar'; + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/changeme.properties/foo', 'bar', function(err, value) { + if (err) return done(err); + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, 'foo=different_bar', "Change a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/changeme.properties/foo', 'different_bar', function(err, value) { + if (err) return done(err); + + done(); + }); + }); + }); + }); + }); + }); + }); + + it ('should handle busted properties files', function(done) { + var sample_key = 'busted.properties'; + //the parser is quite tolerant to syntax. + //the best way to test this case is by using an unset variable, which will throw an error when accessed. + var sample_value = 'foo=${bar}'; + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/busted.properties', sample_value, function(err, value) { + if (err) return done(err); + + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(sample_key, 'foo=bar', "Change a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/busted.properties/foo', 'bar', function(err, value) { + if (err) return done(err); + + done(); + }); + }); + }); + }); + }); + }); + }); + + /* common */ + it ('should handle different files commingled together', function(done) { var json_key = 'happy.json'; var json_value = '{ "happy" : "json" }'; + var property_key = 'simple.properties'; + var property_value = 'foo=bar'; var sample_key = 'not_a_json_key'; var sample_value = 'password: calvin12345'; @@ -137,15 +261,29 @@ describe('Expand keys', function() { branch.handleRefChange(0, function(err) { if (err) return done(err); - // At this point, the repo should have populated consul with our sample_key - consul_utils.validateValue('test_repo/master/happy.json/happy', 'json', function(err, value) { + // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. + git_utils.addFileToGitRepo(property_key, property_value, "Add another file.", function(err) { if (err) return done(err); - // At this point, the repo should have populated consul with our sample_key - consul_utils.validateValue('test_repo/master/not_a_json_key', sample_value, function(err, value) { + branch.handleRefChange(0, function(err) { if (err) return done(err); - done(); + // At this point, the repo should have populated consul with our sample_key + consul_utils.validateValue('test_repo/master/happy.json/happy', 'json', 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/simple.properties/foo', 'bar', 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/not_a_json_key', sample_value, function(err, value) { + if (err) return done(err); + + done(); + }); + }); + }); }); }); }); @@ -153,33 +291,143 @@ describe('Expand keys', function() { }); }); }); +}); + +describe('Expand keys with common properties', 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, 'common_properties': "common.properties"}), function(err, repo) { + if (err) return done(err); + + // The default repo created by initRepo has a single branch, master. + branch = repo.branches['master']; + + done(); + }); + }); + + it('should handle simple common-properties injection', function(done) { + var common_key = 'common.properties'; + var common_value = 'bar=bar'; + var sample_key = 'simple.properties'; + var sample_value = 'foo=${bar}'; + + git_utils.addFileToGitRepo(common_key, common_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + consul_utils.validateValue('test_repo/master/simple.properties/foo', 'bar', function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + }); + }); - it ('should handle JSON files with special characters', function(done) { - var sample_key = 'special.json'; - var sample_value = { - "fuzzy" : { - "second level": "ain\'t no one got time for that", - "second/level": { - "ok?": "yes" - } - } - }; + it('should store as flat file if unset property', function(done) { + var common_key = 'common.properties'; + var common_value = 'bar=bar'; + var sample_key = 'simple.properties'; + var sample_value = 'foo=${unset}'; - // Add the file, call branch.handleRef to sync the commit, then validate that consul contains the correct info. - git_utils.addFileToGitRepo(sample_key, JSON.stringify(sample_value), "Add a file.", function(err) { + git_utils.addFileToGitRepo(common_key, common_value, "Add a file.", function(err) { if (err) return done(err); branch.handleRefChange(0, function(err) { - if (err) return done(err); - // At this point, the repo should have populated consul with our sample_key - consul_utils.validateValue('test_repo/master/special.json/fuzzy/second%20level', sample_value['fuzzy']['second level'], function(err, value) { - // At this point, the repo should have populated consul with our sample_key - consul_utils.validateValue('test_repo/master/special.json/fuzzy/second%2Flevel/ok%3F', sample_value['fuzzy']['second/level']['ok?'], function(err, value) { + + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { if (err) return done(err); - done(); + + consul_utils.validateValue('test_repo/master/simple.properties', sample_value, function(err, value) { + if (err) return done(err); + done(); + }); }); }); }); }); }); + + it('should store as flat file if common properties file is invalid / not found', function(done) { + var sample_key = 'simple.properties'; + var sample_value = 'foo=${unset}'; + + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + consul_utils.validateValue('test_repo/master/simple.properties', sample_value, function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + }); + +describe('Expand keys with invalid common properties path ', 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, 'common_properties': "wrong/path/common.properties"}), function(err, repo) { + if (err) return done(err); + + // The default repo created by initRepo has a single branch, master. + branch = repo.branches['master']; + + done(); + }); + }); + + it('should store as flat file if invalid path in config', function(done) { + var common_key = 'common.properties'; + var common_value = 'bar=bar'; + var sample_key = 'simple.properties'; + var sample_value = 'foo=${bar}'; + + git_utils.addFileToGitRepo(common_key, common_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + + git_utils.addFileToGitRepo(sample_key, sample_value, "Add a file.", function(err) { + if (err) return done(err); + + branch.handleRefChange(0, function(err) { + if (err) return done(err); + + consul_utils.validateValue('test_repo/master/simple.properties', sample_value, function(err, value) { + if (err) return done(err); + done(); + }); + }); + }); + }); + }); + }); + +}); \ No newline at end of file