balena-supervisor/src/compose.coffee

89 lines
3.0 KiB
CoffeeScript

Promise = require 'bluebird'
YAML = require 'yamljs'
_ = require 'lodash'
dockerUtils = require './docker-utils'
{ docker } = dockerUtils
fs = Promise.promisifyAll(require('fs'))
{ spawn, execAsync } = Promise.promisifyAll(require('child_process'))
mkdirp = Promise.promisify(require('mkdirp'))
path = require 'path'
utils = require './utils'
config = require './config'
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', 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
resolve()
child.stdout.on 'data', (data) ->
reportStatus(status: '' + data)
child.stderr.on 'data', (data) ->
reportStatus(status: '' + data)
writeComposeFile = (composeSpec, dstPath) ->
mkdirp(path.dirname(dstPath))
.then ->
YAML.stringify(composeSpec)
.then (yml) ->
fs.writeFileAsync(dstPath, yml)
.then ->
execAsync('sync')
# Runs docker-compose up using the compose YAML at "path".
# Reports status and errors in JSON to the onStatus function.
# 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(composePathSrc(appId))
.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.pullAndProtectImage(service.image, reportStatus)
.then ->
utils.validateKeys(service, utils.validComposeOptions)
.then ->
services[serviceName].volumes = utils.defaultBinds(composeDataPath(appId, serviceName))
.then ->
writeComposeFile(composeSpec, composePathDst(appId))
.then ->
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 = (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