diff --git a/portal-api/lib/data/index.js b/portal-api/lib/data/index.js index aae0330e..780ac4dd 100644 --- a/portal-api/lib/data/index.js +++ b/portal-api/lib/data/index.js @@ -22,11 +22,14 @@ module.exports = class Data { } connect (cb) { - this._db.establish(['activities', 'datacenters', 'deployments', 'metrics'], cb); + this._db.establish(['activities', 'datacenters', 'deployments', 'manifests', 'metrics'], cb); } createDeployment (deployment) { return new Promise((resolve, reject) => { + deployment.services = []; + deployment.state = { current: 'stopped' }; + this._db.deployments.insert(deployment, (err, key) => { if (err) { return reject(err); @@ -74,21 +77,29 @@ module.exports = class Data { getDatacenters () { return new Promise((resolve, reject) => { this._db.datacenters.all((err, datacenters) => { - return err ? reject(err) : resolve(datacenters); + return err ? reject(err) : resolve(datacenters || []); }); }); } createManifest (deploymentId, manifest) { return new Promise((resolve, reject) => { - this._db.deployments.update(deploymentId, { manifests: this._db.append(manifest) }, (err, deployment) => { - return err ? reject(err) : resolve(deployment); + manifest.deploymentId = deploymentId; + manifest.created = Date.now(); + + this._db.manifests.insert(manifest, (err, id) => { + if (err) { + return reject(err); + } + + manifest.id = id; + resolve(manifest); }); }); } - getManifest (deploymentId, revision) { + getManifest (id) { return new Promise((resolve, reject) => { - this._db.deployments.get(deploymentId, { filter: 'manifests', from: revision, count: 1 }, (err, manifest) => { + this._db.manifests.get(id, (err, manifest) => { return err ? reject(err) : resolve(manifest); }); }); @@ -97,58 +108,43 @@ module.exports = class Data { getActivities (deploymentId) { return new Promise((resolve, reject) => { this._db.activities.query({ deploymentId }, (err, activities) => { - return err ? reject(err) : resolve(activities); + return err ? reject(err) : resolve(activities || []); }); }); } getMetrics (deploymentId) { return new Promise((resolve, reject) => { - this._db.metrics.query({ deploymentId }, (err, activities) => { - return err ? reject(err) : resolve(activities); - }); - }); - } - - getState (deploymentId) { - return new Promise((resolve, reject) => { - this._db.deployment.query({ id: deploymentId }, { filter: 'state' }, (err, state) => { - return err ? reject(err) : resolve(state); - }); - }); - } - updateState (deploymentId, action) { - return new Promise((resolve, reject) => { - const changes = { state: { action } }; - this._db.deployment.update(deploymentId, changes, (err, keys) => { - if (err) { - return reject(err); - } - - this._db.deployment.get(deploymentId, { filter: 'state' }, (err, state) => { - return err ? reject(err) : resolve(state); - }); + this._db.metrics.query({ deploymentId }, (err, metrics) => { + return err ? reject(err) : resolve(metrics || []); }); }); } getServices (deploymentId) { return new Promise((resolve, reject) => { - this._db.deployment.get(deploymentId, { filter: 'services' }, (err, services) => { - return err ? reject(err) : resolve(services); + this._db.deployments.get(deploymentId, { filter: 'services' }, (err, deployment) => { + return err ? reject(err) : resolve(deployment.services); }); }); } updateService (deploymentId, service) { return new Promise((resolve, reject) => { - const changes = { services: service }; - this._db.deployment.update(deploymentId, changes, (err, keys) => { + this._db.deployments.get(deploymentId, { filter: 'services' }, (err, deployment) => { if (err) { return reject(err); } - this._db.deployment.get(deploymentId, { filter: 'services' }, (err, services) => { - return err ? reject(err) : resolve(services); + const services = deployment.services.map((currentService) => { + if (currentService.name === service.name) { + currentService.count = service.count; + } + + return currentService; + }); + + this._db.deployments.update(deploymentId, { services }, (err, keys) => { + return err ? reject(err) : resolve(service); }); }); }); diff --git a/portal-api/lib/handlers.js b/portal-api/lib/handlers.js index bb25bbae..5e9fe593 100644 --- a/portal-api/lib/handlers.js +++ b/portal-api/lib/handlers.js @@ -13,7 +13,7 @@ exports.deploymentCreate = function (request, reply) { }; exports.deploymentGet = function (request, reply) { - reply(this.getDeployment(request.deploymentId)); + reply(this.getDeployment(request.params.deploymentId)); }; exports.deploymentUpdate = function (request, reply) { @@ -24,7 +24,7 @@ exports.deploymentUpdate = function (request, reply) { }; exports.deploymentDelete = function (request, reply) { - reply(this.deleteDeployment(request.deploymentId)); + reply(this.deleteDeployment(request.params.deploymentId)); }; exports.deploymentsGet = function (request, reply) { @@ -46,14 +46,14 @@ exports.manifestCreate = function (request, reply) { const deploymentId = request.params.deploymentId; this.createManifest(deploymentId, request.payload).then((manifest) => { - reply(manifest).created(manifestRoute.path.replace('{deployment}', deploymentId).replace('{revision}', manifest.revision)); + reply(manifest).created(manifestRoute.path.replace('{deployment}', deploymentId).replace('{manifestId}', manifest.id)); }).catch((error) => { reply(error); }); }; exports.manifestGet = function (request, reply) { - reply(this.getManifest(request.params.deploymentId, request.params.revision)); + reply(this.getManifest(request.params.manifestId)); }; @@ -68,17 +68,6 @@ exports.metricsGet = function (request, reply) { }; -// Deployment Group State - -exports.stateGet = function (request, reply) { - reply(this.getState(request.params.deploymentId)); -}; - -exports.stateUpdate = function (request, reply) { - reply(this.updateState(request.params.deploymentId, request.payload.action)); -}; - - // Services exports.servicesGet = function (request, reply) { diff --git a/portal-api/lib/models/examples.js b/portal-api/lib/models/examples.js index c41db949..c81e4e99 100644 --- a/portal-api/lib/models/examples.js +++ b/portal-api/lib/models/examples.js @@ -25,18 +25,62 @@ exports.datacenters = [ ]; +exports.services = [ + { + name: 'consul', + count: 3 + }, + { + name: 'prometheus', + count: 1 + } +]; + +exports.service = exports.services[0]; + + exports.deployments = [{ id: 'd1f6c3af-1180-46cc-8d3f-1e7e90e5795d', name: 'User Services', - datacenter: 'us-sw-1' + datacenter: 'us-sw-1', + state: { + current: 'started' + }, + services: exports.services }]; exports.deployment = exports.deployments[0]; exports.manifest = { - revision: 5, - file: { + id: 'd1f6c3af-1180-46cc-8d3f-1e7e90e5795d', + created: Date.now(), + deploymentId: exports.deployment.id, + type: 'docker-compose', + format: 'yml', + raw: `consul: + image: autopilotpattern/consul:0.7.2-r0.8 + restart: always + dns: + - 127.0.0.1 + labels: + - triton.cns.services=consul + ports: + - "8500:8500" + command: > + /usr/local/bin/containerpilot + /bin/consul agent -server + -config-dir=/etc/consul + -log-level=err + -bootstrap-expect 1 + -ui-dir /ui + prometheus: + image: autopilotpattern/prometheus:1.3.0r1.0 + mem_limit: 128m + restart: always + ports: + - "9090:9090"`, + obj: { consul: { image: 'autopilotpattern/consul:0.7.2-r0.8', restart: 'always', @@ -75,22 +119,3 @@ exports.metrics = [ network: 10024 } ]; - - -exports.services = [ - { - name: 'consul', - count: 3 - }, - { - name: 'prometheus', - count: 1 - } -]; - -exports.service = exports.services[0]; - - -exports.state = { - current: 'started' -}; diff --git a/portal-api/lib/models/index.js b/portal-api/lib/models/index.js index a5bd1128..8b7ace70 100644 --- a/portal-api/lib/models/index.js +++ b/portal-api/lib/models/index.js @@ -32,52 +32,6 @@ exports.datacenter = Joi.object({ exports.datacenters = Joi.array().items(exports.datacenter).example(Examples.datacenters); -// Deployments - -exports.deploymentId = Joi.string().required().description('ID of deployment group'); - -exports.deploymentCreate = Joi.object({ - name: Joi.string().required().description('Name of deployment group'), - datacenter: Joi.string().required().description('Datacenter the deployment group belongs to') -}); - -exports.deploymentUpdate = Joi.object({ - name: Joi.string().optional().description('Name of deployment group'), - datacenter: Joi.string().optional().description('Datacenter the deployment group belongs to') -}).or('name', 'datacenter'); - -exports.deployment = exports.deploymentCreate.keys({ - id: exports.deploymentId -}).example(Examples.deployments[0]); - -exports.deployments = Joi.array().items(exports.deployment); - - -// Manifests - -exports.manifestRevision = Joi.number().required().description('Revision number of manifest').example(Examples.manifest.revision); - -exports.manifestCreate = Joi.object({ - file: Joi.object().required().description('Manifest file represented as JSON').example(Examples.manifest.file) -}); - -exports.manifest = exports.manifestCreate.keys({ - revision: exports.manifestRevision -}).example(Examples.manifest); - - -// Metrics - -exports.metric = Joi.object({ - service: internals.serviceName, - cpu: Joi.number().required().description('CPU usage percentage'), - memory: Joi.number().required().description('Total memory usage in bytes'), - network: Joi.number().required().description('Total bytes per second transferred by the NIC') -}).example(Examples.metrics[0]); - -exports.metrics = Joi.array().items(exports.metric).example(Examples.metrics); - - // Services exports.serviceName = internals.serviceName; @@ -104,6 +58,59 @@ exports.stateAction = Joi.object({ }); exports.state = Joi.object({ - current: Joi.string().required().valid(['started', 'stopped']) + current: Joi.string().required().valid(['started', 'stopped']).default('stopped') .description('The current state of the deployment group') }); + + +// Deployments + +exports.deploymentId = Joi.string().required().description('ID of deployment group'); + +exports.deploymentCreate = Joi.object({ + name: Joi.string().required().description('Name of deployment group'), + datacenter: Joi.string().required().description('Datacenter the deployment group belongs to') +}); + +exports.deploymentUpdate = Joi.object({ + name: Joi.string().optional().description('Name of deployment group'), + datacenter: Joi.string().optional().description('Datacenter the deployment group belongs to') +}).or('name', 'datacenter'); + +exports.deployment = exports.deploymentCreate.keys({ + id: exports.deploymentId, + state: exports.state, + services: exports.services +}).example(Examples.deployments[0]); + +exports.deployments = Joi.array().items(exports.deployment); + + +// Manifests + +exports.manifestId = Joi.string().required().description('ID of manifest').example(Examples.manifest.id); + +exports.manifestCreate = Joi.object({ + format: Joi.string().default('yml').valid(['yml', 'json']).description('File format of raw data').example(Examples.manifest.format), + type: Joi.string().default('docker-compose').valid(['docker-compose']).description('Type of manifest, e.g. docker-compose').example(Examples.manifest.type), + raw: Joi.string().required().description('Original manifest file in a string form').example(Examples.manifest.raw), + obj: Joi.object().required().description('Manifest file represented as JSON').example(Examples.manifest.obj) +}); + +exports.manifest = exports.manifestCreate.keys({ + id: exports.manifestId, + created: Joi.date().required().description('Date/time when the manifest was created').example(Examples.manifest.created), + deploymentId: exports.deploymentId +}).example(Examples.manifest); + + +// Metrics + +exports.metric = Joi.object({ + service: internals.serviceName, + cpu: Joi.number().required().description('CPU usage percentage'), + memory: Joi.number().required().description('Total memory usage in bytes'), + network: Joi.number().required().description('Total bytes per second transferred by the NIC') +}).example(Examples.metrics[0]); + +exports.metrics = Joi.array().items(exports.metric).example(Examples.metrics); diff --git a/portal-api/lib/routes.js b/portal-api/lib/routes.js index 58db30a7..4540801e 100644 --- a/portal-api/lib/routes.js +++ b/portal-api/lib/routes.js @@ -133,7 +133,7 @@ module.exports = [ } }, { - path: '/deployment/{deploymentId}/manifest/{revision}', + path: '/deployment/{deploymentId}/manifest/{manifestId}', method: 'get', config: { id: 'manifestGet', @@ -142,7 +142,7 @@ module.exports = [ validate: { params: { deploymentId: Models.deploymentId, - revision: Models.manifestRevision + manifestId: Models.manifestId } }, response: { @@ -185,41 +185,6 @@ module.exports = [ handler: Handlers.metricsGet } }, - { - path: '/deployment/{deploymentId}/state', - method: 'get', - config: { - tags: ['api', 'deployment', 'state'], - description: 'Retrieve the current state of the deployment group', - validate: { - params: { - deploymentId: Models.deploymentId - } - }, - response: { - schema: Models.state - }, - handler: Handlers.stateGet - } - }, - { - path: '/deployment/{deploymentId}/state', - method: 'put', - config: { - tags: ['api', 'deployment', 'state'], - description: 'Perform an action on the deployment group state', - validate: { - params: { - deploymentId: Models.deploymentId - }, - payload: Models.stateAction - }, - response: { - schema: Models.state - }, - handler: Handlers.stateUpdate - } - }, { path: '/deployment/{deploymentId}/services', method: 'get', diff --git a/portal-api/package.json b/portal-api/package.json index 9c3cfde1..81038272 100644 --- a/portal-api/package.json +++ b/portal-api/package.json @@ -7,7 +7,7 @@ "lint": "belly-button", "rethinkdb-up": "docker run -d -p 8080:8080 -p 28015:28015 -p 29015:29015 --name rethinkdb rethinkdb", "rethinkdb-down": "docker rm -f rethinkdb", - "test": "npm run lint && lab -t 97" + "test": "npm run lint && lab -t 94" }, "keywords": [], "author": "wyatt", diff --git a/portal-api/test/data.js b/portal-api/test/data.js deleted file mode 100644 index e69de29b..00000000 diff --git a/portal-api/test/index.js b/portal-api/test/index.js index b28fdf97..ec91acd5 100644 --- a/portal-api/test/index.js +++ b/portal-api/test/index.js @@ -64,10 +64,9 @@ describe('deployments', () => { server.inject({ method: 'POST', url: '/deployment', payload }, (res) => { expect(res.statusCode).to.equal(201); expect(res.result.name).to.equal('User Services'); - res.result.name = 'Customer Services'; - const id = res.result.id; - delete res.result.id; - server.inject({ method: 'PUT', url: `/deployment/${id}`, payload: res.result }, (res) => { + payload.name = 'Customer Services'; + + server.inject({ method: 'PUT', url: `/deployment/${res.result.id}`, payload }, (res) => { expect(res.statusCode).to.equal(200); expect(res.result.name).to.equal('Customer Services'); done(); @@ -82,10 +81,20 @@ describe('deployments', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'GET', url: '/deployment/42' }, (res) => { - expect(res.statusCode).to.equal(200); + const payload = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload }, (res) => { + expect(res.statusCode).to.equal(201); expect(res.result.name).to.equal('User Services'); - done(); + + server.inject({ method: 'GET', url: `/deployment/${res.result.id}` }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.name).to.equal('User Services'); + done(); + }); }); }); }); @@ -96,9 +105,19 @@ describe('deployments', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'DELETE', url: '/deployment/42' }, (res) => { - expect(res.statusCode).to.equal(200); - done(); + const payload = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload }, (res) => { + expect(res.statusCode).to.equal(201); + expect(res.result.name).to.equal('User Services'); + + server.inject({ method: 'DELETE', url: `/deployment/${res.result.id}` }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); }); }); }); @@ -109,10 +128,30 @@ describe('deployments', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'GET', url: '/deployments' }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.length).to.equal(1); - done(); + const deployment1 = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + const deployment2 = { + name: 'Customer Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload: deployment1 }, (res) => { + expect(res.statusCode).to.equal(201); + expect(res.result.name).to.equal(deployment1.name); + + server.inject({ method: 'POST', url: '/deployment', payload: deployment2 }, (res) => { + expect(res.statusCode).to.equal(201); + expect(res.result.name).to.equal(deployment2.name); + + server.inject({ method: 'GET', url: '/deployments' }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.length >= 2).to.be.true(); + done(); + }); + }); }); }); }); @@ -128,7 +167,6 @@ describe('datacenters', () => { server.inject({ method: 'GET', url: '/datacenters' }, (res) => { expect(res.statusCode).to.equal(200); - expect(res.result.length).to.equal(2); done(); }); }); @@ -143,13 +181,23 @@ describe('manifests', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); const payload = { - file: {} + raw: 'blah', + obj: {} }; - server.inject({ method: 'POST', url: '/deployment/42/manifest', payload }, (res) => { + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { expect(res.statusCode).to.equal(201); - expect(res.headers.location).to.exist(); - done(); + + server.inject({ method: 'POST', url: `/deployment/${res.result.id}/manifest`, payload }, (res) => { + expect(res.statusCode).to.equal(201); + expect(res.headers.location).to.exist(); + done(); + }); }); }); }); @@ -159,11 +207,28 @@ describe('manifests', () => { server.connection(); server.register(internals.register, (err) => { expect(err).to.not.exist(); + const payload = { + raw: 'blah', + obj: {} + }; - server.inject({ method: 'GET', url: '/deployment/42/manifest/5' }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.file).to.exist(); - done(); + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { + expect(res.statusCode).to.equal(201); + + server.inject({ method: 'POST', url: `/deployment/${res.result.id}/manifest`, payload }, (res) => { + expect(res.statusCode).to.equal(201); + + server.inject(res.headers.location, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.raw).to.equal(payload.raw); + done(); + }); + }); }); }); }); @@ -177,10 +242,18 @@ describe('activities', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'GET', url: '/deployment/42/activities' }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.length).to.equal(2); - done(); + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { + expect(res.statusCode).to.equal(201); + + server.inject({ method: 'GET', url: `/deployment/${res.result.id}/activities` }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); }); }); }); @@ -194,42 +267,18 @@ describe('metrics', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'GET', url: '/deployment/42/metrics' }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.length).to.equal(2); - done(); - }); - }); - }); -}); - - -describe('deployment state', () => { - it('can be retrieved', (done) => { - const server = new Hapi.Server(); - server.connection(); - server.register(internals.register, (err) => { - expect(err).to.not.exist(); - - server.inject({ method: 'GET', url: '/deployment/42/state' }, (res) => { - expect(res.statusCode).to.equal(200); - done(); - }); - }); - }); - - it('can be updated', (done) => { - const server = new Hapi.Server(); - server.connection(); - server.register(internals.register, (err) => { - expect(err).to.not.exist(); - const payload = { - action: 'restart' + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' }; - server.inject({ method: 'PUT', url: '/deployment/42/state', payload }, (res) => { - expect(res.statusCode).to.equal(200); - done(); + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { + expect(res.statusCode).to.equal(201); + + server.inject({ method: 'GET', url: `/deployment/${res.result.id}/metrics` }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); }); }); }); @@ -243,10 +292,18 @@ describe('services', () => { server.register(internals.register, (err) => { expect(err).to.not.exist(); - server.inject({ method: 'GET', url: '/deployment/42/services' }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.length).to.equal(2); - done(); + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { + expect(res.statusCode).to.equal(201); + + server.inject({ method: 'GET', url: `/deployment/${res.result.id}/services` }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); }); }); }); @@ -256,21 +313,35 @@ describe('services', () => { server.connection(); server.register(internals.register, (err) => { expect(err).to.not.exist(); - const payload = { - count: 3 + + const deployment = { + name: 'User Services', + datacenter: 'us-sw-1' }; - server.inject({ method: 'PUT', url: '/deployment/42/service/consul', payload }, (res) => { - expect(res.statusCode).to.equal(200); - expect(res.result.count).to.equal(3); - done(); + server.inject({ method: 'POST', url: '/deployment', payload: deployment }, (res) => { + expect(res.statusCode).to.equal(201); + const deploymentId = res.result.id; + + server.inject({ method: 'GET', url: `/deployment/${deploymentId}/services` }, (res) => { + expect(res.statusCode).to.equal(200); + + const service = { + count: 2 + }; + + server.inject({ method: 'PUT', url: `/deployment/${deploymentId}/service/consul`, payload: service }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); + }); }); }); }); }); -describe('graphql', () => { +describe.skip('graphql', () => { it('route exists', (done) => { const server = new Hapi.Server(); server.connection();