Merge pull request #2046 from balena-io/add-support

Add new command `support`
This commit is contained in:
bulldozer-balena[bot] 2020-09-25 15:11:05 +00:00 committed by GitHub
commit 340ca6577b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 282 additions and 2 deletions

View File

@ -40,6 +40,7 @@ const capitanoDoc = {
'build/actions-oclif/app/index.js', 'build/actions-oclif/app/index.js',
'build/actions-oclif/app/create.js', 'build/actions-oclif/app/create.js',
'build/actions-oclif/app/rm.js', 'build/actions-oclif/app/rm.js',
'build/actions-oclif/app/rename.js',
'build/actions-oclif/app/restart.js', 'build/actions-oclif/app/restart.js',
], ],
}, },
@ -62,6 +63,7 @@ const capitanoDoc = {
'build/actions-oclif/device/register.js', 'build/actions-oclif/device/register.js',
'build/actions-oclif/device/rename.js', 'build/actions-oclif/device/rename.js',
'build/actions-oclif/device/rm.js', 'build/actions-oclif/device/rm.js',
'build/actions-oclif/device/restart.js',
'build/actions-oclif/device/shutdown.js', 'build/actions-oclif/device/shutdown.js',
'build/actions-oclif/devices/index.js', 'build/actions-oclif/devices/index.js',
'build/actions-oclif/devices/supported.js', 'build/actions-oclif/devices/supported.js',
@ -166,6 +168,10 @@ const capitanoDoc = {
title: 'Utilities', title: 'Utilities',
files: ['build/actions-oclif/util/available-drives.js'], files: ['build/actions-oclif/util/available-drives.js'],
}, },
{
title: 'Support',
files: ['build/actions-oclif/support.js'],
},
], ],
}; };

View File

