From 9975e5d9ac566e9535b43041186f0ad158a19b19 Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Thu, 25 May 2023 11:59:27 -0400 Subject: [PATCH] Add `balena block create` command for creating Blocks Change-type: minor --- completion/_balena | 6 +- completion/balena-completion.bash | 6 +- lib/commands/block/create.ts | 150 ++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 lib/commands/block/create.ts diff --git a/completion/_balena b/completion/_balena index 1cf2e354..c48886ef 100644 --- a/completion/_balena +++ b/completion/_balena @@ -8,10 +8,11 @@ _balena() { local context state line curcontext="$curcontext" # Valid top-level completions - main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app config device device devices env fleet fleet internal key key local os release release tag util ) + main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app block config device device devices env fleet fleet internal key key local os release release tag util ) # Sub-completions api_key_cmds=( generate ) app_cmds=( create ) + block_cmds=( create ) config_cmds=( generate inject read reconfigure write ) device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet ) devices_cmds=( supported ) @@ -47,6 +48,9 @@ _balena_sec_cmds() { "app") _describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0 ;; + "block") + _describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0 + ;; "config") _describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0 ;; diff --git a/completion/balena-completion.bash b/completion/balena-completion.bash index 4b1794fe..c5d75bdc 100644 --- a/completion/balena-completion.bash +++ b/completion/balena-completion.bash @@ -7,10 +7,11 @@ _balena_complete() local cur prev # Valid top-level completions - main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app config device device devices env fleet fleet internal key key local os release release tag util" + main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key api-keys app block config device device devices env fleet fleet internal key key local os release release tag util" # Sub-completions api_key_cmds="generate" app_cmds="create" + block_cmds="create" config_cmds="generate inject read reconfigure write" device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown track-fleet" devices_cmds="supported" @@ -41,6 +42,9 @@ _balena_complete() app) COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) ) ;; + block) + COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) ) + ;; config) COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) ) ;; diff --git a/lib/commands/block/create.ts b/lib/commands/block/create.ts new file mode 100644 index 00000000..9303d064 --- /dev/null +++ b/lib/commands/block/create.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright 2016-2021 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, stripIndent } from '../../utils/lazy'; + +interface FlagsDef { + organization?: string; + type?: string; // application device type + help: void; +} + +interface ArgsDef { + name: string; +} + +export default class BlockCreateCmd extends Command { + public static description = stripIndent` + Create an block. + + Create a new balena block. + + You can specify the organization the block should belong to using + the \`--organization\` option. The organization's handle, not its name, + should be provided. Organization handles can be listed with the + \`balena orgs\` command. + + The block's default device type is specified with the \`--type\` option. + The \`balena devices supported\` command can be used to list the available + device types. + + Interactive dropdowns will be shown for selection if no device type or + organization is specified and there are multiple options to choose from. + If there is a single option to choose from, it will be chosen automatically. + This interactive behavior can be disabled by explicitly specifying a device + type and organization. + `; + + public static examples = [ + '$ balena block create MyBlock', + '$ balena block create MyBlock --organization mmyorg', + '$ balena block create MyBlock -o myorg --type raspberry-pi', + ]; + + public static args = [ + { + name: 'name', + description: 'block name', + required: true, + }, + ]; + + public static usage = 'block create '; + + public static flags: flags.Input = { + organization: flags.string({ + char: 'o', + description: 'handle of the organization the block should belong to', + }), + type: flags.string({ + char: 't', + description: + 'block device type (Check available types with `balena devices supported`)', + }), + help: cf.help, + }; + + public static authenticated = true; + + public async run() { + const { args: params, flags: options } = this.parse( + BlockCreateCmd, + ); + + // Ascertain device type + const deviceType = + options.type || + (await (await import('../../utils/patterns')).selectDeviceType()); + + // Ascertain organization + const organization = + options.organization?.toLowerCase() || (await this.getOrganization()); + + // Create application + try { + const application = await getBalenaSdk().models.application.create({ + name: params.name, + deviceType, + organization, + applicationClass: 'block', + }); + + // Output + console.log( + `Block created: slug "${application.slug}", device type "${deviceType}"`, + ); + } catch (err) { + if ((err.message || '').toLowerCase().includes('unique')) { + // BalenaRequestError: Request error: "organization" and "app_name" must be unique. + throw new ExpectedError( + `Error: An app or block or fleet with the name "${params.name}" already exists in organization "${organization}".`, + ); + } else if ((err.message || '').toLowerCase().includes('unauthorized')) { + // BalenaRequestError: Request error: Unauthorized + throw new ExpectedError( + `Error: You are not authorized to create blocks in organization "${organization}".`, + ); + } + + throw err; + } + } + + async getOrganization() { + const { getOwnOrganizations } = await import('../../utils/sdk'); + const organizations = await getOwnOrganizations(getBalenaSdk(), { + $select: ['name', 'handle'], + }); + + if (organizations.length === 0) { + // User is not a member of any organizations (should not happen). + throw new Error('This account is not a member of any organizations'); + } else if (organizations.length === 1) { + // User is a member of only one organization - use this. + return organizations[0].handle; + } else { + // User is a member of multiple organizations - + const { selectOrganization } = await import('../../utils/patterns'); + return selectOrganization(organizations); + } + } +}