From c86cdc8f842e4076ae55cd4c0e5ea087e81b2635 Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Tue, 21 Dec 2021 00:02:45 +0000 Subject: [PATCH] balena SDK v16: Ensure all SDK calls use fleet slug rather than name Change-type: patch --- docs/balena-cli.md | 4 +- lib/commands/device/init.ts | 2 +- lib/commands/device/register.ts | 2 +- lib/commands/env/add.ts | 3 +- lib/commands/fleet/index.ts | 2 +- lib/commands/fleets.ts | 15 +++--- lib/commands/preload.ts | 10 ++-- lib/commands/releases.ts | 5 +- lib/commands/support.ts | 13 +++-- lib/commands/tag/rm.ts | 4 +- lib/commands/tag/set.ts | 4 +- lib/commands/tags.ts | 4 +- lib/utils/cloud.ts | 4 +- lib/utils/helpers.ts | 46 ++++++----------- lib/utils/patterns.ts | 2 +- lib/utils/promote.ts | 9 ++-- lib/utils/sdk.ts | 87 ++++++++++++++------------------- lib/utils/validation.ts | 5 +- 18 files changed, 98 insertions(+), 123 deletions(-) diff --git a/docs/balena-cli.md b/docs/balena-cli.md index 084c183d..c78a7216 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -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 diff --git a/lib/commands/device/init.ts b/lib/commands/device/init.ts index 2f3be70e..0ad5bc22 100644 --- a/lib/commands/device/init.ts +++ b/lib/commands/device/init.ts @@ -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); diff --git a/lib/commands/device/register.ts b/lib/commands/device/register.ts index ae4d8745..89cc29eb 100644 --- a/lib/commands/device/register.ts +++ b/lib/commands/device/register.ts @@ -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); diff --git a/lib/commands/env/add.ts b/lib/commands/env/add.ts index f7dbbe1c..4eb4475e 100644 --- a/lib/commands/env/add.ts +++ b/lib/commands/env/add.ts @@ -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, ); diff --git a/lib/commands/fleet/index.ts b/lib/commands/fleet/index.ts index 49a4a2f9..76f496c5 100644 --- a/lib/commands/fleet/index.ts +++ b/lib/commands/fleet/index.ts @@ -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', diff --git a/lib/commands/fleets.ts b/lib/commands/fleets.ts index 1d2dc50d..04a6cd28 100644 --- a/lib/commands/fleets.ts +++ b/lib/commands/fleets.ts @@ -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) => { diff --git a/lib/commands/preload.ts b/lib/commands/preload.ts index f6fe6b4c..766c0450 100644 --- a/lib/commands/preload.ts +++ b/lib/commands/preload.ts @@ -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, { diff --git a/lib/commands/releases.ts b/lib/commands/releases.ts index a3294061..43f375c9 100644 --- a/lib/commands/releases.ts +++ b/lib/commands/releases.ts @@ -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 }, ); diff --git a/lib/commands/support.ts b/lib/commands/support.ts index 45decd06..c467ce2c 100644 --- a/lib/commands/support.ts +++ b/lib/commands/support.ts @@ -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(); } diff --git a/lib/commands/tag/rm.ts b/lib/commands/tag/rm.ts index b23b5dbc..d1ff1b41 100644 --- a/lib/commands/tag/rm.ts +++ b/lib/commands/tag/rm.ts @@ -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, ); } diff --git a/lib/commands/tag/set.ts b/lib/commands/tag/set.ts index 21e9f29e..82a62e26 100644 --- a/lib/commands/tag/set.ts +++ b/lib/commands/tag/set.ts @@ -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, ); diff --git a/lib/commands/tags.ts b/lib/commands/tags.ts index 53e56705..0eac8ce6 100644 --- a/lib/commands/tags.ts +++ b/lib/commands/tags.ts @@ -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) { diff --git a/lib/utils/cloud.ts b/lib/utils/cloud.ts index 99b3b1f8..c4111c9d 100644 --- a/lib/utils/cloud.ts +++ b/lib/utils/cloud.ts @@ -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] || []) diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 8cf35e9b..35077c25 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -151,26 +151,8 @@ export async function osProgressHandler(step: InitializeEmitter) { export async function getAppWithArch( applicationName: string, ): Promise { - 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 { - // 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 = { + const { getApplication } = await import('./sdk'); + const options: BalenaSdk.PineOptions = { $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; - } - - return balena.models.application.get( + const app = (await getApplication( + balena, applicationName, - extraOptions, - ) as Promise; + 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 diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts index 40d65857..9503d1c8 100644 --- a/lib/utils/patterns.ts +++ b/lib/utils/patterns.ts @@ -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', diff --git a/lib/utils/promote.ts b/lib/utils/promote.ts index dc80c9f3..6e51ab6f 100644 --- a/lib/utils/promote.ts +++ b/lib/utils/promote.ts @@ -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 { // 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: '/' } } }, ], }, diff --git a/lib/utils/sdk.ts b/lib/utils/sdk.ts index 048d9254..978d519e 100644 --- a/lib/utils/sdk.ts +++ b/lib/utils/sdk.ts @@ -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, ): Promise { - 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 { - 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 diff --git a/lib/utils/validation.ts b/lib/utils/validation.ts index 54ecbfa8..94f54fcc 100644 --- a/lib/utils/validation.ts +++ b/lib/utils/validation.ts @@ -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('/'); }