Merge pull request #1764 from balena-io/js-lib-utils-docker-coffee

Convert lib/utils/docker-coffee.coffee to javascript
This commit is contained in:
Page- 2020-04-24 16:01:58 +01:00 committed by GitHub
commit a10d5b9abe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 282 additions and 263 deletions

View File

@ -1,219 +0,0 @@
###*
# @license
# Copyright 2017-2019 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###
# Functions to help actions which rely on using docker
Promise = require('bluebird')
_ = require('lodash')
# 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.appendConnectionOptions = appendConnectionOptions = (opts) ->
opts.concat [
{
signature: 'docker'
parameter: 'docker'
description: 'Path to a local docker socket (e.g. /var/run/docker.sock)'
alias: 'P'
},
{
signature: 'dockerHost'
parameter: 'dockerHost'
description: 'Docker daemon hostname or IP address (dev machine or balena device) '
alias: 'h'
},
{
signature: 'dockerPort'
parameter: 'dockerPort'
description: 'Docker daemon TCP port number (hint: 2375 for balena devices)'
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'
},
]
# 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) ->
appendConnectionOptions(opts).concat [
{
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: 'cache-from'
parameter: 'image-list'
description: '
Comma-separated list (no spaces) of image names for build cache resolution.
Implements the same feature as the "docker build --cache-from" option.'
},
{
signature: 'nocache'
description: "Don't use docker layer caching when building"
boolean: true
},
{
signature: 'squash'
description: 'Squash newly built layers into a single new layer'
boolean: true
}
]
generateConnectOpts = (opts) ->
fs = require('mz/fs')
_ = require('lodash')
Promise.try ->
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
# Use docker-modem defaults which take the DOCKER_HOST env var into account
# https://github.com/apocas/docker-modem/blob/v2.0.2/lib/modem.js#L16-L65
Modem = require('docker-modem')
defaultOpts = new Modem()
connectOpts[opt] = defaultOpts[opt] for opt in ['host', 'port', 'socketPath']
# 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')
certBodies = {
ca: fs.readFile(opts.ca, 'utf-8')
cert: fs.readFile(opts.cert, 'utf-8')
key: fs.readFile(opts.key, 'utf-8')
}
return Promise.props(certBodies)
.then (toMerge) ->
_.merge(connectOpts, toMerge)
return connectOpts
parseBuildArgs = (args) ->
if not Array.isArray(args)
args = [ args ]
buildArgs = {}
args.forEach (arg) ->
# note: [^] matches any character, including line breaks
pair = /^([^\s]+?)=([^]*)$/.exec(arg)
if pair?
buildArgs[pair[1]] = pair[2] ? ''
else
throw new Error("Could not parse build argument: '#{arg}'")
return buildArgs
exports.generateBuildOpts = (options) ->
opts = {}
if options.tag?
opts.t = options.tag
if options.nocache?
opts.nocache = true
if options['cache-from']?.trim()
opts.cachefrom = options['cache-from'].split(',').filter((i) -> !!i.trim())
if options.squash?
opts.squash = true
if options.buildArg?
opts.buildargs = parseBuildArgs(options.buildArg)
if not _.isEmpty(options['registry-secrets'])
opts.registryconfig = options['registry-secrets']
return opts
exports.getDocker = (options) ->
generateConnectOpts(options)
.then(createClient)
.tap(ensureDockerSeemsAccessible)
getDockerToolbelt = _.once ->
Docker = require('docker-toolbelt')
Promise.promisifyAll Docker.prototype, {
filter: (name) -> name == 'run'
multiArgs: true
}
Promise.promisifyAll(Docker.prototype)
Promise.promisifyAll(new Docker({}).getImage().constructor.prototype)
Promise.promisifyAll(new Docker({}).getContainer().constructor.prototype)
return Docker
# docker-toolbelt v3 is not backwards compatible as it removes all *Async
# methods that are in wide use in the CLI. The workaround for now is to
# manually promisify the client and replace all `new Docker()` calls with
# this shared function that returns a promisified client.
#
# **New code must not use the *Async methods.**
#
exports.createClient = createClient = (opts) ->
Docker = getDockerToolbelt()
docker = new Docker(opts)
modem = docker.modem
# Workaround for a docker-modem 2.0.x bug where it sets a default
# socketPath on Windows even if the input options specify a host/port.
if modem.socketPath and modem.host
if opts.socketPath
modem.host = undefined
modem.port = undefined
else if opts.host
modem.socketPath = undefined
return docker
ensureDockerSeemsAccessible = (docker) ->
{ exitWithExpectedError } = require('./patterns')
docker.ping().catch ->
exitWithExpectedError('Docker seems to be unavailable. Is it installed and running?')

