chore: data layer

This commit is contained in:
geek 2017-05-27 11:35:38 -05:00 committed by Sérgio Ramos
parent cb1dfa20f8
commit cf5f476898
23 changed files with 699 additions and 1620 deletions

42
.yarnclean Normal file
View File

@ -0,0 +1,42 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
# misc
*.gz
*.md

View File

@ -15,9 +15,7 @@
"good": "^7.2.0",
"good-console": "^6.4.0",
"good-squeeze": "^5.0.2",
"graphql": "^0.9.6",
"graphql-server-hapi": "^0.7.2",
"graphql-tools": "^0.11.0",
"graphi": "^2.0.0",
"hapi": "^16.1.1",
"joi": "^10.5.0",
"joyent-cp-gql-schema": "^1.0.4"

View File

@ -1,6 +1,5 @@
const schema = require('joyent-cp-gql-schema');
const { graphqlHapi, graphiqlHapi } = require('graphql-server-hapi');
const { makeExecutableSchema } = require('graphql-tools');
const graphi = require('graphi');
const Good = require('good');
const Hapi = require('hapi');
const resolvers = require('./resolvers');
@ -45,27 +44,12 @@ server.register(
}
},
{
register: graphiqlHapi,
register: graphi,
options: {
path: '/graphiql',
graphiqlOptions: {
endpointURL: '/graphql'
}
}
},
{
register: graphqlHapi,
options: {
path: '/graphql',
graphqlOptions: {
schema: makeExecutableSchema({
typeDefs: schema.sync(),
resolvers
})
},
route: {
cors: true
}
graphqlPath: '/graphql',
graphiqlPath: '/graphiql',
schema,
resolvers
}
}
],

View File

@ -2,60 +2,10 @@
# yarn lockfile v1
"@types/boom@*":
version "4.3.2"
resolved "https://registry.yarnpkg.com/@types/boom/-/boom-4.3.2.tgz#8f40fb142322958ff1bb520c2d8d20cf1dd3d468"
"@types/catbox@*":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-7.1.0.tgz#adb0962f1cd9ba14d15efc0de91503364845e5f6"
dependencies:
"@types/boom" "*"
"@types/graphql@^0.9.0":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.1.tgz#b04ebe84bc997cc60dbea2ed4d0d4342c737f99d"
"@types/hapi@^16.0.0":
version "16.1.2"
resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-16.1.2.tgz#e782b399a11ef8934c3de3ab07aae9fb53cb0968"
dependencies:
"@types/boom" "*"
"@types/catbox" "*"
"@types/joi" "*"
"@types/mimos" "*"
"@types/node" "*"
"@types/podium" "*"
"@types/shot" "*"
"@types/joi@*":
version "10.3.2"
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.3.2.tgz#bc4ce6577c294710663d347ebe59087f375075a5"
"@types/mime-db@*":
version "1.27.0"
resolved "https://registry.yarnpkg.com/@types/mime-db/-/mime-db-1.27.0.tgz#9bc014a1fd1fdf47649c1a54c6dd7966b8284792"
"@types/mimos@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/mimos/-/mimos-3.0.0.tgz#20356e7381e9529c2a7e764c798be5a48cdeff96"
dependencies:
"@types/mime-db" "*"
"@types/node@*":
version "7.0.22"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.22.tgz#4593f4d828bdd612929478ea40c67b4f403ca255"
"@types/podium@*":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/podium/-/podium-1.0.0.tgz#bfaa2151be2b1d6109cc69f7faa9dac2cba3bb20"
"@types/shot@*":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@types/shot/-/shot-3.4.0.tgz#459477c5187d3ebd303660ab099e7e9e0f3b656f"
dependencies:
"@types/node" "*"
abbrev@1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
@ -229,7 +179,7 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
boom@4.x.x, boom@^4.3.1:
boom@4.x.x:
version "4.3.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
dependencies:
@ -439,10 +389,6 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
deprecated-decorator@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37"
doctrine@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
@ -886,38 +832,26 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
graphql-server-core@^0.7.0:
graphi@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/graphi/-/graphi-2.2.1.tgz#234a4752e09accd182e5b6cb93d59c75535702d8"
dependencies:
boom "4.x.x"
graphql "0.9.x"
graphql-server-core "0.7.x"
graphql-server-module-graphiql "0.7.x"
graphql-server-core@0.7.x:
version "0.7.0"
resolved "https://registry.yarnpkg.com/graphql-server-core/-/graphql-server-core-0.7.0.tgz#a658b3b0704f8a19b24254b64bef6b3d0ddce8ba"
optionalDependencies:
"@types/graphql" "^0.9.0"
graphql-server-hapi@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/graphql-server-hapi/-/graphql-server-hapi-0.7.2.tgz#2fa55b8a000952f125af038404563e72d0ba05ab"
dependencies:
boom "^4.3.1"
graphql-server-core "^0.7.0"
graphql-server-module-graphiql "^0.7.2"
optionalDependencies:
"@types/graphql" "^0.9.0"
"@types/hapi" "^16.0.0"
graphql-server-module-graphiql@^0.7.2:
graphql-server-module-graphiql@0.7.x:
version "0.7.2"
resolved "https://registry.yarnpkg.com/graphql-server-module-graphiql/-/graphql-server-module-graphiql-0.7.2.tgz#aa1f2a26eadbf7127c1b077e633d5086da52b330"
graphql-tools@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-0.11.0.tgz#14c372f6ddad7e63a757094d541a937d6b31b7da"
dependencies:
deprecated-decorator "^0.1.6"
lodash "^4.3.0"
uuid "^3.0.1"
optionalDependencies:
"@types/graphql" "^0.9.0"
graphql@^0.9.6:
graphql@0.9.x:
version "0.9.6"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.9.6.tgz#514421e9d225c29dfc8fd305459abae58815ef2c"
dependencies:
@ -2149,7 +2083,7 @@ uuid@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.0.0, uuid@^3.0.1:
uuid@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"

View File

