balena-cli/lib/utils/docker.coffee
2017-05-11 14:14:43 +03:00

269 lines
7.5 KiB
CoffeeScript

# Functions to help actions which rely on using docker
# Use this function to seed an action's list of capitano options
# with the docker options. Using this interface means that
# all functions using docker will expose the same interface
#
# NOTE: Care MUST be taken when using the function, so as to
# not redefine/override options already provided.
exports.appendOptions = (opts) ->
opts.concat [
{
signature: 'docker'
parameter: 'docker'
description: 'Path to a local docker socket'
alias: 'P'
},
{
signature: 'dockerHost'
parameter: 'dockerHost'
description: 'The address of the host containing the docker daemon'
alias: 'h'
},
{
signature: 'dockerPort'
parameter: 'dockerPort'
description: 'The port on which the host docker daemon is listening'
alias: 'p'
},
{
signature: 'ca'
parameter: 'ca'
description: 'Docker host TLS certificate authority file'
},
{
signature: 'cert'
parameter: 'cert'
description: 'Docker host TLS certificate file'
},
{
signature: 'key'
parameter: 'key'
description: 'Docker host TLS key file'
},
{
signature: 'tag'
parameter: 'tag'
description: 'The alias to the generated image'
alias: 't'
},
{
signature: 'arg'
parameter: 'arg'
description: 'Set a build-time variable (eg. "-e \'ARG=value\'"). Can be specified multiple times.'
alias: 'e'
},
{
signature: 'nocache'
description: "Don't use docker layer caching when building"
boolean: true
},
]
exports.generateConnectOpts = generateConnectOpts = (opts) ->
connectOpts = {}
# Firsly need to decide between a local docker socket
# and a host available over a host:port combo
if opts.docker? and not opts.dockerHost?
# good, local docker socket
connectOpts.socketPath = opts.docker
else if opts.dockerHost? and not opts.docker?
# Good a host is provided, and local socket isn't
connectOpts.host = opts.dockerHost
connectOpts.port = opts.dockerPort || 2376
else if opts.docker? and opts.dockerHost?
# Both provided, no obvious way to continue
throw new Error("Both a local docker socket and docker host have been provided. Don't know how to continue.")
else
# None provided, assume default docker local socket
connectOpts.socketPath = '/var/run/docker.sock'
# Now need to check if the user wants to connect over TLS
# to the host
# If any are set...
if (opts.ca? or opts.cert? or opts.key?)
# but not all
if not (opts.ca? and opts.cert? and opts.key?)
throw new Error('You must provide a CA, certificate and key in order to use TLS')
connectOpts.ca = opts.ca
connectOpts.cert = opts.cert
connectOpts.key = opts.key
return connectOpts
exports.tarDirectory = tarDirectory = (dir) ->
Promise = require('bluebird')
tar = require('tar-stream')
klaw = require('klaw')
path = require('path')
fs = require('mz/fs')
streamToPromise = require('stream-to-promise')
getFiles = ->
streamToPromise(klaw(dir))
.filter((item) -> not item.stats.isDirectory())
.map((item) -> item.path)
pack = tar.pack()
getFiles(dir)
.map (file) ->
relPath = path.relative(path.resolve(dir), file)
Promise.join relPath, fs.stat(file), fs.readFile(file),
(filename, stats, data) ->
pack.entryAsync({ name: filename, size: stats.size }, data)
.then ->
pack.finalize()
return pack
cacheHighlightStream = ->
colors = require('colors/safe')
es = require('event-stream')
{ EOL } = require('os')
extractArrowMessage = (message) ->
arrowTest = /^\s*-+>\s*(.+)/i
if (match = arrowTest.exec(message))
match[1]
else
undefined
es.mapSync (data) ->
msg = extractArrowMessage(data)
if msg? and msg.toLowerCase() == 'using cache'
data = colors.bgGreen.black(msg)
return data + EOL
parseBuildArgs = (args, onError) ->
_ = require('lodash')
if not _.isArray(args)
args = [ args ]
obj = {}
args.forEach (str) ->
pair = /^([^\s]+?)=(.*)$/.exec(str)
if pair?
obj[pair[1]] = pair[2]
else
onError(str)
return obj
# Pass in the command line parameters and options and also
# a function which will return the information about the bundle
exports.runBuild = (params, options, getBundleInfo, logStreams) ->
Promise = require('bluebird')
dockerBuild = require('resin-docker-build')
resolver = require('resin-bundle-resolve')
es = require('event-stream')
doodles = require('resin-doodles')
logging = require('../utils/logging')
# The default build context is the current directory
params.source ?= '.'
logs = ''
# Tar up the directory, ready for the build stream
tarDirectory(params.source)
.then (tarStream) ->
new Promise (resolve, reject) ->
hooks =
buildSuccess: (image) ->
if options.tag?
console.log("Tagging image as #{options.tag}")
# Show charlie. In the interest of cloud parity,
# use console.log, not the standard logging streams
doodle = doodles.getDoodle()
console.log()
console.log(doodle)
console.log()
resolve({ image, log: logs + '\n' + doodle + '\n' } )
buildFailure: reject
buildStream: (stream) ->
getBundleInfo(options)
.then (info) ->
if !info?
logging.logWarn logStreams, '''
Warning: No architecture/device type or application information provided.
Dockerfile/project pre-processing will not be performed.
'''
tarStream.pipe(stream)
else
[arch, deviceType] = info
# Perform type resolution on the project
bundle = new resolver.Bundle(tarStream, deviceType, arch)
resolver.resolveBundle(bundle, resolver.getDefaultResolvers())
.then (resolved) ->
logging.logInfo(logStreams, "Building #{resolved.projectType} project")
# Send the resolved tar stream to the docker daemon
resolved.tarStream.pipe(stream)
.catch(reject)
# And print the output
throughStream = es.through (data) ->
logs += data.toString()
this.emit('data', data)
stream
.pipe(throughStream)
.pipe(cacheHighlightStream())
.pipe(logStreams.build)
# Create a builder
connectOpts = generateConnectOpts(options)
# Allow degugging output, hidden behind an env var
logging.logDebug(logStreams, 'Connecting with the following options:')
logging.logDebug(logStreams, JSON.stringify(connectOpts, null, ' '))
builder = new dockerBuild.Builder(connectOpts)
opts = {}
if options.tag?
opts['t'] = options.tag
if options.nocache?
opts['nocache'] = true
if options.arg?
opts['buildargs'] = parseBuildArgs options.arg, (arg) ->
logging.logWarn(logStreams, "Could not parse variable: '#{arg}'")
builder.createBuildStream(opts, hooks, reject)
# Given an image id or tag, export the image to a tar archive.
# Also needs the options generated by the appendOptions()
# function, and a tmpFile to buffer the data into.
exports.bufferImage = (docker, imageId, tmpFile) ->
Promise = require('bluebird')
fs = require('fs')
stream = fs.createWriteStream(tmpFile)
image = docker.getImage(imageId)
image.get()
.then (img) ->
new Promise (resolve, reject) ->
img
.on('error', reject)
.on('end', resolve)
.pipe(stream)
.then ->
new Promise (resolve, reject) ->
fs.createReadStream(tmpFile)
.on 'open', ->
resolve(this)
.on('error', reject)
exports.getDocker = (options) ->
Docker = require('dockerode')
Promise = require('bluebird')
connectOpts = generateConnectOpts(options)
# Use bluebird's promises
connectOpts['Promise'] = Promise
new Docker(connectOpts)
exports.getImageSize = (docker, image) ->
docker.getImage(image).inspectAsync()
.get('Size')