From 5628824bee935f6520fad3be498ea768e591755b Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Wed, 19 Jul 2023 19:14:06 +0300 Subject: [PATCH] promote: Allow joining fleets of discontinued device types Change-type: patch --- lib/commands/fleets.ts | 22 +++++---- lib/commands/os/configure.ts | 4 +- lib/utils/promote.ts | 91 +++++++++++++++++++++--------------- typings/global.d.ts | 7 ++- 4 files changed, 75 insertions(+), 49 deletions(-) diff --git a/lib/commands/fleets.ts b/lib/commands/fleets.ts index 35e6ee22..6146009c 100644 --- a/lib/commands/fleets.ts +++ b/lib/commands/fleets.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import type * as BalenaSdk from 'balena-sdk'; import { flags } from '@oclif/command'; import Command from '../command'; @@ -22,7 +23,7 @@ import * as cf from '../utils/common-flags'; import { getBalenaSdk, stripIndent } from '../utils/lazy'; import type { DataSetOutputOptions } from '../framework'; -interface ExtendedApplication extends ApplicationWithDeviceType { +interface ExtendedApplication extends ApplicationWithDeviceTypeSlug { device_count: number; online_devices: number; device_type?: string; @@ -60,15 +61,20 @@ export default class FleetsCmd extends Command { const balena = getBalenaSdk(); + const pineOptions = { + $select: ['id', 'app_name', 'slug'], + $expand: { + is_for__device_type: { $select: 'slug' }, + owns__device: { $select: 'is_online' }, + }, + } satisfies BalenaSdk.PineOptions; // Get applications 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[]; + (await balena.models.application.getAllDirectlyAccessible( + pineOptions, + )) as Array< + BalenaSdk.PineTypedResult + > as ExtendedApplication[]; // Add extended properties applications.forEach((application) => { diff --git a/lib/commands/os/configure.ts b/lib/commands/os/configure.ts index c7f7aeaf..01db1a71 100644 --- a/lib/commands/os/configure.ts +++ b/lib/commands/os/configure.ts @@ -204,7 +204,7 @@ export default class OsConfigureCmd extends Command { const helpers = await import('../../utils/helpers'); const { getApplication } = await import('../../utils/sdk'); - let app: ApplicationWithDeviceType | undefined; + let app: ApplicationWithDeviceTypeSlug | undefined; let device; let deviceTypeSlug: string; @@ -223,7 +223,7 @@ export default class OsConfigureCmd extends Command { $expand: { is_for__device_type: { $select: 'slug' }, }, - })) as ApplicationWithDeviceType; + })) as ApplicationWithDeviceTypeSlug; await checkDeviceTypeCompatibility(options, app); deviceTypeSlug = options['device-type'] || app.is_for__device_type[0].slug; diff --git a/lib/utils/promote.ts b/lib/utils/promote.ts index b0257d51..3e585531 100644 --- a/lib/utils/promote.ts +++ b/lib/utils/promote.ts @@ -228,8 +228,8 @@ async function selectLocalDevice(): Promise { } async function selectAppFromList( - applications: ApplicationWithDeviceType[], -): Promise { + applications: ApplicationWithDeviceTypeSlug[], +): Promise { const _ = await import('lodash'); const { selectFromList } = await import('../utils/patterns'); @@ -247,7 +247,7 @@ async function getOrSelectApplication( sdk: BalenaSdk.BalenaSDK, deviceTypeSlug: string, appName?: string, -): Promise { +): Promise { const pineOptions = { $select: 'slug', $expand: { @@ -256,51 +256,72 @@ async function getOrSelectApplication( }, }, } satisfies BalenaSdk.PineOptions; - const [deviceType, allDeviceTypes] = await Promise.all([ - sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise< - BalenaSdk.PineTypedResult - >, - sdk.models.deviceType.getAllSupported(pineOptions) as Promise< - Array> - >, - ]); + const deviceType = (await sdk.models.deviceType.get( + deviceTypeSlug, + pineOptions, + )) as BalenaSdk.PineTypedResult; + const allCpuArches = await sdk.pine.get({ + resource: 'cpu_architecture', + options: { + $select: ['id', 'slug'], + }, + }); - const compatibleDeviceTypes = allDeviceTypes - .filter((dt) => + const compatibleCpuArchIds = allCpuArches + .filter((cpuArch) => sdk.models.os.isArchitectureCompatibleWith( deviceType.is_of__cpu_architecture[0].slug, - dt.is_of__cpu_architecture[0].slug, + cpuArch.slug, ), ) - .map((type) => type.slug); + .map((cpu) => cpu.id); if (!appName) { - return createOrSelectApp(sdk, compatibleDeviceTypes, deviceTypeSlug); + return createOrSelectApp( + sdk, + { + is_for__device_type: { + $any: { + $alias: 'dt', + $expr: { + dt: { + is_of__cpu_architecture: { $in: compatibleCpuArchIds }, + }, + }, + }, + }, + }, + deviceTypeSlug, + ); } - const options: BalenaSdk.PineOptions = { + const options = { $expand: { - is_for__device_type: { $select: 'slug' }, + is_for__device_type: { $select: ['slug', 'is_of__cpu_architecture'] }, }, - }; + } satisfies BalenaSdk.PineOptions; // Check for a fleet slug of the form `user/fleet` and update the API query. let name: string; const match = appName.split('/'); if (match.length > 1) { // These will match at most one fleet - options.$filter = { slug: appName.toLowerCase() }; + (options as BalenaSdk.PineOptions).$filter = { + slug: appName.toLowerCase(), + }; name = match[1]; } else { // We're given an application; resolve it if it's ambiguous and also validate // it's of appropriate device type. - options.$filter = { app_name: appName }; + (options as BalenaSdk.PineOptions).$filter = { + app_name: appName, + }; name = appName; } const applications = (await sdk.models.application.getAllDirectlyAccessible( options, - )) as ApplicationWithDeviceType[]; + )) as Array>; if (applications.length === 0) { await confirm( @@ -315,8 +336,11 @@ async function getOrSelectApplication( // We've found at least one fleet with the given name. // Filter out fleets for non-matching device types and see what we're left with. + const compatibleCpuArchIdsSet = new Set(compatibleCpuArchIds); const validApplications = applications.filter((app) => - compatibleDeviceTypes.includes(app.is_for__device_type[0].slug), + compatibleCpuArchIdsSet.has( + app.is_for__device_type[0].is_of__cpu_architecture.__id, + ), ); if (validApplications.length === 0) { @@ -332,21 +356,14 @@ async function getOrSelectApplication( async function createOrSelectApp( sdk: BalenaSdk.BalenaSDK, - compatibleDeviceTypes: string[], + compatibleDeviceTypesFilter: BalenaSdk.PineFilter, deviceType: string, -): Promise { +): Promise { // No fleet specified, show a list to select one. const applications = (await sdk.models.application.getAllDirectlyAccessible({ $expand: { is_for__device_type: { $select: 'slug' } }, - $filter: { - is_for__device_type: { - $any: { - $alias: 'dt', - $expr: { dt: { slug: { $in: compatibleDeviceTypes } } }, - }, - }, - }, - })) as ApplicationWithDeviceType[]; + $filter: compatibleDeviceTypesFilter, + })) as ApplicationWithDeviceTypeSlug[]; if (applications.length === 0) { await confirm( @@ -366,7 +383,7 @@ async function createApplication( sdk: BalenaSdk.BalenaSDK, deviceType: string, name?: string, -): Promise { +): Promise { const validation = await import('./validation'); const username = await sdk.auth.whoami(); @@ -414,12 +431,12 @@ async function createApplication( $expand: { is_for__device_type: { $select: 'slug' }, }, - })) as ApplicationWithDeviceType; + })) as ApplicationWithDeviceTypeSlug; } async function generateApplicationConfig( sdk: BalenaSdk.BalenaSDK, - app: ApplicationWithDeviceType, + app: ApplicationWithDeviceTypeSlug, options: { version: string; appUpdatePollInterval?: number; diff --git a/typings/global.d.ts b/typings/global.d.ts index 5be7c315..7fb3b4b3 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -1,8 +1,11 @@ import { Application, DeviceType, Device } from 'balena-sdk'; declare global { - type ApplicationWithDeviceType = Application & { - is_for__device_type: [DeviceType]; + type ApplicationWithDeviceTypeSlug = Omit< + Application, + 'is_for__device_type' + > & { + is_for__device_type: [Pick]; }; type DeviceWithDeviceType = Device & { is_of__device_type: [DeviceType];