@ -1,8 +1,6 @@
const path = require('path');
const { readFile } = require('mz/fs');
const { readFileSync } = require('fs');
const file = path.join(__dirname, 'schema.gql');
module.exports = () => readFile(file, 'utf-8');
module.exports.sync = () => readFileSync(file, 'utf-8');
module.exports = readFileSync(file, 'utf-8');

View File

@ -9,9 +9,7 @@
"lint": "eslint . --fix --format=tap",
"test": "exit 0"
},
"dependencies": {
"mz": "^2.6.0"
},
"dependencies": {},
"devDependencies": {
"eslint": "^3.19.0",
"eslint-config-joyent-portal": "1.0.0"

View File

@ -9,7 +9,7 @@
}
type User {
uuid: ID!
id: ID!
firstName: String!
lastName: String!
email: String!
@ -17,7 +17,7 @@
}
type DeploymentGroup {
uuid: ID!
id: ID!
name: String!
slug: String!
services(slug: String): [Service]!
@ -26,7 +26,7 @@
}
type ServiceScale {
uuid: ID!
id: ID!
serviceName: String!
replicas: Int!
}
@ -39,14 +39,14 @@
}
type ConvergenceAction {
uuid: String!
id: String!
type: ConvergenceActionType!
service: String! # service name
machines: [String]! # instance machine ids
}
type StateConvergencePlan {
uuid: String!
id: String!
running: Boolean!
actions: [ConvergenceAction]!
}
@ -69,7 +69,7 @@
}
type Manifest {
uuid: String!
id: String!
created: Date!
type: ManifestType!
format: ManifestFormat!
@ -79,21 +79,21 @@
# immutable
type Service {
uuid: String! # unique id for db row
id: String! # unique id for db row
hash: String! # unique id for version of service
name: String! # human readable name
slug: String!
instances: [Instance]!
# metrics: [MetricType]!
currentMetrics: [CurrentMetric]!
connections: [String!] # list of serviceUuids
parent: ID # parent service uuid
connections: [String!] # list of serviceIds
parent: ID # parent service id
package: Package! # we don't have this in current mock data
}
# for metrics max / min (I guess)
type Package {
uuid: ID!
id: ID!
name: String!
type: String!
memory: Float!
@ -115,7 +115,7 @@
}
type Instance {
uuid: String!
id: String!
name: String!
machineId: String!
status: InstanceStatus!
@ -123,8 +123,9 @@
}
type Datacenter {
uuid: String!
# name: String! # Do we have 'official' human readable names?
id: String!
url: String!
name: String!
region: String!
}
@ -140,7 +141,7 @@
}
type MetricType {
uuid: String!
id: String!
name: String!
id: String!
}
@ -154,13 +155,13 @@
type Query {
portal: Portal
deploymentGroups: [DeploymentGroup]
deploymentGroup(uuid: String, slug: String): DeploymentGroup
services(deploymentGroupUuid: String, deploymentGroupSlug: String): [Service]
service(uuid: String, slug: String): Service
instances(serviceUuid: String, serviceSlug: String): [Instance]
instance(uuid: String, machineId: String): Instance
deploymentGroup(id: String, slug: String): DeploymentGroup
services(deploymentGroupId: String, deploymentGroupSlug: String): [Service]
service(id: String, slug: String): Service
instances(serviceId: String, serviceSlug: String): [Instance]
instance(id: String, machineId: String): Instance
metricTypes: [MetricType]
metricData(instanceUuid: String!, metricType: String!, from: Date!, to: Date!): [InstanceMetric]!
metricData(instanceId: String!, metricType: String!, from: Date!, to: Date!): [InstanceMetric]!
package: Package
datacenters: [Datacenter]
# tmp test
@ -173,41 +174,42 @@
portal: Portal
user: User
deploymentGroups(name: String, slug: String): [DeploymentGroup]
deploymentGroup(uuid: ID, name: String, slug: String): DeploymentGroup
serviceScales(serviceName: String, versionUuid: ID): [ServiceScale]
serviceScale(uuid: ID!): ServiceScale
convergenceActions(type: ConvergenceActionType, service: String, versionUuid: ID): [ConvergenceAction]
convergenceAction(uuid: ID!): ConvergenceAction
stateConvergencePlans(running: Boolean, versionUuid: ID): [StateConvergencePlan]
stateConvergencePlan(uuid: ID!): StateConvergencePlan
versions(manifestUuid: ID, deploymentGroupUuid: ID): [Version]
version(uuid: ID, manifestUuid: ID): Version
manifests(type: String, deploymentGroupUuid: ID): [Manifest]
manifest(uuid: ID!): Manifest
services(name: String, slug: String, parentUuid: ID, deploymentGroupUuid: ID, deploymentGroupSlug: String): [Service]
service(uuid: ID, hash: ID): Service
deploymentGroup(id: ID, name: String, slug: String): DeploymentGroup
serviceScales(serviceName: String, versionId: ID): [ServiceScale]
serviceScale(id: ID!): ServiceScale
convergenceActions(type: ConvergenceActionType, service: String, versionId: ID): [ConvergenceAction]
convergenceAction(id: ID!): ConvergenceAction
stateConvergencePlans(running: Boolean, versionId: ID): [StateConvergencePlan]
stateConvergencePlan(id: ID!): StateConvergencePlan
versions(manifestId: ID, deploymentGroupId: ID): [Version]
version(id: ID, manifestId: ID): Version
manifests(type: String, deploymentGroupId: ID): [Manifest]
manifest(id: ID!): Manifest
services(name: String, slug: String, parentId: ID, deploymentGroupId: ID, deploymentGroupSlug: String): [Service]
service(id: ID, hash: ID): Service
packages(name: String, type: String, memory: Int, disk: Int, swap: Int, lwps: Int, vcpus: Int, version: String, group: String): [Package]
package(uuid: ID!): Package
instances(name: String!, machineId: ID, status: InstanceStatus, serviceUuid: ID, serviceSlug: String, deploymentGroupUuid: ID, deploymentGroupSlug: String): [Instance]
instance(uuid: ID!): Instance
datacenter(uuid: ID, region: String): Datacenter
package(id: ID!): Package
instances(name: String!, machineId: ID, status: InstanceStatus, serviceId: ID, serviceSlug: String, deploymentGroupId: ID, deploymentGroupSlug: String): [Instance]
instance(id: ID!): Instance
datacenter(id: ID, region: String): Datacenter
datacenters: [Datacenter]
}
type Mutation {
createDeploymentGroup(name: String!) : DeploymentGroup
updateDeploymentGroup(uuid: ID!, name: String!) : DeploymentGroup
updateDeploymentGroup(id: ID!, name: String!) : DeploymentGroup
provisionManifest(deploymentGroupUuid: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!) : Version
provisionManifest(deploymentGroupId: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!) : Version
scale(service: ID!, replicas: Int!) : Version
stopServices(uuids: [ID]!) : [Service]
startServices(uuids: [ID]!) : [Service]
restartServices(uuids: [ID]!) : [Service]
deleteServices(uuids: [ID]!) : [Service]
stopServices(ids: [ID]!) : [Service]
startServices(ids: [ID]!) : [Service]
restartServices(ids: [ID]!) : [Service]
deleteServices(ids: [ID]!) : [Service]
stopInstances(uuids: [ID]!) : [Instance]
startInstances(uuids: [ID]!) : [Instance]
restartInstances(uuids: [ID]!) : [Instance]
stopInstances(ids: [ID]!) : [Instance]
startInstances(ids: [ID]!) : [Instance]
restartInstances(ids: [ID]!) : [Instance]
# reprovision() ???
}

