diff --git a/.travis.yml b/.travis.yml index 0c595a7..f147c3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - chmod +x docker-compose - sudo mv docker-compose /usr/local/bin + - docker build -t juttle/juttle-engine:latest . script: - gulp lint diff --git a/package.json b/package.json index a01d16f..3eaf1f0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "juttle-cloudwatch-adapter": "^0.3.0", "juttle-elastic-adapter": "^0.5.0", "juttle-gmail-adapter": "^0.5.0", - "juttle-graphite-adapter": "^0.4.0", + "juttle-graphite-adapter": "^0.4.2", "juttle-influx-adapter": "^0.5.0", "juttle-mysql-adapter": "^0.5.0", "juttle-opentsdb-adapter": "^0.2.0", @@ -69,6 +69,7 @@ "selenium-grid-status": "^0.2.0", "selenium-webdriver": "^2.48.2", "tmp": "0.0.28", - "underscore": "^1.8.3" + "underscore": "^1.8.3", + "uuid": "^2.0.1" } } diff --git a/test/README.md b/test/README.md index 741ba9a..117f469 100644 --- a/test/README.md +++ b/test/README.md @@ -14,6 +14,9 @@ To run the built in system tests simply run: gulp test --sys ``` +**NOTE:** be sure to run `docker build -t juttle-engine:local .` after each +code change if you intend on running the system tests locally. + The system tests rely on having docker to bring up the selenium containers and other supporting [docker](https://www.docker.com/) containers. If you happen to want to run the selenium tests locally by using your own available chrome diff --git a/test/adapters/adapters.spec.js b/test/adapters/adapters.spec.js new file mode 100644 index 0000000..2b9e75e --- /dev/null +++ b/test/adapters/adapters.spec.js @@ -0,0 +1,196 @@ +'use strict'; + +let Promise = require('bluebird'); +let docker = require('../lib/docker'); +let expect = require('chai').expect; +let fs = Promise.promisifyAll(require('fs')); +let logger = require('mocha-logger'); +let path = require('path'); +let retry = require('bluebird-retry'); +let tmp = require('tmp'); +let uuid = require('uuid'); + +function juttleBin(juttle) { + logger.log(`juttle -e "${juttle}"`); + return docker.exec('juttle-engine-local', + ['/opt/juttle-engine/bin/juttle', + '-e', + juttle], { quiet: true }); +} + +describe('adapters', () => { + + before(() => { + var tmpdir = tmp.dirSync().name; + return Promise.all([ + docker.destroy('elasticsearch-adapter-test'), + docker.destroy('influxdb-adapter-test'), + docker.destroy('graphite-adapter-test'), + docker.destroy('juttle-engine-local') + ]) + .then(() => { + return docker.checkJuttleEngineLocalExists(); + }) + .then(() => { + return Promise.all([ + docker.run({ + name: 'elasticsearch-adapter-test', + image: 'elasticsearch:1.5.2', + detach: true + }), + docker.run({ + name: 'influxdb-adapter-test', + image: 'tutum/influxdb:0.10', + detach: true + }), + docker.run({ + name: 'graphite-adapter-test', + image: 'sitespeedio/graphite:0.9.14', + detach: true + }) + ]); + }) + .then(() => { + var config = JSON.stringify({ + adapters: { + elastic: { + address: 'elasticsearch', + port: 9200 + }, + influx: { + url: 'http://influxdb:8086' + }, + graphite: { + carbon: { + host: 'graphite', + port: 2003 + }, + webapp: { + host: 'graphite', + port: 80, + username: 'guest', + password: 'guest' + } + } + } + }); + + var filename = path.join(tmpdir, '.juttle-config.json') + return fs.writeFileAsync(filename, config) + }) + .then(() => { + return docker.run({ + name: 'juttle-engine-local', + image: 'juttle/juttle-engine:latest', + ports: ['8080:8080'], + links: [ + 'elasticsearch-adapter-test:elasticsearch', + 'influxdb-adapter-test:influxdb', + 'graphite-adapter-test:graphite' + ], + volumes: [`${tmpdir}:/tmp`], + workdir: '/tmp', + detach: true + }); + }) + .then(() => { + return Promise.all([ + docker.waitForSuccess('elasticsearch-adapter-test', + ['curl', 'http://localhost:9200']), + docker.waitForSuccess('influxdb-adapter-test', + ['influx', '-execute', 'show databases']), + docker.waitForSuccess('graphite-adapter-test', + ['curl', '-u', 'guest:guest', 'http://localhost:80']) + ]); + }); + }); + + after(() => { + return Promise.all([ + docker.destroy('elasticsearch-adapter-test'), + docker.destroy('influxdb-adapter-test'), + docker.destroy('graphite-adapter-test'), + docker.destroy('juttle-engine-local') + ]); + }); + + describe('juttle-elastic-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + + return juttleBin(`emit -limit 5 -from :2014-01-01: | put name="test-${id}",value=count() | write elastic`) + .then(() => { + return retry(() => { + return juttleBin(`read elastic -from :2014-01-01: name="test-${id}" | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { time: '2014-01-01T00:00:00.000Z', name: `test-${id}`, value: 1 }, + { time: '2014-01-01T00:00:01.000Z', name: `test-${id}`, value: 2 }, + { time: '2014-01-01T00:00:02.000Z', name: `test-${id}`, value: 3 }, + { time: '2014-01-01T00:00:03.000Z', name: `test-${id}`, value: 4 }, + { time: '2014-01-01T00:00:04.000Z', name: `test-${id}`, value: 5 } + ]); + }); + },{ timeout: 50000 } ); + }); + }); + }); + + describe('juttle-influx-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + var db = `testdb_${id}`; + + return juttleBin(`read influx -db 'dummy' -raw 'CREATE DATABASE ${db}';`) + .then(() => { + return juttleBin(`emit -limit 5 -from :2014-01-01: | put name="test-${id}", value=count() | write influx -db '${db}'`); + }) + .then(() => { + return retry(() => { + return juttleBin(`read influx -db '${db}' -from :2014-01-01: name="test-${id}" | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { time: '2014-01-01T00:00:00.000Z', name: `test-${id}`, value: 1 }, + { time: '2014-01-01T00:00:01.000Z', name: `test-${id}`, value: 2 }, + { time: '2014-01-01T00:00:02.000Z', name: `test-${id}`, value: 3 }, + { time: '2014-01-01T00:00:03.000Z', name: `test-${id}`, value: 4 }, + { time: '2014-01-01T00:00:04.000Z', name: `test-${id}`, value: 5 } + ]); + }); + }, { timeout: 5000 }); + }); + }); + }); + + describe('juttle-graphite-adapter', () => { + it('can write data and read back data using the juttle CLI', () => { + var id = uuid.v1().slice(0, 8); + + // default data retention for graphite server is: + // + // retentions = 5m:1d,15m:21d,30m:60d + // + // so we'll generate data from 30 minutes ago every 5 minutes and + // then read back the exact points + return juttleBin(`emit -limit 5 -every :5m: -from :30 minutes ago: | put name="test-${id}", value=count() | write graphite`) + .then(() => { + return retry(() => { + return juttleBin(`read graphite -from :60 minutes ago: name="test-${id}" | keep name, value | view text`) + .then((output) => { + var data = JSON.parse(output); + expect(data).to.deep.equal([ + { name: `test-${id}`, value: 1 }, + { name: `test-${id}`, value: 2 }, + { name: `test-${id}`, value: 3 }, + { name: `test-${id}`, value: 4 }, + { name: `test-${id}`, value: 5 } + ]); + }) + }, { timeout: 5000 }); + }); + }); + }); + +}); diff --git a/test/examples/examples.spec.js b/test/examples/examples.spec.js index 962f173..e9f8eed 100644 --- a/test/examples/examples.spec.js +++ b/test/examples/examples.spec.js @@ -3,47 +3,40 @@ let _ = require('underscore'); let Promise = require('bluebird'); let chakram = require('chakram'); +let docker = require('../lib/docker'); var expect = chakram.expect; -let execAsync = Promise.promisify(require('child_process').exec); let fs = require('fs'); let http = require('http'); -let logger = require('mocha-logger'); let path = require('path'); +let cmd = require('../lib/cmd'); let retry = require('bluebird-retry'); -let spawn = require('child_process').spawn; let WebSocket = require('ws'); -function spawnify(command, args, options) { - return new Promise((resolve, reject) => { - args = args || []; - logger.log('spawning "' + command + ' ' + args.join(' ')); - options = _.extend( { - detached: true, - stdio: ['ignore', 'pipe', 'pipe'] - }, options); - - var spawned = spawn(command, args, options); - var stdout = ''; - var stderr = ''; - - spawned.stdout.on('data', (data) => { - logger.log('STDOUT:', data); - stdout += data; - }); +const YMLS = [ + 'dc-juttle-engine.yml', + 'cadvisor-influx/dc-cadvisor-influx.yml', + 'postgres-diskstats/dc-postgres.yml' +] - spawned.stderr.on('data', (data) => { - logger.log('STDERR:', data); - stderr += data; - }); +function dockerCompose(ymls, command, commandArgs) { + commandArgs = commandArgs || []; + var args = []; - spawned.on('close', (code) => { - if (code === 0) { - resolve(stdout, stderr); - } else { - reject(Error('command: "' + command + ' ' + args.join(' ') + - '", failed with ' + code)); - } - }); + _.each(ymls, (yml) => { + args.push('-f'); + args.push(yml); + }); + + args.push(command); + args = args.concat(commandArgs); + + var env = Object.create(process.env); + // we need to set the PWD used by those yml files to . otherwise we inherit + // from the current process running at the root of the source code + env.PWD = '.'; + return cmd.spawnAsync('docker-compose', args, { + cwd: 'examples', + env: env }); } @@ -56,38 +49,54 @@ describe('examples', () => { let retryHTTPOptions = { interval: 500, timeout: 10000 - } + }; before(() => { - return execAsync('docker run ubuntu route | grep default | awk \'{print $2}\'') - .then((stdout) => { - hostAddress = stdout.trim(); - return spawnify('docker', ['build', '-q', '-t', 'juttle/juttle-engine:latest', '.']); + return docker.getHostAddress() + .then((address) => { + hostAddress = address; + return docker.checkJuttleEngineLocalExists(); + }) + .then(() => { + return dockerCompose(YMLS, 'stop'); }) .then(() => { - return spawnify('test/examples/start-example-containers.sh'); + return dockerCompose(YMLS, 'rm', ['--force']); }) .then(() => { - return retry(() => { - return new Promise((resolve, reject) => { - http.get(baseUrl, (resp) => { - if (resp.statusCode !== 200) { - reject(Error(`juttle-engine not responding on ${baseUrl}`)); - } else { - resolve(); - } - }) - .on('error', (err) => { - reject(err); + return dockerCompose(YMLS, 'up', ['-d']); + }) + .then(() => { + // wait for various underlying storages to be up and running + return Promise.all([ + docker.waitForSuccess('examples_postgres_1', + ['pgrep', 'postgres']), + docker.waitForSuccess('examples_influxdb_1', + ['influx', '-execute', 'show databases']), + retry(() => { + return new Promise((resolve, reject) => { + http.get(baseUrl, (resp) => { + if (resp.statusCode !== 200) { + reject(Error(`juttle-engine not responding on ${baseUrl}`)); + } else { + resolve(); + } + }) + .on('error', (err) => { + reject(err); + }); }); - }); - }, retryHTTPOptions); + }, retryHTTPOptions) + ]); }); }); after(() => { // shutdown and remove containers - return spawnify('test/examples/stop-example-containers.sh'); + return dockerCompose(YMLS, 'stop') + .then(() => { + return dockerCompose(YMLS, 'rm', ['--force']); + }); }); _.each({ diff --git a/test/examples/start-example-containers.sh b/test/examples/start-example-containers.sh deleted file mode 100755 index fd051bd..0000000 --- a/test/examples/start-example-containers.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# -# helper script to start/stop example docker containers -# - -pushd examples - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - stop - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - rm -f - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - up -d - -popd diff --git a/test/examples/stop-example-containers.sh b/test/examples/stop-example-containers.sh deleted file mode 100755 index 84af103..0000000 --- a/test/examples/stop-example-containers.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# -# helper script to start example docker containers -# - -pushd examples - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - stop - -docker-compose -f dc-juttle-engine.yml \ - -f cadvisor-influx/dc-cadvisor-influx.yml \ - -f postgres-diskstats/dc-postgres.yml \ - rm -f - -popd diff --git a/test/lib/cmd.js b/test/lib/cmd.js new file mode 100644 index 0000000..a8be931 --- /dev/null +++ b/test/lib/cmd.js @@ -0,0 +1,51 @@ +'use strict'; + +let _ = require('underscore'); +let Promise = require('bluebird'); +let logger = require('mocha-logger'); +let spawn = require('child_process').spawn; + +module.exports = { + spawnAsync: function(command, args, options) { + return new Promise((resolve, reject) => { + options = options || {}; + args = args || []; + + if (!options.quiet) { + logger.log('spawning "' + command + ' ' + args.join(' ') + '"'); + } + + options = _.extend( { + detached: true, + stdio: ['ignore', 'pipe', 'pipe'] + }, options); + + var spawned = spawn(command, args, options); + var stdout = ''; + var stderr = ''; + + spawned.stdout.on('data', (data) => { + if (!options.quiet) { + logger.log('STDOUT:', data); + } + stdout += data; + }); + + spawned.stderr.on('data', (data) => { + if (!options.quiet) { + logger.log('STDERR:', data); + } + stderr += data; + }); + + spawned.on('close', (code) => { + if (code === 0) { + resolve(stdout); + } else { + reject(Error('command: "' + command + ' ' + args.join(' ') + + '", failed with ' + code)); + } + }); + }); + } +}; diff --git a/test/lib/docker.js b/test/lib/docker.js new file mode 100644 index 0000000..fd88f83 --- /dev/null +++ b/test/lib/docker.js @@ -0,0 +1,150 @@ +'use strict'; + +let _ = require('underscore'); +let cmd = require('./cmd'); +let logger = require('mocha-logger'); +let retry = require('bluebird-retry'); +let uuid = require('uuid'); + +module.exports = { + getHostAddress: function() { + var name = uuid.v1(); + return cmd.spawnAsync('docker', ['run', '--name', name, 'ubuntu', 'route'], { quiet: true }) + .then((stdout) => { + var address; + _.each(stdout.split('\n'), (line) => { + if (line.indexOf('default') !== -1) { + address = line.split(/[\r\n\t ]+/)[1]; + } + }); + return address; + }) + .finally(() => { + return this.destroy(name); + }); + }, + + getContainerIPAddress: function(name) { + return cmd.spawnAsync('docker', ['inspect', name], { quiet: true }) + .then((stdout) => { + var data = JSON.parse(stdout); + return data[0].NetworkSettings.IPAddress; + }); + }, + + buildLocalJuttleEngine: function(options) { + // this obviously only works when running tests from the root of the + // repository + var tag = options.tag ? options.tag : 'juttle/juttle-engine:latest'; + return cmd.spawnAsync('docker', ['build', '-q', '-t', tag, '.']); + }, + + run: function(options) { + var args = ['run', '--name', options.name]; + + if (options.links) { + _.each(options.links, (link) => { + args.push('--link'); + args.push(link); + }); + } + + if (options.ports) { + _.each(options.ports, (port) => { + args.push('-p'); + args.push(port); + }); + } + + if (options.volumes) { + _.each(options.volumes, (volume) => { + args.push('--volume'); + args.push(volume); + }); + } + + if (options.workdir) { + args.push('--workdir'); + args.push(options.workdir); + } + + if (options.detach) { + args.push('-d'); + } + + args.push(options.image); + + return cmd.spawnAsync('docker', args); + }, + + stop: function(name, options) { + return cmd.spawnAsync('docker', ['stop', name], options); + }, + + rm: function(name, options) { + return cmd.spawnAsync('docker', ['rm', name], options); + }, + + containerExists: function(name) { + return cmd.spawnAsync('docker', ['inspect', name], { quiet: true }) + .then(() => { + return true; + }) + .catch(() => { + return false; + }); + }, + + destroy: function(name, options) { + return this.containerExists(name) + .then((exists) => { + if (exists) { + return this.stop(name, { quiet: true }) + .then(() => { + return this.rm(name, { quiet: true }); + }); + } + }); + }, + + exec: function(name, args, options) { + var dockerArgs = ['exec', name].concat(args); + return cmd.spawnAsync('docker', dockerArgs, options); + }, + + imageExists: function(image, tag, message) { + return cmd.spawnAsync('docker', ['images', image], { quiet: true }) + .then((output) => { + if (output.indexOf(tag) === -1) { + throw Error(message); + } + }); + }, + + checkJuttleEngineLocalExists: function() { + var message = 'You need to build the local juttle-engine container: ' + + '`docker build -t juttle/juttle-engine:latest .` before you ' + + 'can run these tests'; + return this.imageExists('juttle/juttle-engine', 'latest', message); + }, + + waitForSuccess: function(containerName, args, options) { + /* + * runs the command supplied as a list of arguments in the container + * specified until it exits with a zero return code + */ + options = options || {}; + options = _.defaults(options, { quiet: true }); + logger.log(`${containerName}: "${args.join(' ')}" waiting for success`); + return retry(() => { + return this.exec(containerName, args, options); + }, { interval: 1000, max_tries: 10 }) + .then(() => { + logger.log(`${containerName}: "${args.join(' ')}" succeeded`); + }) + .catch((err) => { + logger.log(`${containerName}: "${args.join(' ')}" failed`); + throw err; + }); + } +};