diff --git a/packages/cp-frontend/.eslintrc b/packages/cp-frontend/.eslintrc index 6f477689..a847796a 100644 --- a/packages/cp-frontend/.eslintrc +++ b/packages/cp-frontend/.eslintrc @@ -5,6 +5,7 @@ "new-cap": 0, // temp "no-undef": 1, - "no-debugger": 1 + "no-debugger": 1, + "no-negated-condition": 0 } } diff --git a/packages/cp-frontend/package.json b/packages/cp-frontend/package.json index ffad4fdf..fd892a5f 100644 --- a/packages/cp-frontend/package.json +++ b/packages/cp-frontend/package.json @@ -53,7 +53,6 @@ "unitcalc": "^1.0.8" }, "devDependencies": { - "apr-find": "^1.0.5", "apr-for-each": "^1.0.6", "apr-main": "^1.0.7", "babel-plugin-inline-react-svg": "^0.4.0", diff --git a/packages/cp-frontend/public/index.html b/packages/cp-frontend/public/index.html index 3c722c45..de211200 100644 --- a/packages/cp-frontend/public/index.html +++ b/packages/cp-frontend/public/index.html @@ -14,6 +14,15 @@ border: solid 1px #d8d8d8; margin-bottom: 8px; } + + html, body, #root { + height: 100%; + } + + #root { + display: flex; + flex-flow: column; + } diff --git a/packages/cp-frontend/scripts/postinstall.js b/packages/cp-frontend/scripts/postinstall.js index 010ecb23..c2e0f68f 100644 --- a/packages/cp-frontend/scripts/postinstall.js +++ b/packages/cp-frontend/scripts/postinstall.js @@ -1,7 +1,6 @@ const { readFile, writeFile, exists } = require('mz/fs'); const main = require('apr-main'); const forEach = require('apr-for-each'); -const find = require('apr-find'); const path = require('path'); const ROOT = path.join(__dirname, '../node_modules/react-scripts/config'); diff --git a/packages/cp-frontend/src/app.js b/packages/cp-frontend/src/app.js index ad8b0ca7..1fcf0086 100644 --- a/packages/cp-frontend/src/app.js +++ b/packages/cp-frontend/src/app.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import { Article } from 'normalized-styled-components'; import { ThemeProvider, injectGlobal } from 'styled-components'; import { theme, global } from 'joyent-ui-toolkit'; import { ApolloProvider } from 'react-apollo'; @@ -19,9 +18,7 @@ class App extends Component { return ( -
- {Router} -
+ {Router}
); diff --git a/packages/cp-frontend/src/components/deployment-groups/empty.js b/packages/cp-frontend/src/components/deployment-groups/empty.js deleted file mode 100644 index 36f6cb9b..00000000 --- a/packages/cp-frontend/src/components/deployment-groups/empty.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -import { Col, Row } from 'react-styled-flexboxgrid'; - -export default () => - - -

you don't have any deployment groups

