diff --git a/lib/actions/build.coffee b/lib/actions/build.coffee deleted file mode 100644 index f34f82b7..00000000 --- a/lib/actions/build.coffee +++ /dev/null @@ -1,186 +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. -### - -# Imported here because it's needed for the setup -# of this action -Promise = require('bluebird') -dockerUtils = require('../utils/docker') -compose = require('../utils/compose') -{ registrySecretsHelp } = require('../utils/messages') -{ getBalenaSdk } = require('../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 -### -buildProject = (docker, logger, composeOpts, opts) -> - { loadProject } = require('../utils/compose_ts') - Promise.resolve(loadProject(logger, composeOpts)) - .then (project) -> - appType = opts.app?.application_type?[0] - if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer - logger.logWarn( - 'Target application does not support multiple containers.\n' + - 'Continuing with build, but you will not be able to deploy.' - ) - - compose.buildProject( - docker - logger - project.path - project.name - project.composition - opts.arch - opts.deviceType - opts.buildEmulated - opts.buildOpts - composeOpts.inlineLogs - opts.convertEol - composeOpts.dockerfilePath - ) - .then -> - logger.outputDeferredMessages() - logger.logSuccess('Build succeeded!') - .tapCatch (e) -> - logger.logError('Build failed') - -module.exports = - 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} - - 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' - }, - ] - action: (params, options) -> - # compositions with many services trigger misleading warnings - require('events').defaultMaxListeners = 1000 - - sdk = getBalenaSdk() - { ExpectedError } = require('../errors') - { checkLoggedIn } = require('../utils/patterns') - { validateProjectDirectory } = require('../utils/compose_ts') - helpers = require('../utils/helpers') - Logger = require('../utils/logger') - - logger = Logger.getLogger() - logger.logDebug('Parsing input...') - - # `build` accepts `[source]` as a parameter, but compose expects it - # as an option. swap them here - options.source ?= params.source - delete params.source - - options.convertEol = options['convert-eol'] || false - delete options['convert-eol'] - - { application, arch, deviceType } = options - - Promise.try -> - if (not (arch? and deviceType?) and not application?) or (application? and (arch? or deviceType?)) - throw new ExpectedError('You must specify either an application or an arch/deviceType pair to build for') - if (application) - checkLoggedIn() - .then -> - validateProjectDirectory(sdk, { - dockerfilePath: options.dockerfile, - noParentCheck: options['noparent-check'] || false, - projectPath: options.source || '.', - registrySecretsPath: options['registry-secrets'], - }) - .then ({ dockerfilePath, registrySecrets }) -> - options.dockerfile = dockerfilePath - options['registry-secrets'] = registrySecrets - - if arch? and deviceType? - [ undefined, arch, deviceType ] - else - Promise.join( - helpers.getApplication(application) - helpers.getArchAndDeviceType(application) - (app, { arch, device_type }) -> - app.arch = arch - app.device_type = device_type - return app - ) - .then (app) -> - [ app, app.arch, app.device_type ] - - .then ([ app, arch, deviceType ]) -> - Promise.join( - dockerUtils.getDocker(options) - dockerUtils.generateBuildOpts(options) - compose.generateOpts(options) - (docker, buildOpts, composeOpts) -> - buildProject(docker, logger, composeOpts, { - app - arch - deviceType - buildEmulated: !!options.emulated - buildOpts - convertEol: options.convertEol - }) - ) diff --git a/lib/actions/build.js b/lib/actions/build.js new file mode 100644 index 00000000..f2f03b35 --- /dev/null +++ b/lib/actions/build.js @@ -0,0 +1,221 @@ +/** + * @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. + */ + +// Imported here because it's needed for the setup +// of this action +import * as Promise from 'bluebird'; + +import * as dockerUtils from '../utils/docker'; +import * as compose from '../utils/compose'; +import { 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 +*/ +const buildProject = function(docker, logger, composeOpts, opts) { + const { loadProject } = require('../utils/compose_ts'); + return Promise.resolve(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, + opts.convertEol, + composeOpts.dockerfilePath, + ); + }) + .then(function() { + logger.outputDeferredMessages(); + logger.logSuccess('Build succeeded!'); + }) + .tapCatch(() => { + logger.logError('Build failed'); + }); +}; + +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} + +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', + }, + ]), + ), + 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; + + options.convertEol = options['convert-eol'] || false; + delete options['convert-eol']; + + const { application, arch, deviceType } = options; + + return Promise.try(function() { + 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) { + return checkLoggedIn(); + } + }) + .then(() => + 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 Promise.join( + helpers.getApplication(application), + helpers.getArchAndDeviceType(application), + function( + app, + { arch: resolvedArch, device_type: resolvedDeviceType }, + ) { + // @ts-ignore extending the app object + app.arch = resolvedArch; + app.device_type = resolvedDeviceType; + return app; + }, + ).then(app => + // @ts-ignore using the extended prop + [app, app.arch, app.device_type], + ); + } + }) + + .then(function([app, resolvedArch, resolvedDeviceType]) { + return Promise.join( + dockerUtils.getDocker(options), + dockerUtils.generateBuildOpts(options), + compose.generateOpts(options), + (docker, buildOpts, composeOpts) => + buildProject(docker, logger, composeOpts, { + app, + arch: resolvedArch, + deviceType: resolvedDeviceType, + buildEmulated: !!options.emulated, + buildOpts, + convertEol: options.convertEol, + }), + ); + }); + }, +}; diff --git a/lib/actions/index.coffee b/lib/actions/index.coffee index 524c8978..eb7fbb26 100644 --- a/lib/actions/index.coffee +++ b/lib/actions/index.coffee @@ -26,7 +26,7 @@ module.exports = os: require('./os') config: require('./config') ssh: require('./ssh') - build: require('./build') + build: require('./build').build deploy: require('./deploy') util: require('./util') preload: require('./preload').preload