balena SDK v16: Ensure all SDK calls use fleet slug rather than name

Change-type: patch
This commit is contained in:
Paulo Castro 2021-12-21 00:02:45 +00:00
parent 84f02dc063
commit c86cdc8f84
18 changed files with 98 additions and 123 deletions

View File

@ -1148,7 +1148,7 @@ Examples:
#### FLEET
fleet name or slug
fleet name or slug (preferred)
### Options
@ -3431,7 +3431,7 @@ comma-separated list (no spaces) of device UUIDs
#### -f, --fleet FLEET
comma-separated list (no spaces) of fleet names or org/name slugs
comma-separated list (no spaces) of fleet names or slugs (preferred)
#### -t, --duration DURATION

View File

@ -135,7 +135,7 @@ export default class DeviceInitCmd extends Command {
// Register new device
const deviceUuid = balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${deviceUuid}`);
console.info(`Registering to ${application.slug}: ${deviceUuid}`);
await balena.models.device.register(application.id, deviceUuid);
const device = await balena.models.device.get(deviceUuid);

View File

@ -75,7 +75,7 @@ export default class DeviceRegisterCmd extends Command {
const application = await getApplication(balena, params.fleet);
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
console.info(`Registering to ${application.app_name}: ${uuid}`);
console.info(`Registering to ${application.slug}: ${uuid}`);
const result = await balena.models.device.register(application.id, uuid);

View File

@ -151,10 +151,11 @@ export default class EnvAddCmd extends Command {
const varType = isConfigVar ? 'configVar' : 'envVar';
if (options.fleet) {
const { getFleetSlug } = await import('../../utils/sdk');
for (const app of options.fleet.split(',')) {
try {
await balena.models.application[varType].set(
app,
await getFleetSlug(balena, app),
params.name,
params.value,
);

View File

@ -89,7 +89,7 @@ export default class FleetCmd extends Command {
);
} else {
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
console.log(`== ${application.app_name}`);
console.log(`== ${application.slug}`);
console.log(
getVisuals().table.vertical(application, [
'id',

View File

@ -62,13 +62,14 @@ export default class FleetsCmd extends Command {
const balena = getBalenaSdk();
// Get applications
const applications = (await balena.models.application.getAll({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
const applications =
(await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'app_name', 'slug'],
$expand: {
is_for__device_type: { $select: 'slug' },
owns__device: { $select: 'is_online' },
},
})) as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {

View File

@ -343,8 +343,8 @@ Can be repeated to add multiple certificates.\
} catch {
throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
}
return (await balena.models.application.getAll({
$select: ['id', 'app_name', 'should_track_latest_release'],
return (await balena.models.application.getAllDirectlyAccessible({
$select: ['id', 'slug', 'should_track_latest_release'],
$expand: this.applicationExpandOptions,
$filter: {
// get the apps that are of the same arch as the device type of the image
@ -387,7 +387,7 @@ Can be repeated to add multiple certificates.\
},
},
},
$orderby: 'app_name asc',
$orderby: 'slug asc',
})) as Array<
ApplicationWithDeviceType & {
should_be_running__release: [Release?];
@ -416,7 +416,7 @@ Can be repeated to add multiple certificates.\
message: 'Select a fleet',
type: 'list',
choices: applications.map((app) => ({
name: app.app_name,
name: app.slug,
value: app,
})),
});
@ -491,7 +491,7 @@ Would you like to disable automatic updates for this fleet now?\
});
}
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {
async getAppWithReleases(balenaSdk: BalenaSDK, appId: string) {
const { getApplication } = await import('../utils/sdk');
return (await getApplication(balenaSdk, appId, {

View File

@ -49,7 +49,7 @@ export default class ReleasesCmd extends Command {
public static args = [
{
name: 'fleet',
description: 'fleet name or slug',
description: 'fleet name or slug (preferred)',
required: true,
},
];
@ -69,9 +69,10 @@ export default class ReleasesCmd extends Command {
];
const balena = getBalenaSdk();
const { getFleetSlug } = await import('../utils/sdk');
const releases = await balena.models.release.getAllByApplication(
params.fleet,
await getFleetSlug(balena, params.fleet),
{ $select: fields },
);

View File

@ -74,7 +74,7 @@ export default class SupportCmd extends Command {
fleet: {
...cf.fleet,
description:
'comma-separated list (no spaces) of fleet names or org/name slugs',
'comma-separated list (no spaces) of fleet names or slugs (preferred)',
},
duration: flags.string({
description:
@ -130,14 +130,17 @@ export default class SupportCmd extends Command {
ux.action.stop();
}
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 ${appName}`);
await balena.models.application.grantSupportAccess(appName, expiryTs);
ux.action.start(`${enablingMessage} fleet ${slug}`);
await balena.models.application.grantSupportAccess(slug, expiryTs);
} else if (params.action === 'disable') {
ux.action.start(`${disablingMessage} fleet ${appName}`);
await balena.models.application.revokeSupportAccess(appName);
ux.action.start(`${disablingMessage} fleet ${slug}`);
await balena.models.application.revokeSupportAccess(slug);
}
ux.action.stop();
}

