mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-20 22:23:07 +00:00
Convert deploy command to oclif
Change-type: patch Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
parent
e96fca551e
commit
8cb413c1c9
@ -156,7 +156,7 @@ const capitanoDoc = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Deploy',
|
title: 'Deploy',
|
||||||
files: ['build/actions-oclif/build.js', 'build/actions/deploy.js'],
|
files: ['build/actions-oclif/build.js', 'build/actions-oclif/deploy.js'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Platform',
|
title: 'Platform',
|
||||||
|
119
doc/cli.markdown
119
doc/cli.markdown
@ -256,7 +256,7 @@ Users are encouraged to regularly update the balena CLI to the latest version.
|
|||||||
- Deploy
|
- Deploy
|
||||||
|
|
||||||
- [build [source]](#build-source)
|
- [build [source]](#build-source)
|
||||||
- [deploy <appName> [image]](#deploy-appname-image)
|
- [deploy <appname> [image]](#deploy-appname-image)
|
||||||
|
|
||||||
- Platform
|
- Platform
|
||||||
|
|
||||||
@ -2488,6 +2488,30 @@ Don't use docker layer caching when building
|
|||||||
|
|
||||||
Squash newly built layers into a single new layer
|
Squash newly built layers into a single new layer
|
||||||
|
|
||||||
|
#### -P, --docker DOCKER
|
||||||
|
|
||||||
|
Path to a local docker socket (e.g. /var/run/docker.sock)
|
||||||
|
|
||||||
|
#### -h, --dockerHost DOCKERHOST
|
||||||
|
|
||||||
|
Docker daemon hostname or IP address (dev machine or balena device)
|
||||||
|
|
||||||
|
#### -p, --dockerPort 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
|
||||||
|
|
||||||
## deploy <appName> [image]
|
## deploy <appName> [image]
|
||||||
|
|
||||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||||
@ -2509,9 +2533,6 @@ will try to generate one.
|
|||||||
To deploy to an app on which you're a collaborator, use
|
To deploy to an app on which you're a collaborator, use
|
||||||
`balena deploy <appOwnerUsername>/<appName>`.
|
`balena deploy <appOwnerUsername>/<appName>`.
|
||||||
|
|
||||||
When --build is used, all options supported by `balena build` are also supported
|
|
||||||
by this command.
|
|
||||||
|
|
||||||
REGISTRY SECRETS
|
REGISTRY SECRETS
|
||||||
The --registry-secrets option specifies a JSON or YAML file containing private
|
The --registry-secrets option specifies a JSON or YAML file containing private
|
||||||
Docker registry usernames and passwords to be used when pulling base images.
|
Docker registry usernames and passwords to be used when pulling base images.
|
||||||
@ -2591,25 +2612,35 @@ Examples:
|
|||||||
$ balena deploy myApp --build --source myBuildDir/
|
$ balena deploy myApp --build --source myBuildDir/
|
||||||
$ balena deploy myApp myApp/myImage
|
$ balena deploy myApp myApp/myImage
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
#### APPNAME
|
||||||
|
|
||||||
|
the name of the application to deploy to
|
||||||
|
|
||||||
|
#### IMAGE
|
||||||
|
|
||||||
|
the image to deploy
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
#### --source, -s <source>
|
#### -s, --source SOURCE
|
||||||
|
|
||||||
Specify an alternate source directory; default is the working directory
|
specify an alternate source directory; default is the working directory
|
||||||
|
|
||||||
#### --build, -b
|
#### -b, --build
|
||||||
|
|
||||||
Force a rebuild before deploy
|
force a rebuild before deploy
|
||||||
|
|
||||||
#### --nologupload
|
#### --nologupload
|
||||||
|
|
||||||
Don't upload build logs to the dashboard with image (if building)
|
don't upload build logs to the dashboard with image (if building)
|
||||||
|
|
||||||
#### --emulated, -e
|
#### -e, --emulated
|
||||||
|
|
||||||
Run an emulated build using Qemu
|
Run an emulated build using Qemu
|
||||||
|
|
||||||
#### --dockerfile <Dockerfile>
|
#### --dockerfile DOCKERFILE
|
||||||
|
|
||||||
Alternative Dockerfile name/path, relative to the source folder
|
Alternative Dockerfile name/path, relative to the source folder
|
||||||
|
|
||||||
@ -2621,17 +2652,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)
|
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
|
Consider .gitignore files in addition to the .dockerignore file. This reverts
|
||||||
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
to the CLI v11 behavior/implementation (deprecated) if compatibility is required
|
||||||
until your project can be adapted.
|
until your project can be adapted.
|
||||||
|
|
||||||
#### --multi-dockerignore, -m
|
#### -m, --multi-dockerignore
|
||||||
|
|
||||||
Have each service use its own .dockerignore file. See "balena help build".
|
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".
|
No-op (default behavior) since balena CLI v12.0.0. See "balena help build".
|
||||||
|
|
||||||
@ -2639,11 +2670,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
|
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
|
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
|
No-op and deprecated since balena CLI v12.0.0
|
||||||
|
|
||||||
@ -2651,43 +2682,19 @@ No-op and deprecated since balena CLI v12.0.0
|
|||||||
|
|
||||||
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
|
||||||
|
|
||||||
#### --projectName, -n <projectName>
|
#### -n, --projectName PROJECTNAME
|
||||||
|
|
||||||
Specify an alternate project name; default is the directory name
|
Specify an alternate project name; default is the directory name
|
||||||
|
|
||||||
#### --docker, -P <docker>
|
#### -t, --tag TAG
|
||||||
|
|
||||||
Path to a local docker socket (e.g. /var/run/docker.sock)
|
|
||||||
|
|
||||||
#### --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>
|
|
||||||
|
|
||||||
The alias to the generated image
|
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.
|
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.
|
Comma-separated list (no spaces) of image names for build cache resolution. Implements the same feature as the "docker build --cache-from" option.
|
||||||
|
|
||||||
@ -2699,6 +2706,30 @@ Don't use docker layer caching when building
|
|||||||
|
|
||||||
Squash newly built layers into a single new layer
|
Squash newly built layers into a single new layer
|
||||||
|
|
||||||
|
#### -P, --docker DOCKER
|
||||||
|
|
||||||
|
Path to a local docker socket (e.g. /var/run/docker.sock)
|
||||||
|
|
||||||
|
#### -h, --dockerHost DOCKERHOST
|
||||||
|
|
||||||
|
Docker daemon hostname or IP address (dev machine or balena device)
|
||||||
|
|
||||||
|
#### -p, --dockerPort 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
|
||||||
|
|
||||||
# Platform
|
# Platform
|
||||||
|
|
||||||
## join [deviceIpOrHostname]
|
## join [deviceIpOrHostname]
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { flags } from '@oclif/command';
|
||||||
import Command from '../command';
|
import Command from '../command';
|
||||||
import * as cf from '../utils/common-flags';
|
|
||||||
import { getBalenaSdk } from '../utils/lazy';
|
import { getBalenaSdk } from '../utils/lazy';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../utils/compose';
|
||||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||||
@ -94,7 +93,9 @@ ${dockerignoreHelp}
|
|||||||
}),
|
}),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
help: cf.help,
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
|
// Revisit this in future release.
|
||||||
|
help: flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
354
lib/actions-oclif/deploy.ts
Normal file
354
lib/actions-oclif/deploy.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
/**
|
||||||
|
* @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 { ExpectedError } from '../errors';
|
||||||
|
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||||
|
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||||
|
import * as compose from '../utils/compose';
|
||||||
|
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||||
|
import type { DockerCliFlags } from '../utils/docker';
|
||||||
|
import { composeCliFlags } from '../utils/compose_ts';
|
||||||
|
import { dockerCliFlags } from '../utils/docker';
|
||||||
|
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
|
||||||
|
|
||||||
|
interface ApplicationWithArch extends Application {
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
|
source?: string;
|
||||||
|
build: boolean;
|
||||||
|
nologupload: boolean;
|
||||||
|
help: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArgsDef {
|
||||||
|
appName: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DeployCmd extends Command {
|
||||||
|
public static description = `\
|
||||||
|
Deploy a single image or a multicontainer project to a balena application.
|
||||||
|
|
||||||
|
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
||||||
|
|
||||||
|
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
|
||||||
|
the image in the balenaCloud build servers.)
|
||||||
|
|
||||||
|
Unless an image is specified, this command will look into the current directory
|
||||||
|
(or the one specified by --source) for a docker-compose.yml 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[.template] file (or alternative
|
||||||
|
Dockerfile specified with the \`-f\` option), and if yet that isn't found, it
|
||||||
|
will try to generate one.
|
||||||
|
|
||||||
|
To deploy to an app on which you're a collaborator, use
|
||||||
|
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||||
|
|
||||||
|
${registrySecretsHelp}
|
||||||
|
|
||||||
|
${dockerignoreHelp}
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = [
|
||||||
|
'$ balena deploy myApp',
|
||||||
|
'$ balena deploy myApp --build --source myBuildDir/',
|
||||||
|
'$ balena deploy myApp myApp/myImage',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = [
|
||||||
|
{
|
||||||
|
name: 'appName',
|
||||||
|
description: 'the name of the application to deploy to',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
description: 'the image to deploy',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
public static usage = 'deploy <appName> [image]';
|
||||||
|
|
||||||
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
source: flags.string({
|
||||||
|
description:
|
||||||
|
'specify an alternate source directory; default is the working directory',
|
||||||
|
char: 's',
|
||||||
|
}),
|
||||||
|
build: flags.boolean({
|
||||||
|
description: 'force a rebuild before deploy',
|
||||||
|
char: 'b',
|
||||||
|
}),
|
||||||
|
nologupload: flags.boolean({
|
||||||
|
description:
|
||||||
|
"don't upload build logs to the dashboard with image (if building)",
|
||||||
|
}),
|
||||||
|
...composeCliFlags,
|
||||||
|
...dockerCliFlags,
|
||||||
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
|
// Revisit this in future release.
|
||||||
|
help: flags.help({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public static primary = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
|
DeployCmd,
|
||||||
|
);
|
||||||
|
|
||||||
|
// compositions with many services trigger misleading warnings
|
||||||
|
// @ts-ignore editing property that isn't typed but does exist
|
||||||
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
|
const logger = await Command.getLogger();
|
||||||
|
logger.logDebug('Parsing input...');
|
||||||
|
|
||||||
|
const { appName, image } = params;
|
||||||
|
|
||||||
|
if (image != null && options.build) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
'Build option is not applicable when specifying an image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sdk = getBalenaSdk();
|
||||||
|
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||||
|
'../utils/compose_ts'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
options['registry-secrets'] = await getRegistrySecrets(
|
||||||
|
sdk,
|
||||||
|
options['registry-secrets'],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const {
|
||||||
|
dockerfilePath,
|
||||||
|
registrySecrets,
|
||||||
|
} = await validateProjectDirectory(sdk, {
|
||||||
|
dockerfilePath: options.dockerfile,
|
||||||
|
noParentCheck: options['noparent-check'] || false,
|
||||||
|
projectPath: options.source || '.',
|
||||||
|
registrySecretsPath: options['registry-secrets'],
|
||||||
|
});
|
||||||
|
options.dockerfile = dockerfilePath;
|
||||||
|
options['registry-secrets'] = registrySecrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpers = await import('../utils/helpers');
|
||||||
|
const app = await helpers.getAppWithArch(appName);
|
||||||
|
|
||||||
|
const dockerUtils = await import('../utils/docker');
|
||||||
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
|
dockerUtils.getDocker(options),
|
||||||
|
dockerUtils.generateBuildOpts(options),
|
||||||
|
compose.generateOpts(options),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this.deployProject(docker, logger, composeOpts, {
|
||||||
|
app,
|
||||||
|
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||||
|
image,
|
||||||
|
shouldPerformBuild: !!options.build,
|
||||||
|
shouldUploadLogs: !options.nologupload,
|
||||||
|
buildEmulated: !!options.emulated,
|
||||||
|
buildOpts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deployProject(
|
||||||
|
docker: import('docker-toolbelt'),
|
||||||
|
logger: import('../utils/logger'),
|
||||||
|
composeOpts: ComposeOpts,
|
||||||
|
opts: {
|
||||||
|
app: ApplicationWithArch; // the application instance to deploy to
|
||||||
|
appName: string;
|
||||||
|
image?: string;
|
||||||
|
dockerfilePath?: string; // alternative Dockerfile
|
||||||
|
shouldPerformBuild: boolean;
|
||||||
|
shouldUploadLogs: boolean;
|
||||||
|
buildEmulated: boolean;
|
||||||
|
buildOpts: any; // arguments to forward to docker build command
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const Bluebird = await import('bluebird');
|
||||||
|
const _ = await import('lodash');
|
||||||
|
const doodles = await import('resin-doodles');
|
||||||
|
const sdk = getBalenaSdk();
|
||||||
|
const { deployProject: $deployProject, loadProject } = await import(
|
||||||
|
'../utils/compose_ts'
|
||||||
|
);
|
||||||
|
|
||||||
|
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||||
|
|
||||||
|
return loadProject(logger, composeOpts, opts.image)
|
||||||
|
.then(function (project) {
|
||||||
|
if (
|
||||||
|
project.descriptors.length > 1 &&
|
||||||
|
!appType?.supports_multicontainer
|
||||||
|
) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
'Target application does not support multiple containers. Aborting!',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// find which services use images that already exist locally
|
||||||
|
return (
|
||||||
|
Bluebird.map(project.descriptors, function (d: any) {
|
||||||
|
// unconditionally build (or pull) if explicitly requested
|
||||||
|
if (opts.shouldPerformBuild) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return docker
|
||||||
|
.getImage(
|
||||||
|
(typeof d.image === 'string' ? d.image : d.image.tag) || '',
|
||||||
|
)
|
||||||
|
.inspect()
|
||||||
|
.then(() => {
|
||||||
|
return d.serviceName;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Ignore
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter((d) => !!d)
|
||||||
|
.then(function (servicesToSkip: any[]) {
|
||||||
|
// multibuild takes in a composition and always attempts to
|
||||||
|
// build or pull all services. we workaround that here by
|
||||||
|
// passing a modified composition.
|
||||||
|
const compositionToBuild = _.cloneDeep(project.composition);
|
||||||
|
compositionToBuild.services = _.omit(
|
||||||
|
compositionToBuild.services,
|
||||||
|
servicesToSkip,
|
||||||
|
);
|
||||||
|
if (_.size(compositionToBuild.services) === 0) {
|
||||||
|
logger.logInfo(
|
||||||
|
'Everything is up to date (use --build to force a rebuild)',
|
||||||
|
);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return compose
|
||||||
|
.buildProject(
|
||||||
|
docker,
|
||||||
|
logger,
|
||||||
|
project.path,
|
||||||
|
project.name,
|
||||||
|
compositionToBuild,
|
||||||
|
opts.app.arch,
|
||||||
|
(opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||||
|
opts.buildEmulated,
|
||||||
|
opts.buildOpts,
|
||||||
|
composeOpts.inlineLogs,
|
||||||
|
composeOpts.convertEol,
|
||||||
|
composeOpts.dockerfilePath,
|
||||||
|
composeOpts.nogitignore,
|
||||||
|
composeOpts.multiDockerignore,
|
||||||
|
)
|
||||||
|
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
||||||
|
})
|
||||||
|
.then((builtImages: any) =>
|
||||||
|
project.descriptors.map(
|
||||||
|
(d) =>
|
||||||
|
builtImages[d.serviceName] ?? {
|
||||||
|
serviceName: d.serviceName,
|
||||||
|
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||||
|
logs: 'Build skipped; image for service already exists.',
|
||||||
|
props: {},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// @ts-ignore slightly different return types of partial vs non-partial release
|
||||||
|
.then(function (images) {
|
||||||
|
if (appType?.is_legacy) {
|
||||||
|
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||||
|
|
||||||
|
const msg = getChalk().yellow(
|
||||||
|
'Target application requires legacy deploy method.',
|
||||||
|
);
|
||||||
|
logger.logWarn(msg);
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
sdk.auth.getToken(),
|
||||||
|
sdk.auth.whoami(),
|
||||||
|
sdk.settings.get('balenaUrl'),
|
||||||
|
{
|
||||||
|
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||||
|
appName: opts.appName,
|
||||||
|
imageName: images[0].name,
|
||||||
|
buildLogs: images[0].logs,
|
||||||
|
shouldUploadLogs: opts.shouldUploadLogs,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.then(([token, username, url, options]) => {
|
||||||
|
return deployLegacy(
|
||||||
|
docker,
|
||||||
|
logger,
|
||||||
|
token,
|
||||||
|
username,
|
||||||
|
url,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then((releaseId) =>
|
||||||
|
sdk.models.release.get(releaseId, { $select: ['commit'] }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all([
|
||||||
|
sdk.auth.getUserId(),
|
||||||
|
sdk.auth.getToken(),
|
||||||
|
sdk.settings.get('apiUrl'),
|
||||||
|
]).then(([userId, auth, apiEndpoint]) =>
|
||||||
|
$deployProject(
|
||||||
|
docker,
|
||||||
|
logger,
|
||||||
|
project.composition,
|
||||||
|
images,
|
||||||
|
opts.app.id,
|
||||||
|
userId,
|
||||||
|
`Bearer ${auth}`,
|
||||||
|
apiEndpoint,
|
||||||
|
!opts.shouldUploadLogs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(function (release: any) {
|
||||||
|
logger.outputDeferredMessages();
|
||||||
|
logger.logSuccess('Deploy succeeded!');
|
||||||
|
logger.logSuccess(`Release: ${release.commit}`);
|
||||||
|
console.log();
|
||||||
|
console.log(doodles.getDoodle()); // Show charlie
|
||||||
|
console.log();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.logError('Deploy failed');
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,318 +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 { ExpectedError } from '../errors';
|
|
||||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opts must be an object with the following keys:
|
|
||||||
* app: the application instance to deploy to
|
|
||||||
* image: the image to deploy; optional
|
|
||||||
* dockerfilePath: name of an alternative Dockerfile; optional
|
|
||||||
* shouldPerformBuild
|
|
||||||
* shouldUploadLogs
|
|
||||||
* buildEmulated
|
|
||||||
* buildOpts: arguments to forward to docker build command
|
|
||||||
*
|
|
||||||
* @param {any} docker
|
|
||||||
* @param {import('../utils/logger')} logger
|
|
||||||
* @param {import('../utils/compose-types').ComposeOpts} composeOpts
|
|
||||||
* @param {any} opts
|
|
||||||
*/
|
|
||||||
const deployProject = function (docker, logger, composeOpts, opts) {
|
|
||||||
const Bluebird = require('bluebird');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const doodles = require('resin-doodles');
|
|
||||||
const sdk = getBalenaSdk();
|
|
||||||
const {
|
|
||||||
deployProject: $deployProject,
|
|
||||||
loadProject,
|
|
||||||
} = require('../utils/compose_ts');
|
|
||||||
|
|
||||||
return loadProject(logger, composeOpts, opts.image)
|
|
||||||
.then(function (project) {
|
|
||||||
if (
|
|
||||||
project.descriptors.length > 1 &&
|
|
||||||
!opts.app.application_type?.[0]?.supports_multicontainer
|
|
||||||
) {
|
|
||||||
throw new ExpectedError(
|
|
||||||
'Target application does not support multiple containers. Aborting!',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// find which services use images that already exist locally
|
|
||||||
return (
|
|
||||||
Bluebird.map(project.descriptors, function (d) {
|
|
||||||
// unconditionally build (or pull) if explicitly requested
|
|
||||||
if (opts.shouldPerformBuild) {
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
return docker
|
|
||||||
.getImage(typeof d.image === 'string' ? d.image : d.image.tag)
|
|
||||||
.inspect()
|
|
||||||
.return(d.serviceName)
|
|
||||||
.catchReturn();
|
|
||||||
})
|
|
||||||
.filter((d) => !!d)
|
|
||||||
.then(function (servicesToSkip) {
|
|
||||||
// multibuild takes in a composition and always attempts to
|
|
||||||
// build or pull all services. we workaround that here by
|
|
||||||
// passing a modified composition.
|
|
||||||
const compositionToBuild = _.cloneDeep(project.composition);
|
|
||||||
compositionToBuild.services = _.omit(
|
|
||||||
compositionToBuild.services,
|
|
||||||
servicesToSkip,
|
|
||||||
);
|
|
||||||
if (_.size(compositionToBuild.services) === 0) {
|
|
||||||
logger.logInfo(
|
|
||||||
'Everything is up to date (use --build to force a rebuild)',
|
|
||||||
);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return compose
|
|
||||||
.buildProject(
|
|
||||||
docker,
|
|
||||||
logger,
|
|
||||||
project.path,
|
|
||||||
project.name,
|
|
||||||
compositionToBuild,
|
|
||||||
opts.app.arch,
|
|
||||||
opts.app.is_for__device_type[0].slug,
|
|
||||||
opts.buildEmulated,
|
|
||||||
opts.buildOpts,
|
|
||||||
composeOpts.inlineLogs,
|
|
||||||
composeOpts.convertEol,
|
|
||||||
composeOpts.dockerfilePath,
|
|
||||||
composeOpts.nogitignore,
|
|
||||||
composeOpts.multiDockerignore,
|
|
||||||
)
|
|
||||||
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
|
||||||
})
|
|
||||||
.then((builtImages) =>
|
|
||||||
project.descriptors.map(
|
|
||||||
(d) =>
|
|
||||||
builtImages[d.serviceName] ?? {
|
|
||||||
serviceName: d.serviceName,
|
|
||||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
|
||||||
logs: 'Build skipped; image for service already exists.',
|
|
||||||
props: {},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// @ts-ignore slightly different return types of partial vs non-partial release
|
|
||||||
.then(function (images) {
|
|
||||||
if (opts.app.application_type?.[0]?.is_legacy) {
|
|
||||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
|
||||||
|
|
||||||
const msg = getChalk().yellow(
|
|
||||||
'Target application requires legacy deploy method.',
|
|
||||||
);
|
|
||||||
logger.logWarn(msg);
|
|
||||||
|
|
||||||
return Bluebird.join(
|
|
||||||
docker,
|
|
||||||
logger,
|
|
||||||
sdk.auth.getToken(),
|
|
||||||
sdk.auth.whoami(),
|
|
||||||
sdk.settings.get('balenaUrl'),
|
|
||||||
{
|
|
||||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
|
||||||
appName: opts.appName,
|
|
||||||
imageName: images[0].name,
|
|
||||||
buildLogs: images[0].logs,
|
|
||||||
shouldUploadLogs: opts.shouldUploadLogs,
|
|
||||||
},
|
|
||||||
deployLegacy,
|
|
||||||
).then((releaseId) =>
|
|
||||||
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
|
|
||||||
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to
|
|
||||||
// Promise.join typings
|
|
||||||
sdk.models.release.get(releaseId, { $select: ['commit'] }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.all([
|
|
||||||
sdk.auth.getUserId(),
|
|
||||||
sdk.auth.getToken(),
|
|
||||||
sdk.settings.get('apiUrl'),
|
|
||||||
]).then(([userId, auth, apiEndpoint]) =>
|
|
||||||
$deployProject(
|
|
||||||
docker,
|
|
||||||
logger,
|
|
||||||
project.composition,
|
|
||||||
images,
|
|
||||||
opts.app.id,
|
|
||||||
userId,
|
|
||||||
`Bearer ${auth}`,
|
|
||||||
apiEndpoint,
|
|
||||||
!opts.shouldUploadLogs,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(function (release) {
|
|
||||||
logger.outputDeferredMessages();
|
|
||||||
logger.logSuccess('Deploy succeeded!');
|
|
||||||
logger.logSuccess(`Release: ${release.commit}`);
|
|
||||||
console.log();
|
|
||||||
console.log(doodles.getDoodle()); // Show charlie
|
|
||||||
console.log();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.logError('Deploy failed');
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deploy = {
|
|
||||||
signature: 'deploy <appName> [image]',
|
|
||||||
description:
|
|
||||||
'Deploy a single image or a multicontainer project to a balena application',
|
|
||||||
help: `\
|
|
||||||
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
|
||||||
|
|
||||||
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
|
|
||||||
the image in the balenaCloud build servers.)
|
|
||||||
|
|
||||||
Unless an image is specified, this command will look into the current directory
|
|
||||||
(or the one specified by --source) for a docker-compose.yml 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[.template] file (or alternative
|
|
||||||
Dockerfile specified with the \`-f\` option), and if yet that isn't found, it
|
|
||||||
will try to generate one.
|
|
||||||
|
|
||||||
To deploy to an app on which you're a collaborator, use
|
|
||||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
|
||||||
|
|
||||||
When --build is used, all options supported by \`balena build\` are also supported
|
|
||||||
by this command.
|
|
||||||
|
|
||||||
${registrySecretsHelp}
|
|
||||||
|
|
||||||
${dockerignoreHelp}
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ balena deploy myApp
|
|
||||||
$ balena deploy myApp --build --source myBuildDir/
|
|
||||||
$ balena deploy myApp myApp/myImage\
|
|
||||||
`,
|
|
||||||
permission: 'user',
|
|
||||||
primary: true,
|
|
||||||
options: dockerUtils.appendOptions(
|
|
||||||
compose.appendOptions([
|
|
||||||
{
|
|
||||||
signature: 'source',
|
|
||||||
parameter: 'source',
|
|
||||||
description:
|
|
||||||
'Specify an alternate source directory; default is the working directory',
|
|
||||||
alias: 's',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
signature: 'build',
|
|
||||||
boolean: true,
|
|
||||||
description: 'Force a rebuild before deploy',
|
|
||||||
alias: 'b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
signature: 'nologupload',
|
|
||||||
description:
|
|
||||||
"Don't upload build logs to the dashboard with image (if building)",
|
|
||||||
boolean: true,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
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 {
|
|
||||||
getRegistrySecrets,
|
|
||||||
validateProjectDirectory,
|
|
||||||
} = require('../utils/compose_ts');
|
|
||||||
const helpers = require('../utils/helpers');
|
|
||||||
const Logger = require('../utils/logger');
|
|
||||||
|
|
||||||
const logger = Logger.getLogger();
|
|
||||||
logger.logDebug('Parsing input...');
|
|
||||||
|
|
||||||
// when Capitano converts a positional parameter (but not an option)
|
|
||||||
// to a number, the original value is preserved with the _raw suffix
|
|
||||||
let { appName, appName_raw, image } = params;
|
|
||||||
|
|
||||||
// look into "balena build" options if appName isn't given
|
|
||||||
appName = appName_raw || appName || options.application;
|
|
||||||
delete options.application;
|
|
||||||
|
|
||||||
if (appName == null) {
|
|
||||||
throw new ExpectedError(
|
|
||||||
'Please specify the name of the application to deploy',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image != null && options.build) {
|
|
||||||
throw new ExpectedError(
|
|
||||||
'Build option is not applicable when specifying an image',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (image) {
|
|
||||||
const registrySecrets = await getRegistrySecrets(
|
|
||||||
sdk,
|
|
||||||
options['registry-secrets'],
|
|
||||||
);
|
|
||||||
options['registry-secrets'] = registrySecrets;
|
|
||||||
} else {
|
|
||||||
const {
|
|
||||||
dockerfilePath,
|
|
||||||
registrySecrets,
|
|
||||||
} = await validateProjectDirectory(sdk, {
|
|
||||||
dockerfilePath: options.dockerfile,
|
|
||||||
noParentCheck: options['noparent-check'] || false,
|
|
||||||
projectPath: options.source || '.',
|
|
||||||
registrySecretsPath: options['registry-secrets'],
|
|
||||||
});
|
|
||||||
options.dockerfile = dockerfilePath;
|
|
||||||
options['registry-secrets'] = registrySecrets;
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = await helpers.getAppWithArch(appName);
|
|
||||||
|
|
||||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
|
||||||
dockerUtils.getDocker(options),
|
|
||||||
dockerUtils.generateBuildOpts(options),
|
|
||||||
compose.generateOpts(options),
|
|
||||||
]);
|
|
||||||
await deployProject(docker, logger, composeOpts, {
|
|
||||||
app,
|
|
||||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
|
||||||
image,
|
|
||||||
shouldPerformBuild: !!options.build,
|
|
||||||
shouldUploadLogs: !options.nologupload,
|
|
||||||
buildEmulated: !!options.emulated,
|
|
||||||
buildOpts,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
@ -16,5 +16,4 @@ limitations under the License.
|
|||||||
|
|
||||||
export * as help from './help';
|
export * as help from './help';
|
||||||
|
|
||||||
export { deploy } from './deploy';
|
|
||||||
export { preload } from './preload';
|
export { preload } from './preload';
|
||||||
|
@ -49,9 +49,6 @@ capitano.command(actions.help.help);
|
|||||||
// ---------- Preload Module ----------
|
// ---------- Preload Module ----------
|
||||||
capitano.command(actions.preload);
|
capitano.command(actions.preload);
|
||||||
|
|
||||||
// ------------ Local build and deploy -------
|
|
||||||
capitano.command(actions.deploy);
|
|
||||||
|
|
||||||
export function run(argv: string[]) {
|
export function run(argv: string[]) {
|
||||||
const cli = capitano.parse(argv.slice(2));
|
const cli = capitano.parse(argv.slice(2));
|
||||||
const runCommand = function () {
|
const runCommand = function () {
|
||||||
|
@ -142,18 +142,19 @@ function checkDeletedCommand(argvSlice: string[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const convertedCommands = [
|
export const convertedCommands = [
|
||||||
|
'api-key:generate',
|
||||||
'app',
|
'app',
|
||||||
'app:create',
|
'app:create',
|
||||||
'app:restart',
|
'app:restart',
|
||||||
'app:rm',
|
'app:rm',
|
||||||
'apps',
|
'apps',
|
||||||
'api-key:generate',
|
'build',
|
||||||
'config:generate',
|
'config:generate',
|
||||||
'config:inject',
|
'config:inject',
|
||||||
'config:read',
|
'config:read',
|
||||||
'config:reconfigure',
|
'config:reconfigure',
|
||||||
'config:write',
|
'config:write',
|
||||||
'build',
|
'deploy',
|
||||||
'device',
|
'device',
|
||||||
'device:identify',
|
'device:identify',
|
||||||
'device:init',
|
'device:init',
|
||||||
|
@ -91,6 +91,7 @@ Implements the same feature as the "docker build --cache-from" option.`,
|
|||||||
squash: flags.boolean({
|
squash: flags.boolean({
|
||||||
description: 'Squash newly built layers into a single new layer',
|
description: 'Squash newly built layers into a single new layer',
|
||||||
}),
|
}),
|
||||||
|
...dockerConnectionCliFlags,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
export async function isBalenaEngine(docker: dockerode): Promise<boolean> {
|
||||||
|
@ -37,7 +37,7 @@ Primary commands:
|
|||||||
tunnel <deviceorapplication> tunnel local ports to your balenaOS device
|
tunnel <deviceorapplication> tunnel local ports to your balenaOS device
|
||||||
preload <image> preload an app on a disk image (or Edison zip archive)
|
preload <image> preload an app on a disk image (or Edison zip archive)
|
||||||
build [source] build a project locally
|
build [source] build a project locally
|
||||||
deploy <appName> [image] Deploy a single image or a multicontainer project to a balena application
|
deploy <appname> [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
|
join [deviceiporhostname] move a local device to an application on another balena server
|
||||||
leave [deviceiporhostname] remove a local device from its balena application
|
leave [deviceiporhostname] remove a local device from its balena application
|
||||||
scan scan for balenaOS devices on your local network
|
scan scan for balenaOS devices on your local network
|
||||||
|
Loading…
Reference in New Issue
Block a user