From 0a8b3ce4e4a5b333f8aa6fd5f51e98444a42d966 Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Fri, 25 Sep 2020 11:00:18 +0200 Subject: [PATCH] Add new command `support` Change-type: minor Resolves: #766 #1546 Signed-off-by: Scott Lowe --- automation/capitanodoc/capitanodoc.ts | 6 + doc/cli.markdown | 100 +++++++++++++++ lib/actions-oclif/support.ts | 173 ++++++++++++++++++++++++++ lib/errors.ts | 2 +- lib/preparser.ts | 1 + tests/errors.spec.ts | 2 +- 6 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 lib/actions-oclif/support.ts diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index 8d1efdcf..bfc6279f 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -40,6 +40,7 @@ const capitanoDoc = { 'build/actions-oclif/app/index.js', 'build/actions-oclif/app/create.js', 'build/actions-oclif/app/rm.js', + 'build/actions-oclif/app/rename.js', 'build/actions-oclif/app/restart.js', ], }, @@ -62,6 +63,7 @@ const capitanoDoc = { 'build/actions-oclif/device/register.js', 'build/actions-oclif/device/rename.js', 'build/actions-oclif/device/rm.js', + 'build/actions-oclif/device/restart.js', 'build/actions-oclif/device/shutdown.js', 'build/actions-oclif/devices/index.js', 'build/actions-oclif/devices/supported.js', @@ -166,6 +168,10 @@ const capitanoDoc = { title: 'Utilities', files: ['build/actions-oclif/util/available-drives.js'], }, + { + title: 'Support', + files: ['build/actions-oclif/support.js'], + }, ], }; diff --git a/doc/cli.markdown b/doc/cli.markdown index a53b7081..7b24eb2f 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -163,6 +163,7 @@ Users are encouraged to regularly update balenaCLI to the latest version. - [app <name>](#app-name) - [app create <name>](#app-create-name) - [app rm <name>](#app-rm-name) + - [app rename <name> [newname]](#app-rename-name-newname) - [app restart <name>](#app-restart-name) - Authentication @@ -181,6 +182,7 @@ Users are encouraged to regularly update balenaCLI to the latest version. - [device register <application>](#device-register-application) - [device rename <uuid> [newname]](#device-rename-uuid-newname) - [device rm <uuid(s)>](#device-rm-uuid-s) + - [device restart <uuid>](#device-restart-uuid) - [device shutdown <uuid>](#device-shutdown-uuid) - [devices](#devices) - [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) +- Support + + - [support <action>](#support-action) + # API keys ## api-key generate <name> @@ -381,6 +387,30 @@ application name or numeric ID 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> 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) +## 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> 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. ### 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 diff --git a/lib/actions-oclif/support.ts b/lib/actions-oclif/support.ts new file mode 100644 index 00000000..d90306be --- /dev/null +++ b/lib/actions-oclif/support.ts @@ -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 '; + + public static flags: flags.Input = { + 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( + 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; + } +} diff --git a/lib/errors.ts b/lib/errors.ts index 9b5e9efc..66e108c0 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -148,13 +148,13 @@ const EXPECTED_ERROR_REGEXES = [ /^BalenaDeviceNotFound/, // balena-sdk /^BalenaExpiredToken/, // balena-sdk /^BalenaInvalidDeviceType/, // balena-sdk - /^Missing \w+$/, // Capitano, /^Missing \d+ required arg/, // oclif parser: RequiredArgsError /Missing required flag/, // oclif parser: RequiredFlagError /^Unexpected argument/, // oclif parser: UnexpectedArgsError /to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError /must also be provided when using/, // oclif parser (depends-on) /^Expected an integer/, // oclif parser (flags.integer) + /^Flag .* expects a value/, // oclif parser ]; // Support unit testing of handleError diff --git a/lib/preparser.ts b/lib/preparser.ts index 8a024aa9..a935e3cb 100644 --- a/lib/preparser.ts +++ b/lib/preparser.ts @@ -199,6 +199,7 @@ export const oclifCommandIds = [ 'scan', 'settings', 'ssh', + 'support', 'tags', 'tag:rm', 'tag:set', diff --git a/tests/errors.spec.ts b/tests/errors.spec.ts index 7fed2016..4f0fa261 100644 --- a/tests/errors.spec.ts +++ b/tests/errors.spec.ts @@ -117,7 +117,6 @@ describe('handleError() function', () => { }); const messagesToMatch = [ - 'Missing uuid', // Capitano 'Missing 1 required argument', // oclif 'Missing 2 required arguments', // oclif 'Missing required flag', // oclif @@ -126,6 +125,7 @@ describe('handleError() function', () => { 'to be one of', // oclif 'must also be provided when using', // oclif 'Expected an integer', // oclif + 'Flag --foo expects a value', // oclif ]; messagesToMatch.forEach((message) => {