Merge pull request #2653 from balena-io/promote-discontinued-dt

promote: Allow joining fleets of discontinued device types
This commit is contained in:
flowzone-app[bot] 2023-07-20 08:29:20 +00:00 committed by GitHub
commit 2b39d5d111
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 55 deletions

View File

@ -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<BalenaSdk.Application>;
// 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<BalenaSdk.Application, typeof pineOptions>
> as ExtendedApplication[];
// Add extended properties
applications.forEach((application) => {

View File

@ -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;

View File

@ -228,8 +228,8 @@ async function selectLocalDevice(): Promise<string> {
}
async function selectAppFromList(
applications: ApplicationWithDeviceType[],
): Promise<ApplicationWithDeviceType> {
applications: ApplicationWithDeviceTypeSlug[],
): Promise<ApplicationWithDeviceTypeSlug> {
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<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
const pineOptions = {
$select: 'slug',
$expand: {
@ -256,51 +256,72 @@ async function getOrSelectApplication(
},
},
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
const [deviceType, allDeviceTypes] = await Promise.all([
sdk.models.deviceType.get(deviceTypeSlug, pineOptions) as Promise<
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
>,
sdk.models.deviceType.getAllSupported(pineOptions) as Promise<
Array<BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>>
>,
]);
const deviceType = (await sdk.models.deviceType.get(
deviceTypeSlug,
pineOptions,
)) as BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>;
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<BalenaSdk.Application> = {
const options = {
$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.
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<BalenaSdk.Application>).$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<BalenaSdk.Application>).$filter = {
app_name: appName,
};
name = appName;
}
const applications = (await sdk.models.application.getAllDirectlyAccessible(
options,
)) as ApplicationWithDeviceType[];
)) as Array<BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof options>>;
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<BalenaSdk.Application>,
deviceType: string,
): Promise<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
// 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<ApplicationWithDeviceType> {
): Promise<ApplicationWithDeviceTypeSlug> {
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;

12
npm-shrinkwrap.json generated
View File

@ -1823,9 +1823,9 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@oclif/command": {
"version": "1.8.32",
"resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.32.tgz",
"integrity": "sha512-vPoSvNM08RCHRcwBK9AyXQW+Np4Ba1ny6tSHTT8G+mqrYDWoCNBx0BD4n58laB1X8rFJ8rMi+L5S8tPAnAZ93w==",
"version": "1.8.33",
"resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.33.tgz",
"integrity": "sha512-7ZPvThrZaICX1hoZ/S82DaGgjI3UGG2rveBfxCE9JlgvrDQQiHLA6a/N7Hf3jq6t51AkXbBMhaMOBzXtSd73QA==",
"dependencies": {
"@oclif/config": "^1.18.2",
"@oclif/errors": "^1.3.6",
@ -23637,9 +23637,9 @@
}
},
"@oclif/command": {
"version": "1.8.32",
"resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.32.tgz",
"integrity": "sha512-vPoSvNM08RCHRcwBK9AyXQW+Np4Ba1ny6tSHTT8G+mqrYDWoCNBx0BD4n58laB1X8rFJ8rMi+L5S8tPAnAZ93w==",
"version": "1.8.33",
"resolved": "https://registry.npmjs.org/@oclif/command/-/command-1.8.33.tgz",
"integrity": "sha512-7ZPvThrZaICX1hoZ/S82DaGgjI3UGG2rveBfxCE9JlgvrDQQiHLA6a/N7Hf3jq6t51AkXbBMhaMOBzXtSd73QA==",
"requires": {
"@oclif/config": "^1.18.2",
"@oclif/errors": "^1.3.6",

7
typings/global.d.ts vendored
View File

@ -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<DeviceType, 'slug'>];
};
type DeviceWithDeviceType = Device & {
is_of__device_type: [DeviceType];