From c5e8f0d6ea4c958b3b3c63e70ac8540fbf83b779 Mon Sep 17 00:00:00 2001 From: myarmolinsky Date: Thu, 25 May 2023 11:05:47 -0400 Subject: [PATCH] Add `balena app create` command for creatings Apps Change-type: minor --- completion/_balena | 6 +- completion/balena-completion.bash | 6 +- lib/commands/app/create.ts | 150 ++++++++++++++++++++++++++++++ lib/commands/fleet/create.ts | 2 +- 4 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 lib/commands/app/create.ts diff --git a/completion/_balena b/completion/_balena index 89e25433..1cf2e354 100644 --- a/completion/_balena +++ b/completion/_balena @@ -8,9 +8,10 @@ _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 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 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 ) 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 ) @@ -43,6 +44,9 @@ _balena_sec_cmds() { "api-key") _describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0 ;; + "app") + _describe -t app_cmds 'app_cmd' app_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 d91c6aba..4b1794fe 100644 --- a/completion/balena-completion.bash +++ b/completion/balena-completion.bash @@ -7,9 +7,10 @@ _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 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 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" 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" @@ -37,6 +38,9 @@ _balena_complete() api-key) COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) ) ;; + app) + COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) ) + ;; config) COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) ) ;; diff --git a/lib/commands/app/create.ts b/lib/commands/app/create.ts new file mode 100644 index 00000000..aed57b6a --- /dev/null +++ b/lib/commands/app/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 AppCreateCmd extends Command { + public static description = stripIndent` + Create an app. + + Create a new balena app. + + You can specify the organization the app 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 app'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 app create MyApp', + '$ balena app create MyApp --organization mmyorg', + '$ balena app create MyApp -o myorg --type raspberry-pi', + ]; + + public static args = [ + { + name: 'name', + description: 'app name', + required: true, + }, + ]; + + public static usage = 'app create '; + + public static flags: flags.Input = { + organization: flags.string({ + char: 'o', + description: 'handle of the organization the app should belong to', + }), + type: flags.string({ + char: 't', + description: + 'app 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( + AppCreateCmd, + ); + + // 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: 'app', + }); + + // Output + console.log( + `App 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 apps 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); + } + } +} diff --git a/lib/commands/fleet/create.ts b/lib/commands/fleet/create.ts index c08f7bc3..f99dd74a 100644 --- a/lib/commands/fleet/create.ts +++ b/lib/commands/fleet/create.ts @@ -115,7 +115,7 @@ export default class FleetCreateCmd extends Command { if ((err.message || '').toLowerCase().includes('unique')) { // BalenaRequestError: Request error: "organization" and "app_name" must be unique. throw new ExpectedError( - `Error: fleet "${params.name}" already exists in organization "${organization}".`, + `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