From d5ecdd1cbc09b8ccbfc0efd8d59646e9a4e2d693 Mon Sep 17 00:00:00 2001 From: geek Date: Thu, 27 Apr 2017 22:59:03 +0100 Subject: [PATCH] add portal-api --- portal-api/.gitignore | 3 + portal-api/.travis.yml | 8 + portal-api/LICENSE | 362 ++++++++++++++++++++++++++++++ portal-api/README.md | 14 ++ portal-api/example.js | 42 ++++ portal-api/lib/data.js | 86 +++++++ portal-api/lib/handlers.js | 93 ++++++++ portal-api/lib/index.js | 51 +++++ portal-api/lib/models/examples.js | 96 ++++++++ portal-api/lib/models/graphql.js | 155 +++++++++++++ portal-api/lib/models/index.js | 109 +++++++++ portal-api/lib/routes.js | 259 +++++++++++++++++++++ portal-api/package.json | 28 +++ portal-api/test/index.js | 276 +++++++++++++++++++++++ 14 files changed, 1582 insertions(+) create mode 100644 portal-api/.gitignore create mode 100644 portal-api/.travis.yml create mode 100644 portal-api/LICENSE create mode 100644 portal-api/README.md create mode 100644 portal-api/example.js create mode 100644 portal-api/lib/data.js create mode 100644 portal-api/lib/handlers.js create mode 100644 portal-api/lib/index.js create mode 100644 portal-api/lib/models/examples.js create mode 100644 portal-api/lib/models/graphql.js create mode 100644 portal-api/lib/models/index.js create mode 100644 portal-api/lib/routes.js create mode 100644 portal-api/package.json create mode 100644 portal-api/test/index.js diff --git a/portal-api/.gitignore b/portal-api/.gitignore new file mode 100644 index 00000000..825fc67c --- /dev/null +++ b/portal-api/.gitignore @@ -0,0 +1,3 @@ +node_modules +.DS_Store +npm-debug.log diff --git a/portal-api/.travis.yml b/portal-api/.travis.yml new file mode 100644 index 00000000..58df036f --- /dev/null +++ b/portal-api/.travis.yml @@ -0,0 +1,8 @@ +language: node_js + +node_js: + - "6" + - "7" + - "node" + +sudo: false diff --git a/portal-api/LICENSE b/portal-api/LICENSE new file mode 100644 index 00000000..be2cc4df --- /dev/null +++ b/portal-api/LICENSE @@ -0,0 +1,362 @@ +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. diff --git a/portal-api/README.md b/portal-api/README.md new file mode 100644 index 00000000..607acabf --- /dev/null +++ b/portal-api/README.md @@ -0,0 +1,14 @@ +ContainerPilot Monitor API + +[![Build Status](https://secure.travis-ci.org/geek/portal-api.svg)](http://travis-ci.org/geek/portal-api) + +Lead Maintainer: [Wyatt Preul](https://github.com/geek) + + +## Example usage + +``` +NODE_ENV=dev node example +``` + +Navigate to http://localhost:8000/graphiql to run graphQL queries. Navigate to http://localhost:8000/documentation for documentation on each route. diff --git a/portal-api/example.js b/portal-api/example.js new file mode 100644 index 00000000..c9b3f85d --- /dev/null +++ b/portal-api/example.js @@ -0,0 +1,42 @@ +'use strict'; + +const Hapi = require('hapi'); +const Inert = require('inert'); +const Vision = require('vision'); +const HapiSwagger = require('hapi-swagger'); +const Pack = require('./package'); +const Portal = require('./lib'); + +const server = new Hapi.Server(); +server.connection({ port: 8000 }); + +const options = { + info: { + 'title': 'Portal API Documentation', + 'version': Pack.version + } +}; + +server.register([ + Inert, + Vision, + Portal, + { + register: HapiSwagger, + options + }], + (err) => { + handlerError(err); + server.start((err) => { + handlerError(err); + console.log(`server started at http://localhost:${server.info.port}`); + }); + } +); + +function handlerError (error) { + if (error) { + console.error(error); + process.exit(1); + } +} diff --git a/portal-api/lib/data.js b/portal-api/lib/data.js new file mode 100644 index 00000000..c3998f7e --- /dev/null +++ b/portal-api/lib/data.js @@ -0,0 +1,86 @@ +'use strict'; + +const Examples = require('./models/examples'); + + +module.exports = class Data { + constructor (options) { + this._options = options; + } + + createDeployment (deployment) { + return new Promise((resolve, reject) => { + resolve(Examples.deployment); + }); + } + getDeployment (id) { + return new Promise((resolve, reject) => { + resolve(Examples.deployment); + }); + } + updateDeployment (deployment) { + return new Promise((resolve, reject) => { + resolve(Examples.deployment); + }); + } + deleteDeployment (id) { + return new Promise((resolve, reject) => { + resolve(); + }); + } + getDeployments () { + return new Promise((resolve, reject) => { + resolve(Examples.deployments); + }); + } + + getDatacenters () { + return new Promise((resolve, reject) => { + resolve(Examples.datacenters); + }); + } + + createManifest (deploymentId, manifest) { + return new Promise((resolve, reject) => { + resolve(Examples.manifest); + }); + } + getManifest (deploymentId, revision) { + return new Promise((resolve, reject) => { + resolve(Examples.manifest); + }); + } + + getActivities (deploymentId) { + return new Promise((resolve, reject) => { + resolve(Examples.activities); + }); + } + getMetrics (deploymentId) { + return new Promise((resolve, reject) => { + resolve(Examples.metrics); + }); + } + + getState (deploymentId) { + return new Promise((resolve, reject) => { + resolve(Examples.state); + }); + } + updateState (deploymentId, action) { + return new Promise((resolve, reject) => { + resolve(Examples.state); + }); + } + + getServices (deploymentId) { + return new Promise((resolve, reject) => { + resolve(Examples.services); + }); + } + updateService (deploymentId, service) { + return new Promise((resolve, reject) => { + resolve(Examples.service); + }); + } +}; diff --git a/portal-api/lib/handlers.js b/portal-api/lib/handlers.js new file mode 100644 index 00000000..4bb84669 --- /dev/null +++ b/portal-api/lib/handlers.js @@ -0,0 +1,93 @@ +'use strict'; + +// Deployments + +exports.deploymentCreate = function (request, reply) { + const deploymentRoute = request.server.lookup('deploymentGet'); + + this.createDeployment(request.payload).then((deployment) => { + reply(deployment).created(deploymentRoute.path.replace('{deployment}', deployment.id)); + }).catch((error) => { + reply(error); + }); +}; + +exports.deploymentGet = function (request, reply) { + reply(this.getDeployment(request.deploymentId)); +}; + +exports.deploymentUpdate = function (request, reply) { + const payload = request.payload; + payload.id = request.deploymentId; + + reply(this.updateDeployment(payload)); +}; + +exports.deploymentDelete = function (request, reply) { + reply(this.deleteDeployment(request.deploymentId)); +}; + +exports.deploymentsGet = function (request, reply) { + reply(this.getDeployments()); +}; + + +// Datacenters + +exports.datacentersGet = function (request, reply) { + reply(this.getDatacenters()); +}; + + +// Manifests + +exports.manifestCreate = function (request, reply) { + const manifestRoute = request.server.lookup('manifestGet'); + const deploymentId = request.params.deploymentId; + + this.createManifest(deploymentId, request.payload).then((manifest) => { + reply(manifest).created(manifestRoute.path.replace('{deployment}', deploymentId).replace('{revision}', manifest.revision)); + }).catch((error) => { + reply(error); + }); +}; + +exports.manifestGet = function (request, reply) { + reply(this.getManifest(request.params.deploymentId, request.params.revision)); +}; + + +// Activities and Metrics + +exports.activitiesGet = function (request, reply) { + reply(this.getActivities(request.params.deploymentId)); +}; + +exports.metricsGet = function (request, reply) { + reply(this.getMetrics(request.params.deploymentId)); +}; + + +// 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) { + reply(this.getServices(request.params.deploymentId)); +}; + +exports.serviceUpdate = function (request, reply) { + const service = request.payload; + service.name = request.params.name; + + reply(this.updateService(request.params.deploymentId, service)); +}; diff --git a/portal-api/lib/index.js b/portal-api/lib/index.js new file mode 100644 index 00000000..f5eac30b --- /dev/null +++ b/portal-api/lib/index.js @@ -0,0 +1,51 @@ +'use strict'; + +const GraphqlHapi = require('graphql-server-hapi'); +const Data = require('./data'); +const Graphql = require('./models/graphql'); +const Pack = require('../package.json'); +const Routes = require('./routes'); + + +module.exports = function (server, options, next) { + const data = new Data(options.data); + server.bind(data); + + + server.register([ + { + register: GraphqlHapi.graphqlHapi, + options: { + path: '/graphql', + graphqlOptions: Graphql.options(data), + route: { + cors: true + } + } + } + ]); + + if (process.env.NODE_ENV === 'dev') { + server.register({ + register: GraphqlHapi.graphiqlHapi, + options: { + path: '/graphiql', + graphiqlOptions: Graphql.options(data), + route: { + cors: true + } + } + }); + } + + server.route(Routes); + + next(); +}; + +module.exports.attributes = { + name: Pack.name, + version: Pack.version, + once: true, + multiple: false +}; diff --git a/portal-api/lib/models/examples.js b/portal-api/lib/models/examples.js new file mode 100644 index 00000000..437a7c46 --- /dev/null +++ b/portal-api/lib/models/examples.js @@ -0,0 +1,96 @@ +'use strict'; + + +exports.activities = [ + { + date: Date.now(), + type: 'start', + meta: { + user: 'Tom' + } + }, + { + date: Date.now(), + type: 'stop', + meta: { + user: 'Dave' + } + } +]; + + +exports.datacenters = [ + { name: 'us-sw-1', url: 'https://us-sw-1.api.joyentcloud.com' }, + { name: 'us-west-1', url: 'https://us-west-1.api.joyentcloud.com' } +]; + + +exports.deployments = [{ + id: 42, + name: 'User Services', + datacenter: 'us-sw-1' +}]; + +exports.deployment = exports.deployments[0]; + + +exports.manifest = { + revision: 5, + file: { + 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'] + } + } +}; + + +exports.metrics = [ + { + service: 'consul', + cpu: 1.2, + memory: 23344523, + network: 5024 + }, + { + service: 'prometheus', + cpu: 24.2, + memory: 514234453, + 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/graphql.js b/portal-api/lib/models/graphql.js new file mode 100644 index 00000000..097e00ff --- /dev/null +++ b/portal-api/lib/models/graphql.js @@ -0,0 +1,155 @@ +'use strict'; + +const Graphql = require('graphql'); + + +const internals = { + schema: ` + scalar Object + scalar Date + + type Activity { + date: Date! + type: String! + meta: Object + } + + type Datacenter { + name: String! + url: String! + } + + input DeploymentCreate { + name: String! + datacenter: String! + } + + input DeploymentUpdate { + id: ID! + name: String! + datacenter: String! + } + + type Deployment { + id: ID! + name: String! + datacenter: String! + } + + type Manifest { + revision: Int! + file: Object! + } + + input ManifestCreate { + file: Object! + } + + type Metric { + service: String! + cpu: Float! + memory: Float! + network: Float! + } + + type Service { + name: String! + count: Int! + } + + input ServiceUpdate { + name: String! + count: Int! + } + + type Mutation { + createDeployment(deployment: DeploymentCreate!): Deployment! + deleteDeployment(deploymentId: ID!): String + updateDeployment(deployment: DeploymentUpdate!): Deployment! + createManifest(deploymentId: ID!, manifest: ManifestCreate!): Manifest! + updateService(deploymentId: ID!, service: ServiceUpdate!): Service! + } + + type Query { + getActivities: [Activity] + getDatacenters: [Datacenter] + getDeployment(id: ID!): Deployment + getDeployments: [Deployment] + getManifest(deploymentId: ID!, revision: Int!): Manifest + getMetrics(deploymentId: ID!): [Metric] + getServices(deploymentId: ID!): [Service] + } + ` +}; + + +exports.options = (data) => { + const schema = Graphql.buildSchema(internals.schema); + + const createDeployment = function (args) { + return data.createDeployment(args.deployment); + }; + + const deleteDeployment = function (args) { + return data.deleteDeployment(args.deploymentId); + }; + + const updateDeployment = function (args) { + return data.updateDeployment(args.deployment); + }; + + const createManifest = function (args) { + return data.createManifest(args.deploymentId, args.manifest); + }; + + const updateService = function (args) { + return data.updateService(args.deploymentId, args.service); + }; + + const getActivities = function () { + return data.getActivities(); + }; + + const getDatacenters = function () { + return data.getDatacenters(); + }; + + const getDeployment = function (args) { + return data.getDeployment(args.id); + }; + + const getDeployments = function () { + return data.getDeployments(); + }; + + const getManifest = function (args) { + return data.getManifest(args.deploymentId, args.revision); + }; + + const getMetrics = function (args) { + return data.getMetrics(args.deploymentId); + }; + + const getServices = function (args) { + return data.v(args.deploymentId); + }; + + return { + schema, + endpointURL: '/graphql', + rootValue: { + createDeployment, + deleteDeployment, + updateDeployment, + createManifest, + updateService, + getActivities, + getDatacenters, + getDeployment, + getDeployments, + getManifest, + getMetrics, + getServices + } + }; +}; diff --git a/portal-api/lib/models/index.js b/portal-api/lib/models/index.js new file mode 100644 index 00000000..cb957dd4 --- /dev/null +++ b/portal-api/lib/models/index.js @@ -0,0 +1,109 @@ +'use strict'; + +const Joi = require('joi'); +const Examples = require('./examples'); + + +// Shared schema between schema sections + +const internals = { + serviceName: Joi.string().required().description('Unique name to identify the service') +}; + + +// Activity + +exports.activity = Joi.object({ + date: Joi.date().required().description('Date/time when the activity occurred'), + type: Joi.string().required().description('The type of activity that occurred'), + meta: Joi.object().optional().description('Any metadata related to the activity') +}).example(Examples.activities[0]); + +exports.activities = Joi.array().items(exports.activity).example(Examples.activities); + + +// Datacenters + +exports.datacenter = Joi.object({ + name: Joi.string().required().description('Name of datacenter'), + url: Joi.string().required().description('URL of datacenter') +}).example(Examples.datacenters[0]); + +exports.datacenters = Joi.array().items(exports.datacenter).example(Examples.datacenters); + + +// Deployments + +exports.deploymentId = Joi.number().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; + +exports.serviceCount = Joi.number().default(1).description('Number of instances of the service'); + +exports.service = Joi.object({ + name: internals.serviceName, + count: exports.serviceCount +}).example(Examples.services[0]); + +exports.services = Joi.array().items(exports.service).example(Examples.services); + +exports.serviceUpdate = Joi.object({ + count: exports.serviceCount.required() +}); + + +// State + +exports.stateAction = Joi.object({ + action: Joi.string().required().valid(['start', 'stop', 'restart']) + .description('Action being performed on the deployment group') +}); + +exports.state = Joi.object({ + current: Joi.string().required().valid(['started', 'stopped']) + .description('The current state of the deployment group') +}); diff --git a/portal-api/lib/routes.js b/portal-api/lib/routes.js new file mode 100644 index 00000000..58db30a7 --- /dev/null +++ b/portal-api/lib/routes.js @@ -0,0 +1,259 @@ +'use strict'; + +const Handlers = require('./handlers'); +const Models = require('./models'); + + +module.exports = [ + { + path: '/deployment', + method: 'post', + config: { + tags: ['api', 'deployment'], + description: 'Create new deployment group', + validate: { + payload: Models.deploymentCreate + }, + response: { + schema: Models.deployment + }, + handler: Handlers.deploymentCreate, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + description: 'Deployment group created', + schema: Models.deployment + } + } + } + } + } + }, + { + path: '/deployment/{deploymentId}', + method: 'get', + config: { + id: 'deploymentGet', + tags: ['api', 'deployment'], + description: 'Retrieve a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + } + }, + response: { + schema: Models.deployment + }, + handler: Handlers.deploymentGet + } + }, + { + path: '/deployment/{deploymentId}', + method: 'put', + config: { + tags: ['api', 'deployment'], + description: 'Update a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + }, + payload: Models.deploymentUpdate + }, + response: { + schema: Models.deployment + }, + handler: Handlers.deploymentUpdate + } + }, + { + path: '/deployment/{deploymentId}', + method: 'delete', + config: { + tags: ['api', 'deployment'], + description: 'Delete a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + } + }, + handler: Handlers.deploymentDelete + } + }, + { + path: '/deployments', + method: 'get', + config: { + tags: ['api', 'deployment'], + description: 'Retrieve a list of deployment groups', + response: { + schema: Models.deployments + }, + handler: Handlers.deploymentsGet + } + }, + { + path: '/datacenters', + method: 'get', + config: { + tags: ['api', 'datacenter'], + description: 'Retrieve a list of available datacenters', + response: { + schema: Models.datacenters + }, + handler: Handlers.datacentersGet + } + }, + { + path: '/deployment/{deploymentId}/manifest', + method: 'post', + config: { + tags: ['api', 'deployment', 'manifest'], + description: 'Create a new manifest revision for a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + }, + payload: Models.manifestCreate + }, + response: { + schema: Models.manifest + }, + handler: Handlers.manifestCreate, + plugins: { + 'hapi-swagger': { + responses: { + '201': { + description: 'Manifest revision created', + schema: Models.manifest + } + } + } + } + } + }, + { + path: '/deployment/{deploymentId}/manifest/{revision}', + method: 'get', + config: { + id: 'manifestGet', + tags: ['api', 'deployment', 'manifest'], + description: 'Retrieve a manifest revision for a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId, + revision: Models.manifestRevision + } + }, + response: { + schema: Models.manifest + }, + handler: Handlers.manifestGet + } + }, + { + path: '/deployment/{deploymentId}/activities', + method: 'get', + config: { + tags: ['api', 'deployment', 'activity'], + description: 'Retrieve the recent activities for the deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + } + }, + response: { + schema: Models.activities + }, + handler: Handlers.activitiesGet + } + }, + { + path: '/deployment/{deploymentId}/metrics', + method: 'get', + config: { + tags: ['api', 'deployment', 'metric'], + description: 'Retrieve metrics for the deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + } + }, + response: { + schema: Models.metrics + }, + 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', + config: { + tags: ['api', 'deployment', 'service'], + description: 'Retrieve the services for a deployment group', + validate: { + params: { + deploymentId: Models.deploymentId + } + }, + response: { + schema: Models.services + }, + handler: Handlers.servicesGet + } + }, + { + path: '/deployment/{deploymentId}/service/{name}', + method: 'put', + config: { + tags: ['api', 'deployment', 'service'], + description: 'Perform an action on the named service', + validate: { + params: { + deploymentId: Models.deploymentId, + name: Models.serviceName + }, + payload: Models.serviceUpdate + }, + response: { + schema: Models.service + }, + handler: Handlers.serviceUpdate + } + } +]; diff --git a/portal-api/package.json b/portal-api/package.json new file mode 100644 index 00000000..4c9000e3 --- /dev/null +++ b/portal-api/package.json @@ -0,0 +1,28 @@ +{ + "name": "portal-api", + "version": "1.0.0", + "description": "", + "main": "./lib/index.js", + "scripts": { + "lint": "belly-button", + "test": "npm run lint && lab -t 97" + }, + "keywords": [], + "author": "wyatt", + "license": "MPL-2.0", + "devDependencies": { + "belly-button": "^3.1.0", + "code": "^4.0.0", + "hapi": "^16.1.1", + "hapi-swagger": "^7.7.0", + "inert": "^4.2.0", + "lab": "^13.0.2", + "vision": "^4.1.1" + }, + "dependencies": { + "boom": "^4.3.1", + "graphql": "^0.9.3", + "graphql-server-hapi": "^0.7.2", + "joi": "^10.4.1" + } +} diff --git a/portal-api/test/index.js b/portal-api/test/index.js new file mode 100644 index 00000000..8a3c37e6 --- /dev/null +++ b/portal-api/test/index.js @@ -0,0 +1,276 @@ +'use strict'; + +const Code = require('code'); +const Hapi = require('hapi'); +const Lab = require('lab'); +const PortalApi = require('../'); + + +// Test shortcuts + +const lab = exports.lab = Lab.script(); +const describe = lab.describe; +const it = lab.it; +const expect = Code.expect; + + +describe('portal-api plugin', () => { + it('can be registered with hapi', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + done(); + }); + }); +}); + + +describe('deployments', () => { + it('can be created', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + 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.headers.location).to.exist(); + done(); + }); + }); + }); + + it('can be updated', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + const payload = { + name: 'User Services', + datacenter: 'us-sw-1' + }; + + server.inject({ method: 'PUT', url: '/deployment/42', payload }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + + server.inject({ method: 'GET', url: '/deployment/42' }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.name).to.equal('User Services'); + done(); + }); + }); + }); + + it('can be deleted', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + + server.inject({ method: 'DELETE', url: '/deployment/42' }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); + + it('can all be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (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(); + }); + }); + }); +}); + + +describe('datacenters', () => { + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + + server.inject({ method: 'GET', url: '/datacenters' }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.length).to.equal(2); + done(); + }); + }); + }); +}); + + +describe('manifests', () => { + it('can be created', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + const payload = { + file: {} + }; + + server.inject({ method: 'POST', url: '/deployment/42/manifest', payload }, (res) => { + expect(res.statusCode).to.equal(201); + expect(res.headers.location).to.exist(); + done(); + }); + }); + }); + + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + + server.inject({ method: 'GET', url: '/deployment/42/manifest/5' }, (res) => { + expect(res.statusCode).to.equal(200); + expect(res.result.file).to.exist(); + done(); + }); + }); + }); +}); + + +describe('activities', () => { + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (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(); + }); + }); + }); +}); + + +describe('metrics', () => { + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (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(PortalApi, (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(PortalApi, (err) => { + expect(err).to.not.exist(); + const payload = { + action: 'restart' + }; + + server.inject({ method: 'PUT', url: '/deployment/42/state', payload }, (res) => { + expect(res.statusCode).to.equal(200); + done(); + }); + }); + }); +}); + + +describe('services', () => { + it('can be retrieved', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (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(); + }); + }); + }); + + it('can be updated', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + const payload = { + count: 3 + }; + + 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(); + }); + }); + }); +}); + + +describe('graphql', () => { + it('route exists', (done) => { + const server = new Hapi.Server(); + server.connection(); + server.register(PortalApi, (err) => { + expect(err).to.not.exist(); + const url = '/graphql?query=%7B%0A%20%20getDeployment(id%3A%201)%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D'; + + server.inject({ method: 'GET', url }, (res) => { + expect(res.statusCode).to.equal(200); + const result = JSON.parse(res.result); + expect(result.data).to.exist(); + expect(result.data.getDeployment).to.exist(); + done(); + }); + }); + }); +});