diff --git a/package.json b/package.json index 392c46b3..b16273d5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lodash": "^3.0.0", "log-timestamp": "^0.1.2", "mixpanel": "0.0.20", + "mkdirp": "^0.5.1", "network-checker": "~0.0.5", "pinejs-client": "^1.7.1", "pubnub": "^3.7.13", diff --git a/src/api.coffee b/src/api.coffee index 63bd33ed..f118dce2 100644 --- a/src/api.coffee +++ b/src/api.coffee @@ -184,7 +184,7 @@ module.exports = (application) -> utils.getKnexApp(appId) .then (app) -> res.status(200) - compose.up(application.composePath(appId), onStatus) + compose.up(appId, onStatus) .catch (err) -> console.log('Error on compose up:', err, err.stack) .finally -> @@ -202,7 +202,7 @@ module.exports = (application) -> utils.getKnexApp(appId) .then (app) -> res.status(200) - compose.down(application.composePath(appId), onStatus) + compose.down(appId, onStatus) .catch (err) -> console.log('Error on compose down:', err, err.stack) .finally -> diff --git a/src/application.coffee b/src/application.coffee index cd481cd6..273e7fa8 100644 --- a/src/application.coffee +++ b/src/application.coffee @@ -309,9 +309,6 @@ 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 index 5ce45f85..7ec6ed2a 100644 --- a/src/compose.coffee +++ b/src/compose.coffee @@ -4,14 +4,23 @@ _ = require 'lodash' dockerUtils = require './docker-utils' { docker } = dockerUtils fs = Promise.promisifyAll(require('fs')) -spawn = require('child_process').spawn +{ spawn, execAsync } = Promise.promisifyAll(require('child_process')) +mkdirp = Promise.promisify(require('mkdirp')) +path = require 'path' +utils = require './utils' -runComposeCommand = (composeArgs, path, onStatus) -> - onStatus ?= console.log.bind(console) - reportStatus = (status) -> - try onStatus(status) +composePathSrc = (appId) -> + return "/mnt/root#{config.dataPath}/#{appId}/docker-compose.yml" + +composePathDst = (appId) -> + return "/mnt/root#{config.dataPath}/resin-supervisor/compose/#{appId}/docker-compose.yml" + +composeDataPath = (appId, serviceName) -> + return "compose/#{appId}/#{serviceName}" + +runComposeCommand = (composeArgs, appId, reportStatus) -> new Promise (resolve, reject) -> - child = spawn('docker-compose', ['-f', path].concat(composeArgs), stdio: 'pipe') + child = spawn('docker-compose', ['-f', composePathDst(appId)].concat(composeArgs), stdio: 'pipe') .on 'error', reject .on 'exit', (code) -> return reject(new Error("docker-compose exited with code #{code}")) if code isnt 0 @@ -20,18 +29,30 @@ runComposeCommand = (composeArgs, path, onStatus) -> reportStatus(status: '' + data) child.stderr.on 'data', (data) -> reportStatus(status: '' + data) - .catch (err) -> - msg = err?.message or err - reportStatus(error: msg) - throw err + +writeComposeFile = (composeSpec, dstPath) -> + mkdirp(path.dirname(dstPath)) + .then -> + YAML.stringify(composeSpec) + .then (yml) -> + fs.writeFileAsync(dstPath, yml) + .then -> + execAsync('sync') + +validateServiceOptions = (service) -> + Promise.try -> + options = _.keys(service) + _.each options, (option) -> + throw new Error("Using #{option} is not allowed.") if !_.includes(utils.validComposeOptions, option) # Runs docker-compose up using the compose YAML at "path". # Reports status and errors in JSON to the onStatus function. -exports.up = (path, onStatus) -> +# Copies the compose file from srcPath to dstPath adding default volumes +exports.up = (appId, onStatus) -> onStatus ?= console.log.bind(console) reportStatus = (status) -> try onStatus(status) - fs.readFileAsync(path) + fs.readFileAsync(composePathSrc(appId)) .then (data) -> YAML.parse(data.toString()) .then (composeSpec) -> @@ -46,9 +67,27 @@ exports.up = (path, onStatus) -> docker.getImage(service.image).inspectAsync() .catch -> dockerUtils.pullAndProtectImage(service.image, reportStatus) + .then -> + validateServiceOptions(service) + .then -> + services[serviceName].volumes = utils.defaultBinds(composeDataPath(appId, serviceName)) + .then -> + writeComposeFile(composeSpec, dstPath) .then -> - runComposeCommand(['up', '-d'], path, onStatus) + runComposeCommand(['up', '-d'], appId, reportStatus) + .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 = _.partial(runComposeCommand, 'down') +exports.down = (appId, onStatus) + onStatus ?= console.log.bind(console) + reportStatus = (status) -> + try onStatus(status) + runComposeCommand([ 'down' ], appId, reportStatus) + .catch (err) -> + msg = err?.message or err + reportStatus(error: msg) + throw err diff --git a/src/utils.coffee b/src/utils.coffee index 13ec0138..d61e7eb1 100644 --- a/src/utils.coffee +++ b/src/utils.coffee @@ -223,3 +223,47 @@ exports.defaultBinds = (dataPath) -> '/etc/resolv.conf:/etc/resolv.conf:rw' '/var/lib/connman:/host/var/lib/connman' ] + +exports.validComposeOptions = [ + 'command' + 'entrypoint' + 'env_file' + 'environment' + 'expose' + 'image' + 'labels' + 'ports' + 'stop_signal' + 'volumes' + 'user' + 'working_dir' + 'cap_add' + 'cap_drop' + 'devices' + 'dns' + 'dns_search' + 'tmpfs' + 'extra_hosts' + 'links' + 'net' + 'network_mode' + 'ulimits' + 'volumes_from' + 'cpu_shares' + 'cpu_quota' + 'cpuset' + 'domainname' + 'hostname' + 'mac_address' + 'mem_limit' + 'memswap_limit' + 'privileged' + 'tty' + 'read_only' + 'shm_size' + 'ipc' + 'restart' + 'security_opt' + 'networks' + 'pid' +]