mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-17 15:28:08 +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:
@ -345,17 +345,26 @@ application name or org/name slug
|
|||||||
|
|
||||||
Create a new balena application.
|
Create a new balena application.
|
||||||
|
|
||||||
You can specify the application device type with the `--type` option.
|
You can specify the organization the application should belong to using
|
||||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
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:
|
Examples:
|
||||||
|
|
||||||
$ balena app create MyApp
|
$ 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
|
### Arguments
|
||||||
|
|
||||||
@ -365,6 +374,10 @@ application name
|
|||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
|
#### -o, --organization ORGANIZATION
|
||||||
|
|
||||||
|
handle of the organization the application should belong to
|
||||||
|
|
||||||
#### -t, --type TYPE
|
#### -t, --type TYPE
|
||||||
|
|
||||||
application device type (Check available types with `balena devices supported`)
|
application device type (Check available types with `balena devices supported`)
|
||||||
|
@ -20,9 +20,10 @@ import Command from '../../command';
|
|||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import type * as BalenaSDK from 'balena-sdk';
|
import type { Application } from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
|
organization?: string;
|
||||||
type?: string; // application device type
|
type?: string; // application device type
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
@ -37,16 +38,26 @@ export default class AppCreateCmd extends Command {
|
|||||||
|
|
||||||
Create a new balena application.
|
Create a new balena application.
|
||||||
|
|
||||||
You can specify the application device type with the \`--type\` option.
|
You can specify the organization the application should belong to using
|
||||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
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 = [
|
public static examples = [
|
||||||
'$ balena app create MyApp',
|
'$ 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 = [
|
public static args = [
|
||||||
@ -60,6 +71,11 @@ export default class AppCreateCmd extends Command {
|
|||||||
public static usage = 'app create <name>';
|
public static usage = 'app create <name>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags: flags.Input<FlagsDef> = {
|
||||||
|
organization: flags.string({
|
||||||
|
char: 'o',
|
||||||
|
description:
|
||||||
|
'handle of the organization the application should belong to',
|
||||||
|
}),
|
||||||
type: flags.string({
|
type: flags.string({
|
||||||
char: 't',
|
char: 't',
|
||||||
description:
|
description:
|
||||||
@ -75,30 +91,62 @@ export default class AppCreateCmd extends Command {
|
|||||||
AppCreateCmd,
|
AppCreateCmd,
|
||||||
);
|
);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
// Ascertain device type
|
||||||
|
|
||||||
// Create application
|
|
||||||
const deviceType =
|
const deviceType =
|
||||||
options.type ||
|
options.type ||
|
||||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
(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 {
|
try {
|
||||||
application = await balena.models.application.create({
|
application = await getBalenaSdk().models.application.create({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
deviceType,
|
deviceType,
|
||||||
organization: (await balena.auth.whoami())!,
|
organization,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// BalenaRequestError: Request error: Unique key constraint violated
|
|
||||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||||
|
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||||
throw new ExpectedError(
|
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;
|
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
|
/^BalenaInvalidDeviceType/, // balena-sdk
|
||||||
/Cannot deactivate devices/i, // balena-api
|
/Cannot deactivate devices/i, // balena-api
|
||||||
/Devices must be offline in order to be deactivated\.$/i, // balena-api
|
/Devices must be offline in order to be deactivated\.$/i, // balena-api
|
||||||
|
/^BalenaOrganizationNotFound/, // balena-sdk
|
||||||
/Request error: Unauthorized$/, // balena-sdk
|
/Request error: Unauthorized$/, // balena-sdk
|
||||||
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
|
/^Missing \d+ required arg/, // oclif parser: RequiredArgsError
|
||||||
/Missing required flag/, // oclif parser: RequiredFlagError
|
/Missing required flag/, // oclif parser: RequiredFlagError
|
||||||
|
@ -26,6 +26,7 @@ import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
|||||||
import validation = require('./validation');
|
import validation = require('./validation');
|
||||||
import { delay } from './helpers';
|
import { delay } from './helpers';
|
||||||
import { isV13 } from './version';
|
import { isV13 } from './version';
|
||||||
|
import { Organization } from 'balena-sdk';
|
||||||
|
|
||||||
export function authenticate(options: {}): Promise<void> {
|
export function authenticate(options: {}): Promise<void> {
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
@ -210,53 +211,17 @@ export function selectApplication(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectOrCreateApplication() {
|
export async function selectOrganization(organizations?: Organization[]) {
|
||||||
const balena = getBalenaSdk();
|
// Use either provided orgs (if e.g. already loaded) or load from cloud
|
||||||
return balena.models.application
|
organizations =
|
||||||
.hasAny()
|
organizations || (await getBalenaSdk().models.organization.getAll());
|
||||||
.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({
|
return getCliForm().ask({
|
||||||
message: 'Select an application',
|
message: 'Select an organization',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: appOptions,
|
choices: organizations.map((org) => ({
|
||||||
});
|
name: `${org.name} (${org.handle})`,
|
||||||
});
|
value: org.handle,
|
||||||
})
|
})),
|
||||||
.then((application) => {
|
|
||||||
if (application) {
|
|
||||||
return application;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getCliForm().ask({
|
|
||||||
message: 'Choose a Name for your new application',
|
|
||||||
type: 'input',
|
|
||||||
validate: validation.validateApplicationName,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,10 +32,6 @@ export async function getApplication(
|
|||||||
nameOrSlugOrId: string | number,
|
nameOrSlugOrId: string | number,
|
||||||
options?: PineOptions<Application>,
|
options?: PineOptions<Application>,
|
||||||
): Promise<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');
|
const { looksLikeInteger } = await import('./validation');
|
||||||
if (looksLikeInteger(nameOrSlugOrId as string)) {
|
if (looksLikeInteger(nameOrSlugOrId as string)) {
|
||||||
try {
|
try {
|
||||||
|
Reference in New Issue
Block a user