2017-12-11 22:37:56 +00:00
|
|
|
# Imported here because it's needed for the setup
|
|
|
|
# of this action
|
2017-03-29 11:03:40 +00:00
|
|
|
Promise = require('bluebird')
|
2017-04-24 19:05:18 +00:00
|
|
|
dockerUtils = require('../utils/docker')
|
2017-12-11 22:37:56 +00:00
|
|
|
compose = require('../utils/compose')
|
2019-02-27 18:01:47 +00:00
|
|
|
{ registrySecretsHelp } = require('../utils/messages')
|
2017-12-11 22:37:56 +00:00
|
|
|
|
|
|
|
###
|
|
|
|
Opts must be an object with the following keys:
|
|
|
|
|
|
|
|
app: the application instance to deploy to
|
|
|
|
image: the image to deploy; optional
|
|
|
|
shouldPerformBuild
|
|
|
|
shouldUploadLogs
|
|
|
|
buildEmulated
|
|
|
|
buildOpts: arguments to forward to docker build command
|
|
|
|
###
|
|
|
|
deployProject = (docker, logger, composeOpts, opts) ->
|
|
|
|
_ = require('lodash')
|
|
|
|
doodles = require('resin-doodles')
|
2018-10-19 14:38:50 +00:00
|
|
|
sdk = require('balena-sdk').fromSharedOptions()
|
2017-12-11 22:37:56 +00:00
|
|
|
|
|
|
|
compose.loadProject(
|
|
|
|
logger
|
|
|
|
composeOpts.projectPath
|
|
|
|
composeOpts.projectName
|
|
|
|
opts.image
|
|
|
|
)
|
|
|
|
.then (project) ->
|
2018-03-19 18:58:57 +00:00
|
|
|
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
|
|
|
|
throw new Error('Target application does not support multiple containers. Aborting!')
|
|
|
|
|
2017-12-11 22:37:56 +00:00
|
|
|
# find which services use images that already exist locally
|
|
|
|
Promise.map project.descriptors, (d) ->
|
|
|
|
# unconditionally build (or pull) if explicitly requested
|
|
|
|
return d if opts.shouldPerformBuild
|
|
|
|
docker.getImage(d.image.tag ? d.image).inspect()
|
|
|
|
.return(d.serviceName)
|
|
|
|
.catchReturn()
|
|
|
|
.filter (d) -> !!d
|
|
|
|
.then (servicesToSkip) ->
|
|
|
|
# multibuild takes in a composition and always attempts to
|
|
|
|
# build or pull all services. we workaround that here by
|
|
|
|
# passing a modified composition.
|
|
|
|
compositionToBuild = _.cloneDeep(project.composition)
|
|
|
|
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
|
|
|
|
if _.size(compositionToBuild.services) is 0
|
|
|
|
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
|
|
|
|
return {}
|
|
|
|
compose.buildProject(
|
|
|
|
docker
|
|
|
|
logger
|
|
|
|
project.path
|
|
|
|
project.name
|
|
|
|
compositionToBuild
|
|
|
|
opts.app.arch
|
|
|
|
opts.app.device_type
|
|
|
|
opts.buildEmulated
|
|
|
|
opts.buildOpts
|
|
|
|
composeOpts.inlineLogs
|
|
|
|
)
|
|
|
|
.then (builtImages) ->
|
|
|
|
_.keyBy(builtImages, 'serviceName')
|
|
|
|
.then (builtImages) ->
|
|
|
|
project.descriptors.map (d) ->
|
|
|
|
builtImages[d.serviceName] ? {
|
|
|
|
serviceName: d.serviceName,
|
|
|
|
name: d.image.tag ? d.image
|
|
|
|
logs: 'Build skipped; image for service already exists.'
|
|
|
|
props: {}
|
|
|
|
}
|
|
|
|
.then (images) ->
|
2018-03-19 18:58:05 +00:00
|
|
|
if opts.app.application_type?[0]?.is_legacy
|
|
|
|
chalk = require('chalk')
|
|
|
|
legacyDeploy = require('../utils/deploy-legacy')
|
|
|
|
|
|
|
|
msg = chalk.yellow('Target application requires legacy deploy method.')
|
|
|
|
logger.logWarn(msg)
|
|
|
|
|
|
|
|
return Promise.join(
|
|
|
|
docker
|
|
|
|
logger
|
|
|
|
sdk.auth.getToken()
|
|
|
|
sdk.auth.whoami()
|
2018-10-19 14:38:50 +00:00
|
|
|
sdk.settings.get('balenaUrl')
|
2018-03-19 18:58:05 +00:00
|
|
|
{
|
2019-03-07 00:49:47 +00:00
|
|
|
# opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
|
|
|
appName: opts.appName
|
2018-03-19 18:58:05 +00:00
|
|
|
imageName: images[0].name
|
|
|
|
buildLogs: images[0].logs
|
|
|
|
shouldUploadLogs: opts.shouldUploadLogs
|
|
|
|
}
|
|
|
|
legacyDeploy
|
|
|
|
)
|
|
|
|
.then (releaseId) ->
|
2018-03-20 12:05:14 +00:00
|
|
|
sdk.models.release.get(releaseId, $select: [ 'commit' ])
|
2017-12-11 22:37:56 +00:00
|
|
|
Promise.join(
|
|
|
|
sdk.auth.getUserId()
|
|
|
|
sdk.auth.getToken()
|
|
|
|
sdk.settings.get('apiUrl')
|
|
|
|
(userId, auth, apiEndpoint) ->
|
|
|
|
compose.deployProject(
|
|
|
|
docker
|
|
|
|
logger
|
|
|
|
project.composition
|
|
|
|
images
|
|
|
|
opts.app.id
|
|
|
|
userId
|
|
|
|
"Bearer #{auth}"
|
|
|
|
apiEndpoint
|
|
|
|
!opts.shouldUploadLogs
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.then (release) ->
|
|
|
|
logger.logSuccess('Deploy succeeded!')
|
|
|
|
logger.logSuccess("Release: #{release.commit}")
|
|
|
|
console.log()
|
|
|
|
console.log(doodles.getDoodle()) # Show charlie
|
|
|
|
console.log()
|
|
|
|
.tapCatch (e) ->
|
|
|
|
logger.logError('Deploy failed')
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
module.exports =
|
|
|
|
signature: 'deploy <appName> [image]'
|
2018-10-19 14:38:50 +00:00
|
|
|
description: 'Deploy a single image or a multicontainer project to a balena application'
|
2019-02-27 18:01:47 +00:00
|
|
|
help: """
|
2017-10-09 17:10:50 +00:00
|
|
|
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
|
|
|
|
2019-02-26 13:32:27 +00:00
|
|
|
Use this command to deploy an image or a complete multicontainer project to an
|
|
|
|
application, optionally building it first. The source images are searched for
|
|
|
|
(and optionally built) using the docker daemon in your development machine or
|
|
|
|
balena device. (See also the `balena push` command for the option of building
|
2019-02-27 18:01:47 +00:00
|
|
|
the image in the balenaCloud build servers.)
|
2019-02-26 13:32:27 +00:00
|
|
|
|
2017-12-11 22:37:56 +00:00
|
|
|
Unless an image is specified, this command will look into the current directory
|
|
|
|
(or the one specified by --source) for a compose file. If one is found, this
|
|
|
|
command will deploy each service defined in the compose file, building it first
|
|
|
|
if an image for it doesn't exist. If a compose file isn't found, the command
|
|
|
|
will look for a Dockerfile, and if yet that isn't found, it will try to
|
|
|
|
generate one.
|
|
|
|
|
2017-10-09 17:10:50 +00:00
|
|
|
To deploy to an app on which you're a collaborator, use
|
2018-10-19 14:38:50 +00:00
|
|
|
`balena deploy <appOwnerUsername>/<appName>`.
|
2017-03-29 11:03:40 +00:00
|
|
|
|
2019-02-27 18:01:47 +00:00
|
|
|
When --build is used, all options supported by `balena build` are also
|
|
|
|
supported by this command.
|
|
|
|
|
|
|
|
#{registrySecretsHelp}
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
Examples:
|
2017-12-11 22:37:56 +00:00
|
|
|
|
2018-10-19 14:38:50 +00:00
|
|
|
$ balena deploy myApp
|
|
|
|
$ balena deploy myApp --build --source myBuildDir/
|
|
|
|
$ balena deploy myApp myApp/myImage
|
2019-02-27 18:01:47 +00:00
|
|
|
"""
|
2017-03-29 11:03:40 +00:00
|
|
|
permission: 'user'
|
2017-12-11 22:37:56 +00:00
|
|
|
primary: true
|
|
|
|
options: dockerUtils.appendOptions compose.appendOptions [
|
2017-03-29 11:03:40 +00:00
|
|
|
{
|
|
|
|
signature: 'source'
|
|
|
|
parameter: 'source'
|
2017-12-11 22:37:56 +00:00
|
|
|
description: 'Specify an alternate source directory; default is the working directory'
|
2017-03-29 11:03:40 +00:00
|
|
|
alias: 's'
|
2017-04-24 10:06:53 +00:00
|
|
|
},
|
2017-12-11 22:37:56 +00:00
|
|
|
{
|
|
|
|
signature: 'build'
|
|
|
|
boolean: true
|
|
|
|
description: 'Force a rebuild before deploy'
|
|
|
|
alias: 'b'
|
|
|
|
},
|
2017-04-24 10:06:53 +00:00
|
|
|
{
|
|
|
|
signature: 'nologupload'
|
|
|
|
description: "Don't upload build logs to the dashboard with image (if building)"
|
|
|
|
boolean: true
|
2017-03-29 11:03:40 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
action: (params, options, done) ->
|
2017-12-11 22:37:56 +00:00
|
|
|
# compositions with many services trigger misleading warnings
|
|
|
|
require('events').defaultMaxListeners = 1000
|
2019-02-26 13:32:27 +00:00
|
|
|
{ validateComposeOptions } = require('../utils/compose_ts')
|
2017-12-11 22:37:56 +00:00
|
|
|
helpers = require('../utils/helpers')
|
2017-06-28 16:30:37 +00:00
|
|
|
Logger = require('../utils/logger')
|
2017-12-11 22:37:56 +00:00
|
|
|
|
2017-06-28 16:30:37 +00:00
|
|
|
logger = new Logger()
|
2017-04-24 12:48:43 +00:00
|
|
|
|
2017-12-11 22:37:56 +00:00
|
|
|
logger.logDebug('Parsing input...')
|
|
|
|
|
2019-03-07 00:49:47 +00:00
|
|
|
appName = undefined
|
2017-12-11 22:37:56 +00:00
|
|
|
Promise.try ->
|
|
|
|
{ appName, image } = params
|
|
|
|
|
2018-10-19 14:38:50 +00:00
|
|
|
# look into "balena build" options if appName isn't given
|
2017-12-11 22:37:56 +00:00
|
|
|
appName = options.application if not appName?
|
|
|
|
delete options.application
|
|
|
|
|
2019-02-26 13:32:27 +00:00
|
|
|
validateComposeOptions(options)
|
|
|
|
|
2017-12-11 22:37:56 +00:00
|
|
|
if not appName?
|
|
|
|
throw new Error('Please specify the name of the application to deploy')
|
|
|
|
|
|
|
|
if image? and options.build
|
|
|
|
throw new Error('Build option is not applicable when specifying an image')
|
|
|
|
|
|
|
|
Promise.join(
|
|
|
|
helpers.getApplication(appName)
|
|
|
|
helpers.getArchAndDeviceType(appName)
|
|
|
|
(app, { arch, device_type }) ->
|
|
|
|
app.arch = arch
|
|
|
|
app.device_type = device_type
|
|
|
|
return app
|
|
|
|
)
|
|
|
|
.then (app) ->
|
|
|
|
[ app, image, !!options.build, !options.nologupload ]
|
|
|
|
|
|
|
|
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
|
|
|
Promise.join(
|
|
|
|
dockerUtils.getDocker(options)
|
|
|
|
dockerUtils.generateBuildOpts(options)
|
|
|
|
compose.generateOpts(options)
|
|
|
|
(docker, buildOpts, composeOpts) ->
|
|
|
|
deployProject(docker, logger, composeOpts, {
|
|
|
|
app
|
2019-03-07 00:49:47 +00:00
|
|
|
appName # may be prefixed by 'owner/', unlike app.app_name
|
2017-12-11 22:37:56 +00:00
|
|
|
image
|
|
|
|
shouldPerformBuild
|
|
|
|
shouldUploadLogs
|
|
|
|
buildEmulated: !!options.emulated
|
|
|
|
buildOpts
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.asCallback(done)
|