diff --git a/README.md b/README.md index aeda1d5..18458de 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,18 @@ [![Docker Automated build](https://img.shields.io/docker/automated/clapclapexcitement/concourse-consul-kv-resource.svg?style=flat)](https://hub.docker.com/r/clapclapexcitement/concourse-consul-kv-resource/) [![Build Status](https://travis-ci.org/mdb/concourse-consul-kv-resource.svg?branch=master)](https://travis-ci.org/mdb/concourse-consul-kv-resource) - # concourse-consul-kv-resource A [Concourse](http://concourse.ci/) resource for interacting with [Consul's KV store](https://www.consul.io/api/kv.html). -`concourse-consul-kv-resource` can be used to get or set a key in Consul's KV store. +`concourse-consul-kv-resource` can be used to get or set a key/value in Consul's KV store. ## Source configuration * `key`: _Required_. The Consul key to interact with. Note that all URL path parts following `/v1/kv` are required. For example, if your key is `my-consul:8500/v1/kv/my/key`, then `key` should be "my/key". * `host`: _Required_. The Consul host. -* `token`: _Required_. A Consul ACL token. -* `tls_cert`: _Required_. A TLS cert for the Consul. +* `token`: _Optional_. A Consul ACL token. +* `tls_cert`: _Optional_. A TLS cert for the Consul. * `tls_key`: _Required_. A TLS cert key for the Consul. * `port`: _Optional_. The port on which the Consul API is hosted. Defaults to `8500`. * `protocol`: _Optional_. The protocol to use in calling the Consul API. Defaults to `https`. @@ -23,7 +22,30 @@ A [Concourse](http://concourse.ci/) resource for interacting with [Consul's KV s ### `in`: Get a Consul KV key's value -Gets the value of the Consul KV key configured in the source. +Gets the value of the Consul KV key configured in the source. The key's plain text value is written to a `/` file. + +For example, the following pipeline's `get-my-consul-key` job writes the `foo` key's value to a `my-consul-key/my/key` file: + +```yaml +... + +resources: + +- name: my-consul-key + type: consul-kv + source: + token: my-acl-token + host: my-consul.com + tls_cert: my-cert-string + tls_key: my-cert-key-string + key: my/key + +jobs: + +- name: get-my-consul-key + plan: + - get: my-consul-key +``` ### `out`: Set a Consul KV key's value @@ -38,7 +60,7 @@ Sets the Consul KV key configured in the source to the value specified in the pa ## Example pipeline -``` +```yaml resources: - name: my-consul-key @@ -60,6 +82,10 @@ resource_types: jobs: +- name: get-my-consul-key + plan: + - get: my-consul-key + - name: set-my-consul-key plan: - put: my-consul-key diff --git a/assets/check.js b/assets/check.js index 7acedda..64d0062 100644 --- a/assets/check.js +++ b/assets/check.js @@ -1,11 +1,12 @@ #!/usr/bin/env node -'use strict'; - const checkAction = require('./lib/check'); const handlers = require('./lib/handlers'); checkAction() - .then((result) => { + .then(result => { handlers.success(result); + }) + .catch(problem => { + handlers.fail(problem); }); diff --git a/assets/in.js b/assets/in.js index e1858a9..dfc38d5 100644 --- a/assets/in.js +++ b/assets/in.js @@ -1,11 +1,12 @@ #!/usr/bin/env node -'use strict'; - const inAction = require('./lib/in'); const handlers = require('./lib/handlers'); inAction(process.argv[2]) .then(result => { handlers.success(result); + }) + .catch(problem => { + handlers.fail(problem); }); diff --git a/assets/lib/check.js b/assets/lib/check.js index 0547515..2925b5d 100644 --- a/assets/lib/check.js +++ b/assets/lib/check.js @@ -1,22 +1,32 @@ -'use strict'; - const Client = require('./client'); -const handlers = require('./handlers'); function checkAction() { - return new Promise(resolve => { + return new Promise((resolve, reject) => { process.stdin.on('data', stdin => { - let data = JSON.parse(stdin); - let source = data.source || {}; - let client = new Client(source); + const data = JSON.parse(stdin); + const source = data.source || {}; + const client = new Client(source); + const previousVersion = data.version && data.version.value ? data.version.value : undefined; client.get(source.key) .then(value => { - resolve([{ - value: value.value - }]); + if (!value.value) { + reject(new Error(`${source.key} has no value`)); + + return; + } + + if (!previousVersion || previousVersion !== value.value) { + resolve([{ + value: value.value + }]); + + return; + } + + resolve([]); }, rejected => { - handlers.fail(rejected); + reject(rejected); }); }); }); diff --git a/assets/lib/client.js b/assets/lib/client.js index d1d6749..162af68 100644 --- a/assets/lib/client.js +++ b/assets/lib/client.js @@ -1,5 +1,3 @@ -'use strict'; - const Consul = require('consul-kv'); class Client { diff --git a/assets/lib/handlers.js b/assets/lib/handlers.js index 032e031..f915332 100644 --- a/assets/lib/handlers.js +++ b/assets/lib/handlers.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = { fail: function(err) { if (err) { diff --git a/assets/lib/in.js b/assets/lib/in.js index 408d10e..63ca0d1 100644 --- a/assets/lib/in.js +++ b/assets/lib/in.js @@ -1,35 +1,42 @@ -'use strict'; - const fs = require('fs-extra'); const Client = require('./client'); -const handlers = require('./handlers'); function inAction(destDir) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { process.stdin.on('data', stdin => { - let data = JSON.parse(stdin); - let source = data.source || {}; - let client = new Client(source); - let file = `${destDir}/${source.key}`; + const data = JSON.parse(stdin); + const source = data.source || {}; + const client = new Client(source); + const file = `${destDir}/${source.key}`; client.get(source.key).then(value => { - fs.ensureFile(file, (err) => { - if (err) handlers.fail(err); + fs.ensureFile(file, err => { + if (err) { + reject(err); + + return; + } + + fs.writeFile(file, value.value, err => { + if (err) { + reject(err); - fs.writeFile(file, value.value, (err) => { - if (err) handlers.fail(err); + return; + } resolve({ version: { - value: value.value, - // timestamp in milliseconds: - ref: Date.now().toString() - } + value: value.value + }, + metadata: [{ + name: 'value', + value: value.value + }] }); }); }); }, rejected => { - handlers.fail(rejected); + reject(rejected); }); }); }); diff --git a/assets/lib/out.js b/assets/lib/out.js index e74b130..9153571 100644 --- a/assets/lib/out.js +++ b/assets/lib/out.js @@ -1,48 +1,54 @@ -'use strict'; - const Client = require('./client'); -const handlers = require('./handlers'); const fs = require('fs'); function getValue(params, sourceDir) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { if (params.value && params.file) { - handlers.fail(new Error('Both `file` and `value` present in params')); + reject(new Error('Both `file` and `value` present in params')); } if (params.file) { fs.readFile(`${sourceDir}/${params.file}`, (err, val) => { - if (err) handlers.fail(err); + if (err) { + reject(err); + + return; + } resolve(val.toString().replace(/\n$/, '')); }); - } else { - resolve(params.value); + + return; } + + resolve(params.value); }); } function outAction(sourceDir) { - return new Promise(resolve => { + return new Promise((resolve, reject) => { process.stdin.on('data', stdin => { - let data = JSON.parse(stdin); - let source = data.source || {}; - let client = new Client(source); + const data = JSON.parse(stdin); + const source = data.source || {}; + const client = new Client(source); getValue(data.params, sourceDir).then(value => { client.set(source.key, value).then(() => { resolve({ version: { - // timestamp in milliseconds: - ref: Date.now().toString() + value: value }, metadata: [{ + name: 'timestamp', + // timestamp in milliseconds: + value: Date.now().toString() + }, { name: 'value', value: value }] }); }, rejected => { - handlers.fail(rejected); + reject(rejected); }); }); }); diff --git a/assets/out.js b/assets/out.js index de15929..51d435b 100644 --- a/assets/out.js +++ b/assets/out.js @@ -1,11 +1,12 @@ #!/usr/bin/env node -'use strict'; - const outAction = require('./lib/out'); const handlers = require('./lib/handlers'); outAction(process.argv[2]) .then(result => { handlers.success(result); + }) + .catch(problem => { + handlers.fail(problem); }); diff --git a/package-lock.json b/package-lock.json index 30de97c..e92a849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "consul-kv-resource", - "version": "0.0.8", + "version": "0.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0765df8..b343489 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "consul-kv-resource", - "version": "0.0.8", + "version": "0.9.0", "description": "Get and update Consul K/V from Concourse pipelines", "author": "Mike Ball", "scripts": { diff --git a/test/check_test.js b/test/check_test.js index 27b5870..8440adc 100644 --- a/test/check_test.js +++ b/test/check_test.js @@ -1,5 +1,3 @@ -'use strict'; - const assert = require('assert'); const nock = require('nock'); const checkAction = require('../assets/lib/check'); @@ -12,6 +10,26 @@ function mockGet() { }]); } +function mockGetNotFound() { + return nock('https://my-consul.com:8500') + .get('/v1/kv/my/key?token=my-token') + .reply(404, ''); +} + +function sourceJson(extra) { + extra = extra || {}; + + return JSON.stringify(Object.assign({ + source: { + host: 'my-consul.com', + tls_cert: 'my-cert', + tls_key: 'my-cert-key', + token: 'my-token', + key: 'my/key' + } + }, extra)); +} + describe('checkAction', () => { let stdin; @@ -19,27 +37,80 @@ describe('checkAction', () => { stdin = require('mock-stdin').stdin(); }); - it('gets the Consul key configured in the source and resolves its promise with the value', () => { - mockGet(); - - process.nextTick(() => { - stdin.send(JSON.stringify({ - source: { - host: 'my-consul.com', - tls_cert: 'my-cert', - tls_key: 'my-cert-key', - token: 'my-token', - key: 'my/key' - } - })); + describe('when there is no key value in Consul', () => { + it('rejects its promise with a meaningful message', () => { + mockGetNotFound(); + + process.nextTick(() => { + stdin.send(sourceJson()); + }); + + return checkAction() + .then() + .catch(rejected => { + assert.equal(rejected.message, 'my/key has no value'); + }); }); + }); + + describe('when there is no existing version', () => { + it('gets the Consul key configured in the source and resolves its promise with the value', () => { + mockGet(); - return checkAction() - .then(result => { - assert.equal(result.length, 1); - assert.deepEqual(result[0], { value: 'my-value' }); - }, rejected => { - console.log('rejected: ', rejected); + process.nextTick(() => { + stdin.send(sourceJson()); }); + + return checkAction() + .then(result => { + assert.equal(result.length, 1); + assert.deepEqual(result[0], { value: 'my-value' }); + }, rejected => { + console.log('rejected: ', rejected); + }); + }); + }); + + describe('when there is an existing version but it has the same value as the current Consul key', () => { + it('resolves its promise with an empty array', () => { + mockGet(); + + process.nextTick(() => { + stdin.send(sourceJson({ + version: { + value: 'my-value' + } + })); + }); + + return checkAction() + .then(result => { + assert.equal(result.length, 0); + }, rejected => { + console.log('rejected: ', rejected); + }); + }); + }); + + describe('when there is an existing version and it is different from the current Consul key value', () => { + it('gets the Consul key configured in the source and resolves its promise with the new value', () => { + mockGet(); + + process.nextTick(() => { + stdin.send(sourceJson({ + version: { + value: 'my-original-value' + } + })); + }); + + return checkAction() + .then(result => { + assert.equal(result.length, 1); + assert.deepEqual(result[0], { value: 'my-value' }); + }, rejected => { + console.log('rejected: ', rejected); + }); + }); }); }); diff --git a/test/in_test.js b/test/in_test.js index d26b58d..677e30a 100644 --- a/test/in_test.js +++ b/test/in_test.js @@ -1,5 +1,3 @@ -'use strict'; - const assert = require('assert'); const inAction = require('../assets/lib/in'); const nock = require('nock'); @@ -13,36 +11,55 @@ function mockGet() { }]); } +function sourceJson() { + return JSON.stringify({ + source: { + host: 'my-consul.com', + tls_cert: 'my-cert', + tls_key: 'my-cert-key', + token: 'my-token', + key: 'my/key' + } + }); +} + describe('inAction', () => { let stdin; + let result; beforeEach(() => { stdin = require('mock-stdin').stdin(); - }); - it('gets the Consul key configured in the source and resolves the promise with the proper metadata', () => { mockGet(); process.nextTick(() => { - stdin.send(JSON.stringify({ - source: { - host: 'my-consul.com', - tls_cert: 'my-cert', - tls_key: 'my-cert-key', - token: 'my-token', - key: 'my/key' - } - })); + stdin.send(sourceJson()); }); return inAction('test-dir') - .then(result => { - assert.equal(result.version.value, 'my-value'); + .then(res => { + result = res; }); }); - afterEach((done) => { - fs.remove('test-dir', (err) => { + it('gets the Consul key configured in the source and resolves the promise with the proper version', () => { + assert.equal(result.version.value, 'my-value'); + }); + + it('gets the Consul key configured in the source and resolves the promise with the proper metadata', () => { + assert.equal(result.metadata[0].value, 'my-value'); + }); + + it('writes the Consul key configured in the source to a file in the destination dir it is passed', () => { + fs.readFile('test-dir/my/key', (err, val) => { + if (!err) { + assert.equal(val, 'my-value'); + } + }); + }); + + afterEach(done => { + fs.remove('test-dir', err => { if (!err) done(); }); }); diff --git a/test/out_test.js b/test/out_test.js index c5555e7..18e3e9c 100644 --- a/test/out_test.js +++ b/test/out_test.js @@ -1,5 +1,3 @@ -'use strict'; - const assert = require('assert'); const outAction = require('../assets/lib/out'); const nock = require('nock'); @@ -10,6 +8,19 @@ function mockPut(value) { .reply(200, 'true'); } +function sourceJson(params) { + return JSON.stringify({ + source: { + host: 'my-consul.com', + tls_cert: 'my-cert', + tls_key: 'my-cert-key', + token: 'my-token', + key: 'my/key' + }, + params: params + }); +} + describe('outAction', () => { let stdin; @@ -18,56 +29,66 @@ describe('outAction', () => { }); describe('when it is passed params with a `value`', () => { - it('sets the Consul key to the value cited in the params.value and resolves the promise with the proper metadata', () => { + let result; + + beforeEach(() => { mockPut('my-value'); process.nextTick(() => { - stdin.send(JSON.stringify({ - source: { - host: 'my-consul.com', - tls_cert: 'my-cert', - tls_key: 'my-cert-key', - token: 'my-token', - key: 'my/key' - }, - params: { - value: 'my-value' - } + stdin.send(sourceJson({ + value: 'my-value' })); }); return outAction() - .then(result => { - assert.equal(result.metadata[0].name, 'value'); - assert.equal(result.metadata[0].value, 'my-value'); + .then(res => { + result = res; }); }); + + it('sets the Consul key to the value cited in the params.value and resolves the promise with the proper version', () => { + assert.equal(result.version.value, 'my-value'); + }); + + it('sets the Consul key to the value cited in the params.value and resolves the promise with metadata that includes a "timestamp"', () => { + assert.equal(result.metadata[0].name, 'timestamp'); + }); + + it('sets the Consul key to the value cited in the params.value and resolves the promise with metadata that includes a "value"', () => { + assert.equal(result.metadata[1].name, 'value'); + assert.equal(result.metadata[1].value, 'my-value'); + }); }); describe('when it is passed params with a `file`', () => { - it('sets the Consul key to the value cited in the file and resolves the promise with the proper metadata', () => { + let result; + + beforeEach(() => { mockPut('my-value-from-file'); process.nextTick(() => { - stdin.send(JSON.stringify({ - source: { - host: 'my-consul.com', - tls_cert: 'my-cert', - tls_key: 'my-cert-key', - token: 'my-token', - key: 'my/key' - }, - params: { - file: 'test/fixtures/value_from_file' - } + stdin.send(sourceJson({ + file: 'test/fixtures/value_from_file' })); }); return outAction('.') - .then(result => { - assert.equal(result.metadata[0].name, 'value'); - assert.equal(result.metadata[0].value, 'my-value-from-file'); + .then(res => { + result = res; }); }); + + it('sets the Consul key to the value cited in the file and resolves the promise with the proper version', () => { + assert.equal(result.version.value, 'my-value-from-file'); + }); + + it('sets the Consul key to the value cited in the params.value and resolves the promise with metadata that includes a "timestamp"', () => { + assert.equal(result.metadata[0].name, 'timestamp'); + }); + + it('sets the Consul key to the value cited in the params.value and resolves the promise with metadata that includes a "value"', () => { + assert.equal(result.metadata[1].name, 'value'); + assert.equal(result.metadata[1].value, 'my-value-from-file'); + }); }); });