promote: Allow joining fleets of discontinued device types

Change-type: patch
This commit is contained in:
Thodoris Greasidis 2023-07-19 19:14:06 +03:00
parent d12d7996bc
commit 5628824bee
4 changed files with 75 additions and 49 deletions

View File

@ -15,6 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type * as BalenaSdk from 'balena-sdk';
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import Command from '../command'; import Command from '../command';
@ -22,7 +23,7 @@ import * as cf from '../utils/common-flags';
import { getBalenaSdk, stripIndent } from '../utils/lazy'; import { getBalenaSdk, stripIndent } from '../utils/lazy';
import type { DataSetOutputOptions } from '../framework'; import type { DataSetOutputOptions } from '../framework';
interface ExtendedApplication extends ApplicationWithDeviceType { interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
device_count: number; device_count: number;
online_devices: number; online_devices: number;
device_type?: string; device_type?: string;
@ -60,15 +61,20 @@ export default class FleetsCmd extends Command {
const balena = getBalenaSdk(); 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<BalenaSdk.Application>;
// Get applications // Get applications
const applications = const applications =
(await balena.models.application.getAllDirectlyAccessible({ (await balena.models.application.getAllDirectlyAccessible(
$select: ['id', 'app_name', 'slug'], pineOptions,
$expand: { )) as Array<
is_for__device_type: { $select: 'slug' }, BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof pineOptions>
owns__device: { $select: 'is_online' }, > as ExtendedApplication[];
},
})) as ExtendedApplication[];
// Add extended properties // Add extended properties
applications.forEach((application) => { applications.forEach((application) => {

View File

@ -204,7 +204,7 @@ export default class OsConfigureCmd extends Command {
const helpers = await import('../../utils/helpers'); const helpers = await import('../../utils/helpers');
const { getApplication } = await import('../../utils/sdk'); const { getApplication } = await import('../../utils/sdk');
let app: ApplicationWithDeviceType | undefined; let app: ApplicationWithDeviceTypeSlug | undefined;
let device; let device;
let deviceTypeSlug: string; let deviceTypeSlug: string;
@ -223,7 +223,7 @@ export default class OsConfigureCmd extends Command {
$expand: { $expand: {
is_for__device_type: { $select: 'slug' }, is_for__device_type: { $select: 'slug' },
}, },
})) as ApplicationWithDeviceType; })) as ApplicationWithDeviceTypeSlug;
await checkDeviceTypeCompatibility(options, app); await checkDeviceTypeCompatibility(options, app);
deviceTypeSlug = deviceTypeSlug =
options['device-type'] || app.is_for__device_type[0].slug; options['device-type'] || app.is_for__device_type[0].slug;

View File

@ -228,8 +228,8 @@ async function selectLocalDevice(): Promise<string> {
} }
async function selectAppFromList( async function selectAppFromList(
applications: ApplicationWithDeviceType[], applications: ApplicationWithDeviceTypeSlug[],
): Promise<ApplicationWithDeviceType> { ): Promise<ApplicationWithDeviceTypeSlug> {
const _ = await import('lodash'); const _ = await import('lodash');
const { selectFromList } = await import('../utils/patterns'); const { selectFromList } = await import('../utils/patterns');
@ -247,7 +247,7 @@ async function getOrSelectApplication(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
deviceTypeSlug: string, deviceTypeSlug: string,
appName?: string, appName?: string,
): Promise<ApplicationWithDeviceType> { ): Promise<ApplicationWithDeviceTypeSlug> {
const pineOptions = { const pineOptions = {
$select: 'slug', $select: 'slug',
$expand: { $expand: {
@ -256,51 +256,72 @@ async function getOrSelectApplication(
}, },
}, },
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>; } satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const [deviceType, allDeviceTypes] = await Promise.all([ const deviceType = (await sdk.models.deviceType.get(
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise< deviceTypeSlug,
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions> pineOptions,
>, )) as BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>;
sdk.models.deviceType.getAllSupported(pineOptions) as Promise< const allCpuArches = await sdk.pine.get({
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>> resource: 'cpu_architecture',
>, options: {
]); $select: ['id', 'slug'],
},
});
const compatibleDeviceTypes = allDeviceTypes const compatibleCpuArchIds = allCpuArches
.filter((dt) => .filter((cpuArch) =>
sdk.models.os.isArchitectureCompatibleWith( sdk.models.os.isArchitectureCompatibleWith(
deviceType.is_of__cpu_architecture[0].slug, 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) { 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<BalenaSdk.Application> = { const options = {
$expand: { $expand: {
is_for__device_type: { $select: 'slug' }, is_for__device_type: { $select: ['slug', 'is_of__cpu_architecture'] },
}, },
}; } satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
// Check for a fleet slug of the form `user/fleet` and update the API query. // Check for a fleet slug of the form `user/fleet` and update the API query.
let name: string; let name: string;
const match = appName.split('/'); const match = appName.split('/');
if (match.length > 1) { if (match.length > 1) {
// These will match at most one fleet // These will match at most one fleet
options.$filter = { slug: appName.toLowerCase() }; (options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
slug: appName.toLowerCase(),
};
name = match[1]; name = match[1];
} else { } else {
// We're given an application; resolve it if it's ambiguous and also validate // We're given an application; resolve it if it's ambiguous and also validate
// it's of appropriate device type. // it's of appropriate device type.
options.$filter = { app_name: appName }; (options as BalenaSdk.PineOptions<BalenaSdk.Application>).$filter = {
app_name: appName,
};
name = appName; name = appName;
} }
const applications = (await sdk.models.application.getAllDirectlyAccessible( const applications = (await sdk.models.application.getAllDirectlyAccessible(
options, options,
)) as ApplicationWithDeviceType[]; )) as Array<BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof options>>;
if (applications.length === 0) { if (applications.length === 0) {
await confirm( await confirm(
@ -315,8 +336,11 @@ async function getOrSelectApplication(
// We've found at least one fleet with the given name. // 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. // 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) => 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) { if (validApplications.length === 0) {
@ -332,21 +356,14 @@ async function getOrSelectApplication(
async function createOrSelectApp( async function createOrSelectApp(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
compatibleDeviceTypes: string[], compatibleDeviceTypesFilter: BalenaSdk.PineFilter<BalenaSdk.Application>,
deviceType: string, deviceType: string,
): Promise<ApplicationWithDeviceType> { ): Promise<ApplicationWithDeviceTypeSlug> {
// No fleet specified, show a list to select one. // No fleet specified, show a list to select one.
const applications = (await sdk.models.application.getAllDirectlyAccessible({ const applications = (await sdk.models.application.getAllDirectlyAccessible({
$expand: { is_for__device_type: { $select: 'slug' } }, $expand: { is_for__device_type: { $select: 'slug' } },
$filter: { $filter: compatibleDeviceTypesFilter,
is_for__device_type: { })) as ApplicationWithDeviceTypeSlug[];
$any: {
$alias: 'dt',
$expr: { dt: { slug: { $in: compatibleDeviceTypes } } },
},
},
},
})) as ApplicationWithDeviceType[];
if (applications.length === 0) { if (applications.length === 0) {
await confirm( await confirm(
@ -366,7 +383,7 @@ async function createApplication(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
deviceType: string, deviceType: string,
name?: string, name?: string,
): Promise<ApplicationWithDeviceType> { ): Promise<ApplicationWithDeviceTypeSlug> {
const validation = await import('./validation'); const validation = await import('./validation');
const username = await sdk.auth.whoami(); const username = await sdk.auth.whoami();
@ -414,12 +431,12 @@ async function createApplication(
$expand: { $expand: {
is_for__device_type: { $select: 'slug' }, is_for__device_type: { $select: 'slug' },
}, },
})) as ApplicationWithDeviceType; })) as ApplicationWithDeviceTypeSlug;
} }
async function generateApplicationConfig( async function generateApplicationConfig(
sdk: BalenaSdk.BalenaSDK, sdk: BalenaSdk.BalenaSDK,
app: ApplicationWithDeviceType, app: ApplicationWithDeviceTypeSlug,
options: { options: {
version: string; version: string;
appUpdatePollInterval?: number; appUpdatePollInterval?: number;

7
typings/global.d.ts vendored
View File

@ -1,8 +1,11 @@
import { Application, DeviceType, Device } from 'balena-sdk'; import { Application, DeviceType, Device } from 'balena-sdk';
declare global { declare global {
type ApplicationWithDeviceType = Application & { type ApplicationWithDeviceTypeSlug = Omit<
is_for__device_type: [DeviceType]; Application,
'is_for__device_type'
> & {
is_for__device_type: [Pick<DeviceType, 'slug'>];
}; };
type DeviceWithDeviceType = Device & { type DeviceWithDeviceType = Device & {
is_of__device_type: [DeviceType]; is_of__device_type: [DeviceType];