View File

@ -1,43 +0,0 @@
/**
* @license
* Copyright 2019 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as Bluebird from 'bluebird';
import DockerToolbelt = require('docker-toolbelt');
export interface BuildDockerOptions {
ca?: string; // path to ca (Certificate Authority) file (TLS)
cert?: string; // path to cert (Certificate) file (TLS)
key?: string; // path to key file (TLS)
docker?: string; // dockerode DockerOptions.socketPath
dockerHost?: string; // dockerode DockerOptions.host
dockerPort?: number; // dockerode DockerOptions.port
host?: string;
port?: number;
timeout?: number;
}
export interface DockerToolbeltOpts {
host: string;
port: number;
timeout?: number;
}
export function getDocker(
options: BuildDockerOptions,
): Bluebird<DockerToolbelt>;
export function createClient(opts: DockerToolbeltOpts): DockerToolbelt;

281
lib/utils/docker-js.js Normal file
View File

@ -0,0 +1,281 @@
/**
* @license
* Copyright 2017-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Functions to help actions which rely on using docker
import * as Promise from 'bluebird';
import * as _ from 'lodash';
// 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.
export const appendConnectionOptions = opts =>
opts.concat([
{
signature: 'docker',
parameter: 'docker',
description: 'Path to a local docker socket (e.g. /var/run/docker.sock)',
alias: 'P',
},
{
signature: 'dockerHost',
parameter: 'dockerHost',
description:
'Docker daemon hostname or IP address (dev machine or balena device) ',
alias: 'h',
},
{
signature: 'dockerPort',
parameter: 'dockerPort',
description:
'Docker daemon TCP port number (hint: 2375 for balena devices)',
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',
},
]);
// 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.
export function appendOptions(opts) {
return appendConnectionOptions(opts).concat([
{
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: 'cache-from',
parameter: 'image-list',
description: `\
Comma-separated list (no spaces) of image names for build cache resolution. \
Implements the same feature as the "docker build --cache-from" option.`,
},
{
signature: 'nocache',
description: "Don't use docker layer caching when building",
boolean: true,
},
{
signature: 'squash',
description: 'Squash newly built layers into a single new layer',
boolean: true,
},
]);
}
const generateConnectOpts = function(opts) {
const fs = require('mz/fs');
return Promise.try(function() {
const connectOpts = {};
// Firsly need to decide between a local docker socket
// and a host available over a host:port combo
if (opts.docker != null && opts.dockerHost == null) {
// good, local docker socket
connectOpts.socketPath = opts.docker;
} else if (opts.dockerHost != null && opts.docker == null) {
// Good a host is provided, and local socket isn't
connectOpts.host = opts.dockerHost;
connectOpts.port = opts.dockerPort || 2376;
} else if (opts.docker != null && opts.dockerHost != null) {
// 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 {
// Use docker-modem defaults which take the DOCKER_HOST env var into account
// https://github.com/apocas/docker-modem/blob/v2.0.2/lib/modem.js#L16-L65
const Modem = require('docker-modem');
const defaultOpts = new Modem();
for (let opt of ['host', 'port', 'socketPath']) {
connectOpts[opt] = defaultOpts[opt];
}
}
// Now need to check if the user wants to connect over TLS
// to the host
// If any are set...
if (opts.ca != null || opts.cert != null || opts.key != null) {
// but not all
if (!(opts.ca != null && opts.cert != null && opts.key != null)) {
throw new Error(
'You must provide a CA, certificate and key in order to use TLS',
);
}
const certBodies = {
ca: fs.readFile(opts.ca, 'utf-8'),
cert: fs.readFile(opts.cert, 'utf-8'),
key: fs.readFile(opts.key, 'utf-8'),
};
return Promise.props(certBodies).then(toMerge =>
_.merge(connectOpts, toMerge),
);
}
return connectOpts;
});
};
const parseBuildArgs = function(args) {
if (!Array.isArray(args)) {
args = [args];
}
const buildArgs = {};
args.forEach(function(arg) {
// note: [^] matches any character, including line breaks
const pair = /^([^\s]+?)=([^]*)$/.exec(arg);
if (pair != null) {
buildArgs[pair[1]] = pair[2] ?? '';
} else {
throw new Error(`Could not parse build argument: '${arg}'`);
}
});
return buildArgs;
};
export function generateBuildOpts(options) {
const opts = {};
if (options.tag != null) {
opts.t = options.tag;
}
if (options.nocache != null) {
opts.nocache = true;
}
if (options['cache-from']?.trim()) {
opts.cachefrom = options['cache-from'].split(',').filter(i => !!i.trim());
}
if (options.squash != null) {
opts.squash = true;
}
if (options.buildArg != null) {
opts.buildargs = parseBuildArgs(options.buildArg);
}
if (!_.isEmpty(options['registry-secrets'])) {
opts.registryconfig = options['registry-secrets'];
}
return opts;
}
/**
* @param {{
* ca?: string; // path to ca (Certificate Authority) file (TLS)
* cert?: string; // path to cert (Certificate) file (TLS)
* key?: string; // path to key file (TLS)
* docker?: string; // dockerode DockerOptions.socketPath
* dockerHost?: string; // dockerode DockerOptions.host
* dockerPort?: number; // dockerode DockerOptions.port
* host?: string;
* port?: number;
* timeout?: number;
* }} options
* @returns {Promise<import('docker-toolbelt')>}
*/
export function getDocker(options) {
return generateConnectOpts(options)
.then(createClient)
.tap(ensureDockerSeemsAccessible);
}
const getDockerToolbelt = _.once(function() {
const Docker = require('docker-toolbelt');
Promise.promisifyAll(Docker.prototype, {
filter(name) {
return name === 'run';
},
multiArgs: true,
});
Promise.promisifyAll(Docker.prototype);
// @ts-ignore `getImage()` should have a param but this whole thing is a hack that should be removed
Promise.promisifyAll(new Docker({}).getImage().constructor.prototype);
// @ts-ignore `getContainer()` should have a param but this whole thing is a hack that should be removed
Promise.promisifyAll(new Docker({}).getContainer().constructor.prototype);
return Docker;
});
// docker-toolbelt v3 is not backwards compatible as it removes all *Async
// methods that are in wide use in the CLI. The workaround for now is to
// manually promisify the client and replace all `new Docker()` calls with
// this shared function that returns a promisified client.
//
// **New code must not use the *Async methods.**
//
/**
* @param {{
* host: string;
* port: number;
* timeout?: number;
* socketPath?: string
* }} opts
* @returns {import('docker-toolbelt')}
*/
export const createClient = function(opts) {
const Docker = getDockerToolbelt();
const docker = new Docker(opts);
const { modem } = docker;
// Workaround for a docker-modem 2.0.x bug where it sets a default
// socketPath on Windows even if the input options specify a host/port.
if (modem.socketPath && modem.host) {
if (opts.socketPath) {
modem.host = undefined;
modem.port = undefined;
} else if (opts.host) {
modem.socketPath = undefined;
}
}
return docker;
};
var ensureDockerSeemsAccessible = function(docker) {
const { exitWithExpectedError } = require('./patterns');
return docker
.ping()
.catch(() =>
exitWithExpectedError(
'Docker seems to be unavailable. Is it installed and running?',
),
);
};

View File

@ -18,7 +18,7 @@
import Dockerode = require('dockerode'); import Dockerode = require('dockerode');
import * as dockerode from 'dockerode'; import * as dockerode from 'dockerode';
export * from './docker-coffee'; export * from './docker-js';
interface BalenaEngineVersion extends dockerode.DockerVersion { interface BalenaEngineVersion extends dockerode.DockerVersion {
Engine?: string; Engine?: string;