View File

@ -1,82 +0,0 @@
'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.params.deploymentId));
};
exports.deploymentUpdate = function (request, reply) {
const payload = request.payload;
payload.id = request.params.deploymentId;
reply(this.updateDeployment(payload));
};
exports.deploymentDelete = function (request, reply) {
reply(this.deleteDeployment(request.params.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('{manifestId}', manifest.id));
}).catch((error) => {
reply(error);
});
};
exports.manifestGet = function (request, reply) {
reply(this.getManifest(request.params.manifestId));
};
// 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));
};
// 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));
};

View File

@ -1,10 +1,10 @@
'use strict';
const Schema = require('joyent-cp-gql-schema');
const Graphi = require('graphi');
const PortalData = require('portal-data');
const Graphql = require('./models/graphql');
const Pack = require('../package.json');
const Routes = require('./routes');
const Resolvers = require('./resolvers');
module.exports = function (server, options, next) {
@ -19,12 +19,13 @@ module.exports = function (server, options, next) {
server.register([
{
register: Graphi,
options: Graphql.options(data)
options: {
schema: Schema,
resolvers: Resolvers(data)
}
}
]);
server.route(Routes);
next();
});
};

View File

@ -1,121 +0,0 @@
'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.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',
state: {
current: 'started'
},
services: exports.services
}];
exports.deployment = exports.deployments[0];
exports.manifest = {
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',
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
}
];

View File

@ -1,89 +0,0 @@
'use strict';
const Fs = require('fs');
const Path = require('path');
const internals = {
schema: Fs.readFileSync(Path.join(__dirname, 'schema.gql')).toString()
};
exports.options = (data) => {
const queryWrap = function (name) {
return function (args, request) {
return data[name](args);
};
};
const queries = [
'portal',
'deploymentGroups',
'deploymentGroup',
'services',
'service',
'instances',
'instance',
'metricTypes',
'metricData',
'package',
'datacenters',
'instanceMetric'
];
const resolvers = {
createDeploymentGroup: (args, request) => {
return data.createDeploymentGroup(args.name);
},
updateDeploymentGroup: (args, request) => {
return data.updateDeploymentGroup(args.uuid, args.name);
},
provisionManifest: (args, request) => {
return data.provisionManifest(args.deploymentGroupUuid, args.type, args.format, args.raw);
},
scale: (args, request) => {
return data.scale(args.service, args.replicas);
},
stopServices: (args, request) => {
return data.stopServices(args.uuids);
},
startServices: (args, request) => {
return data.startServices(args.uuids);
},
restartServices: (args, request) => {
return data.restartServices(args.uuids);
},
deleteServices: (args, request) => {
return data.deleteServices(args.uuids);
},
stopInstances: (args, request) => {
return data.stopInstances(args.uuids);
},
startInstances: (args, request) => {
return data.startInstances(args.uuids);
},
restartInstances: (args, request) => {
return data.restartInstances(args.uuids);
}
};
queries.forEach((query) => {
const functionName = 'get' + query[0].toUpperCase() + query.slice(1);
resolvers[query] = queryWrap(functionName);
});
return {
schema: internals.schema,
resolvers
};
};

View File

@ -1,116 +0,0 @@
'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);
// 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']).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);

View File

