diff --git a/completion/_balena b/completion/_balena index 0058ceab..7ed61a70 100644 --- a/completion/_balena +++ b/completion/_balena @@ -15,9 +15,9 @@ _balena() { block_cmds=( create ) config_cmds=( generate inject read reconfigure write ) device_type_cmds=( list ) - device_cmds=( deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel ) + device_cmds=( deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service support track-fleet tunnel ) env_cmds=( list rename rm set ) - fleet_cmds=( create list pin purge rename restart rm track-latest ) + fleet_cmds=( create list pin purge rename restart rm support track-latest ) internal_cmds=( osinit ) local_cmds=( configure flash ) organization_cmds=( list ) diff --git a/completion/balena-completion.bash b/completion/balena-completion.bash index 3aa1c25b..ac366b6b 100644 --- a/completion/balena-completion.bash +++ b/completion/balena-completion.bash @@ -14,9 +14,9 @@ _balena_complete() block_cmds="create" config_cmds="generate inject read reconfigure write" device_type_cmds="list" - device_cmds="deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service track-fleet tunnel" + device_cmds="deactivate detect identify init list local-mode logs move note os-update pin public-url purge reboot register rename restart rm shutdown ssh start-service stop-service support track-fleet tunnel" env_cmds="list rename rm set" - fleet_cmds="create list pin purge rename restart rm track-latest" + fleet_cmds="create list pin purge rename restart rm support track-latest" internal_cmds="osinit" local_cmds="configure flash" organization_cmds="list" diff --git a/docs/balena-cli.md b/docs/balena-cli.md index f1ac7804..3f983cc2 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -222,6 +222,7 @@ are encouraged to regularly update the balena CLI to the latest version. - [device ssh](#device-ssh) - [device start-service](#device-start-service) - [device stop-service](#device-stop-service) + - [device support](#device-support) - [device track-fleet](#device-track-fleet) - [device tunnel](#device-tunnel) @@ -242,6 +243,7 @@ are encouraged to regularly update the balena CLI to the latest version. - [fleet rename](#fleet-rename) - [fleet restart](#fleet-restart) - [fleet rm](#fleet-rm) + - [fleet support](#fleet-support) - [fleet track-latest](#fleet-track-latest) - Local @@ -2104,6 +2106,40 @@ comma-separated list (no blank spaces) of service names ### Options +## device support + +### Description + +Grant or revoke balena support agent access to devices +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'. + +Multiple values can specified as a comma-separated list (with no spaces). + +Examples: + + balena support enable ab346f,cd457a --duration 3d + balena support disable ab346f,cd457a + +### Arguments + +#### ACTION + +enable|disable support access + +#### UUID + +comma-separated list (no blank spaces) of device UUIDs to be moved + +### Options + +#### -t, --duration DURATION + +length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d + ## device track-fleet ### Description @@ -2752,6 +2788,50 @@ fleet name or slug (preferred) answer "yes" to all questions (non interactive use) +## fleet support + +### Description + +Grant or revoke balena support agent access to fleets +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'. + +Multiple values can specified as a comma-separated list (with no spaces). + +Fleets may be specified by fleet name or slug. Fleet slugs are +the recommended option, as they are unique and unambiguous. Slugs can be +listed with the `balena fleet list` command. Note that slugs may change if the +fleet is renamed. Fleet names are not unique and may result in "Fleet is +ambiguous" errors at any time (even if it "used to work in the past"), for +example if the name clashes with a newly created public fleet, or with fleets +from other balena accounts that you may be invited to join under any role. +For this reason, fleet names are especially discouraged in scripts (e.g. CI +environments). + +Examples: + + balena support enable myorg/myfleet,notmyorg/notmyfleet --duration 3d + balena support disable myorg/myfleet + +### Arguments + +#### ACTION + +enable|disable support access + +#### FLEET + +comma-separated list (no spaces) of fleet names or slugs (preferred) + +### Options + +#### -t, --duration DURATION + +length of time to enable support for, in (h)ours or (d)ays, e.g. 12h, 2d + ## fleet track-latest ### Description diff --git a/src/commands/device/support.ts b/src/commands/device/support.ts new file mode 100644 index 00000000..74baa810 --- /dev/null +++ b/src/commands/device/support.ts @@ -0,0 +1,111 @@ +/** + * @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, Args, Command } from '@oclif/core'; +import { ExpectedError } from '../../errors'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy'; + +export default class DeviceSupportCmd extends Command { + public static description = stripIndent` + Grant or revoke support access for devices. + + Grant or revoke balena support agent access to devices + 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'. + + Multiple values can specified as a comma-separated list (with no spaces). + `; + + public static examples = [ + 'balena support enable ab346f,cd457a --duration 3d', + 'balena support disable ab346f,cd457a', + ]; + + public static args = { + action: Args.string({ + description: 'enable|disable support access', + options: ['enable', 'disable'], + required: true, + }), + uuid: Args.string({ + description: + 'comma-separated list (no blank spaces) of device UUIDs to be moved', + required: true, + }), + }; + + public static flags = { + 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 } = await this.parse(DeviceSupportCmd); + + const balena = getBalenaSdk(); + const ux = getCliUx(); + + const enabling = params.action === 'enable'; + + 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 { parseDuration } = await import('../../utils/helpers'); + const expiryTs = Date.now() + parseDuration(duration); + + const deviceUuids = params.uuid?.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(); + } + + if (enabling) { + console.log( + `Access has been granted for ${duration}, expiring ${new Date( + expiryTs, + ).toISOString()}`, + ); + } + } +} diff --git a/src/commands/fleet/support.ts b/src/commands/fleet/support.ts new file mode 100644 index 00000000..b28d39f3 --- /dev/null +++ b/src/commands/fleet/support.ts @@ -0,0 +1,117 @@ +/** + * @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, Args, Command } from '@oclif/core'; +import { ExpectedError } from '../../errors'; +import * as cf from '../../utils/common-flags'; +import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy'; +import { applicationIdInfo } from '../../utils/messages'; + +export default class FleetSupportCmd extends Command { + public static description = stripIndent` + Grant or revoke support access for fleets. + + Grant or revoke balena support agent access to fleets + 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'. + + Multiple values can specified as a comma-separated list (with no spaces). + + ${applicationIdInfo.split('\n').join('\n\t\t')} + `; + + public static examples = [ + 'balena support enable myorg/myfleet,notmyorg/notmyfleet --duration 3d', + 'balena support disable myorg/myfleet', + ]; + + public static args = { + action: Args.string({ + description: 'enable|disable support access', + options: ['enable', 'disable'], + required: true, + }), + fleet: Args.string({ + description: + 'comma-separated list (no spaces) of fleet names or slugs (preferred)', + required: true, + }), + }; + + public static flags = { + 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 } = await this.parse(FleetSupportCmd); + + const balena = getBalenaSdk(); + const ux = getCliUx(); + + const enabling = params.action === 'enable'; + + 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 { parseDuration } = await import('../../utils/helpers'); + const expiryTs = Date.now() + parseDuration(duration); + + const appNames = params.fleet?.split(',') || []; + + const enablingMessage = 'Enabling support access for'; + const disablingMessage = 'Disabling support access for'; + + const { getFleetSlug } = await import('../../utils/sdk'); + + // Process applications + for (const appName of appNames) { + const slug = await getFleetSlug(balena, appName); + if (enabling) { + ux.action.start(`${enablingMessage} fleet ${slug}`); + await balena.models.application.grantSupportAccess(slug, expiryTs); + } else if (params.action === 'disable') { + ux.action.start(`${disablingMessage} fleet ${slug}`); + await balena.models.application.revokeSupportAccess(slug); + } + ux.action.stop(); + } + + if (enabling) { + console.log( + `Access has been granted for ${duration}, expiring ${new Date( + expiryTs, + ).toISOString()}`, + ); + } + } +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index f44b4515..b4e4fb1b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -21,6 +21,7 @@ import * as _ from 'lodash'; import { promisify } from 'util'; import { getBalenaSdk, getChalk, getVisuals } from './lazy'; +import { ExpectedError } from '../errors'; export function getGroupDefaults(group: { options: Array<{ name: string; default: string | number }>; @@ -478,3 +479,25 @@ export function pickAndRename>( }); return _.mapKeys(_.pick(obj, fields), (_val, key) => rename[key]); } + +export const parseDuration = (duration: string) => { + 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; +};