@ -163,6 +163,7 @@ Users are encouraged to regularly update balenaCLI to the latest version.
- [app <name>](#app-name) - [app <name>](#app-name)
- [app create <name>](#app-create-name) - [app create <name>](#app-create-name)
- [app rm <name>](#app-rm-name) - [app rm <name>](#app-rm-name)
- [app rename <name> [newname]](#app-rename-name-newname)
- [app restart <name>](#app-restart-name) - [app restart <name>](#app-restart-name)
- Authentication - Authentication
@ -181,6 +182,7 @@ Users are encouraged to regularly update balenaCLI to the latest version.
- [device register <application>](#device-register-application) - [device register <application>](#device-register-application)
- [device rename <uuid> [newname]](#device-rename-uuid-newname) - [device rename <uuid> [newname]](#device-rename-uuid-newname)
- [device rm <uuid(s)>](#device-rm-uuid-s) - [device rm <uuid(s)>](#device-rm-uuid-s)
- [device restart <uuid>](#device-restart-uuid)
- [device shutdown <uuid>](#device-shutdown-uuid) - [device shutdown <uuid>](#device-shutdown-uuid)
- [devices](#devices) - [devices](#devices)
- [devices supported](#devices-supported) - [devices supported](#devices-supported)
@ -273,6 +275,10 @@ Users are encouraged to regularly update balenaCLI to the latest version.
- [util available-drives](#util-available-drives) - [util available-drives](#util-available-drives)
- Support
- [support <action>](#support-action)
# API keys # API keys
## api-key generate <name> ## api-key generate <name>
@ -381,6 +387,30 @@ application name or numeric ID
answer "yes" to all questions (non interactive use) answer "yes" to all questions (non interactive use)
## app rename <name> [newName]
Rename an application.
Note, if the `newName` parameter is omitted, it will be
prompted for interactively.
Examples:
$ balena app rename OldName
$ balena app rename OldName NewName
### Arguments
#### NAME
application name or numeric ID
#### NEWNAME
the new name for the application
### Options
## app restart <name> ## app restart <name>
Restart all devices that belongs to a certain application. Restart all devices that belongs to a certain application.
@ -673,6 +703,36 @@ comma-separated list (no blank spaces) of device UUIDs to be removed
answer "yes" to all questions (non interactive use) answer "yes" to all questions (non interactive use)
## device restart <uuid>
Restart containers on a device.
If the --service flag is provided, then only those services' containers
will be restarted, otherwise all containers on the device will be restarted.
Multiple devices and services may be specified with a comma-separated list
of values (no spaces).
Note this does not reboot the device, to do so use instead `balena device reboot`.
Examples:
$ balena device restart 23c73a1
$ balena device restart 55d43b3,23c73a1
$ balena device restart 23c73a1 --service myService
$ balena device restart 23c73a1 -s myService1,myService2
### Arguments
#### UUID
comma-separated list (no blank spaces) of device UUIDs to restart
### Options
#### -s, --service SERVICE
comma-separated list (no blank spaces) of service names to restart
## device shutdown <uuid> ## device shutdown <uuid>
Remotely shutdown a device. Remotely shutdown a device.
@ -2864,3 +2924,43 @@ List available drives which are usable for writing an OS image to.
Does not list system drives. Does not list system drives.
### Options ### Options
# Support
## support <action>
Grant or revoke balena support agent access to devices and applications
on balenaCloud. (This command does not apply to openBalena.)
Access will be automatically revoked once the specified duration has elapsed.
Duration defaults to 24h, but can be specified using --duration flag in days
or hours, e.g. '12h', '2d'.
Both --device and --application flags accept multiple values, specified as
a comma-separated list (with no spaces).
Examples:
balena support enable --device ab346f,cd457a --duration 3d
balena support enable --application app3 --duration 12h
balena support disable -a myApp
### Arguments
#### ACTION
enable|disable support access
### Options
#### -d, --device DEVICE
comma-separated list (no spaces) of device UUIDs
#### -a, --application APPLICATION
comma-separated list (no spaces) of application names
#### -t, --duration DURATION
length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d

View File

@ -0,0 +1,173 @@
/**
* @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 * as cf from '../utils/common-flags';
import { getBalenaSdk, getCliUx, stripIndent } from '../utils/lazy';
interface FlagsDef {
application?: string;
device?: string;
duration?: string;
help: void;
}
interface ArgsDef {
action: string;
}
export default class SupportCmd extends Command {
public static description = stripIndent`
Grant or revoke support access for devices and applications.
Grant or revoke balena support agent access to devices and applications
on balenaCloud. (This command does not apply to openBalena.)
Access will be automatically revoked once the specified duration has elapsed.
Duration defaults to 24h, but can be specified using --duration flag in days
or hours, e.g. '12h', '2d'.
Both --device and --application flags accept multiple values, specified as
a comma-separated list (with no spaces).
`;
public static examples = [
'balena support enable --device ab346f,cd457a --duration 3d',
'balena support enable --application app3 --duration 12h',
'balena support disable -a myApp',
];
public static args = [
{
name: 'action',
description: 'enable|disable support access',
options: ['enable', 'disable'],
},
];
public static usage = 'support <action>';
public static flags: flags.Input<FlagsDef> = {
device: flags.string({
description: 'comma-separated list (no spaces) of device UUIDs',
char: 'd',
}),
application: flags.string({
description: 'comma-separated list (no spaces) of application names',
char: 'a',
}),
duration: flags.string({
description:
'length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d',
char: 't',
}),
help: cf.help,
};
public static authenticated = true;
public async run() {
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
SupportCmd,
);
const balena = getBalenaSdk();
const ux = getCliUx();
const enabling = params.action === 'enable';
// Validation
if (!options.device && !options.application) {
throw new ExpectedError(
'At least one device or application must be specified',
);
}
if (options.duration != null && !enabling) {
throw new ExpectedError(
'--duration option is only applicable when enabling support',
);
}
// Calculate expiry ts
const durationDefault = '24h';
const duration = options.duration || durationDefault;
const expiryTs = Date.now() + this.parseDuration(duration);
const deviceUuids = options.device?.split(',') || [];
const appNames = options.application?.split(',') || [];
const enablingMessage = 'Enabling support access for';
const disablingMessage = 'Disabling support access for';
// Process devices
for (const deviceUuid of deviceUuids) {
if (enabling) {
ux.action.start(`${enablingMessage} device ${deviceUuid}`);
await balena.models.device.grantSupportAccess(deviceUuid, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} device ${deviceUuid}`);
await balena.models.device.revokeSupportAccess(deviceUuid);
}
ux.action.stop();
}
// Process applications
for (const appName of appNames) {
if (enabling) {
ux.action.start(`${enablingMessage} application ${appName}`);
await balena.models.application.grantSupportAccess(appName, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} application ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
}
ux.action.stop();
}
if (enabling) {
console.log(
`Access has been granted for ${duration}, expiring ${new Date(
expiryTs,
).toLocaleString()}`,
);
}
}
parseDuration(duration: string): number {
const parseErrorMsg =
'Duration must be specified as number followed by h or d, e.g. 24h, 1d';
const unit = duration.slice(duration.length - 1);
const amount = Number(duration.substring(0, duration.length - 1));
if (isNaN(amount)) {
throw new ExpectedError(parseErrorMsg);
}
let durationMs;
if (['h', 'H'].includes(unit)) {
durationMs = amount * 60 * 60 * 1000;
} else if (['d', 'D'].includes(unit)) {
durationMs = amount * 24 * 60 * 60 * 1000;
} else {
throw new ExpectedError(parseErrorMsg);
}
return durationMs;
}
}

View File

@ -148,13 +148,13 @@ const EXPECTED_ERROR_REGEXES = [
/^BalenaDeviceNotFound/, // balena-sdk /^BalenaDeviceNotFound/, // balena-sdk
/^BalenaExpiredToken/, // balena-sdk /^BalenaExpiredToken/, // balena-sdk
/^BalenaInvalidDeviceType/, // balena-sdk /^BalenaInvalidDeviceType/, // balena-sdk
/^Missing \w+$/, // Capitano,
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError /^Missing \d+ required arg/, // oclif parser: RequiredArgsError
/Missing required flag/, // oclif parser: RequiredFlagError /Missing required flag/, // oclif parser: RequiredFlagError
/^Unexpected argument/, // oclif parser: UnexpectedArgsError /^Unexpected argument/, // oclif parser: UnexpectedArgsError
/to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError /to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError
/must also be provided when using/, // oclif parser (depends-on) /must also be provided when using/, // oclif parser (depends-on)
/^Expected an integer/, // oclif parser (flags.integer) /^Expected an integer/, // oclif parser (flags.integer)
/^Flag .* expects a value/, // oclif parser
]; ];
// Support unit testing of handleError // Support unit testing of handleError

View File

@ -199,6 +199,7 @@ export const oclifCommandIds = [
'scan', 'scan',
'settings', 'settings',
'ssh', 'ssh',
'support',
'tags', 'tags',
'tag:rm', 'tag:rm',
'tag:set', 'tag:set',

View File

@ -117,7 +117,6 @@ describe('handleError() function', () => {
}); });
const messagesToMatch = [ const messagesToMatch = [
'Missing uuid', // Capitano
'Missing 1 required argument', // oclif 'Missing 1 required argument', // oclif
'Missing 2 required arguments', // oclif 'Missing 2 required arguments', // oclif
'Missing required flag', // oclif 'Missing required flag', // oclif
@ -126,6 +125,7 @@ describe('handleError() function', () => {
'to be one of', // oclif 'to be one of', // oclif
'must also be provided when using', // oclif 'must also be provided when using', // oclif
'Expected an integer', // oclif 'Expected an integer', // oclif
'Flag --foo expects a value', // oclif
]; ];
messagesToMatch.forEach((message) => { messagesToMatch.forEach((message) => {