mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-20 17:33:18 +00:00
Add organizations support to app create command
Change-type: minor Connects-to: #2119 Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
parent
c6430274e5
commit
611f59a0da
@ -345,17 +345,26 @@ application name or org/name slug
|
||||
|
||||
Create a new balena application.
|
||||
|
||||
You can specify the application device type with the `--type` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
You can specify the organization the application 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.
|
||||
|
||||
You can see a list of supported device types with:
|
||||
The application's default device type is specified with the `--type` option.
|
||||
The `balena devices supported` command can be used to list the available
|
||||
device types.
|
||||
|
||||
$ balena devices supported
|
||||
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.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app create MyApp
|
||||
$ balena app create MyApp --type raspberry-pi
|
||||
$ balena app create MyApp --organization mmyorg
|
||||
$ balena app create MyApp -o myorg --type raspberry-pi
|
||||
|
||||
### Arguments
|
||||
|
||||
@ -365,6 +374,10 @@ application name
|
||||
|
||||
### Options
|
||||
|
||||
#### -o, --organization ORGANIZATION
|
||||
|
||||
handle of the organization the application should belong to
|
||||
|
||||
#### -t, --type TYPE
|
||||
|
||||
application device type (Check available types with `balena devices supported`)
|
||||
|
@ -20,9 +20,10 @@ import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSDK from 'balena-sdk';
|
||||
import type { Application } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
organization?: string;
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
@ -37,16 +38,26 @@ export default class AppCreateCmd extends Command {
|
||||
|
||||
Create a new balena application.
|
||||
|
||||
You can specify the application device type with the \`--type\` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
You can specify the organization the application 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.
|
||||
|
||||
You can see a list of supported device types with:
|
||||
The application'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.
|
||||
`;
|
||||
|
||||
$ balena devices supported
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --type raspberry-pi',
|
||||
'$ balena app create MyApp --organization mmyorg',
|
||||
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
@ -60,6 +71,11 @@ export default class AppCreateCmd extends Command {
|
||||
public static usage = 'app create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
organization: flags.string({
|
||||
char: 'o',
|
||||
description:
|
||||
'handle of the organization the application should belong to',
|
||||
}),
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
@ -75,30 +91,62 @@ export default class AppCreateCmd extends Command {
|
||||
AppCreateCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Create application
|
||||
// Ascertain device type
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
let application: BalenaSDK.Application;
|
||||
|
||||
// Ascertain organization
|
||||
const organization =
|
||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
||||
|
||||
// Create application
|
||||
let application: Application;
|
||||
try {
|
||||
application = await balena.models.application.create({
|
||||
application = await getBalenaSdk().models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization: (await balena.auth.whoami())!,
|
||||
organization,
|
||||
});
|
||||
} catch (err) {
|
||||
// BalenaRequestError: Request error: Unique key constraint violated
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
throw new ExpectedError(
|
||||
`Error: application "${params.name}" already exists`,
|
||||
`Error: application "${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 applications in organization "${organization}".`,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
console.info(
|
||||
`Application created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
|
||||
// Output
|
||||
const { isV13 } = await import('../../utils/version');
|
||||
console.log(
|
||||
isV13()
|
||||
? `Application created: slug "${application.slug}", device type "${deviceType}"`
|
||||
: `Application created: ${application.slug} (${deviceType}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
async getOrganization() {
|
||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +189,7 @@ const EXPECTED_ERROR_REGEXES = [
|
||||
/^BalenaInvalidDeviceType/, // balena-sdk
|
||||
/Cannot deactivate devices/i, // balena-api
|
||||
/Devices must be offline in order to be deactivated\.$/i, // balena-api
|
||||
/^BalenaOrganizationNotFound/, // balena-sdk
|
||||
/Request error: Unauthorized$/, // balena-sdk
|
||||
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
|
||||
/Missing required flag/, // oclif parser: RequiredFlagError
|
||||
|
@ -26,6 +26,7 @@ import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
||||
import validation = require('./validation');
|
||||
import { delay } from './helpers';
|
||||
import { isV13 } from './version';
|
||||
import { Organization } from 'balena-sdk';
|
||||
|
||||
export function authenticate(options: {}): Promise<void> {
|
||||
const balena = getBalenaSdk();
|
||||
@ -210,54 +211,18 @@ export function selectApplication(
|
||||
});
|
||||
}
|
||||
|
||||
export function selectOrCreateApplication() {
|
||||
const balena = getBalenaSdk();
|
||||
return balena.models.application
|
||||
.hasAny()
|
||||
.then((hasAnyApplications) => {
|
||||
if (!hasAnyApplications) {
|
||||
// Just to make TS happy
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return (balena.models.application.getAll({
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
}) as Promise<ApplicationWithDeviceType[]>).then((applications) => {
|
||||
const appOptions: Array<{ name: string; value: string | null }> = _.map(
|
||||
applications,
|
||||
(application) => ({
|
||||
name: `${application.app_name} (${application.is_for__device_type[0].slug})`,
|
||||
value: application.app_name,
|
||||
}),
|
||||
);
|
||||
|
||||
appOptions.unshift({
|
||||
name: 'Create a new application',
|
||||
value: null,
|
||||
});
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Select an application',
|
||||
type: 'list',
|
||||
choices: appOptions,
|
||||
});
|
||||
});
|
||||
})
|
||||
.then((application) => {
|
||||
if (application) {
|
||||
return application;
|
||||
}
|
||||
|
||||
return getCliForm().ask({
|
||||
message: 'Choose a Name for your new application',
|
||||
type: 'input',
|
||||
validate: validation.validateApplicationName,
|
||||
});
|
||||
});
|
||||
export async function selectOrganization(organizations?: Organization[]) {
|
||||
// Use either provided orgs (if e.g. already loaded) or load from cloud
|
||||
organizations =
|
||||
organizations || (await getBalenaSdk().models.organization.getAll());
|
||||
return getCliForm().ask({
|
||||
message: 'Select an organization',
|
||||
type: 'list',
|
||||
choices: organizations.map((org) => ({
|
||||
name: `${org.name} (${org.handle})`,
|
||||
value: org.handle,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
export async function awaitDevice(uuid: string) {
|
||||
|
@ -32,10 +32,6 @@ export async function getApplication(
|
||||
nameOrSlugOrId: string | number,
|
||||
options?: PineOptions<Application>,
|
||||
): Promise<Application> {
|
||||
// TODO: Consider whether it would be useful to generally include interactive selection of application here,
|
||||
// when nameOrSlugOrId not provided.
|
||||
// e.g. nameOrSlugOrId || (await (await import('../../utils/patterns')).selectApplication()),
|
||||
// See commands/device/init.ts ~ln100 for example
|
||||
const { looksLikeInteger } = await import('./validation');
|
||||
if (looksLikeInteger(nameOrSlugOrId as string)) {
|
||||
try {
|
||||
|
Loading…
x
Reference in New Issue
Block a user