mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-23 10:30:26 +00:00
Merge pull request #2470 from balena-io/2469-build-docker-tls
build: Ensure HTTPS is used with dockerPort 2376 or with ca/cert/key
This commit is contained in:
commit
7fbd1de063
17
.resinci.yml
17
.resinci.yml
@ -7,9 +7,14 @@ npm:
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "12"
|
||||
- "14"
|
||||
##
|
||||
## Temporarily skip Alpine tests until the following issues are resolved:
|
||||
## * https://github.com/concourse/concourse/issues/7905
|
||||
## * https://github.com/product-os/balena-concourse/issues/631
|
||||
##
|
||||
# - name: linux
|
||||
# os: alpine
|
||||
# architecture: x86_64
|
||||
# node_versions:
|
||||
# - "12"
|
||||
# - "14"
|
||||
|
@ -174,14 +174,8 @@ export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExtendedDockerOptions extends dockerode.DockerOptions {
|
||||
docker?: string; // socket path, e.g. /var/run/docker.sock
|
||||
dockerHost?: string; // host name or IP address
|
||||
dockerPort?: number; // TCP port number, e.g. 2375
|
||||
}
|
||||
|
||||
export async function getDocker(
|
||||
options: ExtendedDockerOptions,
|
||||
options: DockerConnectionCliFlags,
|
||||
): Promise<dockerode> {
|
||||
const connectOpts = await generateConnectOpts(options);
|
||||
const client = await createClient(connectOpts);
|
||||
@ -196,14 +190,18 @@ export async function createClient(
|
||||
return new Docker(opts);
|
||||
}
|
||||
|
||||
async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
let connectOpts: dockerode.DockerOptions = {};
|
||||
|
||||
// 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/v3.0.0/lib/modem.js#L15-L70
|
||||
const Modem = require('docker-modem');
|
||||
const defaultOpts = new Modem();
|
||||
/**
|
||||
* Initialize Docker connection options with the default values from the
|
||||
* 'docker-modem' package, which takes several env vars into account,
|
||||
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||
* https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
||||
*
|
||||
* @param opts Command line options like --dockerHost and --dockerPort
|
||||
*/
|
||||
export function getDefaultDockerModemOpts(
|
||||
opts: DockerConnectionCliFlags,
|
||||
): dockerode.DockerOptions {
|
||||
const connectOpts: dockerode.DockerOptions = {};
|
||||
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
||||
'ca',
|
||||
'cert',
|
||||
@ -215,9 +213,33 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
'username',
|
||||
'timeout',
|
||||
];
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
const Modem = require('docker-modem');
|
||||
const originalDockerHost = process.env.DOCKER_HOST;
|
||||
try {
|
||||
if (opts.dockerHost) {
|
||||
process.env.DOCKER_HOST ||= opts.dockerPort
|
||||
? `${opts.dockerHost}:${opts.dockerPort}`
|
||||
: opts.dockerHost;
|
||||
}
|
||||
const defaultOpts = new Modem();
|
||||
for (const opt of optsOfInterest) {
|
||||
connectOpts[opt] = defaultOpts[opt];
|
||||
}
|
||||
} finally {
|
||||
// Did you know? Any value assigned to `process.env.XXX` becomes a string.
|
||||
// For example, `process.env.DOCKER_HOST = undefined` results in
|
||||
// value 'undefined' (a 9-character string) being assigned.
|
||||
if (originalDockerHost) {
|
||||
process.env.DOCKER_HOST = originalDockerHost;
|
||||
} else {
|
||||
delete process.env.DOCKER_HOST;
|
||||
}
|
||||
}
|
||||
return connectOpts;
|
||||
}
|
||||
|
||||
export async function generateConnectOpts(opts: DockerConnectionCliFlags) {
|
||||
let connectOpts = getDefaultDockerModemOpts(opts);
|
||||
|
||||
// Now override the default options with any explicit command line options
|
||||
if (opts.docker != null && opts.dockerHost == null) {
|
||||
@ -241,9 +263,9 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
// These should be file paths (strings)
|
||||
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
||||
|
||||
// If any are set...
|
||||
// If any tlsOpts are set...
|
||||
if (tlsOpts.some((opt) => opt)) {
|
||||
// but not all ()
|
||||
// but not all
|
||||
if (!tlsOpts.every((opt) => opt)) {
|
||||
throw new ExpectedError(
|
||||
'You must provide a CA, certificate and key in order to use TLS',
|
||||
@ -258,7 +280,11 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
||||
const [ca, cert, key] = await Promise.all(
|
||||
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
|
||||
);
|
||||
connectOpts = { ...connectOpts, ca, cert, key };
|
||||
// Also ensure that the protocol is 'https' like 'docker-modem' does:
|
||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L101-L103
|
||||
// TODO: delete redundant logic from this function now that similar logic
|
||||
// exists in the 'docker-modem' package.
|
||||
connectOpts = { ...connectOpts, ca, cert, key, protocol: 'https' };
|
||||
}
|
||||
|
||||
return connectOpts;
|
||||
|
12
npm-shrinkwrap.json
generated
12
npm-shrinkwrap.json
generated
@ -2557,9 +2557,9 @@
|
||||
}
|
||||
},
|
||||
"@types/dockerode": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
|
||||
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.8.tgz",
|
||||
"integrity": "sha512-/Hip29GzPBWfbSS87lyQDVoB7Ja+kr8oOFWXsySxNFa7jlyj3Yws8LaZRmn1xZl7uJH3Xxsg0oI09GHpT1pIBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/docker-modem": "*",
|
||||
@ -2989,9 +2989,9 @@
|
||||
}
|
||||
},
|
||||
"@types/ssh2": {
|
||||
"version": "0.5.49",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
|
||||
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
|
||||
"version": "0.5.52",
|
||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
|
||||
"integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
|
@ -127,7 +127,7 @@
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/cli-truncate": "^2.0.0",
|
||||
"@types/common-tags": "^1.8.1",
|
||||
"@types/dockerode": "^3.3.0",
|
||||
"@types/dockerode": "^3.3.8",
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
|
137
tests/utils/docker.spec.ts
Normal file
137
tests/utils/docker.spec.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2022 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 { expect } from 'chai';
|
||||
|
||||
import {
|
||||
DockerConnectionCliFlags,
|
||||
generateConnectOpts,
|
||||
getDefaultDockerModemOpts,
|
||||
} from '../../build/utils/docker';
|
||||
|
||||
const defaultSocketPath =
|
||||
process.platform === 'win32'
|
||||
? '//./pipe/docker_engine'
|
||||
: '/var/run/docker.sock';
|
||||
|
||||
describe('getDefaultDockerModemOpts() function', function () {
|
||||
it('should use a Unix socket when --dockerHost is not used', () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
dockerPort: 2376,
|
||||
};
|
||||
const defaultOps = getDefaultDockerModemOpts(cliFlags);
|
||||
expect(defaultOps).to.deep.include({
|
||||
host: undefined,
|
||||
port: undefined,
|
||||
protocol: 'http',
|
||||
socketPath: defaultSocketPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the HTTP protocol when --dockerPort is 2375', () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
dockerHost: 'foo',
|
||||
dockerPort: 2375,
|
||||
};
|
||||
const defaultOps = getDefaultDockerModemOpts(cliFlags);
|
||||
expect(defaultOps).to.deep.include({
|
||||
host: 'foo',
|
||||
port: '2375',
|
||||
protocol: 'http',
|
||||
socketPath: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use the HTTPS protocol when --dockerPort is 2376', () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
dockerHost: 'foo',
|
||||
dockerPort: 2376,
|
||||
};
|
||||
const defaultOps = getDefaultDockerModemOpts(cliFlags);
|
||||
expect(defaultOps).to.deep.include({
|
||||
host: 'foo',
|
||||
port: '2376',
|
||||
protocol: 'https',
|
||||
socketPath: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateConnectOpts() function', function () {
|
||||
it('should use a Unix socket when --docker is used', async () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
docker: 'foo',
|
||||
};
|
||||
const connectOpts = await generateConnectOpts(cliFlags);
|
||||
expect(connectOpts).to.deep.include({
|
||||
protocol: 'http',
|
||||
socketPath: 'foo',
|
||||
});
|
||||
expect(connectOpts).to.not.have.any.keys('host', 'port');
|
||||
});
|
||||
|
||||
it('should use the HTTP protocol when --dockerPort is 2375', async () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
dockerHost: 'foo',
|
||||
dockerPort: 2375,
|
||||
};
|
||||
const connectOpts = await generateConnectOpts(cliFlags);
|
||||
expect(connectOpts).to.deep.include({
|
||||
host: 'foo',
|
||||
port: 2375,
|
||||
protocol: 'http',
|
||||
});
|
||||
expect(connectOpts).to.not.have.any.keys('socketPath');
|
||||
});
|
||||
|
||||
it('should use the HTTPS protocol when --dockerPort is 2376', async () => {
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
dockerHost: 'foo',
|
||||
dockerPort: 2376,
|
||||
};
|
||||
const connectOpts = await generateConnectOpts(cliFlags);
|
||||
expect(connectOpts).to.deep.include({
|
||||
host: 'foo',
|
||||
port: 2376,
|
||||
protocol: 'https',
|
||||
});
|
||||
expect(connectOpts).to.not.have.any.keys('socketPath');
|
||||
});
|
||||
|
||||
it('should use the HTTPS protocol when ca/cert/key are used', async () => {
|
||||
const path = await import('path');
|
||||
const aFile = path.join(
|
||||
__dirname,
|
||||
'../test-data/projects/no-docker-compose/dockerignore1/a.txt',
|
||||
);
|
||||
const cliFlags: DockerConnectionCliFlags = {
|
||||
ca: aFile,
|
||||
cert: aFile,
|
||||
key: aFile,
|
||||
};
|
||||
const connectOpts = await generateConnectOpts(cliFlags);
|
||||
expect(connectOpts).to.deep.include({
|
||||
ca: 'a',
|
||||
cert: 'a',
|
||||
key: 'a',
|
||||
host: undefined,
|
||||
port: undefined,
|
||||
protocol: 'https',
|
||||
socketPath: defaultSocketPath,
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user