- -
; diff --git a/packages/cp-frontend/src/components/deployment-groups/index.js b/packages/cp-frontend/src/components/deployment-groups/index.js index d7d458ff..ad11539a 100644 --- a/packages/cp-frontend/src/components/deployment-groups/index.js +++ b/packages/cp-frontend/src/components/deployment-groups/index.js @@ -1 +1,2 @@ -export { default as EmptyDeployementGroups } from './empty'; +export { default as DeploymentGroupsLoading } from './loading'; +export { default as CreateDeploymentGroup } from './create'; diff --git a/packages/cp-frontend/src/components/deployment-groups/loading.js b/packages/cp-frontend/src/components/deployment-groups/loading.js new file mode 100644 index 00000000..057a1528 --- /dev/null +++ b/packages/cp-frontend/src/components/deployment-groups/loading.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Col, Row } from 'react-styled-flexboxgrid'; +import { Dots2 } from 'styled-text-spinners'; + +const LoadingRow = Row.extend` + flex: 1 1 auto; +`; + +export default () => + + + + + ; diff --git a/packages/cp-frontend/src/components/layout/container.js b/packages/cp-frontend/src/components/layout/container.js index 587b8762..0b15643b 100644 --- a/packages/cp-frontend/src/components/layout/container.js +++ b/packages/cp-frontend/src/components/layout/container.js @@ -1,3 +1,13 @@ import { Grid } from 'react-styled-flexboxgrid'; +import remcalc from 'remcalc'; +import { isNot } from 'styled-is'; -export default Grid; +export default Grid.extend` + padding-top: ${remcalc(19)}; + + ${isNot('plain')` + flex: 1 1 auto; + display: flex; + flex-flow: column; + `}; +`; diff --git a/packages/cp-frontend/src/components/navigation/breadcrumb.js b/packages/cp-frontend/src/components/navigation/breadcrumb.js index e4fac875..86e8baa2 100644 --- a/packages/cp-frontend/src/components/navigation/breadcrumb.js +++ b/packages/cp-frontend/src/components/navigation/breadcrumb.js @@ -19,7 +19,6 @@ const BreadcrumbLink = styled(Link)` const BreadcrumbContainer = styled.div` border-bottom: solid ${remcalc(1)} ${props => props.theme.grey}; - margin-bottom: ${remcalc(19)}; `; const getBreadcrumbItems = (...links) => diff --git a/packages/cp-frontend/src/components/navigation/menu.js b/packages/cp-frontend/src/components/navigation/menu.js index e6ac4925..3b3e58b2 100644 --- a/packages/cp-frontend/src/components/navigation/menu.js +++ b/packages/cp-frontend/src/components/navigation/menu.js @@ -22,7 +22,7 @@ const getMenuItems = (...links) => ); const Menu = ({ links = [] }) => - + {getMenuItems(...links)} diff --git a/packages/cp-frontend/src/containers/deployment-groups/create.js b/packages/cp-frontend/src/containers/deployment-groups/create.js index 06084b1f..8cb38d1b 100644 --- a/packages/cp-frontend/src/containers/deployment-groups/create.js +++ b/packages/cp-frontend/src/containers/deployment-groups/create.js @@ -8,7 +8,7 @@ import paramCase from 'param-case'; import DeploymentGroupBySlug from '@graphql/DeploymentGroupBySlug.gql'; import DeploymentGroupCreateMutation from '@graphql/DeploymentGroupCreate.gql'; import DeploymentGroupProvisionMutation from '@graphql/DeploymentGroupProvision.gql'; -import DeploymentGroupConfigMutation from '@graphql/DeploymentGroupConfig.gql'; +import DeploymentGroupConfigQuery from '@graphql/DeploymentGroupConfig.gql'; import { client } from '@state/store'; import { LayoutContainer } from '@components/layout'; @@ -262,7 +262,7 @@ export default compose( provisionManifest: variables => mutate({ variables }) }) }), - graphql(DeploymentGroupConfigMutation, { + graphql(DeploymentGroupConfigQuery, { props: ({ mutate }) => ({ config: variables => mutate({ variables }) }) diff --git a/packages/cp-frontend/src/containers/deployment-groups/import.js b/packages/cp-frontend/src/containers/deployment-groups/import.js new file mode 100644 index 00000000..c36ec1a6 --- /dev/null +++ b/packages/cp-frontend/src/containers/deployment-groups/import.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import { compose, graphql } from 'react-apollo'; +import intercept from 'apr-intercept'; + +import DeploymentGroupImportMutation from '@graphql/DeploymentGroupImport.gql'; + +import { LayoutContainer } from '@components/layout'; +import { DeploymentGroupsLoading } from '@components/deployment-groups'; +import { Dots2 } from 'styled-text-spinners'; +import { H2 } from 'joyent-ui-toolkit'; + +class DeploymentGroupImport extends Component { + constructor() { + super(); + + this.state = { + loading: true, + error: false + }; + + setTimeout(this.importDeploymentGroup, 16); + } + + importDeploymentGroup = async () => { + const { importDeploymentGroup, match, history } = this.props; + const { slug } = match.params; + + const [error] = await intercept( + importDeploymentGroup({ + slug + }) + ); + + if (error) { + return this.setState({ loading: false, error }); + } + + history.push(`/deployment-groups/${slug}`); + }; + + render() { + const { loading, error } = this.state; + + return ( + +

Importing deployment group

+ {loading && } + {error && {error.toString()}} +
+ ); + } +} + +export default graphql(DeploymentGroupImportMutation, { + props: ({ mutate }) => ({ + importDeploymentGroup: variables => mutate({ variables }) + }) +})(DeploymentGroupImport); diff --git a/packages/cp-frontend/src/containers/deployment-groups/index.js b/packages/cp-frontend/src/containers/deployment-groups/index.js index 6b7bbecd..6f8944a3 100644 --- a/packages/cp-frontend/src/containers/deployment-groups/index.js +++ b/packages/cp-frontend/src/containers/deployment-groups/index.js @@ -1,2 +1,3 @@ export { default as DeploymentGroupList } from './list'; export { default as DeploymentGroupCreate } from './create'; +export { default as DeploymentGroupImport } from './import'; diff --git a/packages/cp-frontend/src/containers/deployment-groups/list.js b/packages/cp-frontend/src/containers/deployment-groups/list.js index e7c1ba8e..204ccedc 100644 --- a/packages/cp-frontend/src/containers/deployment-groups/list.js +++ b/packages/cp-frontend/src/containers/deployment-groups/list.js @@ -1,73 +1,133 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { graphql } from 'react-apollo'; +import { compose, graphql } from 'react-apollo'; import { Link } from 'react-router-dom'; -import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql'; +import styled from 'styled-components'; import { Col, Row } from 'react-styled-flexboxgrid'; +import forceArray from 'force-array'; +import remcalc from 'remcalc'; import { LayoutContainer } from '@components/layout'; import { Loader, ErrorMessage } from '@components/messaging'; -import { EmptyDeployementGroups } from '@components/deployment-groups'; -import { Button } from 'joyent-ui-toolkit'; +import { DeploymentGroupsLoading } from '@components/deployment-groups'; +import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql'; +import DeploymentGroupsImportableQuery from '@graphql/DeploymentGroupsImportable.gql'; +import { Button, H2, H3, Small } from 'joyent-ui-toolkit'; + +const Title = H2.extend` + margin-top: ${remcalc(2)}; +`; + +const DGsRows = Row.extend` + margin-top: ${remcalc(-7)}; +`; + +const Box = Col.withComponent(Link).extend` + text-decoration: none; + color: ${props => props.theme.secondary}; + background-color: ${props => props.theme.white}; + box-shadow: 0 ${remcalc(2)} 0 0 rgba(0, 0, 0, 0.05); + border: solid ${remcalc(1)} ${props => props.theme.grey}; + margin-top: ${remcalc(20)}; + margin-bottom: 0; + padding: ${remcalc(18)}; + min-height: ${remcalc(258)}; + display: flex; + flex-direction: column; + + &:last-child { + margin-bottom: ${remcalc(20)}; + } +`; + +const BoxCreate = Box.extend` + background-color: ${props => props.theme.disabled}; + flex-direction: column; + justify-content: center; + align-items: center; + align-content: center; + display: flex; + + &:hover { + background-color: ${props => props.theme.white}; + } +`; + +const Oval = styled.div` + border: solid ${remcalc(1)} ${props => props.theme.grey}; + border-radius: 50%; + + width: ${remcalc(48)}; + height: ${remcalc(48)}; + line-height: ${remcalc(48)}; + font-size: ${remcalc(24)}; + text-align: center; + + margin-bottom: ${remcalc(20)}; +`; + +const CreateTitle = Small.extend` + font-weight: 600; + text-align: center; +`; + +const ServiceTitle = H3.extend` + margin-top: ${remcalc(10)}; + font-weight: 600; +`; const DeploymentGroupList = ({ location, deploymentGroups, + importable, loading, - error + error, + match }) => { - if (loading) { - return ( - - - - ); - } else if (error) { - return ( - + const _loading = !loading ? null : ; + + // todo improve this error message style according to new designs + const _error = !error + ? null + : - - ); - } + ; - let emptyDeployementGroups = null; - let deploymentGroupList = null; + const groups = forceArray(deploymentGroups).map(({ slug, name }) => + + + {name} + + + ); - if (deploymentGroups.length) { - const list = deploymentGroups.map((deploymentGroup, index) => { - return ( -

- - {deploymentGroup.name} - -

- ); - }); - - deploymentGroupList = ( - - -
    - {list} -
- -
- ); - } else { - emptyDeployementGroups = ; - } + const create = [ + + + + + Create new deployment group + + + ].concat( + forceArray(importable).map(({ slug, name }) => + + + + {name} + + + ) + ); return ( - {emptyDeployementGroups} - - - - - - {deploymentGroupList} + Deployment groups + {_loading} + {_error} + + {groups} + {create} + ); }; @@ -81,15 +141,20 @@ DeploymentGroupList.propTypes = { ) }; -const DeploymentGroupListWithData = graphql(DeploymentGroupsQuery, { - options: { - pollInterval: 1000 - }, - props: ({ data: { deploymentGroups, loading, error } }) => ({ - deploymentGroups, - loading, - error +export default compose( + graphql(DeploymentGroupsQuery, { + options: { + pollInterval: 1000 + }, + props: ({ data: { deploymentGroups, loading, error } }) => ({ + deploymentGroups, + loading, + error + }) + }), + graphql(DeploymentGroupsImportableQuery, { + props: ({ data: { importableDeploymentGroups } }) => ({ + importable: importableDeploymentGroups + }) }) -})(DeploymentGroupList); - -export default DeploymentGroupListWithData; +)(DeploymentGroupList); diff --git a/packages/cp-frontend/src/containers/services/menu.js b/packages/cp-frontend/src/containers/services/menu.js index b708b376..8054d098 100644 --- a/packages/cp-frontend/src/containers/services/menu.js +++ b/packages/cp-frontend/src/containers/services/menu.js @@ -65,7 +65,7 @@ const ServicesMenu = ({ location, history: { push } }) => { }; return ( - +

Services

diff --git a/packages/cp-frontend/src/graphql/DeploymentGroupConfig.gql b/packages/cp-frontend/src/graphql/DeploymentGroupConfig.gql index 48ca2c80..14ea3794 100644 --- a/packages/cp-frontend/src/graphql/DeploymentGroupConfig.gql +++ b/packages/cp-frontend/src/graphql/DeploymentGroupConfig.gql @@ -1,6 +1,6 @@ #import "./ServiceInfo.gql" -mutation config($deploymentGroupName: String!, $type: ManifestType!, $format: ManifestFormat!, $raw: String!) { +query config($deploymentGroupName: String!, $type: ManifestType!, $format: ManifestFormat!, $raw: String!) { config(deploymentGroupName: $deploymentGroupName, type: $type, format: $format, raw: $raw) { image ...ServiceInfo diff --git a/packages/cp-frontend/src/graphql/DeploymentGroupImport.gql b/packages/cp-frontend/src/graphql/DeploymentGroupImport.gql new file mode 100644 index 00000000..677aaf67 --- /dev/null +++ b/packages/cp-frontend/src/graphql/DeploymentGroupImport.gql @@ -0,0 +1,7 @@ +#import "./DeploymentGroupInfo.gql" + +mutation importDeploymentGroup($slug: String!) { + importDeploymentGroup(deploymentGroupSlug: $slug) { + ...DeploymentGroupInfo + } +} diff --git a/packages/cp-frontend/src/graphql/DeploymentGroupsImportable.gql b/packages/cp-frontend/src/graphql/DeploymentGroupsImportable.gql new file mode 100644 index 00000000..1a32e12c --- /dev/null +++ b/packages/cp-frontend/src/graphql/DeploymentGroupsImportable.gql @@ -0,0 +1,7 @@ +#import "./DeploymentGroupInfo.gql" + +query DeploymentGroupsImportable { + importableDeploymentGroups { + ...DeploymentGroupInfo + } +} diff --git a/packages/cp-frontend/src/router.js b/packages/cp-frontend/src/router.js index 8de4c156..57721826 100644 --- a/packages/cp-frontend/src/router.js +++ b/packages/cp-frontend/src/router.js @@ -1,11 +1,15 @@ import React from 'react'; import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; +import styled from 'styled-components'; import { Header, Breadcrumb, Menu } from '@containers/navigation'; +import { ServiceScale, ServiceDelete } from '@containers/service'; +import { InstanceList } from '@containers/instances'; import { DeploymentGroupList, - DeploymentGroupCreate + DeploymentGroupCreate, + DeploymentGroupImport } from '@containers/deployment-groups'; import { @@ -14,9 +18,12 @@ import { ServicesMenu } from '@containers/services'; -import { ServiceScale, ServiceDelete } from '@containers/service'; - -import { InstanceList } from '@containers/instances'; +const Container = styled.div` + display: flex; + flex: 1 1 auto; + position: relative; + flex-flow: column; +`; const rootRedirect = p => ; @@ -33,7 +40,7 @@ const serviceRedirect = p => const Router = ( -
+ @@ -65,6 +72,11 @@ const Router = ( exact component={DeploymentGroupCreate} /> + -
+
); diff --git a/packages/cp-gql-schema/schema.gql b/packages/cp-gql-schema/schema.gql index 59cb1d47..806e7477 100644 --- a/packages/cp-gql-schema/schema.gql +++ b/packages/cp-gql-schema/schema.gql @@ -19,7 +19,7 @@ type User { type DeploymentGroup { id: ID! name: String! - slug: String! + slug: String! services(name: String, slug: String, parentId: ID): [Service] version: Version history: [Version] @@ -186,6 +186,9 @@ type Query { instance(id: ID!): Instance datacenter(id: ID, region: String): Datacenter datacenters: [Datacenter] + + config(deploymentGroupName: String!, type: ManifestType!, format: ManifestFormat!, raw: String!): [Service] + importableDeploymentGroups: [DeploymentGroup] } type Mutation { @@ -194,7 +197,6 @@ type Mutation { provisionManifest(deploymentGroupId: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!): Version scale(serviceId: ID!, replicas: Int!): Version - config(deploymentGroupName: String!, type: ManifestType!, format: ManifestFormat!, raw: String!): [Service] stopServices(ids: [ID]!): [Service] startServices(ids: [ID]!): [Service] @@ -205,5 +207,5 @@ type Mutation { startInstances(ids: [ID]!): [Instance] restartInstances(ids: [ID]!): [Instance] - # reprovision() ??? + importDeploymentGroup(deploymentGroupSlug: String!): DeploymentGroup } diff --git a/packages/portal-api/lib/index.js b/packages/portal-api/lib/index.js index d0e0c13c..f6337904 100644 --- a/packages/portal-api/lib/index.js +++ b/packages/portal-api/lib/index.js @@ -19,11 +19,16 @@ module.exports = function (server, options, next) { } const data = new PortalData(options.data); - - const watch = new PortalWatch(Object.assign(options.watch, { + const watcher = new PortalWatch(Object.assign(options.watch, { data })); + // watcher <-> watcher + // portal depends on watcher and vice-versa + // I'm sure there is a better way to organize this domains + // but this works for now + data.setWatcher(watcher); + data.on('error', (err) => { server.log(['error'], err); }); @@ -33,12 +38,10 @@ module.exports = function (server, options, next) { return next(err); } - server.bind(Object.assign(data, { - watch - })); + server.bind(data); Piloted.on('refresh', internals.refresh(data)); - watch.poll(); + watcher.poll(); server.register([ { diff --git a/packages/portal-api/lib/resolvers.js b/packages/portal-api/lib/resolvers.js index bdd376cc..7f1f47d0 100644 --- a/packages/portal-api/lib/resolvers.js +++ b/packages/portal-api/lib/resolvers.js @@ -25,7 +25,9 @@ module.exports = (data) => { 'metricData', 'package', 'datacenters', - 'instanceMetric' + 'instanceMetric', + 'config', + 'importableDeploymentGroups' ]; const mutations = [ @@ -33,14 +35,14 @@ module.exports = (data) => { 'updateDeploymentGroup', 'provisionManifest', 'scale', - 'config', 'stopServices', 'startServices', 'restartServices', 'deleteServices', 'stopInstances', 'startInstances', - 'restartInstances' + 'restartInstances', + 'importDeploymentGroup' ]; const resolvers = {}; diff --git a/packages/portal-data/lib/index.js b/packages/portal-data/lib/index.js index 30df2506..1609047a 100644 --- a/packages/portal-data/lib/index.js +++ b/packages/portal-data/lib/index.js @@ -3,12 +3,23 @@ const ParamCase = require('param-case'); const EventEmitter = require('events'); const DockerClient = require('docker-compose-client'); +const { DEPLOYMENT_GROUP, SERVICE, HASH } = require('portal-watch'); const Dockerode = require('dockerode'); const Hoek = require('hoek'); const Penseur = require('penseur'); const VAsync = require('vasync'); +const uniqBy = require('lodash.uniqby'); const Transform = require('./transform'); const Uuid = require('uuid/v4'); +const Util = require('util'); + + +const NON_IMPORTABLE_STATES = [ + 'EXITED', + 'DELETED', + 'STOPPED', + 'FAILED' +]; const internals = { defaults: { @@ -51,12 +62,17 @@ module.exports = class Data extends EventEmitter { this._db = new Penseur.Db(settings.name, settings.db); this._dockerCompose = new DockerClient(settings.dockerComposeHost); this._docker = new Dockerode(settings.docker); + this._watcher = null; this._dockerCompose.on('error', (err) => { this.emit('error', err); }); } + setWatcher (watcher) { + this._watcher = watcher; + } + connect (cb) { this._db.establish(internals.tables, cb); } @@ -284,10 +300,8 @@ module.exports = class Data extends EventEmitter { const deploymentGroup = deploymentGroups[0]; const getServices = (args) => { - console.log(args); args = args || {}; args.deploymentGroupId = deploymentGroup.id; - console.log(args); return new Promise((resolve, reject) => { this.getServices(args, resolveCb(resolve, reject)); @@ -1285,7 +1299,7 @@ module.exports = class Data extends EventEmitter { }); } - config ({deploymentGroupName = '', type = '', format = '', raw = '' }, cb) { + getConfig ({deploymentGroupName = '', type = '', format = '', raw = '' }, cb) { if (type.toUpperCase() !== 'COMPOSE') { return cb(new Error('"COMPOSE" is the only `type` supported')); } @@ -1330,4 +1344,133 @@ module.exports = class Data extends EventEmitter { }, [])); }); } + + getImportableDeploymentGroups (args, cb) { + if (!this._watcher) { + return cb(null, []); + } + + const machines = this._watcher.getContainers(); + + if (!Array.isArray(machines)) { + return cb(null, []); + } + + return cb( + null, + uniqBy( + machines + .filter(({ state }) => { return NON_IMPORTABLE_STATES.indexOf(state.toUpperCase()) < 0; }) + .filter(({ tags = {} }) => { return [DEPLOYMENT_GROUP, SERVICE, HASH].every((name) => { return tags[name]; }); } + ) + .map(({ tags = {} }) => { + return ({ + id: Uuid(), + name: tags[DEPLOYMENT_GROUP], + slug: ParamCase(tags[DEPLOYMENT_GROUP]) + }); + }), + 'slug' + ) + ); + } + + importDeploymentGroup ({ deploymentGroupSlug }, cb) { + console.log(`-> import requested for ${deploymentGroupSlug}`); + + if (!this._watcher) { + console.log('-> watcher not yet defined'); + return cb(null, null); + } + + const machines = this._watcher.getContainers(); + + if (!Array.isArray(machines)) { + console.log('-> no machines found'); + return cb(null, null); + } + + const containers = machines + .filter( + ({ tags = {} }) => { return tags[DEPLOYMENT_GROUP] && ParamCase(tags[DEPLOYMENT_GROUP]) === deploymentGroupSlug; } + ) + .filter( + ({ state }) => { return NON_IMPORTABLE_STATES.indexOf(state.toUpperCase()) < 0; } + ); + + if (!containers.length) { + console.log(`-> no containers found for ${deploymentGroupSlug}`); + return cb(null, null); + } + + const { tags = [] } = containers[0]; + + const services = containers.reduce((acc, { tags = [], id = '', state = '', name = '' }) => { + const hash = tags[HASH]; + const slug = ParamCase(tags[SERVICE]); + const attr = `${hash}-${slug}`; + + const instance = { + name: name, + machineId: id, + status: state.toUpperCase() + }; + + if (acc[attr]) { + acc[attr].instances.push(instance); + return acc; + } + + return Object.assign(acc, { + [attr]: { + hash, + name: tags[SERVICE], + slug, + instances: [instance] + } + }); + }, {}); + + const createService = (deploymentGroupId) => { + return (serviceId, next) => { + const service = services[serviceId]; + + console.log(`-> creating Service ${Util.inspect(service)}`); + + VAsync.forEachParallel({ + inputs: service.instances, + func: (instance, next) => { return this.createInstance(instance, next); } + }, (err, results) => { + if (err) { + return cb(err); + } + + console.log(`-> created Instances ${Util.inspect(results.successes)}`); + + this.createService(Object.assign(service, { + instances: results.successes, + deploymentGroupId + }), next); + }); + }; + }; + + const deploymentGroup = { + name: tags[DEPLOYMENT_GROUP], + slug: ParamCase(tags[DEPLOYMENT_GROUP]) + }; + + console.log(`-> creating DeploymentGroup ${Util.inspect(deploymentGroup)}`); + + this.createDeploymentGroup(deploymentGroup, (err, dg) => { + if (err) { + return cb(err); + } + + VAsync.forEachParallel({ + inputs: Object.keys(services), + func: createService(dg.id) + }, (err) => { return cb(err, dg); }); + }); + } }; diff --git a/packages/portal-data/package.json b/packages/portal-data/package.json index 5cb2291a..ae6cc67b 100644 --- a/packages/portal-data/package.json +++ b/packages/portal-data/package.json @@ -21,6 +21,8 @@ "hoek": "^4.1.1", "param-case": "^2.1.1", "penseur": "^7.12.3", + "lodash.uniqby": "^4.7.0", + "portal-watch": "^1.0.0", "uuid": "^3.1.0", "vasync": "^1.6.4", "yamljs": "^0.2.10" diff --git a/packages/portal-watch/lib/index.js b/packages/portal-watch/lib/index.js index 40976420..2b9cbdf0 100644 --- a/packages/portal-watch/lib/index.js +++ b/packages/portal-watch/lib/index.js @@ -9,7 +9,6 @@ const DEPLOYMENT_GROUP = 'docker:label:com.docker.compose.project'; const SERVICE = 'docker:label:com.docker.compose.service'; const HASH = 'docker:label:com.docker.compose.config-hash'; - module.exports = class Watcher { constructor (options) { options = options || {}; @@ -35,6 +34,10 @@ module.exports = class Watcher { this._tritonWatch.poll(); } + getContainers () { + return this._tritonWatch.getContainers(); + } + getDeploymentGroupId (name, cb) { this._data.getDeploymentGroup({ name }, (err, deploymentGroup) => { if (err) { @@ -45,8 +48,8 @@ module.exports = class Watcher { }); } - getService ({ serviceName, deploymentGroupId }, cb) { - this._data.getServices({ name: serviceName, deploymentGroupId }, (err, services) => { + getService ({ serviceName, serviceHash, deploymentGroupId }, cb) { + this._data.getServices({ name: serviceName, hash: serviceHash, deploymentGroupId }, (err, services) => { if (err) { return cb(err); } @@ -131,7 +134,7 @@ module.exports = class Watcher { console.log('-> `change` event received', util.inspect(machine)); - const { id, tags = [] } = machine; + const { id, tags = {} } = machine; // assert id existence if (!id) { @@ -175,7 +178,7 @@ module.exports = class Watcher { // assert that service exists const assertService = (deploymentGroupId) => { - this.getService({ serviceName, deploymentGroupId }, handleError((service) => { + this.getService({ serviceName, serviceHash: tags[HASH], deploymentGroupId }, handleError((service) => { if (!service) { console.error(`Service "${serviceName}" form DeploymentGroup "${deploymentGroupName}" for machine ${id} not found`); return; @@ -200,3 +203,7 @@ module.exports = class Watcher { assertDeploymentGroup(); } }; + +module.exports.DEPLOYMENT_GROUP = DEPLOYMENT_GROUP; +module.exports.SERVICE = SERVICE; +module.exports.HASH = HASH; diff --git a/packages/portal-watch/package.json b/packages/portal-watch/package.json index 04ac40a9..57a145c4 100644 --- a/packages/portal-watch/package.json +++ b/packages/portal-watch/package.json @@ -12,7 +12,7 @@ "test-ci": "echo 0 `# lab -c -r console -o stdout -r tap -o $CIRCLE_TEST_REPORTS/test/portal-watch.xml`" }, "dependencies": { - "triton-watch": "^1.0.1" + "triton-watch": "^1.1.0" }, "devDependencies": { "belly-button": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index f7d74af9..46a67bfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,8 +42,8 @@ resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.40.tgz#ac02de68e66c004a61b7cb16df8b1db3a254cca9" "@types/graphql@^0.9.0": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.1.tgz#b04ebe84bc997cc60dbea2ed4d0d4342c737f99d" + version "0.9.2" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.2.tgz#5e3a2919a998d0bd9bb86b4b23e9630425bff1b2" "@types/isomorphic-fetch@0.0.34": version "0.0.34" @@ -305,15 +305,6 @@ apr-filter@^1.0.5: apr-engine-sum "^1.0.3" apr-map "^1.0.5" -apr-find@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/apr-find/-/apr-find-1.0.5.tgz#e166abc66f53cfd08aadb3ecab38049faa378301" - dependencies: - apr-engine-each "^1.0.3" - apr-engine-sum "^1.0.3" - lodash.defaults "^4.2.0" - lodash.isarraylike "^4.2.0" - apr-for-each@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/apr-for-each/-/apr-for-each-1.0.6.tgz#3947bb25fdb7b79a7f02bfa925fdb79576098903" @@ -502,6 +493,10 @@ ast-types@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.0.tgz#c8721c8747ae4d5b29b929e99c5317b4e8745623" +ast-types@0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -511,8 +506,8 @@ async@^1.4.0, async@^1.5.0: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" async@^2.0.1, async@^2.1.2, async@^2.1.4: - version "2.4.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7" + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" dependencies: lodash "^4.14.0" @@ -896,10 +891,10 @@ babel-plugin-istanbul@^4.1.4: test-exclude "^4.1.1" babel-plugin-styled-components@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.1.4.tgz#b0e6d5bb01059bc7ab9118d3d686f6472ee8e91f" + version "1.1.5" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.1.5.tgz#ff2c8e0e2f3a0d3279e7454a7aaa2973749e714d" dependencies: - stylis "2.0.0" + stylis "^3.0.19" babel-plugin-syntax-async-functions@^6.8.0: version "6.13.0" @@ -1370,7 +1365,7 @@ babelrc-rollup@3.0.0: dependencies: resolve "^1.1.7" -babylon@^6.1.0, babylon@^6.10.0, babylon@^6.12.0, babylon@^6.13.0, babylon@^6.17.0, babylon@^6.17.2: +babylon@^6.1.0, babylon@^6.10.0, babylon@^6.12.0, babylon@^6.17.0, babylon@^6.17.2, babylon@^6.17.4: version "6.17.4" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a" @@ -1766,12 +1761,12 @@ camelcase@^4.0.0, camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30000693" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000693.tgz#8510e7a9ab04adcca23a5dcefa34df9d28c1ce20" + version "1.0.30000694" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000694.tgz#02009f4f82d2f0126e4c691b7cd5adb351935c01" caniuse-lite@^1.0.30000684: - version "1.0.30000693" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000693.tgz#c9c6298697c71fdf6cb13eefe8aa93926f2f8613" + version "1.0.30000694" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000694.tgz#1492dab7c10c608c9d37a723e6e3e7873e0ce94f" capture-stack-trace@^1.0.0: version "1.0.0" @@ -2027,8 +2022,8 @@ coleman-liau@^1.0.0: resolved "https://registry.yarnpkg.com/coleman-liau/-/coleman-liau-1.0.0.tgz#de1f39901e164f49eff2a6ec88f3a9dbbb6686c1" collapse-white-space@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.2.tgz#9c463fb9c6d190d2dcae21a356a01bcae9eeef6d" + version "1.0.3" + resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.3.tgz#4b906f670e5a963a87b76b0e1689643341b6023c" color-convert@^1.0.0: version "1.9.0" @@ -2081,8 +2076,8 @@ command-join@^2.0.0: resolved "https://registry.yarnpkg.com/command-join/-/command-join-2.0.0.tgz#52e8b984f4872d952ff1bdc8b98397d27c7144cf" commander@2, commander@^2.7.1, commander@^2.8.1, commander@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + version "2.10.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.10.0.tgz#e1f5d3245de246d1a5ca04702fa1ad1bd7e405fe" dependencies: graceful-readlink ">= 1.0.0" @@ -3352,8 +3347,8 @@ eslint-plugin-hapi@4.x.x: no-arrowception "1.x.x" eslint-plugin-import@^2.3.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.5.0.tgz#293b5ea7910a901a05a47ccdd7546e611725406c" + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.6.0.tgz#2a4bbad36a078e052a3c830ce3dfbd6b8a12c6e5" dependencies: builtin-modules "^1.1.1" contains-path "^0.1.0" @@ -3498,7 +3493,7 @@ esprima@^2.6.0, esprima@~2.7.1: version "2.7.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" -esprima@^3.1.1: +esprima@^3.1.1, esprima@~3.1.0: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -5009,14 +5004,14 @@ istanbul-lib-hook@^1.0.7: append-transform "^0.4.0" istanbul-lib-instrument@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.2.tgz#6014b03d3470fb77638d5802508c255c06312e56" + version "1.7.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.3.tgz#925b239163eabdd68cc4048f52c2fa4f899ecfa7" dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" babel-traverse "^6.18.0" babel-types "^6.18.0" - babylon "^6.13.0" + babylon "^6.17.4" istanbul-lib-coverage "^1.1.1" semver "^5.3.0" @@ -5196,8 +5191,8 @@ jest-snapshot@^20.0.3: pretty-format "^20.0.3" jest-styled-components@^3.0.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-3.1.3.tgz#eba50074b426e36fd2be99187dffa4b5569eab00" + version "3.1.5" + resolved "https://registry.yarnpkg.com/jest-styled-components/-/jest-styled-components-3.1.5.tgz#c1e4fc60e534e54b26c3b733b65b44afd4f2795e" dependencies: css "^2.2.1" @@ -5460,8 +5455,8 @@ known-css-properties@^0.2.0: resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.2.0.tgz#899c94be368e55b42d7db8d5be7d73a4a4a41454" lab@^14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/lab/-/lab-14.0.1.tgz#1a20100ecc692dbf91e849cd6b945b8e016a0527" + version "14.1.0" + resolved "https://registry.yarnpkg.com/lab/-/lab-14.1.0.tgz#2632b9a416d5f391d9a6f1d98d607b0d69f75629" dependencies: bossy "3.x.x" code "4.1.x" @@ -5934,8 +5929,8 @@ matcher@^0.1.1: escape-string-regexp "^1.0.4" mathml-tag-names@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.0.tgz#eee615112a2b127e70f558d69c9ebe14076503d7" + version "2.0.1" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz#8d41268168bf86d1102b98109e28e531e7a34578" max-safe-int@^1.0.0: version "1.0.0" @@ -6916,8 +6911,8 @@ podium@1.x.x: joi "10.x.x" polished@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/polished/-/polished-1.2.0.tgz#241cbbbd848d62ca8776eee618383d72e95830c5" + version "1.2.1" + resolved "https://registry.yarnpkg.com/polished/-/polished-1.2.1.tgz#83c18a85bf9d7023477cfc7049763b657d50f0f7" pos@0.4.2: version "0.4.2" @@ -7036,8 +7031,8 @@ pretty-ms@^2.0.0: plur "^1.0.0" primer-support@*: - version "4.0.0" - resolved "https://registry.yarnpkg.com/primer-support/-/primer-support-4.0.0.tgz#3dbbb37e4e0f2ed2ea6035e0b79dd0cb33bae85e" + version "4.0.4" + resolved "https://registry.yarnpkg.com/primer-support/-/primer-support-4.0.4.tgz#7f16ad577a61a960d0bccd72f2a6df506a14d69f" primer-utilities@^3.0.0: version "3.5.0" @@ -7526,7 +7521,7 @@ readline2@^1.0.1: is-fullwidth-code-point "^1.0.0" mute-stream "0.0.5" -recast@0.11.12, recast@^0.11.5: +recast@0.11.12: version "0.11.12" resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.12.tgz#a79e4d3f82d5d72a82ee177aeaa791e793bbe5d6" dependencies: @@ -7535,6 +7530,15 @@ recast@0.11.12, recast@^0.11.5: private "~0.1.5" source-map "~0.5.0" +recast@^0.11.5: + version "0.11.23" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3" + dependencies: + ast-types "0.9.6" + esprima "~3.1.0" + private "~0.1.5" + source-map "~0.5.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -8314,8 +8318,8 @@ spache@^1.1.0: resolved "https://registry.yarnpkg.com/spache/-/spache-1.1.0.tgz#8c68ba807630f0199429c2035c82ed96f5438cd5" spawn-wrap@^1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.3.6.tgz#ccec4a949d8ce7e2b1a35cf4671d683d2e76a1d1" + version "1.3.7" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.3.7.tgz#beb8bf4426d64b2b06871e0d7dee2643f1f8d1bc" dependencies: foreground-child "^1.5.6" mkdirp "^0.5.0" @@ -8714,10 +8718,6 @@ stylelint@^7.0.0, stylelint@^7.0.3, stylelint@^7.11.1: svg-tags "^1.0.0" table "^4.0.1" -stylis@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-2.0.0.tgz#6785a6546bd73478799a67d49d67086953b50ad5" - stylis@^3.0.19: version "3.1.9" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.1.9.tgz#638370451f980437f57c59e58d2e296be29fafb7" @@ -9128,9 +9128,9 @@ trim@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" -triton-watch@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/triton-watch/-/triton-watch-1.0.1.tgz#b1087f6a57383f1e83d0a308e65110ca9a2a38d0" +triton-watch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/triton-watch/-/triton-watch-1.1.0.tgz#d2b47fbf6a45174198c196152bb86c696a923c35" dependencies: triton "5.2.x"