Convert deploy command to oclif

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
Scott Lowe 2020-08-21 10:57:14 +02:00
parent e96fca551e
commit 8cb413c1c9
10 changed files with 438 additions and 372 deletions

View File

@ -156,7 +156,7 @@ const capitanoDoc = {
},
{
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',

View File

@ -256,7 +256,7 @@ Users are encouraged to regularly update the balena CLI to the latest version.
- Deploy
- [build [source]](#build-source)
- [deploy &#60;appName&#62; [image]](#deploy-appname-image)
- [deploy &#60;appname&#62; [image]](#deploy-appname-image)
- Platform
@ -2488,6 +2488,30 @@ Don't use docker layer caching when building
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 &#60;appName&#62; [image]
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
`balena deploy <appOwnerUsername>/<appName>`.
When --build is used, all options supported by `balena build` are also supported
by this command.
REGISTRY SECRETS
The --registry-secrets option specifies a JSON or YAML file containing private
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 myApp/myImage
### Arguments
#### APPNAME
the name of the application to deploy to
#### IMAGE
the image to deploy
### Options
#### --source, -s &#60;source&#62;
#### -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
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
#### --dockerfile &#60;Dockerfile&#62;
#### --dockerfile DOCKERFILE
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)
#### --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".
@ -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
#### --registry-secrets, -R &#60;secrets.yml|.json&#62;
#### -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
@ -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).
#### --projectName, -n &#60;projectName&#62;
#### -n, --projectName PROJECTNAME
Specify an alternate project name; default is the directory name
#### --docker, -P &#60;docker&#62;
Path to a local docker socket (e.g. /var/run/docker.sock)
#### --dockerHost, -h &#60;dockerHost&#62;
Docker daemon hostname or IP address (dev machine or balena device)
#### --dockerPort, -p &#60;dockerPort&#62;
Docker daemon TCP port number (hint: 2375 for balena devices)
#### --ca &#60;ca&#62;
Docker host TLS certificate authority file
#### --cert &#60;cert&#62;
Docker host TLS certificate file
#### --key &#60;key&#62;
Docker host TLS key file
#### --tag, -t &#60;tag&#62;
#### -t, --tag TAG
The alias to the generated image
#### --buildArg, -B &#60;arg&#62;
#### -B, --buildArg BUILDARG
Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple times.
#### --cache-from &#60;image-list&#62;
#### --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.
@ -2699,6 +2706,30 @@ Don't use docker layer caching when building
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
## join [deviceIpOrHostname]

View File

@ -17,7 +17,6 @@
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';
@ -94,7 +93,9 @@ ${dockerignoreHelp}
}),
...composeCliFlags,
...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;

354
lib/actions-oclif/deploy.ts Normal file
View 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;
});
}
}

View File

@ -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,
});
},
};

View File

@ -16,5 +16,4 @@ limitations under the License.
export * as help from './help';
export { deploy } from './deploy';
export { preload } from './preload';

View File

@ -49,9 +49,6 @@ capitano.command(actions.help.help);
// ---------- Preload Module ----------
capitano.command(actions.preload);
// ------------ Local build and deploy -------
capitano.command(actions.deploy);
export function run(argv: string[]) {
const cli = capitano.parse(argv.slice(2));
const runCommand = function () {

View File

@ -142,18 +142,19 @@ function checkDeletedCommand(argvSlice: string[]): void {
}
export const convertedCommands = [
'api-key:generate',
'app',
'app:create',
'app:restart',
'app:rm',
'apps',
'api-key:generate',
'build',
'config:generate',
'config:inject',
'config:read',
'config:reconfigure',
'config:write',
'build',
'deploy',
'device',
'device:identify',
'device:init',

View File

@ -91,6 +91,7 @@ Implements the same feature as the "docker build --cache-from" option.`,
squash: flags.boolean({
description: 'Squash newly built layers into a single new layer',
}),
...dockerConnectionCliFlags,
};
export async function isBalenaEngine(docker: dockerode): Promise<boolean> {

View File

@ -37,7 +37,7 @@ Primary commands:
tunnel <deviceorapplication> tunnel local ports to your balenaOS device
preload <image> preload an app on a disk image (or Edison zip archive)
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
leave [deviceiporhostname] remove a local device from its balena application
scan scan for balenaOS devices on your local network