@ -1,204 +0,0 @@
scalar Date
scalar Object
type Portal {
username: String!
datacenter: Datacenter!
deploymentGroups: [DeploymentGroup]!
}
type DeploymentGroup {
uuid: ID!
name: String!
slug: String!
services: [Service]!
version: Version!
history: [Version]!
}
type ServiceScale {
uuid: ID!
serviceName: String!
replicas: Int!
}
enum ConvergenceActionType {
NOOP
CREATE
RECREATE
START
}
type ConvergenceAction {
uuid: String!
type: ConvergenceActionType!
service: String! # service name
machines: [String]! # instance machine ids
}
type StateConvergencePlan {
uuid: String!
running: Boolean!
actions: [ConvergenceAction]!
}
type Version {
created: Date! # Either Int or define scalar
manifest: Manifest!
scale: [ServiceScale]!
plan: StateConvergencePlan
}
enum ManifestType {
COMPOSE
MARIPOSA
}
enum ManifestFormat {
JSON
YAML
}
type Manifest {
uuid: String!
created: Date!
type: ManifestType!
format: ManifestFormat!
raw: String!
obj: Object!
}
# immutable
type Service {
uuid: String! # unique id for db row
hash: String! # unique id for version of service
name: String! # human readable name
slug: String!
instances: [Instance]!
# metrics: [MetricType]!
currentMetrics: [CurrentMetric]!
connections: [String!] # list of serviceUuids
parent: ID # parent service uuid
package: Package! # we don't have this in current mock data
}
# for metrics max / min (I guess)
type Package {
uuid: ID!
name: String!
type: String!
memory: Float!
disk: Float!
swap: Float!
lwps: Int!
vcpus: Int!
version: String!
group: String!
}
enum InstanceStatus {
CREATED
RESTARTING
RUNNING
PAUSED
EXITED
DELETED
}
type Instance {
uuid: String!
name: String!
machineId: String!
status: InstanceStatus!
# metrics: [InstanceMetric]!
}
type Datacenter {
uuid: String!
# name: String! # Do we have 'official' human readable names?
region: String!
}
type InstanceMetric {
type: MetricType!
data: [MetricData]!
}
type CurrentMetric {
name: String!
value: Float!
measurement: String!
}
type MetricType {
uuid: String!
name: String!
id: String!
}
type MetricData {
timestamp: Int!
value: Float!
}
# Need to review queries
type Query {
portal: Portal
deploymentGroups: [DeploymentGroup]
deploymentGroup(uuid: String, slug: String): DeploymentGroup
services(deploymentGroupUuid: String, deploymentGroupSlug: String): [Service]
service(uuid: String, slug: String): Service
instances(serviceUuid: String, serviceSlug: String): [Instance]
instance(uuid: String, machineId: String): Instance
metricTypes: [MetricType]
metricData(instanceUuid: String!, metricType: String!, from: Date!, to: Date!): [InstanceMetric]!
package: Package
datacenters: [Datacenter]
# tmp test
instanceMetric: InstanceMetric!
}
# we probably wont use some of these queries or arguments
# but this way we expose the entire db through gql
type Query {
portal: Portal
deploymentGroups(name: String, slug: String): [DeploymentGroup]
deploymentGroup(uuid: ID, name: String, slug: String): DeploymentGroup
serviceScales(serviceName: String, versionUuid: ID): [ServiceScale]
serviceScale(uuid: ID!): ServiceScale
convergenceActions(type: ConvergenceActionType, service: String, versionUuid: ID): [ConvergenceAction]
convergenceAction(uuid: ID!): ConvergenceAction
stateConvergencePlans(running: Boolean, versionUuid: ID): [StateConvergencePlan]
stateConvergencePlan(uuid: ID!): StateConvergencePlan
versions(manifestUuid: ID, deploymentGroupUuid: ID): [Version]
version(uuid: ID, manifestUuid: ID): Version
manifests(type: String, deploymentGroupUuid: ID): [Manifest]
manifest(uuid: ID!): Manifest
services(name: String, slug: String, parentUuid: ID, deploymentGroupUuid: ID, deploymentGroupSlug: String): [Service]
service(uuid: ID, hash: ID): Service
packages(name: String, type: String, memory: Int, disk: Int, swap: Int, lwps: Int, vcpus: Int, version: String, group: String): [Package]
package(uuid: ID!): Package
instances(name: String!, machineId: ID, status: InstanceStatus, serviceUuid: ID, serviceSlug: String, deploymentGroupUuid: ID, deploymentGroupSlug: String): [Instance]
instance(uuid: ID!): Instance
datacenter(uuid: ID, region: String): Datacenter
}
type Mutation {
createDeploymentGroup(name: String!) : DeploymentGroup
updateDeploymentGroup(uuid: ID!, name: String!) : DeploymentGroup
provisionManifest(deploymentGroupUuid: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!) : Version
scale(service: ID!, replicas: Int!) : Version
stopServices(uuids: [ID]!) : [Service]
startServices(uuids: [ID]!) : [Service]
restartServices(uuids: [ID]!) : [Service]
deleteServices(uuids: [ID]!) : [Service]
stopInstances(uuids: [ID]!) : [Instance]
startInstances(uuids: [ID]!) : [Instance]
restartInstances(uuids: [ID]!) : [Instance]
# reprovision() ???
}

View File

@ -0,0 +1,57 @@
'use strict';
module.exports = (data) => {
const queryWrap = function (name) {
return function (options, request, cb) {
return data[name](options, cb);
};
};
const mutationWrap = function (name) {
return function (options, request, cb) {
return data[name](options, cb);
};
};
const queries = [
'portal',
'deploymentGroups',
'deploymentGroup',
'services',
'service',
'instances',
'instance',
'metricTypes',
'metricData',
'package',
'datacenters',
'instanceMetric'
];
const mutations = [
'createDeploymentGroup',
'updateDeploymentGroup',
'provisionManifest',
'scale',
'stopServices',
'startServices',
'restartServices',
'deleteServices',
'stopInstances',
'startInstances',
'restartInstances'
];
const resolvers = {};
queries.forEach((query) => {
const functionName = 'get' + query[0].toUpperCase() + query.slice(1);
resolvers[query] = queryWrap(functionName);
});
mutations.forEach((mutation) => {
resolvers[mutation] = mutationWrap(mutation);
});
return resolvers;
};

View File

@ -1,224 +0,0 @@
'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/{manifestId}',
method: 'get',
config: {
id: 'manifestGet',
tags: ['api', 'deployment', 'manifest'],
description: 'Retrieve a manifest revision for a deployment group',
validate: {
params: {
deploymentId: Models.deploymentId,
manifestId: Models.manifestId
}
},
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}/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
}
}
];

View File

@ -27,6 +27,7 @@
"graphi": "^2.0.0",
"hoek": "^4.1.1",
"joi": "^10.4.1",
"joyent-cp-gql-schema": "^1.0.4",
"portal-data": "^1.0.0"
}
}

View File