View File

@ -93,9 +93,9 @@ export default class TagRmCmd extends Command {
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.remove(
await getTypedApplicationIdentifier(balena, options.fleet),
await getFleetSlug(balena, options.fleet),
params.tagKey,
);
}

View File

@ -108,9 +108,9 @@ export default class TagSetCmd extends Command {
const { tryAsInteger } = await import('../../utils/validation');
if (options.fleet) {
const { getTypedApplicationIdentifier } = await import('../../utils/sdk');
const { getFleetSlug } = await import('../../utils/sdk');
return balena.models.application.tags.set(
await getTypedApplicationIdentifier(balena, options.fleet),
await getFleetSlug(balena, options.fleet),
params.tagKey,
params.value,
);

View File

@ -81,9 +81,9 @@ export default class TagsCmd extends Command {
let tags;
if (options.fleet) {
const { getTypedApplicationIdentifier } = await import('../utils/sdk');
const { getFleetSlug } = await import('../utils/sdk');
tags = await balena.models.application.tags.getAllByApplication(
await getTypedApplicationIdentifier(balena, options.fleet),
await getFleetSlug(balena, options.fleet),
);
}
if (options.device) {

View File

@ -253,13 +253,13 @@ export async function getFormattedOsVersions(
const sdk = getBalenaSdk();
let slug = deviceType;
let versionsByDT: SDK.OsVersionsByDeviceType =
await sdk.models.hostapp.getAvailableOsVersions([slug]);
await sdk.models.os.getAvailableOsVersions([slug]);
// if slug is an alias, fetch the real slug
if (!versionsByDT[slug]?.length) {
// unaliasDeviceType() produces a nice error msg if slug is invalid
slug = await unaliasDeviceType(sdk, slug);
if (slug !== deviceType) {
versionsByDT = await sdk.models.hostapp.getAvailableOsVersions([slug]);
versionsByDT = await sdk.models.os.getAvailableOsVersions([slug]);
}
}
const versions: SDK.OsVersion[] = (versionsByDT[slug] || [])

View File

@ -151,26 +151,8 @@ export async function osProgressHandler(step: InitializeEmitter) {
export async function getAppWithArch(
applicationName: string,
): Promise<ApplicationWithDeviceType & { arch: string }> {
const app = await getApplication(applicationName);
const { getExpanded } = await import('./pine');
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
// TODO: Drop this. The sdk now has this baked in application.get().
function getApplication(
applicationName: string,
): Promise<ApplicationWithDeviceType> {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = applicationName.split('/');
const extraOptions: BalenaSdk.PineOptions<BalenaSdk.Application> = {
const { getApplication } = await import('./sdk');
const options: BalenaSdk.PineOptions<BalenaSdk.Application> = {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
@ -185,20 +167,20 @@ function getApplication(
},
},
};
const balena = getBalenaSdk();
if (match.length > 1) {
return balena.models.application.getAppByOwner(
match[1],
match[0],
extraOptions,
) as Promise<ApplicationWithDeviceType>;
}
return balena.models.application.get(
const app = (await getApplication(
balena,
applicationName,
extraOptions,
) as Promise<ApplicationWithDeviceType>;
options,
)) as ApplicationWithDeviceType;
const { getExpanded } = await import('./pine');
return {
...app,
arch: getExpanded(
getExpanded(app.is_for__device_type)!.is_of__cpu_architecture,
)!.slug,
};
}
const second = 1000; // 1000 milliseconds

View File

@ -169,7 +169,7 @@ export function selectApplication(
throw new ExpectedError('No fleets found');
}
const apps = (await balena.models.application.getAll({
const apps = (await balena.models.application.getAllDirectlyAccessible({
$expand: {
is_for__device_type: {
$select: 'slug',

View File

@ -235,7 +235,7 @@ async function getOrSelectApplication(
name = appName;
}
const applications = (await sdk.models.application.getAll(
const applications = (await sdk.models.application.getAllDirectlyAccessible(
options,
)) as ApplicationWithDeviceType[];
@ -273,7 +273,7 @@ async function createOrSelectApp(
deviceType: string,
): Promise<ApplicationWithDeviceType> {
// No fleet specified, show a list to select one.
const applications = (await sdk.models.application.getAll({
const applications = (await sdk.models.application.getAllDirectlyAccessible({
$expand: { is_for__device_type: { $select: 'slug' } },
$filter: {
is_for__device_type: {
@ -322,10 +322,13 @@ async function createApplication(
});
try {
await sdk.models.application.get(appName, {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
$or: [
{ slug: { $startswith: `${username!.toLowerCase()}/` } },
// TODO: do we still need the following filter? Is it for
// old openBalena instances where slugs were equal to the
// app name and did not contain the slash character?
{ $not: { slug: { $contains: '/' } } },
],
},

View File

@ -23,21 +23,36 @@ import type {
} from 'balena-sdk';
/**
* Wraps the sdk application.get method,
* adding disambiguation in cases where the provided
* identifier could be interpreted in multiple valid ways.
* Get a fleet object, disambiguating the fleet identifier which may be a
* a fleet slug, name or numeric database ID (as a string).
* TODO: add support for fleet UUIDs.
*/
export async function getApplication(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
options?: PineOptions<Application>,
): Promise<Application> {
const { looksLikeInteger } = await import('./validation');
if (looksLikeInteger(nameOrSlugOrId as string)) {
const { looksLikeFleetSlug, looksLikeInteger } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return await sdk.models.application.getDirectlyAccessible(
nameOrSlugOrId,
options,
);
}
if (typeof nameOrSlugOrId === 'number' || looksLikeInteger(nameOrSlugOrId)) {
try {
// Test for existence of app with this numerical ID
return await sdk.models.application.get(Number(nameOrSlugOrId), options);
return await sdk.models.application.getDirectlyAccessible(
Number(nameOrSlugOrId),
options,
);
} catch (e) {
if (typeof nameOrSlugOrId === 'number') {
throw e;
}
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
@ -46,13 +61,19 @@ export async function getApplication(
// App with this numerical ID not found, but there may be an app with this numerical name.
}
}
return sdk.models.application.get(nameOrSlugOrId, options);
// Not a slug and not a numeric database ID: must be an app name.
// TODO: revisit this logic when we add support for fleet UUIDs.
return await sdk.models.application.getAppByName(
nameOrSlugOrId,
options,
'directly_accessible',
);
}
/**
* Given a fleet name, slug or numeric ID, return its slug.
* Given a fleet name, slug or numeric database ID, return its slug.
* This function conditionally makes an async SDK/API call to retrieve the
* application object, which can be wasteful is the application object is
* application object, which can be wasteful if the application object is
* required before or after the call to this function. If this is the case,
* consider calling `getApplication()` and reusing the application object.
*/
@ -60,52 +81,16 @@ export async function getFleetSlug(
sdk: BalenaSDK,
nameOrSlugOrId: string | number,
): Promise<string> {
if (typeof nameOrSlugOrId === 'string' && nameOrSlugOrId.includes('/')) {
return nameOrSlugOrId;
const { looksLikeFleetSlug } = await import('./validation');
if (
typeof nameOrSlugOrId === 'string' &&
looksLikeFleetSlug(nameOrSlugOrId)
) {
return nameOrSlugOrId.toLowerCase();
}
return (await getApplication(sdk, nameOrSlugOrId)).slug;
}
/**
* Given an string representation of an application identifier,
* which could be one of:
* - name (including numeric names)
* - slug
* - numerical id
* disambiguate and return a properly typed identifier.
*
* Attempts to minimise the number of API calls required.
* TODO: Remove this once support for numeric App IDs is removed.
*/
export async function getTypedApplicationIdentifier(
sdk: BalenaSDK,
nameOrSlugOrId: string,
) {
const { looksLikeInteger } = await import('./validation');
// If there's no possible ambiguity,
// return the passed identifier unchanged
if (!looksLikeInteger(nameOrSlugOrId)) {
return nameOrSlugOrId;
}
// Resolve ambiguity
try {
// Test for existence of app with this numerical ID,
// and return typed id if found
return (await sdk.models.application.get(Number(nameOrSlugOrId))).id;
} catch (e) {
const { instanceOf } = await import('../errors');
const { BalenaApplicationNotFound } = await import('balena-errors');
if (!instanceOf(e, BalenaApplicationNotFound)) {
throw e;
}
}
// App with this numerical id not found
// return the passed identifier unchanged
return nameOrSlugOrId;
}
/**
* Wraps the sdk organization.getAll method,
* restricting to those orgs user is a member of

View File

@ -118,7 +118,6 @@ export function parseAsLocalHostnameOrIp(input: string, paramName?: string) {
return input;
}
export function looksLikeAppSlug(input: string) {
// One or more non whitespace chars, /, 4 or more non whitespace chars
return /[\S]+\/[\S]{4,}/.test(input);
export function looksLikeFleetSlug(input: string) {
return input.includes('/');
}