mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-22 23:12:22 +00:00
269 lines
7.5 KiB
CoffeeScript
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: 'buildArg'
|
|
parameter: 'arg'
|
|
description: 'Set a build-time variable (eg. "-B \'ARG=value\'"). Can be specified multiple times.'
|
|
alias: 'B'
|
|
},
|
|
{
|
|
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 ]
|
|
buildArgs = {}
|
|
args.forEach (str) ->
|
|
pair = /^([^\s]+?)=(.*)$/.exec(str)
|
|
if pair?
|
|
buildArgs[pair[1]] = pair[2]
|
|
else
|
|
onError(str)
|
|
return buildArgs
|
|
|
|
# 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.buildArg?
|
|
opts['buildargs'] = parseBuildArgs options.buildArg, (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')
|