feat: track transitional states

This commit is contained in:
Sérgio Ramos 2017-07-05 14:33:16 +01:00 committed by Judit Greskovits
parent 1bf7913ac3
commit 012a44c00a
15 changed files with 1837 additions and 3199 deletions

View File

@ -16,12 +16,8 @@ const StyledStatusContainer = styled.div`
`; `;
const InstanceStatuses = ({ instanceStatuses }) => { const InstanceStatuses = ({ instanceStatuses }) => {
const statuses = instanceStatuses.map(instanceStatus => { const statuses = instanceStatuses.map(instanceStatus => {
const { const { status, count } = instanceStatus;
status,
count
} = instanceStatus;
return ( return (
<StyledStatus> <StyledStatus>

View File

@ -35,11 +35,11 @@ const Manifest = ({
deploymentGroup && deploymentGroup &&
deploymentGroup.imported && deploymentGroup.imported &&
!manifest !manifest
? null ? <span>
: <span>
Since this DeploymentGroup was imported, it doesn't have the initial Since this DeploymentGroup was imported, it doesn't have the initial
manifest manifest
</span>; </span>
: null;
return ( return (
<LayoutContainer> <LayoutContainer>

View File

@ -44,7 +44,10 @@ class ServiceList extends Component {
startServices startServices
} = this.props; } = this.props;
if (loading) { if (
loading ||
(deploymentGroup.status === 'PROVISIONING' && !services.length)
) {
return ( return (
<LayoutContainer> <LayoutContainer>
<Loader /> <Loader />

View File

@ -3,4 +3,5 @@ fragment DeploymentGroupInfo on DeploymentGroup {
name name
slug slug
imported imported
status
} }

View File

@ -5,12 +5,9 @@ mutation provisionManifest($deploymentGroupId: ID!, $type: ManifestType!, $forma
replicas replicas
} }
plan { plan {
running type
actions { service
type machines
service
machines
}
} }
} }
} }

View File

@ -2,4 +2,5 @@ fragment ServiceInfo on Service {
id id
name name
slug slug
status
} }

View File

@ -16,6 +16,14 @@ type User {
login: String! login: String!
} }
enum DeploymentGroupStatus {
ACTIVE
PROVISIONING
DELETING
DELETED
UNKNOWN
}
type DeploymentGroup { type DeploymentGroup {
id: ID! id: ID!
name: String! name: String!
@ -24,6 +32,7 @@ type DeploymentGroup {
version: Version version: Version
history: [Version] history: [Version]
imported: Boolean imported: Boolean
status: DeploymentGroupStatus
} }
type ServiceScale { type ServiceScale {
@ -35,27 +44,25 @@ enum ConvergenceActionType {
NOOP NOOP
CREATE CREATE
RECREATE RECREATE
REMOVE
START START
EXISTING # special status to mark existing ids in previous version
} }
type ConvergenceAction { type ConvergenceAction {
id: ID!
type: ConvergenceActionType! type: ConvergenceActionType!
service: String! # service name service: String! # service name
machines: [String]! # instance machine ids toProcess: Int, # merely used for book keeping
} processed: [String], # merely used for book keeping
machines: [String]! # current instance machine ids
type StateConvergencePlan {
id: ID!
running: Boolean!
actions(type: ConvergenceActionType, service: String): [ConvergenceAction]!
} }
type Version { type Version {
created: Date! # Either Int or define scalar
manifest: Manifest! manifest: Manifest!
scale(serviceName: String): [ServiceScale]! scale(serviceName: String): [ServiceScale]!
plan(running: Boolean): StateConvergencePlan plan: [ConvergenceAction]
hasPlan: Boolean
error: String
} }
enum ManifestType { enum ManifestType {
@ -70,13 +77,24 @@ enum ManifestFormat {
type Manifest { type Manifest {
id: ID! id: ID!
created: Float
type: ManifestType! type: ManifestType!
format: ManifestFormat! format: ManifestFormat!
raw: String! raw: String!
obj: Object obj: Object
} }
enum ServiceStatus {
ACTIVE # this doesn't mean that the instances are all running
PROVISIONING
SCALING
STOPPING
STOPPED
DELETING
DELETED
RESTARTING
UNKNOWN
}
# immutable # immutable
type Service { type Service {
id: ID! # unique id for db row id: ID! # unique id for db row
@ -84,14 +102,12 @@ type Service {
name: String! # human readable name name: String! # human readable name
slug: String! slug: String!
instances(name: String, machineId: ID, status: InstanceStatus): [Instance]! instances(name: String, machineId: ID, status: InstanceStatus): [Instance]!
# metrics: [MetricType]
currentMetrics: [CurrentMetric]
connections: [String!] # list of serviceIds connections: [String!] # list of serviceIds
parent: ID # parent service id parent: ID # parent service id
package: Package! # we don't have this in current mock data, package: Package! # we don't have this in current mock data,
environment: [Environment] environment: [Environment]
active: Boolean! # let's say that we update the manifest, and remove a service. we still want to track this service even though it might not exist because it might have machines running still
image: String # used only for config image: String # used only for config
status: ServiceStatus
} }
# for metrics max / min (I guess) # for metrics max / min (I guess)
@ -135,7 +151,6 @@ type Instance {
machineId: ID! machineId: ID!
status: InstanceStatus! status: InstanceStatus!
healthy: Boolean healthy: Boolean
# metrics: [InstanceMetric]!
} }
type Datacenter { type Datacenter {
@ -145,28 +160,6 @@ type Datacenter {
region: String! region: String!
} }
type InstanceMetric {
type: MetricType!
data: [MetricData]!
}
type CurrentMetric {
name: String!
value: Float!
measurement: String!
}
type MetricType {
id: ID!
name: String!
id: ID!
}
type MetricData {
timestamp: Int!
value: Float!
}
# we probably wont use some of these queries or arguments # we probably wont use some of these queries or arguments
# but this way we expose the entire db through gql # but this way we expose the entire db through gql
type Query { type Query {
@ -178,8 +171,6 @@ type Query {
serviceScale(id: ID!): ServiceScale serviceScale(id: ID!): ServiceScale
convergenceActions(type: ConvergenceActionType, service: String, versionId: ID): [ConvergenceAction] convergenceActions(type: ConvergenceActionType, service: String, versionId: ID): [ConvergenceAction]
convergenceAction(id: ID!): ConvergenceAction convergenceAction(id: ID!): ConvergenceAction
stateConvergencePlans(running: Boolean, versionId: ID): [StateConvergencePlan]
stateConvergencePlan(id: ID!): StateConvergencePlan
versions(manifestId: ID, deploymentGroupId: ID): [Version] versions(manifestId: ID, deploymentGroupId: ID): [Version]
version(id: ID, manifestId: ID): Version version(id: ID, manifestId: ID): Version
manifests(type: String, deploymentGroupId: ID): [Manifest] manifests(type: String, deploymentGroupId: ID): [Manifest]

View File

@ -2,7 +2,13 @@
// TODO wait for eslint/eslint#3458 // TODO wait for eslint/eslint#3458
module.exports = { module.exports = {
extends: ['eslint:recommended', 'xo-space/esnext', 'react-app', 'prettier', 'prettier/react'], extends: [
'eslint:recommended',
'xo-space/esnext',
'react-app',
'prettier',
'prettier/react'
],
rules: { rules: {
'capitalized-comments': 0 'capitalized-comments': 0
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,11 @@
'use strict'; 'use strict';
const Yamljs = require('yamljs'); const Yamljs = require('yamljs');
const ParamCase = require('param-case');
const clean = (v) => {
return JSON.parse(JSON.stringify(v));
};
exports.fromPortal = function ({ portal, datacenter, deploymentGroups, user }) { exports.fromPortal = function ({ portal, datacenter, deploymentGroups, user }) {
return { return {
@ -30,18 +34,31 @@ exports.fromDeploymentGroup = function (deploymentGroup) {
slug: deploymentGroup.slug, slug: deploymentGroup.slug,
services: deploymentGroup.services, services: deploymentGroup.services,
version: deploymentGroup.version, version: deploymentGroup.version,
history: deploymentGroup.history_version_ids || [] history: deploymentGroup.history,
imported: deploymentGroup.imported,
status: deploymentGroup.status
}; };
}; };
exports.toDeploymentGroup = function ({ name }) { exports.toDeploymentGroup = function (clientDeploymentGroup) {
return { return clean({
name, id: clientDeploymentGroup.id,
slug: name, name: clientDeploymentGroup.name,
service_ids: [], slug: clientDeploymentGroup.slug ?
version_id: '', clientDeploymentGroup.slug :
history_version_ids: [] clientDeploymentGroup.name ?
}; ParamCase(clientDeploymentGroup.name) :
undefined,
service_ids: Array.isArray(clientDeploymentGroup.services) ? clientDeploymentGroup.services.map(({ id }) => {
return id;
}).filter(Boolean) : undefined,
version_id: clientDeploymentGroup.version ? clientDeploymentGroup.version.id : undefined,
history_version_ids: Array.isArray(clientDeploymentGroup.history) ? clientDeploymentGroup.history.map(({ id }) => {
return id;
}).filter(Boolean) : undefined,
imported: clientDeploymentGroup.imported,
status: clientDeploymentGroup.status || 'ACTIVE'
});
}; };
@ -58,36 +75,43 @@ exports.fromService = function ({ service, instances, packages }) {
connections: service.service_dependency_ids, connections: service.service_dependency_ids,
package: packages ? exports.fromPackage(packages) : {}, package: packages ? exports.fromPackage(packages) : {},
parent: service.parent_id || '', parent: service.parent_id || '',
active: service.active status: service.status,
hasPlan: service.has_plan
}; };
}; };
exports.toService = function (clientService) { exports.toService = function (clientService) {
// wat?? // wat??
return JSON.parse(JSON.stringify({ return clean({
id: clientService.id, id: clientService.id,
version_hash: clientService.hash || clientService.name, version_hash: clientService.hash,
deployment_group_id: clientService.deploymentGroupId, deployment_group_id: clientService.deploymentGroupId,
name: clientService.name, name: clientService.name,
slug: clientService.slug, slug: clientService.slug,
environment: clientService.environment || {}, environment: clientService.environment,
instance_ids: clientService.instances ? clientService.instances.map((instance) => { return instance.id; }) : undefined, instance_ids: clientService.instances ?
service_dependency_ids: clientService.connections || [], clientService.instances.map((instance) => {
package_id: clientService.package ? clientService.package.id : '', return instance.id;
parent_id: clientService.parent || '', }) :
active: clientService.ative undefined,
})); service_dependency_ids: clientService.connections,
package_id: clientService.package ? clientService.package.id : undefined,
parent_id: clientService.parent ? clientService.parent : undefined,
status: clientService.status,
has_plan: clientService.hasPlan
});
}; };
exports.toVersion = function (clientVersion) { exports.toVersion = function (clientVersion) {
return { return clean({
id: clientVersion.id, id: clientVersion.id,
created: clientVersion.created || Date.now(), created: clientVersion.created || Date.now(),
manifest_id: (clientVersion.manifest || {}).id, manifest_id: (clientVersion.manifest || {}).id,
service_scales: clientVersion.scale ? clientVersion.scale.map(exports.toScale) : [], service_scales: clientVersion.scale ? clientVersion.scale : undefined,
plan: exports.toPlan(clientVersion.plan || {}) plan: clientVersion.plan ? clientVersion.plan : undefined,
}; error: clientVersion.version
});
}; };
exports.fromVersion = function (version) { exports.fromVersion = function (version) {
@ -95,38 +119,9 @@ exports.fromVersion = function (version) {
id: version.id, id: version.id,
created: version.created, created: version.created,
manifest: version.manifest, manifest: version.manifest,
scale: version.service_scales ? version.service_scales.map(exports.fromScale) : [], scale: version.service_scales,
plan: exports.fromPlan(version.plan || {}) plan: version.plan,
}; error: version.error
};
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
}; };
}; };
@ -170,21 +165,21 @@ exports.fromInstance = function (instance) {
id: instance.id, id: instance.id,
name: instance.name, name: instance.name,
machineId: instance.machine_id, machineId: instance.machine_id,
status: instance.status || '', status: instance.status,
ips: instance.ips || [], ips: instance.ips,
healthy: instance.healthy healthy: instance.healthy
}; };
}; };
exports.toInstance = function (clientInstance) { exports.toInstance = function (clientInstance) {
return { return clean({
id: clientInstance.id, id: clientInstance.id,
name: clientInstance.name, name: clientInstance.name,
machine_id: clientInstance.machineId, machine_id: clientInstance.machineId,
status: clientInstance.status || '', status: clientInstance.status,
ips: clientInstance.ips || [], ips: clientInstance.ips,
healthy: clientInstance.healthy healthy: clientInstance.healthy
}; });
}; };

View File

@ -3,20 +3,62 @@
// const Assert = require('assert'); // const Assert = require('assert');
const Throat = require('throat'); const Throat = require('throat');
const TritonWatch = require('triton-watch'); const TritonWatch = require('triton-watch');
const util = require('util'); const Get = require('lodash.get');
const Find = require('lodash.find');
const Util = require('util');
const ForceArray = require('force-array');
const VAsync = require('vasync');
const DEPLOYMENT_GROUP = 'docker:label:com.docker.compose.project'; const DEPLOYMENT_GROUP = 'docker:label:com.docker.compose.project';
const SERVICE = 'docker:label:com.docker.compose.service'; const SERVICE = 'docker:label:com.docker.compose.service';
const HASH = 'docker:label:com.docker.compose.config-hash'; const HASH = 'docker:label:com.docker.compose.config-hash';
const ACTION_REMOVE_STATUSES = [
'STOPPING',
'STOPPED',
'OFFLINE',
'DELETED',
'DESTROYED',
'FAILED',
'INCOMPLETE',
'UNKNOWN'
];
const ACTION_CREATE_STATUSES = [
'READY',
'ACTIVE',
'RUNNING',
'STOPPED',
'OFFLINE',
'FAILED',
'INCOMPLETE',
'UNKNOWN'
];
const SERVICE_STOPPING_STATUSES = [
'STOPPED',
'OFFLINE',
'FAILED',
'INCOMPLETE',
'UNKNOWN'
];
const SERVICE_DELETING_STATUSES = [
'DELETED',
'DESTROYED',
'FAILED',
'INCOMPLETE',
'UNKNOWN'
];
module.exports = class Watcher { module.exports = class Watcher {
constructor (options) { constructor (options) {
options = options || {}; options = options || {};
// todo assert options // todo assert options
this._data = options.data; this._data = options.data;
this._frequency = 500; this._frequency = 200;
this._tritonWatch = new TritonWatch({ this._tritonWatch = new TritonWatch({
frequency: this._frequency, frequency: this._frequency,
@ -30,50 +72,68 @@ module.exports = class Watcher {
}); });
this._queues = {}; this._queues = {};
this._waitingForPlan = [];
this._isTritonWatchPolling = false;
this._tritonWatch.on('change', (container) => { this._tritonWatch.on('change', (machine) => {
return this.onChange(container); return this.onChange(machine);
}); });
this._tritonWatch.on('all', (containers) => { this._tritonWatch.on('all', (machines) => {
containers.forEach((container) => { machines.forEach((machine) => {
this.onChange(container); this.onChange(machine);
}); });
}); });
} }
poll () { poll () {
this._tritonWatch.poll(); if (!this._isTritonWatchPolling) {
this._tritonWatch.poll();
this._isTritonWatchPolling = true;
}
if (this._isWaitingPolling) {
return;
}
this._isWaitingPolling = true;
setTimeout(() => {
this._isWaitingPolling = false;
this._checkForWaiting();
}, this._frequency);
}
_checkForWaiting () {
this._waitingForPlan.forEach(this.onChange);
} }
getContainers () { getContainers () {
return this._tritonWatch.getContainers(); return this._tritonWatch.getContainers();
} }
pushToQueue ({ serviceName, deploymentGroupId }, cb) { pushToQueue (deploymentGroupId, cb) {
const name = `${deploymentGroupId}-${serviceName}`; if (this._queues[deploymentGroupId]) {
this._queues[deploymentGroupId](cb);
if (this._queues[name]) {
this._queues[name](cb);
return; return;
} }
this._queues[name] = Throat(1); this._queues[deploymentGroupId] = Throat(1);
this._queues[name](cb); this._queues[deploymentGroupId](cb);
} }
getDeploymentGroupId (name, cb) { getDeploymentGroup (name, cb) {
this._data.getDeploymentGroup({ name }, (err, deploymentGroup) => { this._data.getDeploymentGroup({ name }, (err, deploymentGroup) => {
if (err) { if (err) {
return cb(err); return cb(err);
} }
return cb(null, deploymentGroup && deploymentGroup.id); return cb(null, deploymentGroup);
}); });
} }
getService ({ serviceName, serviceHash, deploymentGroupId }, cb) { getService ({ serviceName, deploymentGroupId }, cb) {
this._data.getServices({ name: serviceName, hash: serviceHash, deploymentGroupId }, (err, services) => { this._data.getServices({ name: serviceName, deploymentGroupId }, (err, services) => {
if (err) { if (err) {
return cb(err); return cb(err);
} }
@ -88,88 +148,395 @@ module.exports = class Watcher {
getInstances (service, cb) { getInstances (service, cb) {
service.instances() service.instances()
.then((instances) => { return cb(null, instances); }) .then((instances) => {
.catch((err) => { return cb(err); }); return cb(null, instances);
})
.catch((err) => {
return cb(err);
});
} }
resolveChanges ({ machine, service, instances }, cb) { getVersion (deploymentGroup, cb) {
// 1. if instance doesn't exist, create new deploymentGroup.version()
// 2. if instance exist, update status .then((version) => {
return cb(null, version);
})
.catch((err) => {
return cb(err);
});
}
const handleError = (cb) => { createInstance ({ machine, instances, service }, cb) {
return (err, data) => { console.error(`-> detected that machine ${machine.name} was created`);
if (err) {
console.error(err);
return;
}
if (cb) { const status = (machine.state || '').toUpperCase();
cb(err, data);
} if (status === 'DELETED') {
return cb();
}
const instance = {
name: machine.name,
status,
ips: machine.ips,
machineId: machine.id
};
console.log('-> creating instance', Util.inspect(instance));
this._data.createInstance(instance, (err, instance) => {
if (err) {
return cb(err);
}
const payload = {
id: service.id,
instances: instances.concat(instance)
}; };
console.log('-> updating service', Util.inspect(payload));
this._data.updateService(payload, cb);
});
}
updateInstance ({ machine, instance, instances, service }, cb) {
console.error(`-> detected that machine ${machine.name} was updated`);
const updatedInstance = {
id: instance.id,
ips: machine.ips,
status: (machine.state || '').toUpperCase()
}; };
const isNew = instances console.log('-> updating instance', Util.inspect(updatedInstance));
.every(({ machineId }) => { return machine.id !== machineId; }); this._data.updateInstance(updatedInstance, (err) => {
if (err) {
return cb(err);
}
const instance = instances if (['DELETED', 'DESTROYED'].indexOf(machine.state.toUpperCase()) < 0) {
.filter(({ machineId }) => { return machine.id === machineId; })
.pop();
const updateService = (updatedService, cb) => {
console.log('-> updating service', util.inspect(updatedService));
return this._data.updateService(updatedService, handleError(cb));
};
const create = (cb) => {
const status = (machine.state || '').toUpperCase();
if (status === 'DELETED') {
return cb(); return cb();
} }
const instance = {
name: machine.name, const payload = {
status, id: service.id,
machineId: machine.id, instances: instances.filter(({ id }) => {
ips: machine.ips return id !== instance.id;
})
}; };
console.log('-> creating instance', util.inspect(instance)); console.log('-> updating service', Util.inspect(payload));
return this._data.createInstance(instance, handleError((_, instance) => { this._data.updateService(payload, cb);
return updateService({ });
id: service.id, }
instances: instances.concat(instance)
}, cb); resolveChange ({ deploymentGroup, version, service, instances, machine }, cb) {
})); console.error(`-> resolving change for machine ${machine.name}`);
const SERVICE_STATUS = Get(service, 'status', 'UNKNOWN').toUpperCase();
const MACHINE_STATUS = Get(machine, 'state', 'UNKNOWN').toUpperCase();
const hasPlan = Boolean(Get(version, 'plan.hasPlan', true));
const serviceName = service.name;
console.error(`-> detected meta for machine ${machine.name} ${Util.inspect({
SERVICE_STATUS,
MACHINE_STATUS,
hasPlan,
serviceName
})}`);
const ActionResolvers = {
'_CREATE_OR_REMOVE': (action, cb) => {
console.error(`-> got _CREATE_OR_REMOVE action for "${machine.name}"`);
let processed = ForceArray(action.processed);
const completed = processed.length === action.toProcess;
if (completed) {
console.error('-> action was already completed');
return cb(null, {
action,
completed: true
});
}
if (processed.indexOf(machine.id) >= 0) {
console.error('-> machine was already processed');
return cb(null, {
action,
completed
});
}
processed = processed.concat([machine.id]);
cb(null, {
action: Object.assign({}, action, {
processed
}),
completed: processed.length === action.toProcess
});
},
'NOOP': (action, cb) => {
console.error(`-> got NOOP action for "${machine.name}"`);
cb(null, {
action,
completed: true
});
},
// scenarios: scale down or removed service
// so far, the logic is the same for CREATE and REMOVE
'REMOVE': (action, cb) => {
console.error(`-> got REMOVE action for "${machine.name}"`);
if (ACTION_REMOVE_STATUSES.indexOf(MACHINE_STATUS) < 0) {
console.error(`-> since "${machine.name}" is "${MACHINE_STATUS}", nothing to do here`);
return cb(null, {
action,
completed: false
});
}
if (action.machines.indexOf(machine.id) < 0) {
console.error(`-> since "${machine.name}" didn't exist, no need to process its removal`);
return cb(null, {
action,
completed: false
});
}
ActionResolvers._CREATE_OR_REMOVE(action, cb);
},
// scenarios: scale up, recreate, create
// so far, the logic is the same for CREATE and REMOVE
'CREATE': (action, cb) => {
console.error(`-> got CREATE action for "${machine.name}"`);
if (ACTION_CREATE_STATUSES.indexOf(MACHINE_STATUS) < 0) {
console.error(`-> since "${machine.name}" is "${MACHINE_STATUS}", nothing to do here`);
return cb(null, {
action,
completed: false
});
}
if (action.machines.indexOf(machine.id) >= 0) {
console.error(`-> since "${machine.name}" already existed, no need to process its creation`);
return cb(null, {
action,
completed: false
});
}
ActionResolvers._CREATE_OR_REMOVE(action, cb);
},
'START': (action, cb) => {
console.error(`-> got START action for "${machine.name}". redirecting`);
return ActionResolvers.NOOP(action, cb);
}
}; };
const update = (cb) => { const toBeActiveServiceResolver = (cb) => {
const updatedInstance = { VAsync.forEachParallel({
id: instance.id, inputs: version.plan,
status: (machine.state || '').toUpperCase() func: (action, next) => {
}; if (action.service !== serviceName) {
return next(null, {
action
});
}
console.log('-> updating instance', util.inspect(updatedInstance)); const ACTION_TYPE = Get(action, 'type', 'NOOP').toUpperCase();
return this._data.updateInstance(updatedInstance, handleError(() => { ActionResolvers[ACTION_TYPE](action, next);
if (['DELETED', 'DESTROYED'].indexOf(machine.state.toUpperCase()) < 0) { }
}, (err, result) => {
if (err) {
return cb(err);
}
const newActions = ForceArray(result.successes);
console.error(`-> got new actions for "${service.name}" ${Util.inspect(newActions)}`);
const newServiceActions = newActions.filter(({ action }) => {
return action.service === serviceName;
});
const isCompleted = newServiceActions.every(({ completed }) => {
return completed;
});
console.error(`-> are all actions for "${service.name}" completed? ${isCompleted}`);
const newPlan = newActions.map(({ action }) => {
return action;
});
VAsync.parallel({
funcs: [
(cb) => {
console.error(`-> updating Version ${version.id} with new plan ${Util.inspect(newPlan)}`);
this._data.updateVersion({
id: version.id,
plan: newPlan
}, cb);
},
(cb) => {
if (!isCompleted) {
return cb();
}
console.error(`-> updating Service ${service.name} with new status: ACTIVE`);
return this._data.updateService({
id: service.id,
status: 'ACTIVE'
}, cb);
}
]
}, cb);
});
};
const ServiceResolvers = {
'ACTIVE': (cb) => {
console.error(`-> got ACTIVE service "${service.name}". nothing to do`);
cb();
},
'PROVISIONING': (cb) => {
console.error(`-> got PROVISIONING service "${service.name}"`);
toBeActiveServiceResolver(cb);
},
'SCALING': (cb) => {
console.error(`-> got SCALING service "${service.name}"`);
toBeActiveServiceResolver(cb);
},
'STOPPING': (cb) => {
console.error(`-> got STOPPING service "${service.name}"`);
if (SERVICE_STOPPING_STATUSES.indexOf(MACHINE_STATUS) < 0) {
return cb(); return cb();
} }
return setTimeout(() => { const isComplete = instances
return updateService({ .filter(({ machineId }) => {
id: service.id, return machineId !== machine.id;
instances: instances.filter(({ id }) => { })
return id !== instance.id; .every(({ status }) => {
}) return SERVICE_STOPPING_STATUSES.indexOf(status) >= 0;
}, cb); });
}, this._frequency * 3);
})); if (!isComplete) {
return cb();
}
this._data.updateService({
id: service.id,
status: 'STOPPED'
}, cb);
},
'STOPPED': (cb) => {
return ServiceResolvers.ACTIVE(cb);
},
'DELETING': (cb) => {
console.error(`-> got DELETING service "${service.name}"`);
if (SERVICE_DELETING_STATUSES.indexOf(MACHINE_STATUS) < 0) {
return cb();
}
const isComplete = instances
.filter(({ machineId }) => {
return machineId !== machine.id;
})
.every(({ status }) => {
return SERVICE_DELETING_STATUSES.indexOf(status) >= 0;
});
if (!isComplete) {
return cb();
}
VAsync.parallel({
funcs: [
(cb) => {
console.error(`-> updating Service ${service.name} to set it DELETED`);
this._data.updateService({
id: service.id,
status: 'DELETED'
}, cb);
},
(cb) => {
console.error(`-> updating DeploymentGroup ${deploymentGroup.id} to remove Service ${service.name}`);
deploymentGroup.services({}, (err, services) => {
if (err) {
return cb(err);
}
this._data.updateDeploymentGroup({
id: deploymentGroup.id,
services: services.filter(({ id }) => {
return service.id !== id;
})
}, cb);
});
}
]
}, cb);
},
'DELETED': (cb) => {
return ServiceResolvers.ACTIVE(cb);
},
'RESTARTING': (cb) => {
return ServiceResolvers.ACTIVE(cb);
},
'UNKNOWN': (cb) => {
return ServiceResolvers.ACTIVE(cb);
}
}; };
return isNew ? const instance = Find(instances, ['machineId', machine.id]);
create(cb) :
update(cb); const isNew = instances
.every(({ machineId }) => {
return machine.id !== machineId;
});
const handleCreateOrUpdatedInstance = (err) => {
if (err) {
return cb(err);
}
console.error(`-> created/updated machine ${machine.name}`);
if (!hasPlan) {
console.error(`-> plan for ${service.name} is still not available. queuing`);
this._waitingForPlan.push(machine);
return cb();
}
const serviceResolver = ServiceResolvers[SERVICE_STATUS] ?
ServiceResolvers[SERVICE_STATUS] :
ServiceResolvers.UNKNOWN;
serviceResolver(cb);
};
const createOrUpdateInstance = isNew ?
this.createInstance :
this.updateInstance;
createOrUpdateInstance.call(this, { machine, instances, instance, service }, handleCreateOrUpdatedInstance);
} }
onChange (machine) { onChange (machine) {
@ -178,7 +545,7 @@ module.exports = class Watcher {
return; return;
} }
console.log('-> `change` event received', util.inspect(machine)); console.log('-> `change` event received', Util.inspect(machine));
const { id, tags = {} } = machine; const { id, tags = {} } = machine;
@ -190,7 +557,9 @@ module.exports = class Watcher {
// assert that it's a docker-compose project // assert that it's a docker-compose project
const isCompose = [DEPLOYMENT_GROUP, SERVICE, HASH].every( const isCompose = [DEPLOYMENT_GROUP, SERVICE, HASH].every(
(name) => { return tags[name]; } (name) => {
return tags[name];
}
); );
if (!isCompose) { if (!isCompose) {
@ -201,56 +570,78 @@ module.exports = class Watcher {
const deploymentGroupName = tags[DEPLOYMENT_GROUP]; const deploymentGroupName = tags[DEPLOYMENT_GROUP];
const serviceName = tags[SERVICE]; const serviceName = tags[SERVICE];
const handleError = (next) => { const getInstancesAndVersion = ({
return (err, item) => { service,
deploymentGroup
}, cb) => {
this.getInstances(service, (err, instances) => {
if (err) { if (err) {
console.error(err); return cb(err);
return;
} }
next(item); this.getVersion(deploymentGroup, (err, version) => {
}; if (err) {
}; return cb(err);
}
const getInstances = (service, cb) => { this.resolveChange({
this.getInstances(service, handleError((instances) => { deploymentGroup,
return this.resolveChanges({ version,
machine, service,
service, instances,
instances machine
}, cb); }, cb);
}));
};
// assert that service exists
const assertService = (deploymentGroupId) => {
this.pushToQueue({ serviceName, deploymentGroupId }, () => {
return new Promise((resolve) => {
this.getService({ serviceName, deploymentGroupId }, handleError((service) => {
if (!service) {
console.error(`Service "${serviceName}" form DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`);
return;
}
getInstances(service, resolve);
}));
}); });
}); });
}; };
// assert that project managed by this portal // assert that service exists
const assertDeploymentGroup = () => { const assertService = (deploymentGroup, cb) => {
this.getDeploymentGroupId(deploymentGroupName, handleError((deploymentGroupId) => { this.getService({
if (!deploymentGroupId) { serviceName,
console.error(`DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`); deploymentGroupId: deploymentGroup.id
return; }, (err, service) => {
if (err) {
return cb(err);
} }
assertService(deploymentGroupId); if (!service) {
})); console.error(`Service "${serviceName}" form DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`);
return cb();
}
getInstancesAndVersion({
service,
deploymentGroup
}, cb);
});
}; };
assertDeploymentGroup(); // assert that project managed by this portal
// also, lock into `deploymentGroupId` queue
this.getDeploymentGroup(deploymentGroupName, (err, deploymentGroup) => {
if (err) {
console.error(err);
return;
}
if (!deploymentGroup) {
console.error(`DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`);
return;
}
this.pushToQueue(deploymentGroup.id, () => {
return new Promise((resolve) => {
assertService(deploymentGroup, (err) => {
if (err) {
console.error(err);
}
resolve();
});
});
});
});
} }
}; };

View File

@ -37,6 +37,9 @@
"graphi": "^2.2.1", "graphi": "^2.2.1",
"hoek": "^4.1.1", "hoek": "^4.1.1",
"joyent-cp-gql-schema": "^1.0.4", "joyent-cp-gql-schema": "^1.0.4",
"lodash.find": "^4.6.0",
"lodash.flatten": "^4.4.0",
"lodash.get": "^4.4.2",
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"param-case": "^2.1.1", "param-case": "^2.1.1",
"penseur": "^7.12.3", "penseur": "^7.12.3",

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,8 @@ const tasks = [
task: [ task: [
{ {
title: 'Branch', title: 'Branch',
description: 'Checks if the current branch is `master`. To ignore use the `--any-branch` flag', description:
'Checks if the current branch is `master`. To ignore use the `--any-branch` flag',
filter: () => !argv['any-branch'], filter: () => !argv['any-branch'],
task: async () => { task: async () => {
const branch = await execa.stdout('git', [ const branch = await execa.stdout('git', [
@ -64,7 +65,8 @@ const tasks = [
}, },
{ {
title: 'Working tree', title: 'Working tree',
description: 'Checks if working tree is clean. To ignore use the `--force` flag', description:
'Checks if working tree is clean. To ignore use the `--force` flag',
filter: () => !argv.force, filter: () => !argv.force,
task: async () => { task: async () => {
const status = await execa.stdout('git', ['status', '--porcelain']); const status = await execa.stdout('git', ['status', '--porcelain']);
@ -105,7 +107,8 @@ const tasks = [
}, },
{ {
title: 'Publish', title: 'Publish',
description: 'Publish updated packages, based on the changes from last tag', description:
'Publish updated packages, based on the changes from last tag',
task: async ({ prefix }) => { task: async ({ prefix }) => {
const { publish } = await inquirer.prompt([ const { publish } = await inquirer.prompt([
{ {
@ -176,7 +179,11 @@ const tasks = [
name: 'release', name: 'release',
type: 'confirm', type: 'confirm',
default: false, default: false,
message: `${prefix}No lerna publish detected. Are you sure you want to release? \n ${prefix}${chalk.dim(`(${chalk.yellow(figures.warning)} this can have negative effects on future lerna publishes since it detects changes based on tags)`)}` message: `${prefix}No lerna publish detected. Are you sure you want to release? \n ${prefix}${chalk.dim(
`(${chalk.yellow(
figures.warning
)} this can have negative effects on future lerna publishes since it detects changes based on tags)`
)}`
} }
]) ])
}, },
@ -348,16 +355,20 @@ const tasks = [
const tagBody = `${EOL}${lastCommits}`; const tagBody = `${EOL}${lastCommits}`;
console.log( console.log(
`${prefix}${chalk.yellow('Tag Name: ')}\n${prefix}${prefix}${chalk.dim(tagName)}` `${prefix}${chalk.yellow(
'Tag Name: '
)}\n${prefix}${prefix}${chalk.dim(tagName)}`
); );
console.log(`${prefix}${chalk.yellow('Tag Description: ')}`); console.log(`${prefix}${chalk.yellow('Tag Description: ')}`);
console.log( console.log(
`${chalk.dim(lastCommits `${chalk.dim(
lastCommits
.split(/\n/) .split(/\n/)
.map(line => `${prefix}${prefix}${line}`) .map(line => `${prefix}${prefix}${line}`)
.join('\n'))}` .join('\n')
)}`
); );
const { createTag } = await inquirer.prompt([ const { createTag } = await inquirer.prompt([
@ -393,7 +404,9 @@ const tasks = [
{ {
name: 'pushTag', name: 'pushTag',
type: 'confirm', type: 'confirm',
message: `${prefix}Should ${chalk.yellow(tagName)} be pushed to origin?` message: `${prefix}Should ${chalk.yellow(
tagName
)} be pushed to origin?`
} }
]); ]);

View File

@ -2167,6 +2167,13 @@ consulite@1.6.x:
or-promise "1.x.x" or-promise "1.x.x"
wreck "10.x.x" wreck "10.x.x"
consulite@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/consulite/-/consulite-2.0.0.tgz#4d35228d24f70a8fb01fdb8ed921d4e54d8bbb16"
dependencies:
or-promise "1.x.x"
wreck "12.x.x"
contains-path@^0.1.0: contains-path@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"