mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-24 02:41:23 +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:
|
node_versions:
|
||||||
- "12"
|
- "12"
|
||||||
- "14"
|
- "14"
|
||||||
- name: linux
|
##
|
||||||
os: alpine
|
## Temporarily skip Alpine tests until the following issues are resolved:
|
||||||
architecture: x86_64
|
## * https://github.com/concourse/concourse/issues/7905
|
||||||
node_versions:
|
## * https://github.com/product-os/balena-concourse/issues/631
|
||||||
- "12"
|
##
|
||||||
- "14"
|
# - 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(
|
export async function getDocker(
|
||||||
options: ExtendedDockerOptions,
|
options: DockerConnectionCliFlags,
|
||||||
): Promise<dockerode> {
|
): Promise<dockerode> {
|
||||||
const connectOpts = await generateConnectOpts(options);
|
const connectOpts = await generateConnectOpts(options);
|
||||||
const client = await createClient(connectOpts);
|
const client = await createClient(connectOpts);
|
||||||
@ -196,14 +190,18 @@ export async function createClient(
|
|||||||
return new Docker(opts);
|
return new Docker(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
/**
|
||||||
let connectOpts: dockerode.DockerOptions = {};
|
* Initialize Docker connection options with the default values from the
|
||||||
|
* 'docker-modem' package, which takes several env vars into account,
|
||||||
// Start with docker-modem defaults which take several env vars into account,
|
* including DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH, SSH_AUTH_SOCK
|
||||||
// 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
|
||||||
// https://github.com/apocas/docker-modem/blob/v3.0.0/lib/modem.js#L15-L70
|
*
|
||||||
const Modem = require('docker-modem');
|
* @param opts Command line options like --dockerHost and --dockerPort
|
||||||
const defaultOpts = new Modem();
|
*/
|
||||||
|
export function getDefaultDockerModemOpts(
|
||||||
|
opts: DockerConnectionCliFlags,
|
||||||
|
): dockerode.DockerOptions {
|
||||||
|
const connectOpts: dockerode.DockerOptions = {};
|
||||||
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
const optsOfInterest: Array<keyof dockerode.DockerOptions> = [
|
||||||
'ca',
|
'ca',
|
||||||
'cert',
|
'cert',
|
||||||
@ -215,9 +213,33 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
|||||||
'username',
|
'username',
|
||||||
'timeout',
|
'timeout',
|
||||||
];
|
];
|
||||||
for (const opt of optsOfInterest) {
|
const Modem = require('docker-modem');
|
||||||
connectOpts[opt] = defaultOpts[opt];
|
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
|
// Now override the default options with any explicit command line options
|
||||||
if (opts.docker != null && opts.dockerHost == null) {
|
if (opts.docker != null && opts.dockerHost == null) {
|
||||||
@ -241,9 +263,9 @@ async function generateConnectOpts(opts: ExtendedDockerOptions) {
|
|||||||
// These should be file paths (strings)
|
// These should be file paths (strings)
|
||||||
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
const tlsOpts = [opts.ca, opts.cert, opts.key];
|
||||||
|
|
||||||
// If any are set...
|
// If any tlsOpts are set...
|
||||||
if (tlsOpts.some((opt) => opt)) {
|
if (tlsOpts.some((opt) => opt)) {
|
||||||
// but not all ()
|
// but not all
|
||||||
if (!tlsOpts.every((opt) => opt)) {
|
if (!tlsOpts.every((opt) => opt)) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
'You must provide a CA, certificate and key in order to use TLS',
|
'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(
|
const [ca, cert, key] = await Promise.all(
|
||||||
tlsOpts.map((opt: string) => fs.readFile(opt, 'utf8')),
|
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;
|
return connectOpts;
|
||||||
|
12
npm-shrinkwrap.json
generated
12
npm-shrinkwrap.json
generated
@ -2557,9 +2557,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/dockerode": {
|
"@types/dockerode": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.8.tgz",
|
||||||
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
|
"integrity": "sha512-/Hip29GzPBWfbSS87lyQDVoB7Ja+kr8oOFWXsySxNFa7jlyj3Yws8LaZRmn1xZl7uJH3Xxsg0oI09GHpT1pIBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/docker-modem": "*",
|
"@types/docker-modem": "*",
|
||||||
@ -2989,9 +2989,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/ssh2": {
|
"@types/ssh2": {
|
||||||
"version": "0.5.49",
|
"version": "0.5.52",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz",
|
||||||
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
|
"integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
|
@ -127,7 +127,7 @@
|
|||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
"@types/cli-truncate": "^2.0.0",
|
"@types/cli-truncate": "^2.0.0",
|
||||||
"@types/common-tags": "^1.8.1",
|
"@types/common-tags": "^1.8.1",
|
||||||
"@types/dockerode": "^3.3.0",
|
"@types/dockerode": "^3.3.8",
|
||||||
"@types/ejs": "^3.1.0",
|
"@types/ejs": "^3.1.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/fs-extra": "^9.0.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