@ -32,315 +32,6 @@ describe('portal-api plugin', () => {
});
describe('deployments', () => {
it('can be created', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (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(internals.register, (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.result.name).to.equal('User Services');
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();
});
});
});
});
it('can be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (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.result.name).to.equal('User Services');
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();
});
});
});
});
it('can be deleted', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (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.result.name).to.equal('User Services');
server.inject({ method: 'DELETE', url: `/deployment/${res.result.id}` }, (res) => {
expect(res.statusCode).to.equal(200);
done();
});
});
});
});
it('can all be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
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();
});
});
});
});
});
});
describe('datacenters', () => {
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: '/datacenters' }, (res) => {
expect(res.statusCode).to.equal(200);
done();
});
});
});
});
describe('manifests', () => {
it('can be created', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
const payload = {
raw: 'blah',
obj: {}
};
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);
expect(res.headers.location).to.exist();
done();
});
});
});
});
it('can be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
const payload = {
raw: 'blah',
obj: {}
};
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();
});
});
});
});
});
});
describe('activities', () => {
it('can be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
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();
});
});
});
});
});
describe('metrics', () => {
it.skip('can be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
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}/metrics` }, (res) => {
expect(res.statusCode).to.equal(200);
done();
});
});
});
});
});
describe('services', () => {
it('can be retrieved', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
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();
});
});
});
});
it('can be updated', (done) => {
const server = new Hapi.Server();
server.connection();
server.register(internals.register, (err) => {
expect(err).to.not.exist();
const deployment = {
name: 'User Services',
datacenter: 'us-sw-1'
};
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', () => {
it('route exists', (done) => {
const server = new Hapi.Server();

View File

@ -0,0 +1,42 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
# misc
*.gz
*.md

View File

@ -3,7 +3,9 @@
const Hoek = require('hoek');
const Penseur = require('penseur');
const DCClient = require('docker-compose-client');
const Awaitify = require('apr-awaitify');
const VAsync = require('vasync');
const Transform = require('./transform');
const internals = {
defaults: {
@ -13,13 +15,16 @@ const internals = {
},
dockerHost: 'tcp://0.0.0.0:4242'
},
tables: [
'activities',
'datacenters',
'deployments',
'manifests',
'metrics'
]
tables: {
'portals': { id: { type: 'uuid' } },
'datacenters': { id: { type: 'uuid' } },
'deployment_groups': { id: { type: 'uuid' } },
'versions': { id: { type: 'uuid' } },
'manifests': { id: { type: 'uuid' } },
'services': { id: { type: 'uuid' } },
'packages': { id: { type: 'uuid' } },
'instances': { id: { type: 'uuid' } }
}
};
module.exports = class Data {
@ -31,38 +36,83 @@ module.exports = class Data {
this._docker = new DCClient(settings.dockerHost);
}
connect () {
return new Promise((resolve, reject) => {
this._db.establish(internals.tables, (err) => {
connect (cb) {
this._db.establish(internals.tables, cb);
}
// portals
createPortal (clientPortal, cb) {
const portal = Transform.toPortal(clientPortal);
this._db.portals.insert(portal, (err, key) => {
if (err) {
return cb(err);
}
portal.id = key;
cb(null, Transform.fromPortal({ portal }));
});
}
getPortal (cb) {
this._db.portals.all((err, portals) => {
if (err) {
return cb(err);
}
const portal = portals[0];
VAsync.parallel({
funcs: [
(next) => {
this.getDatacenter({ id: portal.datacenter_id }, next);
},
(next) => {
this.getDeploymentGroups(portal.deployment_group_ids, next);
}
]
}, (err, results) => {
if (err) {
return reject(err);
return cb(err);
}
// promisify Penseur
internals.tables.forEach((tableName) => {
return ['insert', 'get', 'update', 'remove', 'all'].forEach((methodName) => {
this._db[tableName][methodName] = Awaitify(
this._db[tableName][methodName]
);
});
});
resolve();
cb(null, Transform.fromPortal({ portal, datacenter: results.successes[0], deploymentGroups: results.successes[1] }));
});
});
}
/*
* DeploymentGroupUuid
* Manifest
* id: UUID,
* created: Date.now(),
* type: 'docker-compose',
* format: 'yml',
* raw: 'original yml file content',
* obj: { }
*/
createDeployment ({ deploymentGroupUuid, manifest, deployment }) {
// datacenters
createDatacenter (datacenter, cb) {
this._db.datacenters.insert(datacenter, (err, key) => {
if (err) {
return cb(err);
}
datacenter.id = key;
cb(null, datacenter);
});
}
getDatacenters (cb) {
this._db.datacenters.all(cb);
}
getDatacenter ({ id, region }, cb) {
Hoek.assert(id || region, 'id or region are required to retrieve a datacenter');
if (region) {
return this._db.datacenters.single({ region }, cb);
}
this._db.datacenters.single({ id }, cb);
}
// deployment_groups
createDeploymentGroup ({ name }, cb) {
// trigger deployment
// create deployment queue (we should think about what is a deployment queue)
// create the ConvergencePlans
@ -70,111 +120,68 @@ module.exports = class Data {
// create a Version
// update the DeploymentGroup
// TODO
const updateDb = (plan) => {
// deployment.services = [];
// deployment.state = { current: 'stopped' };
this._db.deployments
.insert({
name: deployment.name
})
.then((key) => {
deployment.id = key;
return deployment;
});
};
const provision = ({ name }) => {
return this._docker
.provision({
projectName: name,
manifest: manifest.raw
})
.then(updateDb);
};
this.getDeployment(deploymentGroupUuid).then(provision);
}
getDeployment (id) {
return this._db.deployments.get(id);
}
updateDeployment (deployment) {
return this._db.deployments.update(deployment.id, deployment);
}
deleteDeployment (id) {
return this._db.deployments.remove(id);
}
getDeployments () {
return this._db.deployments.all();
}
getDatacenters () {
return this._db.datacenters.all();
}
createManifest (deploymentId, manifest) {
manifest.deploymentId = deploymentId;
manifest.created = Date.now();
return this._db.manifests.insert().then((id) => {
manifest.id = id;
return manifest;
});
}
getManifest (id) {
return this._db.manifests.get();
}
getActivities (deploymentId) {
return this._db.activities.query({ deploymentId });
}
getMetrics (containerId) {
return this._db.metrics.get(containerId);
}
insertMetrics (containerId, metrics) {
return this._db.metrics.get(containerId).then((existing) => {
if (existing) {
return this._db.metrics.update(containerId, {
metrics: this._db.append(metrics)
});
this._db.deployment_groups.insert({ name }, (err, key) => {
if (err) {
return cb(err);
}
const entry = { id: containerId, metrics };
return this._db.metrics.insert(entry, { merge: true });
cb(null, Transform.fromDeploymentGroup({ id: key, name }));
});
}
getServices (deploymentId) {
this._db.deployments.get(deploymentId, { filter: 'services' });
}
updateService (deploymentId, service) {
this._db.deployments.get(deploymentId, { filter: 'services' }).then((deployment) => {
const serviceToUpdate = deployment.services.find((currentService) => {
return currentService.name === service.name;
});
if (!serviceToUpdate) {
deployment.services.push(service);
} else {
serviceToUpdate.count = service.count;
serviceToUpdate.containers = service.containers;
updateDeploymentGroup ({ id, name }, cb) {
this._db.deployment_groups.update(id, { name }, (err) => {
if (err) {
return cb(err);
}
return this._db.deployments.update(deploymentId, {
services: deployment.services
});
cb(null, Transform.fromDeploymentGroup({ id, name }));
});
}
deploymentChanges (handler) {
return this._db.deployments.changes('*', { reconnect: true, handler });
getDeploymentGroups (ids, cb) {
this._db.deployment_groups.get(ids, (err, deploymentGroups) => {
if (err) {
return cb(err);
}
deploymentGroups = deploymentGroups || [];
cb(null, deploymentGroups.map(Transform.fromDeploymentGroup));
});
}
getDeploymentGroup (id, cb) {
this._db.deployment_groups.single({ id }, (err, deploymentGroup) => {
if (err) {
return cb(err);
}
cb(null, Transform.fromDeploymentGroup(deploymentGroup || {}));
});
}
// versions
createVersion (clientVersion, cb) {
const version = Transform.toVersion(clientVersion);
this._db.versions.insert(version, (err, key) => {
if (err) {
return cb(err);
}
version.id = key;
cb(null, Transform.fromVersion(version));
});
}
getVersion (id, cb) {
this._db.versions.single({ id }, (err, version) => {
if (err) {
return cb(err);
}
cb(null, Transform.fromVersion(version));
});
}
};

View File

@ -0,0 +1,94 @@
'use strict';
exports.fromPortal = function ({ portal, datacenter, deploymentGroups }) {
deploymentGroups = Array.isArray(deploymentGroups) ? deploymentGroups : [];
return {
id: portal.id,
username: portal.username,
datacenter,
deploymentGroups: deploymentGroups.map(exports.fromDeploymentGroup)
};
};
exports.toPortal = function (clientPortal) {
return {
username: clientPortal.username,
datacenter_id: clientPortal.datacenter ? clientPortal.datacenter.id : '',
deployment_group_ids: clientPortal.deploymentGroups ? clientPortal.deploymentGroups.map((deploymentGroup) => {
return deploymentGroup.id;
}) : []
};
};
exports.fromDeploymentGroup = function (deploymentGroup, services) {
if (!Array.isArray(services)) {
services = [];
}
return {
id: deploymentGroup.id,
name: deploymentGroup.name,
slug: deploymentGroup.slug,
services: services.map(exports.fromService),
version: deploymentGroup.version_id,
history: deploymentGroup.history_version_ids || []
};
};
exports.fromService = function (service) {
};
exports.toVersion = function (clientVersion) {
return {
id: clientVersion.id,
created: clientVersion.created || Date.now(),
manifest_id: clientVersion.manifestId,
service_scales: clientVersion.scales ? clientVersion.scales.map(exports.toScale) : [],
plan: exports.toPlan(clientVersion.plan || {})
};
};
exports.fromVersion = function (version) {
return {
id: version.id,
created: version.created,
manifestId: version.manifest_id,
scales: version.service_scales ? version.service_scales.map(exports.fromScale) : [],
plan: exports.fromPlan(version.plan || {})
};
};
exports.toScale = function (clientScale) {
return {
service_name: clientScale.serviceName,
replicas: clientScale.replicas
};
};
exports.fromScale = function (scale) {
return {
serviceName: scale.service_name,
replicas: scale.replicas
};
};
exports.toPlan = function (clientPlan) {
return {
running: clientPlan.running,
actions: clientPlan.actions
};
};
exports.fromPlan = function (plan) {
return {
running: plan.running,
actions: plan.actions
};
};

View File

@ -6,7 +6,6 @@
"scripts": {
"bootstrap": "node ./bootstrap-data",
"lint": "belly-button",
"fmt": "prettier --write --single-quote {lib,test}/**/*.js",
"rethinkdb-up": "docker run -d -p 8080:8080 -p 28015:28015 -p 29015:29015 --name rethinkdb rethinkdb",
"rethinkdb-down": "docker rm -f rethinkdb",
"test": "exit 0 # npm run lint && lab -t 40"
@ -15,10 +14,10 @@
"author": "wyatt",
"license": "MPL-2.0",
"dependencies": {
"apr-awaitify": "^1.0.4",
"docker-compose-client": "^1.0.3",
"hoek": "^4.1.1",
"penseur": "^7.8.1"
"penseur": "^7.8.1",
"vasync": "^1.6.4"
},
"devDependencies": {
"belly-button": "^3.1.0",

View File

@ -12,208 +12,252 @@ const internals = {
options: { name: 'test', db: { test: true } }
};
describe('connect()', function () {
it('connects to the database', () => {
describe('connect()', () => {
it('connects to the database', (done) => {
const data = new PortalData(internals.options);
return data.connect();
data.connect(done);
});
});
describe.skip('createDeployment()', () => {
it('creates a deployment record in the deployment table', (done) => {
const data = new PortalData(internals.options);
const deployment = {
name: 'User Services',
datacenter: 'us-sw-1'
};
data
.connect()
.then(() => {
data
.createDeployment({
deployment
})
.then((deployment) => {
expect(deployment.id).to.exist();
done();
});
})
.catch((err) => {
describe('portals', () => {
describe('createPortal()', () => {
it('creates a new portal', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
});
});
});
describe.skip('getDeployment()', () => {
it('will retrieve an existing deployment', (done) => {
const data = new PortalData(internals.options);
data.connect().then(() => {
const deployment = {
name: 'User Services',
datacenter: 'us-sw-1'
};
const portal = {
username: 'tom'
};
data.createDeployment(deployment).then((deployment) => {
expect(deployment.id).to.exist();
data.getDeployment(deployment.id).then((retrievedDeployment) => {
expect(deployment).to.equal(retrievedDeployment);
data.createPortal(portal, (err, result) => {
expect(err).to.not.exist();
expect(result.id).to.exist();
expect(result.username).to.equal(portal.username);
done();
});
});
});
});
});
describe.skip('updateService()', () => {
it('will update the services for an existing deployment', (done) => {
const data = new PortalData(internals.options);
data.connect().then(() => {
const deployment = {
name: 'User Services',
datacenter: 'us-sw-1'
};
const service = {
name: 'consul',
containers: [
{
server_id: '423e7432-b760-11e2-bf6c-002590c3f1a0',
alias: 'nodejsexample_consul_1',
image_id: '91b757b5-bd29-2126-5ff9-ae9235011ff5',
owner_id: '30f62ec2-24a2-6f8e-8fad-d46b04c8a0b9',
id: '81205d4a-92f4-c4d9-da8a-aafd689eeabb'
}
],
count: 1
};
describe('getPortal()', () => {
it('retrieves a single portal record', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const datacenter = {
region: 'us-sw-1'
};
data.createDeployment(deployment).then((deployment) => {
expect(deployment.id).to.exist();
data.updateService(deployment.id, service).then((updatedService) => {
expect(updatedService).to.equal(service);
done();
});
});
});
});
});
data.createDatacenter(datacenter, (err, createdDatacenter) => {
expect(err).to.not.exist();
const portal = {
username: 'tom',
datacenter: {
id: createdDatacenter.id
}
};
describe.skip('deploymentChanges()', () => {
it('will execute the handler when a deployment service changes', (done) => {
const data = new PortalData(internals.options);
data.connect().then(() => {
const deployment = {
name: 'User Services',
datacenter: 'us-sw-1'
};
const service1 = {
name: 'consul',
containers: [
{
server_id: '423e7432-b760-11e2-bf6c-002590c3f1a0',
alias: 'nodejsexample_consul_1',
image_id: '91b757b5-bd29-2126-5ff9-ae9235011ff5',
owner_id: '30f62ec2-24a2-6f8e-8fad-d46b04c8a0b9',
id: '81205d4a-92f4-c4d9-da8a-aafd689eeabb'
}
],
count: 1
};
const service2 = {
name: 'consul',
containers: [
{
server_id: '423e7432-b760-11e2-bf6c-002590c3f1a0',
alias: 'nodejsexample_consul_1',
image_id: '91b757b5-bd29-2126-5ff9-ae9235011ff5',
owner_id: '30f62ec2-24a2-6f8e-8fad-d46b04c8a0b9',
id: '81205d4a-92f4-c4d9-da8a-aafd689eeabb'
},
{
server_id: '423e7432-b760-11e2-bf6c-002590c3f1a0',
alias: 'nodejsexample_consul_2',
image_id: '91b757b5-bd29-2126-5ff9-ae9235011ff5',
owner_id: '30f62ec2-24a2-6f8e-8fad-d46b04c8a0b9',
id: '81205d4a-92f4-c4d9-da8a-aafd689eeabb'
},
{
server_id: '423e7432-b760-11e2-bf6c-002590c3f1a0',
alias: 'nodejsexample_consul_3',
image_id: '91b757b5-bd29-2126-5ff9-ae9235011ff5',
owner_id: '30f62ec2-24a2-6f8e-8fad-d46b04c8a0b9',
id: '81205d4a-92f4-c4d9-da8a-aafd689eeabb'
}
],
count: 3
};
data.createDeployment(deployment).then((deployment) => {
expect(deployment.id).to.exist();
data.updateService(deployment.id, service1).then((updatedService1) => {
expect(updatedService1).to.equal(service1);
let executed = false;
data
.deploymentChanges((err, changes) => {
data.createPortal(portal, (err, createdPortal) => {
expect(err).to.not.exist();
expect(createdPortal.id).to.exist();
data.getPortal((err, retrievedPortal) => {
expect(err).to.not.exist();
if (executed) {
return;
}
expect(changes.before).to.exist();
expect(changes.after).to.exist();
expect(retrievedPortal.id).to.exist();
expect(retrievedPortal.username).to.equal(portal.username);
done();
executed = true;
})
.then(() => {
data
.updateService(deployment.id, service2)
.then((updatedService2) => {
expect(updatedService2).to.equal(service2);
});
});
});
});
});
});
});
describe.skip('insertMetrics()', () => {
it("will add new metrics to a service and won't overwrite existing ones", (done) => {
const data = new PortalData(internals.options);
data.connect().then(() => {
const containerId = '81205d4a-92f4-c4d9-da8a-aafd689eeabb';
const metrics1 = [
{
timestamp: 1494360995851,
cpu: 1.2,
memory: 23344523,
network: 5024
}
];
const metrics2 = [
{
timestamp: 1495360995851,
cpu: 1.3,
memory: 23344523,
network: 4024
}
];
data.insertMetrics(containerId, metrics1).then((result1) => {
expect(result1.id).to.equal(containerId);
expect(result1.metrics).to.equal(metrics1);
data.insertMetrics(containerId, metrics2).then((result2) => {
expect(result2.id).to.equal(containerId);
data.getMetrics(containerId).then((results) => {
expect(results.metrics.length).to.equal(2);
done();
});
});
});
});
});
});
describe('deployment groups', () => {
describe('createDeploymentGroup()', () => {
it('creates a deployment group record in the deployment_groups table', (done) => {
const data = new PortalData(internals.options);
const name = 'User Services';
data.connect((err) => {
expect(err).to.not.exist();
data.createDeploymentGroup({ name }, (err, deploymentGroup) => {
expect(err).to.not.exist();
expect(deploymentGroup.id).to.exist();
done();
});
});
});
});
describe('getDeploymentGroup()', () => {
it('gets a deployment group record from the deployment_groups table', (done) => {
const data = new PortalData(internals.options);
const name = 'User Services';
data.connect((err) => {
expect(err).to.not.exist();
data.createDeploymentGroup({ name }, (err, createdDeploymentGroup) => {
expect(err).to.not.exist();
expect(createdDeploymentGroup.id).to.exist();
data.getDeploymentGroup(createdDeploymentGroup.id, (err, deploymentGroup) => {
expect(err).to.not.exist();
expect(deploymentGroup).to.equal(createdDeploymentGroup);
done();
});
});
});
});
});
describe('getDeploymentGroups()', () => {
it('gets a list of deployment group records from the deployment_groups table', (done) => {
const data = new PortalData(internals.options);
const name = 'User Services';
data.connect((err) => {
expect(err).to.not.exist();
data.createDeploymentGroup({ name }, (err, createdDeploymentGroup1) => {
expect(err).to.not.exist();
expect(createdDeploymentGroup1.id).to.exist();
data.createDeploymentGroup({ name }, (err, createdDeploymentGroup2) => {
expect(err).to.not.exist();
expect(createdDeploymentGroup1.id).to.exist();
data.getDeploymentGroups([createdDeploymentGroup1.id, createdDeploymentGroup2.id], (err, deploymentGroups) => {
expect(err).to.not.exist();
expect(deploymentGroups.length).to.equal(2);
done();
});
});
});
});
});
});
});
describe('datacenters', () => {
describe('createDatacenter()', () => {
it('creates a new datacenter record', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const datacenter = {
region: 'us-sw-1'
};
data.createDatacenter(datacenter, (err, result) => {
expect(err).to.not.exist();
expect(result.id).to.exist();
expect(result.region).to.equal(datacenter.region);
done();
});
});
});
});
describe('getDatacenter()', () => {
it('retrieves a datacenter record from an id', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const datacenter = {
region: 'us-sw-1'
};
data.createDatacenter(datacenter, (err, createdDatacenter) => {
expect(err).to.not.exist();
expect(createdDatacenter.id).to.exist();
data.getDatacenter({ id: createdDatacenter.id }, (err, retrievedDatacenter) => {
expect(err).to.not.exist();
expect(retrievedDatacenter.region).to.equal(datacenter.region);
done();
});
});
});
});
it('retrieves a datacenter record from a region', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const datacenter = {
region: 'us-sw-1'
};
data.createDatacenter(datacenter, (err, createdDatacenter) => {
expect(err).to.not.exist();
expect(createdDatacenter.id).to.exist();
data.getDatacenter({ region: createdDatacenter.region }, (err, retrievedDatacenter) => {
expect(err).to.not.exist();
expect(retrievedDatacenter.region).to.equal(datacenter.region);
done();
});
});
});
});
});
describe('getDatacenters()', () => {
it('retrieves all datacenter records', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const datacenter1 = {
region: 'us-sw-1'
};
const datacenter2 = {
region: 'us-west-1'
};
data.createDatacenter(datacenter1, (err, createdDatacenter1) => {
expect(err).to.not.exist();
data.createDatacenter(datacenter2, (err, createdDatacenter2) => {
expect(err).to.not.exist();
data.getDatacenters((err, datacenters) => {
expect(err).to.not.exist();
expect(datacenters.length).to.equal(2);
done();
});
});
});
});
});
});
});
describe('versions', () => {
describe('createVersion()', () => {
it('creates a new version record in the versions table', (done) => {
const data = new PortalData(internals.options);
data.connect((err) => {
expect(err).to.not.exist();
const clientVersion = {
manifestId: 'something',
scales: [{
serviceName: 'consul',
replicas: 3
}],
plan: {
running: true,
actions: [{
type: 'start',
service: 'consul',
machines: ['vmid', 'vmid']
}]
}
};
data.createVersion(clientVersion, (err, result) => {
expect(err).to.not.exist();
expect(result.id).to.exist();
expect(result.scales).to.equal(clientVersion.scales);
done();
});
});
});
});
});

View File

@ -276,6 +276,13 @@ diff@3.x.x:
version "3.2.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
docker-compose-client@^1.0.3:
version "1.0.7"
resolved "https://registry.yarnpkg.com/docker-compose-client/-/docker-compose-client-1.0.7.tgz#a2f351aff998fd5323b9b6bb27d4400fff95e43c"
dependencies:
apr-awaitify "^1.0.4"
zerorpc "^0.9.7"
doctrine@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
@ -448,6 +455,10 @@ exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
extsprintf@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.2.0.tgz#5ad946c22f5b32ba7f8cd7426711c6e8a3fc2529"
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@ -1183,6 +1194,18 @@ uuid@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
vasync@^1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/vasync/-/vasync-1.6.4.tgz#dfe93616ad0e7ae801b332a9d88bfc5cdc8e1d1f"
dependencies:
verror "1.6.0"
verror@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.6.0.tgz#7d13b27b1facc2e2da90405eb5ea6e5bdd252ea5"
dependencies:
extsprintf "1.2.0"
window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"