diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index 2b2d1c45..8199210a 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -156,7 +156,7 @@ const capitanoDoc = { }, { title: 'Deploy', - files: ['build/actions/build.js', 'build/actions/deploy.js'], + files: ['build/actions-oclif/build.js', 'build/actions/deploy.js'], }, { title: 'Platform', diff --git a/doc/cli.markdown b/doc/cli.markdown index 32daaedf..9b76442f 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -2288,8 +2288,7 @@ the provided docker daemon in your development machine or balena device. (See also the `balena push` command for the option of building images in the balenaCloud build servers.) -You must provide either an application or a device-type/architecture pair to use -the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile). +You must provide either an application or a device-type/architecture pair. This command will look into the given source directory (or the current working directory if one isn't specified) for a docker-compose.yml file, and if found, @@ -2373,37 +2372,38 @@ adding counter patterns to the applicable .dockerignore file(s), for example Examples: - $ balena build - $ balena build ./source/ + $ balena build --application myApp + $ balena build ./source/ --application myApp $ balena build --deviceType raspberrypi3 --arch armv7hf --emulated - $ balena build --application MyApp ./source/ - $ balena build --docker /var/run/docker.sock # Linux, Mac - $ balena build --docker //./pipe/docker_engine # Windows - $ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem + $ balena build --docker /var/run/docker.sock --application myApp # Linux, Mac + $ balena build --docker //./pipe/docker_engine --application myApp # Windows + $ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -a myApp + +### Arguments + +#### SOURCE + +path of project source directory ### Options -#### --arch, -A <arch> +#### -A, --arch ARCH -The architecture to build for +the architecture to build for -#### --deviceType, -d <deviceType> +#### -d, --deviceType DEVICETYPE -The type of device this build is for +the type of device this build is for -#### --application, -a <application> +#### -a, --application APPLICATION -The target balena application this build is for +name of the target balena application this build is for -#### --projectName, -n <projectName> - -Specify an alternate project name; default is the directory name - -#### --emulated, -e +#### -e, --emulated Run an emulated build using Qemu -#### --dockerfile <Dockerfile> +#### --dockerfile DOCKERFILE Alternative Dockerfile name/path, relative to the source folder @@ -2415,17 +2415,17 @@ No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by defau Hide the image build log output (produce less verbose output) -#### --gitignore, -g +#### -g, --gitignore Consider .gitignore files in addition to the .dockerignore file. This reverts to the CLI v11 behavior/implementation (deprecated) if compatibility is required until your project can be adapted. -#### --multi-dockerignore, -m +#### -m, --multi-dockerignore Have each service use its own .dockerignore file. See "balena help build". -#### --nogitignore, -G +#### -G, --nogitignore No-op (default behavior) since balena CLI v12.0.0. See "balena help build". @@ -2433,11 +2433,11 @@ No-op (default behavior) since balena CLI v12.0.0. See "balena help build". Disable project validation check of 'docker-compose.yml' file in parent folder -#### --registry-secrets, -R <secrets.yml|.json> +#### -R, --registry-secrets REGISTRY-SECRETS Path to a YAML or JSON file with passwords for a private Docker registry -#### --convert-eol, -l +#### -l, --convert-eol No-op and deprecated since balena CLI v12.0.0 @@ -2445,39 +2445,19 @@ No-op and deprecated since balena CLI v12.0.0 Don't convert line endings from CRLF (Windows format) to LF (Unix format). -#### --docker, -P <docker> +#### -n, --projectName PROJECTNAME -Path to a local docker socket (e.g. /var/run/docker.sock) +Specify an alternate project name; default is the directory name -#### --dockerHost, -h <dockerHost> - -Docker daemon hostname or IP address (dev machine or balena device) - -#### --dockerPort, -p <dockerPort> - -Docker daemon TCP port number (hint: 2375 for balena devices) - -#### --ca <ca> - -Docker host TLS certificate authority file - -#### --cert <cert> - -Docker host TLS certificate file - -#### --key <key> - -Docker host TLS key file - -#### --tag, -t <tag> +#### -t, --tag TAG The alias to the generated image -#### --buildArg, -B <arg> +#### -B, --buildArg BUILDARG Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple times. -#### --cache-from <image-list> +#### --cache-from CACHE-FROM Comma-separated list (no spaces) of image names for build cache resolution. Implements the same feature as the "docker build --cache-from" option. @@ -2606,10 +2586,6 @@ Force a rebuild before deploy Don't upload build logs to the dashboard with image (if building) -#### --projectName, -n <projectName> - -Specify an alternate project name; default is the directory name - #### --emulated, -e Run an emulated build using Qemu @@ -2656,6 +2632,10 @@ No-op and deprecated since balena CLI v12.0.0 Don't convert line endings from CRLF (Windows format) to LF (Unix format). +#### --projectName, -n <projectName> + +Specify an alternate project name; default is the directory name + #### --docker, -P <docker> Path to a local docker socket (e.g. /var/run/docker.sock) diff --git a/lib/actions-oclif/build.ts b/lib/actions-oclif/build.ts new file mode 100644 index 00000000..430c0a88 --- /dev/null +++ b/lib/actions-oclif/build.ts @@ -0,0 +1,269 @@ +/** + * @license + * Copyright 2016-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. + */ + +import { flags } from '@oclif/command'; +import Command from '../command'; +import * as cf from '../utils/common-flags'; +import { getBalenaSdk } from '../utils/lazy'; +import * as compose from '../utils/compose'; +import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk'; +import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages'; +import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types'; +import { composeCliFlags } from '../utils/compose_ts'; +import type { DockerCliFlags } from '../utils/docker'; +import { dockerCliFlags } from '../utils/docker'; + +interface FlagsDef extends ComposeCliFlags, DockerCliFlags { + arch?: string; + deviceType?: string; + application?: string; + source?: string; // Not part of command profile - source param copied here. + help: void; +} + +interface ArgsDef { + source?: string; +} + +export default class BuildCmd extends Command { + public static description = `\ +Build a project locally. + +Use this command to build an image or a complete multicontainer project with +the provided docker daemon in your development machine or balena device. +(See also the \`balena push\` command for the option of building images in the +balenaCloud build servers.) + +You must provide either an application or a device-type/architecture pair. + +This command will look into the given source directory (or the current working +directory if one isn't specified) for a docker-compose.yml file, and if found, +each service defined in the compose file will be built. If a compose file isn't +found, it will look for a Dockerfile[.template] file (or alternative Dockerfile +specified with the \`--dockerfile\` option), and if no dockerfile is found, it +will try to generate one. + +${registrySecretsHelp} + +${dockerignoreHelp} +`; + public static examples = [ + '$ balena build --application myApp', + '$ balena build ./source/ --application myApp', + '$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated', + '$ balena build --docker /var/run/docker.sock --application myApp # Linux, Mac', + '$ balena build --docker //./pipe/docker_engine --application myApp # Windows', + '$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -a myApp', + ]; + + public static args = [ + { + name: 'source', + description: 'path of project source directory', + }, + ]; + + public static usage = 'build [source]'; + + public static flags: flags.Input = { + arch: flags.string({ + description: 'the architecture to build for', + char: 'A', + }), + deviceType: flags.string({ + description: 'the type of device this build is for', + char: 'd', + }), + application: flags.string({ + description: 'name of the target balena application this build is for', + char: 'a', + }), + ...composeCliFlags, + ...dockerCliFlags, + help: cf.help, + }; + + public static primary = true; + + public async run() { + const { args: params, flags: options } = this.parse( + BuildCmd, + ); + + await Command.checkLoggedInIf(!!options.application); + + // compositions with many services trigger misleading warnings + // @ts-ignore editing property that isn't typed but does exist + (await import('events')).defaultMaxListeners = 1000; + + const sdk = getBalenaSdk(); + + const logger = await Command.getLogger(); + logger.logDebug('Parsing input...'); + + this.translateParams(params, options); + await this.validateOptions(options, sdk); + + const app = await this.getAppAndResolveArch(options); + + const { docker, buildOpts, composeOpts } = await this.prepareBuild(options); + + try { + await this.buildProject(docker, logger, composeOpts, { + app, + arch: options.arch!, + deviceType: options.deviceType!, + buildEmulated: options.emulated, + buildOpts, + }); + } catch (err) { + logger.logError('Build failed.'); + throw err; + } + + logger.outputDeferredMessages(); + logger.logSuccess('Build succeeded!'); + } + + protected translateParams(params: ArgsDef, options: FlagsDef) { + // Copy flags to those expected by other modules + options.arg = options.buildArg; + delete options.buildArg; + options['image-list'] = options['cache-from']; + delete options['cache-from']; + + // `build` accepts `[source]` as a parameter, but compose expects it + // as an option. swap them here + if (options.source == null) { + options.source = params.source; + } + delete params.source; + } + + protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) { + // Validate option combinations + if ( + (opts.application == null && + (opts.arch == null || opts.deviceType == null)) || + (opts.application != null && + (opts.arch != null || opts.deviceType != null)) + ) { + const { ExpectedError } = await import('../errors'); + throw new ExpectedError( + 'You must specify either an application or an arch/deviceType pair to build for', + ); + } + + // Validate project directory + const { validateProjectDirectory } = await import('../utils/compose_ts'); + const { dockerfilePath, registrySecrets } = await validateProjectDirectory( + sdk, + { + dockerfilePath: opts.dockerfile, + noParentCheck: opts['noparent-check'] || false, + projectPath: opts.source || '.', + registrySecretsPath: opts['registry-secrets'], + }, + ); + + opts.dockerfile = dockerfilePath; + opts['registry-secrets'] = registrySecrets; + } + + protected async getAppAndResolveArch(opts: FlagsDef) { + if (opts.application) { + const { getAppWithArch } = await import('../utils/helpers'); + const app = await getAppWithArch(opts.application); + opts.arch = app.arch; + opts.deviceType = app.device_type; + return app; + } + } + + protected async prepareBuild(options: FlagsDef) { + const { getDocker, generateBuildOpts } = await import('../utils/docker'); + const [docker, buildOpts, composeOpts] = await Promise.all([ + getDocker(options), + generateBuildOpts(options), + compose.generateOpts(options), + ]); + return { + docker, + buildOpts, + composeOpts, + }; + } + + /** + * Opts must be an object with the following keys: + * app: the app this build is for (optional) + * arch: the architecture to build for + * deviceType: the device type to build for + * buildEmulated + * buildOpts: arguments to forward to docker build command + * + * @param {DockerToolbelt} docker + * @param {Logger} logger + * @param {ComposeOpts} composeOpts + * @param opts + */ + protected async buildProject( + docker: import('docker-toolbelt'), + logger: import('../utils/logger'), + composeOpts: ComposeOpts, + opts: { + app?: Application; + arch: string; + deviceType: string; + buildEmulated: boolean; + buildOpts: any; + }, + ) { + const { loadProject } = await import('../utils/compose_ts'); + + const project = await loadProject(logger, composeOpts); + + const appType = (opts.app?.application_type as ApplicationType[])?.[0]; + if ( + appType != null && + project.descriptors.length > 1 && + !appType.supports_multicontainer + ) { + logger.logWarn( + 'Target application does not support multiple containers.\n' + + 'Continuing with build, but you will not be able to deploy.', + ); + } + + await compose.buildProject( + docker, + logger, + project.path, + project.name, + project.composition, + opts.arch, + opts.deviceType, + opts.buildEmulated, + opts.buildOpts, + composeOpts.inlineLogs, + composeOpts.convertEol, + composeOpts.dockerfilePath, + composeOpts.nogitignore, + composeOpts.multiDockerignore, + ); + } +} diff --git a/lib/actions/build.js b/lib/actions/build.js deleted file mode 100644 index 6e5084ed..00000000 --- a/lib/actions/build.js +++ /dev/null @@ -1,206 +0,0 @@ -/** - * @license - * Copyright 2016-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. - */ - -import * as dockerUtils from '../utils/docker'; -import * as compose from '../utils/compose'; -import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages'; -import { getBalenaSdk } from '../utils/lazy'; - -/** - * Opts must be an object with the following keys: - * app: the app this build is for (optional) - * arch: the architecture to build for - * deviceType: the device type to build for - * buildEmulated - * buildOpts: arguments to forward to docker build command - * - * @param {import('docker-toolbelt')} docker - * @param {import('../utils/logger')} logger - * @param {import('../utils/compose-types').ComposeOpts} composeOpts - * @param {any} opts - */ -const buildProject = function (docker, logger, composeOpts, opts) { - const { loadProject } = require('../utils/compose_ts'); - return loadProject(logger, composeOpts) - .then(function (project) { - const appType = opts.app?.application_type?.[0]; - if ( - appType != null && - project.descriptors.length > 1 && - !appType.supports_multicontainer - ) { - logger.logWarn( - 'Target application does not support multiple containers.\n' + - 'Continuing with build, but you will not be able to deploy.', - ); - } - - return compose.buildProject( - docker, - logger, - project.path, - project.name, - project.composition, - opts.arch, - opts.deviceType, - opts.buildEmulated, - opts.buildOpts, - composeOpts.inlineLogs, - composeOpts.convertEol, - composeOpts.dockerfilePath, - composeOpts.nogitignore, - composeOpts.multiDockerignore, - ); - }) - .then(function () { - logger.outputDeferredMessages(); - logger.logSuccess('Build succeeded!'); - }) - .catch((err) => { - logger.logError('Build failed'); - throw err; - }); -}; - -export const build = { - signature: 'build [source]', - description: 'Build a single image or a multicontainer project locally', - primary: true, - help: `\ -Use this command to build an image or a complete multicontainer project with -the provided docker daemon in your development machine or balena device. -(See also the \`balena push\` command for the option of building images in the -balenaCloud build servers.) - -You must provide either an application or a device-type/architecture pair to use -the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile). - -This command will look into the given source directory (or the current working -directory if one isn't specified) for a docker-compose.yml file, and if found, -each service defined in the compose file will be built. If a compose file isn't -found, it will look for a Dockerfile[.template] file (or alternative Dockerfile -specified with the \`--dockerfile\` option), and if no dockerfile is found, it -will try to generate one. - -${registrySecretsHelp} - -${dockerignoreHelp} - -Examples: - - $ balena build - $ balena build ./source/ - $ balena build --deviceType raspberrypi3 --arch armv7hf --emulated - $ balena build --application MyApp ./source/ - $ balena build --docker /var/run/docker.sock # Linux, Mac - $ balena build --docker //./pipe/docker_engine # Windows - $ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem\ -`, - options: dockerUtils.appendOptions( - compose.appendOptions([ - { - signature: 'arch', - parameter: 'arch', - description: 'The architecture to build for', - alias: 'A', - }, - { - signature: 'deviceType', - parameter: 'deviceType', - description: 'The type of device this build is for', - alias: 'd', - }, - { - signature: 'application', - parameter: 'application', - description: 'The target balena application this build is for', - alias: 'a', - }, - ]), - ), - async action(params, options) { - // compositions with many services trigger misleading warnings - // @ts-ignore editing property that isn't typed but does exist - require('events').defaultMaxListeners = 1000; - - const sdk = getBalenaSdk(); - const { ExpectedError } = require('../errors'); - const { checkLoggedIn } = require('../utils/patterns'); - const { validateProjectDirectory } = require('../utils/compose_ts'); - const helpers = require('../utils/helpers'); - const Logger = require('../utils/logger'); - - const logger = Logger.getLogger(); - logger.logDebug('Parsing input...'); - - // `build` accepts `[source]` as a parameter, but compose expects it - // as an option. swap them here - if (options.source == null) { - options.source = params.source; - } - delete params.source; - - const { application, arch, deviceType } = options; - - if ( - (application == null && (arch == null || deviceType == null)) || - (application != null && (arch != null || deviceType != null)) - ) { - throw new ExpectedError( - 'You must specify either an application or an arch/deviceType pair to build for', - ); - } - if (application) { - await checkLoggedIn(); - } - - return validateProjectDirectory(sdk, { - dockerfilePath: options.dockerfile, - noParentCheck: options['noparent-check'] || false, - projectPath: options.source || '.', - registrySecretsPath: options['registry-secrets'], - }) - .then(function ({ dockerfilePath, registrySecrets }) { - options.dockerfile = dockerfilePath; - options['registry-secrets'] = registrySecrets; - - if (arch != null && deviceType != null) { - return [undefined, arch, deviceType]; - } else { - return helpers - .getAppWithArch(application) - .then((app) => [app, app.arch, app.device_type]); - } - }) - - .then(function ([app, resolvedArch, resolvedDeviceType]) { - return Promise.all([ - dockerUtils.getDocker(options), - dockerUtils.generateBuildOpts(options), - compose.generateOpts(options), - ]).then(([docker, buildOpts, composeOpts]) => - buildProject(docker, logger, composeOpts, { - app, - arch: resolvedArch, - deviceType: resolvedDeviceType, - buildEmulated: !!options.emulated, - buildOpts, - }), - ); - }); - }, -}; diff --git a/lib/actions/index.ts b/lib/actions/index.ts index ab3774d2..3d92e232 100644 --- a/lib/actions/index.ts +++ b/lib/actions/index.ts @@ -16,6 +16,5 @@ limitations under the License. export * as help from './help'; -export { build } from './build'; export { deploy } from './deploy'; export { preload } from './preload'; diff --git a/lib/app-capitano.ts b/lib/app-capitano.ts index d764a50c..dab08095 100644 --- a/lib/app-capitano.ts +++ b/lib/app-capitano.ts @@ -50,7 +50,6 @@ capitano.command(actions.help.help); capitano.command(actions.preload); // ------------ Local build and deploy ------- -capitano.command(actions.build); capitano.command(actions.deploy); export function run(argv: string[]) { diff --git a/lib/command.ts b/lib/command.ts index 57be66f6..8c0bb12d 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -78,11 +78,25 @@ export default abstract class BalenaCommand extends Command { * Note, currently public to allow use outside of derived commands * (as some command implementations require this. Can be made protected * if this changes). + * + * @throws {NotLoggedInError} */ public static async checkLoggedIn() { await (await import('./utils/patterns')).checkLoggedIn(); } + /** + * Throw NotLoggedInError if not logged in when condition true. + * + * @param {boolean} doCheck - will check if true. + * @throws {NotLoggedInError} + */ + public static async checkLoggedInIf(doCheck: boolean) { + if (doCheck) { + await this.checkLoggedIn(); + } + } + /** * Read stdin contents and make available to command. * @@ -93,6 +107,13 @@ export default abstract class BalenaCommand extends Command { this.stdin = await (await import('get-stdin'))(); } + /** + * Get a logger instance. + */ + protected static async getLogger() { + return (await import('./utils/logger')).getLogger(); + } + protected async init() { const ctr = this.constructor as typeof BalenaCommand; diff --git a/lib/preparser.ts b/lib/preparser.ts index 0a97fe6f..b3a79d2f 100644 --- a/lib/preparser.ts +++ b/lib/preparser.ts @@ -153,6 +153,7 @@ export const convertedCommands = [ 'config:read', 'config:reconfigure', 'config:write', + 'build', 'device', 'device:identify', 'device:init', diff --git a/lib/utils/compose-types.d.ts b/lib/utils/compose-types.d.ts index 1ae4c62a..8365e73b 100644 --- a/lib/utils/compose-types.d.ts +++ b/lib/utils/compose-types.d.ts @@ -55,6 +55,21 @@ export interface ComposeOpts { projectPath: string; } +export interface ComposeCliFlags { + emulated: boolean; + dockerfile?: string; + logs: boolean; + nologs: boolean; + gitignore: boolean; + 'multi-dockerignore': boolean; + nogitignore: boolean; + 'noparent-check': boolean; + 'registry-secrets'?: string | RegistrySecrets; + 'convert-eol': boolean; + 'noconvert-eol': boolean; + projectName?: string; +} + export interface ComposeProject { path: string; name: string; diff --git a/lib/utils/compose.js b/lib/utils/compose.js index b8cf1d81..3ef02386 100644 --- a/lib/utils/compose.js +++ b/lib/utils/compose.js @@ -16,24 +16,11 @@ */ import * as path from 'path'; - import { ExpectedError } from '../errors'; import { getChalk, stripIndent } from './lazy'; -export const appendProjectOptions = (opts) => - opts.concat([ - { - signature: 'projectName', - parameter: 'projectName', - description: - 'Specify an alternate project name; default is the directory name', - alias: 'n', - }, - ]); - export function appendOptions(opts) { - const { isV12 } = require('./version'); - return appendProjectOptions(opts).concat([ + return opts.concat([ { signature: 'emulated', description: 'Run an emulated build using Qemu', @@ -48,21 +35,16 @@ export function appendOptions(opts) { }, { signature: 'logs', - description: isV12() - ? 'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.' - : 'Display full log output', + description: + 'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.', + boolean: true, + }, + { + signature: 'nologs', + description: + 'Hide the image build log output (produce less verbose output)', boolean: true, }, - ...(isV12() - ? [ - { - signature: 'nologs', - description: - 'Hide the image build log output (produce less verbose output)', - boolean: true, - }, - ] - : []), { signature: 'gitignore', alias: 'g', @@ -100,24 +82,23 @@ export function appendOptions(opts) { }, { signature: 'convert-eol', - description: isV12() - ? 'No-op and deprecated since balena CLI v12.0.0' - : `\ -On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format). \ -Source files are not modified.`, + description: 'No-op and deprecated since balena CLI v12.0.0', boolean: true, alias: 'l', }, - ...(isV12() - ? [ - { - signature: 'noconvert-eol', - description: - "Don't convert line endings from CRLF (Windows format) to LF (Unix format).", - boolean: true, - }, - ] - : []), + { + signature: 'noconvert-eol', + description: + "Don't convert line endings from CRLF (Windows format) to LF (Unix format).", + boolean: true, + }, + { + signature: 'projectName', + parameter: 'projectName', + description: + 'Specify an alternate project name; default is the directory name', + alias: 'n', + }, ]); } @@ -126,7 +107,6 @@ Source files are not modified.`, */ export function generateOpts(options) { const { promises: fs } = require('fs'); - const { isV12 } = require('./version'); if (options.gitignore && options['multi-dockerignore']) { throw new ExpectedError( @@ -136,8 +116,8 @@ export function generateOpts(options) { return fs.realpath(options.source || '.').then((projectPath) => ({ projectName: options.projectName, projectPath, - inlineLogs: !options.nologs && (!!options.logs || isV12()), - convertEol: isV12() ? !options['noconvert-eol'] : !!options['convert-eol'], + inlineLogs: !options.nologs, + convertEol: !options['noconvert-eol'], dockerfilePath: options.dockerfile, multiDockerignore: !!options['multi-dockerignore'], nogitignore: !options.gitignore, diff --git a/lib/utils/compose_ts.ts b/lib/utils/compose_ts.ts index 6e259463..712f0c57 100644 --- a/lib/utils/compose_ts.ts +++ b/lib/utils/compose_ts.ts @@ -24,12 +24,11 @@ import { Composition } from 'resin-compose-parse'; import * as MultiBuild from 'resin-multibuild'; import { Readable } from 'stream'; import * as tar from 'tar-stream'; -import { stripIndent } from './lazy'; - import { ExpectedError } from '../errors'; -import { getBalenaSdk, getChalk } from '../utils/lazy'; +import { getBalenaSdk, getChalk, stripIndent } from './lazy'; import { BuiltImage, + ComposeCliFlags, ComposeOpts, ComposeProject, Release, @@ -38,6 +37,7 @@ import { } from './compose-types'; import { DeviceInfo } from './device/api'; import Logger = require('./logger'); +import { flags } from '@oclif/command'; export interface RegistrySecrets { [registryAddress: string]: { @@ -897,3 +897,60 @@ export function createRunLoop(tick: (...args: any[]) => void) { }; return runloop; } + +export const composeCliFlags: flags.Input = { + emulated: flags.boolean({ + description: 'Run an emulated build using Qemu', + char: 'e', + }), + dockerfile: flags.string({ + description: + 'Alternative Dockerfile name/path, relative to the source folder', + }), + logs: flags.boolean({ + description: + 'No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.', + }), + nologs: flags.boolean({ + description: + 'Hide the image build log output (produce less verbose output)', + }), + gitignore: flags.boolean({ + description: stripIndent` + Consider .gitignore files in addition to the .dockerignore file. This reverts + to the CLI v11 behavior/implementation (deprecated) if compatibility is required + until your project can be adapted.`, + char: 'g', + }), + 'multi-dockerignore': flags.boolean({ + description: + 'Have each service use its own .dockerignore file. See "balena help build".', + char: 'm', + }), + nogitignore: flags.boolean({ + description: `No-op (default behavior) since balena CLI v12.0.0. See "balena help build".`, + char: 'G', + }), + 'noparent-check': flags.boolean({ + description: + "Disable project validation check of 'docker-compose.yml' file in parent folder", + }), + 'registry-secrets': flags.string({ + description: + 'Path to a YAML or JSON file with passwords for a private Docker registry', + char: 'R', + }), + 'convert-eol': flags.boolean({ + description: 'No-op and deprecated since balena CLI v12.0.0', + char: 'l', + }), + 'noconvert-eol': flags.boolean({ + description: + "Don't convert line endings from CRLF (Windows format) to LF (Unix format).", + }), + projectName: flags.string({ + description: + 'Specify an alternate project name; default is the directory name', + char: 'n', + }), +}; diff --git a/lib/utils/docker.ts b/lib/utils/docker.ts index 40e65ac4..d8698a12 100644 --- a/lib/utils/docker.ts +++ b/lib/utils/docker.ts @@ -16,6 +16,8 @@ */ import type * as dockerode from 'dockerode'; +import { flags } from '@oclif/command'; +import { parseAsInteger } from './validation'; export * from './docker-js'; @@ -23,6 +25,77 @@ interface BalenaEngineVersion extends dockerode.DockerVersion { Engine?: string; } +export interface DockerConnectionCliFlags { + docker?: string; + dockerHost?: string; + dockerPort?: number; + ca?: string; + cert?: string; + key?: string; +} + +export interface DockerCliFlags extends DockerConnectionCliFlags { + tag?: string; + buildArg?: string; // maps to 'arg' + arg?: string; // Not part of command profile + 'cache-from'?: string; // maps to 'image-list' + 'image-list'?: string; // Not part of command profile + nocache: boolean; + squash: boolean; +} + +export const dockerConnectionCliFlags: flags.Input = { + docker: flags.string({ + description: 'Path to a local docker socket (e.g. /var/run/docker.sock)', + char: 'P', + }), + dockerHost: flags.string({ + description: + 'Docker daemon hostname or IP address (dev machine or balena device) ', + char: 'h', + }), + dockerPort: flags.integer({ + description: + 'Docker daemon TCP port number (hint: 2375 for balena devices)', + char: 'p', + parse: (p) => parseAsInteger(p, 'dockerPort'), + }), + ca: flags.string({ + description: 'Docker host TLS certificate authority file', + }), + cert: flags.string({ + description: 'Docker host TLS certificate file', + }), + key: flags.string({ + description: 'Docker host TLS key file', + }), +}; + +export const dockerCliFlags: flags.Input = { + tag: flags.string({ + description: 'The alias to the generated image', + char: 't', + }), + buildArg: flags.string({ + description: + 'Set a build-time variable (eg. "-B \'ARG=value\'"). Can be specified multiple times.', + char: 'B', + // Maps to flag `arg` + }), + 'cache-from': flags.string({ + description: `\ +Comma-separated list (no spaces) of image names for build cache resolution. \ +Implements the same feature as the "docker build --cache-from" option.`, + // Maps to flag `image-list` + }), + nocache: flags.boolean({ + description: "Don't use docker layer caching when building", + }), + squash: flags.boolean({ + description: 'Squash newly built layers into a single new layer', + }), +}; + export async function isBalenaEngine(docker: dockerode): Promise { // dockerVersion.Engine should equal 'balena-engine' for the current/latest // version of balenaEngine, but it was at one point (mis)spelt 'balaena': diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index aa8525ce..27f92be0 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -36,7 +36,7 @@ Primary commands: device show info about a single device tunnel tunnel local ports to your balenaOS device preload preload an app on a disk image (or Edison zip archive) - build [source] Build a single image or a multicontainer project locally + build [source] build a project locally deploy [image] Deploy a single image or a multicontainer project to a balena application join [deviceiporhostname] move a local device to an application on another balena server leave [deviceiporhostname] remove a local device from its balena application