2020-04-24 13:41:15 +00:00
|
|
|
/**
|
|
|
|
* @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 _ from 'lodash';
|
2020-06-26 11:46:58 +00:00
|
|
|
import { ExpectedError } from '../errors';
|
2020-04-24 13:41:15 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
const generateConnectOpts = async function (opts) {
|
|
|
|
const { promises: fs } = await import('fs');
|
2020-07-06 16:08:47 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
const connectOpts = {};
|
2020-07-06 16:08:47 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
// Start with docker-modem defaults which take several env vars into account,
|
|
|
|
// including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
|
|
|
// https://github.com/apocas/docker-modem/blob/v2.1.3/lib/modem.js#L15-L65
|
|
|
|
const Modem = await import('docker-modem');
|
|
|
|
const defaultOpts = new Modem();
|
|
|
|
const optsOfInterest = [
|
|
|
|
'ca',
|
|
|
|
'cert',
|
|
|
|
'key',
|
|
|
|
'host',
|
|
|
|
'port',
|
|
|
|
'socketPath',
|
|
|
|
'protocol',
|
|
|
|
'username',
|
|
|
|
'sshAuthAgent',
|
|
|
|
'timeout',
|
|
|
|
];
|
|
|
|
for (const opt of optsOfInterest) {
|
|
|
|
connectOpts[opt] = defaultOpts[opt];
|
|
|
|
}
|
2020-04-24 13:41:15 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
// Now override the default options with any explicit command line options
|
|
|
|
if (opts.docker != null && opts.dockerHost == null) {
|
|
|
|
// good, local docker socket
|
|
|
|
connectOpts.socketPath = opts.docker;
|
|
|
|
delete connectOpts.host;
|
|
|
|
delete connectOpts.port;
|
|
|
|
} 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;
|
|
|
|
delete connectOpts.socketPath;
|
|
|
|
} else if (opts.docker != null && opts.dockerHost != null) {
|
|
|
|
// Both provided, no obvious way to continue
|
|
|
|
throw new ExpectedError(
|
|
|
|
"Both a local docker socket and docker host have been provided. Don't know how to continue.",
|
|
|
|
);
|
|
|
|
}
|
2020-04-24 13:41:15 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
// Now need to check if the user wants to connect over TLS
|
|
|
|
// to the host
|
2020-04-24 13:41:15 +00:00
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
// 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 ExpectedError(
|
|
|
|
'You must provide a CA, certificate and key in order to use TLS',
|
2020-04-24 13:41:15 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-29 17:27:20 +00:00
|
|
|
const [ca, cert, key] = await Promise.all([
|
|
|
|
fs.readFile(opts.ca, 'utf-8'),
|
|
|
|
fs.readFile(opts.cert, 'utf-8'),
|
|
|
|
fs.readFile(opts.key, 'utf-8'),
|
|
|
|
]);
|
|
|
|
return _.merge(connectOpts, {
|
|
|
|
ca,
|
|
|
|
cert,
|
|
|
|
key,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return connectOpts;
|
2020-04-24 13:41:15 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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
|
2020-07-01 14:26:40 +00:00
|
|
|
* @returns {Promise<import('docker-toolbelt')>}
|
2020-04-24 13:41:15 +00:00
|
|
|
*/
|
2020-07-29 17:27:20 +00:00
|
|
|
export async function getDocker(options) {
|
|
|
|
const connectOpts = await generateConnectOpts(options);
|
|
|
|
const client = createClient(connectOpts);
|
|
|
|
await ensureDockerSeemsAccessible(client);
|
|
|
|
return client;
|
2020-04-24 13:41:15 +00:00
|
|
|
}
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
const getDockerToolbelt = _.once(function () {
|
2020-04-24 13:41:15 +00:00
|
|
|
const Docker = require('docker-toolbelt');
|
2020-07-01 14:26:40 +00:00
|
|
|
const Bluebird = require('bluebird');
|
2020-06-23 10:29:03 +00:00
|
|
|
Bluebird.promisifyAll(Docker.prototype, {
|
2020-04-24 13:41:15 +00:00
|
|
|
filter(name) {
|
|
|
|
return name === 'run';
|
|
|
|
},
|
|
|
|
multiArgs: true,
|
|
|
|
});
|
2020-06-23 10:29:03 +00:00
|
|
|
Bluebird.promisifyAll(Docker.prototype);
|
2020-04-24 13:41:15 +00:00
|
|
|
// @ts-ignore `getImage()` should have a param but this whole thing is a hack that should be removed
|
2020-06-23 10:29:03 +00:00
|
|
|
Bluebird.promisifyAll(new Docker({}).getImage().constructor.prototype);
|
2020-04-24 13:41:15 +00:00
|
|
|
// @ts-ignore `getContainer()` should have a param but this whole thing is a hack that should be removed
|
2020-06-23 10:29:03 +00:00
|
|
|
Bluebird.promisifyAll(new Docker({}).getContainer().constructor.prototype);
|
2020-04-24 13:41:15 +00:00
|
|
|
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')}
|
|
|
|
*/
|
2020-06-15 22:53:07 +00:00
|
|
|
export const createClient = function (opts) {
|
2020-04-24 13:41:15 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
var ensureDockerSeemsAccessible = function (docker) {
|
2020-04-30 08:45:16 +00:00
|
|
|
const { exitWithExpectedError } = require('../errors');
|
2020-04-24 13:41:15 +00:00
|
|
|
return docker
|
|
|
|
.ping()
|
2020-07-06 16:08:47 +00:00
|
|
|
.catch((e) =>
|
2020-04-24 13:41:15 +00:00
|
|
|
exitWithExpectedError(
|
2020-07-06 16:08:47 +00:00
|
|
|
`Docker seems to be unavailable. Is it installed and running?\n${e}`,
|
2020-04-24 13:41:15 +00:00
|
|
|
),
|
|
|
|
);
|
|
|
|
};
|