chore(portal-api): create test containers

This commit is contained in:
geek 2017-08-10 14:56:29 -05:00 committed by Judit Greskovits
parent 7a1a890f1a
commit 454c37a3d6
15 changed files with 251 additions and 145 deletions

View File

@ -22,11 +22,6 @@
interval: 5, interval: 5,
ttl: 5 ttl: 5
}, },
tags: [
'traefik.backend=api',
'traefik.frontend.rule=PathPrefixStrip:/api',
'traefik.frontend.entryPoints=http'
],
when: { when: {
source: 'bootstrap', source: 'bootstrap',
once: 'exitSuccess' once: 'exitSuccess'
@ -89,11 +84,23 @@
each: 'changed' each: 'changed'
} }
}, },
{
name: 'onchange-rethinkdb',
exec: 'pkill -SIGHUP node',
when: {
source: 'watch.rethinkdb',
each: 'changed'
}
}
], ],
watches: [ watches: [
{ {
name: 'docker-compose-api', name: 'docker-compose-api',
interval: 3 interval: 3
},
{
name: 'rethinkdb',
interval: 3
} }
], ],
telemetry: { telemetry: {

View File

@ -70,13 +70,11 @@ api:
mem_limit: 512m mem_limit: 512m
links: links:
- consul:consul - consul:consul
- rethinkdb:rethinkdb
env_file: env_file:
- _env - _env
environment: environment:
- CONSUL=consul - CONSUL=consul
- PORT=3000 - PORT=3000
- RETHINK_HOST=rethinkdb
expose: expose:
- 3000 - 3000
@ -95,11 +93,16 @@ compose-api:
restart: always restart: always
rethinkdb: rethinkdb:
image: rethinkdb image: autopilotpattern/rethinkdb:2.3.5r1
restart: always restart: always
mem_limit: 1g mem_limit: 1g
environment:
- CONSUL=consul
- CONSUL_AGENT=1
ports: ports:
- 8080:8080 - 8080:8080
expose: expose:
- 28015 - 28015
- 29015 - 29015
dns:
- 127.0.0.1

View File

@ -3,6 +3,9 @@
// core modules // core modules
const EventEmitter = require('events'); const EventEmitter = require('events');
const Fs = require('fs');
const Path = require('path');
const Url = require('url');
const Util = require('util'); const Util = require('util');
// 3rd party modules // 3rd party modules
@ -37,13 +40,34 @@ const NON_IMPORTABLE_STATES = [
const NEW_INSTANCE_ID = '__NEW__'; const NEW_INSTANCE_ID = '__NEW__';
const UNKNOWN_INSTANCE_ID = '__UNKNOWN__'; const UNKNOWN_INSTANCE_ID = '__UNKNOWN__';
const DOCKER_HOST_URL = process.env.DOCKER_HOST ? Url.parse(process.env.DOCKER_HOST) : {};
const internals = { const internals = {
defaults: { defaults: {
name: 'portal', name: 'portal',
db: { db: {
test: false host: 'rethinkdb'
}, },
dockerComposeHost: 'tcp://0.0.0.0:4242' docker: {
protocol: 'https',
host: DOCKER_HOST_URL.hostname,
port: DOCKER_HOST_URL.port,
ca: process.env.DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'ca.pem')) :
undefined,
cert: process.env.DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'cert.pem')) :
undefined,
key: process.env.DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(process.env.DOCKER_CERT_PATH, 'key.pem')) :
undefined
},
triton: {
url: process.env.SDC_URL,
account: process.env.SDC_ACCOUNT,
keyId: process.env.SDC_KEY_ID
},
dockerComposeHost: 'tcp://compose-api:4242'
}, },
tables: { tables: {
'portals': { id: { type: 'uuid' }, primary: 'id', secondary: false, purge: false }, 'portals': { id: { type: 'uuid' }, primary: 'id', secondary: false, purge: false },
@ -82,18 +106,18 @@ class Data extends EventEmitter {
constructor (options) { constructor (options) {
super(); super();
const settings = Hoek.applyToDefaults(internals.defaults, options || {}); this._settings = Hoek.applyToDefaults(internals.defaults, options || {});
// Penseur will assert that the options are correct // Penseur will assert that the options are correct
this._db = new Penseur.Db(settings.name, settings.db); this._db = new Penseur.Db(this._settings.name, this._settings.db);
this._dockerCompose = new DockerClient(settings.dockerComposeHost); this._dockerCompose = new DockerClient(this._settings.dockerComposeHost);
this._docker = new Dockerode(settings.docker); this._docker = new Dockerode(this._settings.docker);
this._machines = null; this._machines = null;
this._triton = null; this._triton = null;
this._server = settings.server; this._server = this._settings.server;
Triton.createClient({ Triton.createClient({
profile: settings.triton profile: this._settings.triton
}, (err, client) => { }, (err, client) => {
if (err) { if (err) {
this.emit('error', err); this.emit('error', err);
@ -125,6 +149,19 @@ class Data extends EventEmitter {
}); });
} }
reconnectDb (db) {
this._settings.db = db;
this._db.close();
this._db = new Penseur.Db(this._settings.name, this._settings.db);
this.connect((err) => {
if (err) {
this.emit('error', err);
}
});
}
// portals // portals
createPortal (clientPortal, cb) { createPortal (clientPortal, cb) {

View File

@ -2,6 +2,7 @@
const Schema = require('joyent-cp-gql-schema'); const Schema = require('joyent-cp-gql-schema');
const Graphi = require('graphi'); const Graphi = require('graphi');
const Hoek = require('hoek');
const Piloted = require('piloted'); const Piloted = require('piloted');
const Data = require('./data'); const Data = require('./data');
const Pack = require('../package.json'); const Pack = require('../package.json');
@ -9,34 +10,44 @@ const Resolvers = require('./resolvers');
const ContainerPilotWatcher = require('./watch/container-pilot'); const ContainerPilotWatcher = require('./watch/container-pilot');
const MachinesWatcher = require('./watch/machines'); const MachinesWatcher = require('./watch/machines');
const {
NAMESPACE
} = process.env;
const namespace = NAMESPACE ?
`/${NAMESPACE}` :
'';
const internals = {};
const internals = {
namespace: process.env.NAMESPACE ? `/${process.env.NAMESPACE}` : '',
defaults: {
data: {
db: {}
},
watch: {
url: process.env.SDC_URL,
account: process.env.SDC_ACCOUNT,
keyId: process.env.SDC_KEY_ID
}
}
};
module.exports = function (server, options, next) { module.exports = function (server, options, next) {
const settings = Hoek.applyToDefaults(internals.defaults, options || {});
try { try {
const docker = Piloted.service('docker-compose-api'); const docker = Piloted.service('docker-compose-api');
if (docker) { if (docker) {
options.data.dockerComposeHost = `tcp://${docker.address}:${docker.port}`; settings.data.dockerComposeHost = `tcp://${docker.address}:${docker.port}`;
}
const rethinkdb = Piloted.service('rethinkdb');
if (rethinkdb) {
settings.data.db.host = rethinkdb.address;
} }
} catch (ex) { } catch (ex) {
console.error(ex); server.log(['error'], ex);
} }
options.watch.server = server; settings.watch.server = server;
options.data.server = server; settings.data.server = server;
const data = new Data(options.data);
const cpWatcher = new ContainerPilotWatcher(Object.assign(options.watch, { data })); const data = new Data(settings.data);
const machinesWatcher = new MachinesWatcher(Object.assign(options.watch, { const cpWatcher = new ContainerPilotWatcher(Object.assign(settings.watch, { data }));
data const machinesWatcher = new MachinesWatcher(Object.assign(settings.watch, { data }));
}));
// watcher <-> watcher // watcher <-> watcher
// portal depends on watcher and vice-versa // portal depends on watcher and vice-versa
@ -68,8 +79,8 @@ module.exports = function (server, options, next) {
{ {
register: Graphi, register: Graphi,
options: { options: {
graphqlPath: `${namespace}/graphql`, graphqlPath: `${internals.namespace}/graphql`,
graphiqlPath: `${namespace}/graphiql`, graphiqlPath: `${internals.namespace}/graphiql`,
schema: Schema, schema: Schema,
resolvers: Resolvers(data) resolvers: Resolvers(data)
} }
@ -91,10 +102,13 @@ module.exports.attributes = {
internals.refresh = function (data) { internals.refresh = function (data) {
return () => { return () => {
const docker = Piloted.service('docker-compose-api'); const docker = Piloted.service('docker-compose-api');
if (!docker) { if (docker) {
return; data.reconnectCompose(`tcp://${docker.address}:${docker.port}`);
} }
data.reconnectCompose(`tcp://${docker.address}:${docker.port}`); const rethinkdb = Piloted.service('rethinkdb');
if (rethinkdb) {
data.reconnectDb({ host: rethinkdb.address });
}
}; };
}; };

View File

@ -152,6 +152,10 @@ module.exports = class MachineWatcher {
} }
getVersion (deploymentGroup, cb) { getVersion (deploymentGroup, cb) {
if (typeof deploymentGroup.version !== 'function') {
return cb(new Error('version must be a function'));
}
deploymentGroup.version() deploymentGroup.version()
.then((version) => { .then((version) => {
return cb(null, version); return cb(null, version);

View File

@ -10,6 +10,7 @@
"lint": "belly-button --fix", "lint": "belly-button --fix",
"lint-ci": "belly-button", "lint-ci": "belly-button",
"test": "lab -c", "test": "lab -c",
"test-docker": "docker-compose -f test-compose.yml up --abort-on-container-exit --build --force-recreate api",
"test-ci": "echo 0", "test-ci": "echo 0",
"start": "node server.js", "start": "node server.js",
"dev": "CORS=1 NAMESPACE=api node server.js", "dev": "CORS=1 NAMESPACE=api node server.js",
@ -21,17 +22,13 @@
"devDependencies": { "devDependencies": {
"belly-button": "^3.1.0", "belly-button": "^3.1.0",
"brule": "^2.0.0", "brule": "^2.0.0",
"code": "^4.1.0",
"good": "^7.2.0", "good": "^7.2.0",
"good-console": "^6.4.0", "good-console": "^6.4.0",
"good-squeeze": "^5.0.2", "good-squeeze": "^5.0.2",
"hapi": "^16.4.3", "hapi": "^16.4.3",
"hapi-swagger": "^7.7.0", "lab": "^14.1.1",
"inert": "^4.2.0",
"lab": "^14.0.1",
"lodash.findindex": "^4.6.0", "lodash.findindex": "^4.6.0",
"vision": "^4.1.1", "wreck": "^12.2.3"
"wreck": "^12.2.2"
}, },
"dependencies": { "dependencies": {
"boom": "^5.1.0", "boom": "^5.1.0",

View File

@ -3,15 +3,9 @@
const Brule = require('brule'); const Brule = require('brule');
const Good = require('good'); const Good = require('good');
const Hapi = require('hapi'); const Hapi = require('hapi');
const HapiSwagger = require('hapi-swagger');
const Inert = require('inert');
const Toppsy = require('toppsy'); const Toppsy = require('toppsy');
const Vision = require('vision');
const Pack = require('./package'); const Pack = require('./package');
const Portal = require('./lib'); const Portal = require('./lib');
const Path = require('path');
const Fs = require('fs');
const Url = require('url');
const server = new Hapi.Server(); const server = new Hapi.Server();
@ -22,55 +16,6 @@ server.connection({
} }
}); });
const swaggerOptions = {
info: {
'title': 'Portal API Documentation',
'version': Pack.version
}
};
const {
DOCKER_HOST,
DOCKER_CERT_PATH,
SDC_URL,
SDC_ACCOUNT,
SDC_KEY_ID
} = process.env;
const DOCKER_HOST_URL = DOCKER_HOST ? Url.parse(DOCKER_HOST) : {};
const portalOptions = {
data: {
db: {
host: process.env.RETHINK_HOST || 'localhost'
},
docker: {
protocol: 'https',
host: DOCKER_HOST_URL.hostname,
port: DOCKER_HOST_URL.port,
ca: DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(DOCKER_CERT_PATH, 'ca.pem')) :
undefined,
cert: DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(DOCKER_CERT_PATH, 'cert.pem')) :
undefined,
key: DOCKER_CERT_PATH ?
Fs.readFileSync(Path.join(DOCKER_CERT_PATH, 'key.pem')) :
undefined
},
triton: {
url: SDC_URL,
account: SDC_ACCOUNT,
keyId: SDC_KEY_ID
}
},
watch: {
url: SDC_URL,
account: SDC_ACCOUNT,
keyId: SDC_KEY_ID
}
};
const goodOptions = { const goodOptions = {
ops: { ops: {
interval: 1000 interval: 1000
@ -88,20 +33,11 @@ const goodOptions = {
server.register([ server.register([
Brule, Brule,
Inert, Portal,
Vision,
{ {
register: Good, register: Good,
options: goodOptions options: goodOptions
}, },
{
register: Portal,
options: portalOptions
},
{
register: HapiSwagger,
options: swaggerOptions
},
{ {
register: Toppsy, register: Toppsy,
options: { namespace: 'portal', subsystem: 'api' } options: { namespace: 'portal', subsystem: 'api' }

View File

@ -0,0 +1,30 @@
FROM node:8-alpine
# Install dependencies
RUN set -x \
&& apk update \
&& apk add --update curl bash build-base python zeromq-dev openssh \
&& apk upgrade \
&& rm -rf /var/cache/apk/*
# Install ContainerPilot
ENV CP_SHA1 8d680939a8a5c8b27e764d55a78f5e3ae7b42ef4
ENV CONTAINERPILOT_VERSION 3.3.3
RUN curl -Lo /tmp/containerpilot.tar.gz "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VERSION}/containerpilot-${CONTAINERPILOT_VERSION}.tar.gz" \
&& echo "${CP_SHA1} /tmp/containerpilot.tar.gz" | sha1sum -c \
&& tar zxf /tmp/containerpilot.tar.gz -C /bin \
&& rm /tmp/containerpilot.tar.gz
# Copy required files
RUN mkdir -p /opt/app/
COPY ./ /opt/app/
COPY test/containerpilot.json5 /etc/containerpilot.json5
COPY test/prestart.sh /bin/prestart.sh
ENV CONTAINERPILOT /etc/containerpilot.json5
# Install dependencies
WORKDIR /opt/app/
RUN npm install
CMD ["/bin/containerpilot"]

View File

@ -0,0 +1,55 @@
consul:
image: autopilotpattern/consul:0.7.2-r0.8
command: >
/usr/local/bin/containerpilot
/bin/consul agent -server
-config-dir=/etc/consul
-log-level=err
-bootstrap-expect 1
-ui-dir /ui
restart: always
mem_limit: 128m
ports:
- 8500:8500
dns:
- 127.0.0.1
compose-api:
image: joyent/copilot-compose
links:
- consul:consul
expose:
- 4242
env_file:
- ../../_env
environment:
- CONSUL=consul
restart: always
rethinkdb:
image: autopilotpattern/rethinkdb:2.3.5r1
restart: always
mem_limit: 1g
links:
- consul:consul
environment:
- CONSUL=consul
- CONSUL_AGENT=1
ports:
- 8080:8080
expose:
- 28015
- 29015
dns:
- 127.0.0.1
api:
build: ./
dockerfile: ./test-Dockerfile
links:
- consul:consul
- rethinkdb:rethinkdb
- compose-api:compose-api
env_file:
- ../../_env
environment:
- CONSUL=consul
dns:
- 127.0.0.1

View File

@ -0,0 +1,27 @@
{
consul: 'consul:8500',
jobs: [
{
name: 'setup-config',
exec: '/bin/prestart.sh'
},
{
name: 'tests',
exec: 'node node_modules/.bin/lab -c',
when: {
source: 'setup-config',
once: 'exitSuccess'
}
}
],
watches: [
{
name: 'rethinkdb',
interval: 5
},
{
name: 'docker-compose-api',
interval: 5
}
]
}

View File

@ -3,21 +3,17 @@
const Fs = require('fs'); const Fs = require('fs');
const Path = require('path'); const Path = require('path');
const Code = require('code'); const Code = require('code');
const Lab = require('lab'); const { describe, it, afterEach, expect } = exports.lab = require('lab').script();
const PortalData = require('../../lib/data'); const PortalData = require('../../lib/data');
const lab = exports.lab = Lab.script();
const afterEach = lab.afterEach;
const it = lab.it;
const describe = lab.describe;
const expect = Code.expect;
const internals = { const internals = {
options: { options: {
name: 'test', name: 'test',
db: { test: true } db: { test: true },
server: {
log: function () {}
}
}, },
composeFile: Fs.readFileSync(Path.join(__dirname, 'docker-compose.yml')).toString() composeFile: Fs.readFileSync(Path.join(__dirname, 'docker-compose.yml')).toString()
}; };

View File

@ -1,24 +1,13 @@
'use strict'; 'use strict';
const Code = require('code');
const Hapi = require('hapi'); const Hapi = require('hapi');
const Lab = require('lab'); const { describe, it, beforeEach, afterEach, expect } = exports.lab = require('lab').script();
const PortalData = require('../lib/data'); const PortalData = require('../lib/data');
const PortalApi = require('../'); const PortalApi = require('../');
// Test shortcuts
const lab = exports.lab = Lab.script();
const afterEach = lab.afterEach;
const beforeEach = lab.beforeEach;
const describe = lab.describe;
const it = lab.it;
const expect = Code.expect;
const internals = { const internals = {
options: { data: { test: true, name: 'test' } } options: { data: { name: 'test', db: { test: true } } }
}; };
internals.register = { register: PortalApi, options: internals.options }; internals.register = { register: PortalApi, options: internals.options };

View File

@ -0,0 +1,21 @@
#!/bin/bash
# Copy creds from env vars to files on disk
if [ -n ${!TRITON_CREDS_PATH} ] \
&& [ -n ${!TRITON_CA} ] \
&& [ -n ${!TRITON_CERT} ] \
&& [ -n ${!TRITON_KEY} ]
then
mkdir -p ${TRITON_CREDS_PATH}
echo -e "${TRITON_CA}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/ca.pem
echo -e "${TRITON_CERT}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/cert.pem
echo -e "${TRITON_KEY}" | tr '#' '\n' > ${TRITON_CREDS_PATH}/key.pem
fi
eval `/usr/bin/ssh-agent -s`
mkdir -p ~/.ssh
echo -e "${SDC_KEY_PUB}" | tr '#' '\n' > ~/.ssh/id_rsa.pub
echo -e "${SDC_KEY}" | tr '#' '\n' > ~/.ssh/id_rsa
chmod 400 ~/.ssh/id_rsa.pub
chmod 400 ~/.ssh/id_rsa
ssh-add ~/.ssh/id_rsa

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const Lab = require('lab'); const { it, expect } = exports.lab = require('lab').script();
const Uuid = require('uuid/v4'); const Uuid = require('uuid/v4');
const ContainerPilotWatch = require('../../lib/watch/container-pilot'); const ContainerPilotWatch = require('../../lib/watch/container-pilot');
@ -8,11 +8,6 @@ const DataMock = require('../_mocks/data');
const TritonMock = require('../_mocks/triton'); const TritonMock = require('../_mocks/triton');
const lab = exports.lab = Lab.script();
const it = lab.it;
// const expect = Lab.expect;
it('sets instance health statuses appropriately', (done) => { it('sets instance health statuses appropriately', (done) => {
const networks = [{ const networks = [{
id: Uuid(), id: Uuid(),

View File

@ -1,12 +1,7 @@
'use strict'; 'use strict';
const Lab = require('lab'); const { describe, it, expect } = exports.lab = require('lab').script();
const PortalWatch = require('../../lib/watch'); const PortalWatch = require('../../lib/watch/machines');
const lab = exports.lab = Lab.script();
const it = lab.it;
const expect = Lab.expect;
it('updates instances with the current status', (done) => { it('updates instances with the current status', (done) => {