From 4278b6baf1b9f503623900d6e14894d8c24ac778 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Fri, 17 Jun 2016 06:06:42 +0000 Subject: [PATCH] Add endpoints for docker-compose up and down --- CHANGELOG.md | 2 ++ Dockerfile.amd64 | 5 ++++ Dockerfile.armv7hf | 12 ++++++++ package.json | 3 +- src/api.coffee | 35 ++++++++++++++++++++++ src/application.coffee | 3 ++ src/compose.coffee | 65 +++++++++++++++++++++++++++++++++++++++++ src/docker-utils.coffee | 7 +++++ 8 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/compose.coffee diff --git a/CHANGELOG.md b/CHANGELOG.md index 1637439c..1ed5a45d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* Add endpoints for docker-compose up and down [Pablo] + # v1.11.6 * Fixed deltas for older docker daemon versions [petrosagg] diff --git a/Dockerfile.amd64 b/Dockerfile.amd64 index dd1206ac..d29691a5 100644 --- a/Dockerfile.amd64 +++ b/Dockerfile.amd64 @@ -7,12 +7,17 @@ RUN apt-get -q update \ && apt-get install -qqy \ btrfs-tools \ ca-certificates \ + curl \ rsync \ supervisor \ --no-install-recommends \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ +ENV DOCKER_COMPOSE_VERSION 1.8.0dev +ENV DOCKER_COMPOSE_SHA256 9db33b03d9d02ea03aef7806f0272205a7c59b1a66599d559922db0eafaeb271 + + # Copy supervisord configuration files COPY config/supervisor/ /etc/supervisor/ diff --git a/Dockerfile.armv7hf b/Dockerfile.armv7hf index ea693eeb..44cb1808 100644 --- a/Dockerfile.armv7hf +++ b/Dockerfile.armv7hf @@ -2,17 +2,29 @@ FROM resin/armv7hf-node:0.10.40-slim COPY 01_nodoc /etc/dpkg/dpkg.cfg.d/ +ENV DOCKER_COMPOSE_VERSION 1.8.0dev +ENV DOCKER_COMPOSE_SHA256 0f9e0bdedddc9188415e828985289d07e22e42244c758bd0e4579e7a5bfb881d + # Supervisor apt dependencies RUN apt-get -q update \ && apt-get install -qqy \ btrfs-tools \ ca-certificates \ + curl \ rsync \ supervisor \ --no-install-recommends \ + && curl -sLO http://resin-packages.s3.amazonaws.com/docker-compose/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}.tar.gz \ + && echo $DOCKER_COMPOSE_SHA256 docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}.tar.gz > docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}.tar.gz.sha256 \ + && sha256sum -c docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}.tar.gz.sha256 \ + && tar xzf docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}.tar.gz \ + && mv docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}/docker-compose-linux-armhf /usr/bin/docker-compose \ + && rm -rf docker-compose-linux-armhf-${DOCKER_COMPOSE_VERSION}* \ + && apt-get purge -qqy curl \ && apt-get clean \ && rm -rf /var/lib/apt/lists/ + # Copy supervisord configuration files COPY config/supervisor/ /etc/supervisor/ diff --git a/package.json b/package.json index 4596c2a7..791ee4a8 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "resin-register-device": "^2.0.0", "rwlock": "^5.0.0", "sqlite3": "3.0.9", - "typed-error": "~0.1.0" + "typed-error": "~0.1.0", + "yamljs": "^0.2.7" }, "engines": { "node": "0.10.22" diff --git a/src/api.coffee b/src/api.coffee index 69cbd461..3aab734e 100644 --- a/src/api.coffee +++ b/src/api.coffee @@ -9,6 +9,7 @@ config = require './config' device = require './device' dockerUtils = require './docker-utils' _ = require 'lodash' +compose = require './compose' module.exports = (application) -> api = express() @@ -175,6 +176,40 @@ module.exports = (application) -> unparsedRouter.delete '/v1/containers/:id', dockerUtils.deleteContainer unparsedRouter.get '/v1/containers', dockerUtils.listContainers + unparsedRouter.post '/v1/apps/:appId/compose/up', (req, res) -> + appId = req.params.appId + onStatus = (status) -> + status = JSON.stringify(status) if _.isObject(status) + res.write(status) + knex('app').select().where({ appId }) + .then ([ app ]) -> + return res.status(400).send('App not found') if !app? + res.status(200) + compose.up(application.composePath(appId), onStatus) + .catch (err) -> + console.log('Error on compose up:', err, err.stack) + .finally -> + res.end() + .catch (err) -> + res.status(503).send(err?.message or err or 'Unknown error') + + unparsedRouter.post '/v1/apps/:appId/compose/down', (req, res) -> + appId = req.params.appId + onStatus = (status) -> + status = JSON.stringify(status) if _.isObject(status) + res.write(status) + knex('app').select().where({ appId }) + .then ([ app ]) -> + return res.status(400).send('App not found') if !app? + res.status(200) + compose.down(application.composePath(appId), onStatus) + .catch (err) -> + console.log('Error on compose down:', err, err.stack) + .finally -> + res.end() + .catch (err) -> + res.status(503).send(err?.message or err or 'Unknown error') + api.use(unparsedRouter) api.use(parsedRouter) diff --git a/src/application.coffee b/src/application.coffee index 40816822..51198840 100644 --- a/src/application.coffee +++ b/src/application.coffee @@ -311,6 +311,9 @@ killmePath = (app) -> appId = app.appId ? app return "/mnt/root#{config.dataPath}/#{appId}/resin-kill-me" +application.composePath = (appId) -> + return "/mnt/root#{config.dataPath}/#{appId}/docker-compose.yml" + # At boot, all apps should be unlocked *before* start to prevent a deadlock application.unlockAndStart = unlockAndStart = (app) -> lockFile.unlockAsync(lockPath(app)) diff --git a/src/compose.coffee b/src/compose.coffee new file mode 100644 index 00000000..c0edc8a5 --- /dev/null +++ b/src/compose.coffee @@ -0,0 +1,65 @@ +Promise = require 'bluebird' +YAML = require 'yamljs' +_ = require 'lodash' +dockerUtils = require './docker-utils' +{ docker } = dockerUtils +fs = Promise.promisifyAll(require('fs')) +spawn = require('child_process').spawn + +# Runs docker-compose up using the compose YAML at "path". +# Reports status and errors in JSON to the onStatus function. +exports.up = (path, onStatus) -> + onStatus = console.log.bind(console) if !onStatus? + reportStatus = (status) -> + try onStatus(status) + fs.readFileAsync(path) + .then (data) -> + YAML.parse(data.toString()) + .then (composeSpec) -> + if composeSpec.version? && composeSpec.version == '2' + services = composeSpec.services + else + services = composeSpec + throw new Error('No services found') if !_.isObject(services) + servicesArray = _.pairs(services) + Promise.each servicesArray, ([ serviceName, service ]) -> + throw new Error("Service #{serviceName} has no image specified.") if !service.image + docker.getImage(service.image).inspectAsync() + .catch -> + dockerUtils.pullImage(service.image, reportStatus) + .then -> + new Promise (resolve, reject) -> + child = spawn('docker-compose', ['-f', path, 'up', '-d'], stdio: 'pipe') + .on 'error', reject + .on 'exit', (code) -> + return reject(new Error("docker-compose exited with code #{code}")) if code isnt 0 + resolve() + child.stdout.on 'data', (data) -> + reportStatus(status: '' + data) + child.stderr.on 'data', (data) -> + reportStatus(status: '' + data) + .catch (err) -> + msg = err?.message or err + reportStatus(error: msg) + throw err + +# Runs docker-compose down using the compose YAML at "path". +# Reports status and errors in JSON to the onStatus function. +exports.down = (path, onStatus) -> + onStatus = console.log.bind(console) if !onStatus? + reportStatus = (status) -> + try onStatus(status) + new Promise (resolve, reject) -> + child = spawn('docker-compose', ['-f', path, 'down'], stdio: 'pipe') + .on 'error', reject + .on 'exit', (code) -> + return reject(new Error("docker-compose exited with code #{code}")) if code isnt 0 + resolve() + child.stdout.on 'data', (data) -> + reportStatus(status: '' + data) + child.stderr.on 'data', (data) -> + reportStatus(status: '' + data) + .catch (err) -> + msg = err?.message or err + reportStatus(error: msg) + throw err diff --git a/src/docker-utils.coffee b/src/docker-utils.coffee index b6c5c9e0..dd5b487e 100644 --- a/src/docker-utils.coffee +++ b/src/docker-utils.coffee @@ -200,6 +200,13 @@ do -> .catch (err) -> res.status(500).send(err?.message or err or 'Unknown error') + exports.pullImage = (image, onProgress) -> + repoTag = buildRepoTag(image) + Promise.using writeLockImages(), -> + knex('image').insert({ repoTag }) + .then -> + dockerProgress.pull(repoTag, onProgress) + exports.loadImage = (req, res) -> Promise.using writeLockImages(), -> docker.listImagesAsync()