diff --git a/src/features/supervisor-app/hooks/supervisor-app.ts b/src/features/supervisor-app/hooks/supervisor-app.ts index 0a5bfdb482..e31984cf8d 100644 --- a/src/features/supervisor-app/hooks/supervisor-app.ts +++ b/src/features/supervisor-app/hooks/supervisor-app.ts @@ -15,6 +15,40 @@ import type { const { BadRequestError } = pinejsErrors; +hooks.addPureHook('POST', 'resin', 'device', { + async POSTPARSE({ request, api }) { + if ( + request.values.supervisor_version != null && + request.values.is_of__device_type != null + ) { + const deviceType = (await api.get({ + resource: 'device_type', + id: request.values.is_of__device_type, + options: { + $select: 'is_of__cpu_architecture', + }, + })) as PickDeferred | null; + if (deviceType == null) { + return; + } + + const [supervisorRelease] = await getSupervisorReleaseResource( + api, + request.values.supervisor_version, + deviceType.is_of__cpu_architecture.__id, + ); + + if (supervisorRelease == null) { + return; + } + + // since this is a POST, we _know_ the device is being created and has no current/target state, so we can + // just append the target after determining which it is (like a preloaded app) + request.values.should_be_managed_by__release = supervisorRelease.id; + } + }, +}); + hooks.addPureHook('PATCH', 'resin', 'device', { /** * When a device checks in with it's initial supervisor version, set the corresponding should_be_managed_by__release resource @@ -134,7 +168,7 @@ async function checkSupervisorReleaseUpgrades( async function getSupervisorReleaseResource( api: sbvrUtils.PinejsClient, supervisorVersion: string, - archId: string, + archId: string | number, ) { return (await api.get({ resource: 'release', diff --git a/test/16_supervisor_releases.ts b/test/16_supervisor_releases.ts index 0aa1a7459c..3dd4f17837 100644 --- a/test/16_supervisor_releases.ts +++ b/test/16_supervisor_releases.ts @@ -3,6 +3,7 @@ import * as fixtures from './test-lib/fixtures.js'; import * as fakeDevice from './test-lib/fake-device.js'; import { supertest } from './test-lib/supertest.js'; import * as versions from './test-lib/versions.js'; +import type { Device } from '../src/balena-model.js'; export default () => { versions.test((version, pineTest) => { @@ -181,6 +182,124 @@ export default () => { }); }); + ( + [ + [ + 'POST device resource', + async ({ device_type, ...devicePostBody }: AnyObject) => { + return await supertest(ctx.admin) + .post(`/${version}/device`) + .send({ + ...devicePostBody, + is_of__device_type: ( + await pineTest + .get({ + resource: 'device_type', + passthrough: { user: ctx.admin }, + id: { slug: device_type }, + options: { + $select: 'id', + }, + }) + .expect(200) + ).body.id, + }) + .expect(201); + }, + ], + [ + 'POST /device/register', + async ({ + belongs_to__application, + ...restDevicePostBody + }: AnyObject) => { + const { body: provisioningKey } = await supertest(ctx.admin) + .post(`/api-key/application/${ctx.deviceApp.id}/provisioning`) + .expect(200); + const uuid = + 'f716a3e020bd444b885cb394453917520c3cf82e69654f84be0d33e31a0e15'; + return await supertest() + .post(`/device/register?apikey=${provisioningKey}`) + .send({ + user: ctx.admin.id, + application: belongs_to__application, + uuid, + ...restDevicePostBody, + }) + .expect(201); + }, + ], + ] as const + ).forEach(([titlePart, provisionFn]) => { + describe(`provisioning with a supervisor version (using ${titlePart})`, function () { + let registeredDevice: Device; + + after(async function () { + await fixtures.clean({ + devices: [registeredDevice], + }); + }); + + it(`should set the device to a non-null supervisor release`, async () => { + ({ body: registeredDevice } = await provisionFn({ + belongs_to__application: ctx.deviceApp.id, + device_type: 'intel-nuc', + os_version: '2.38.0+rev1', + os_variant: 'dev', + supervisor_version: '5.0.1', + })); + const { + body: { + d: [fetchedDevice], + }, + } = await supertest(ctx.admin) + .get( + `/${version}/device(${registeredDevice.id})?$select=should_be_managed_by__release`, + ) + .expect(200); + expect(fetchedDevice).to.have.nested.property( + 'should_be_managed_by__release.__id', + ctx.supervisorReleases['5.0.1'].id, + ); + }); + + it(`should create a service install for the linked supervisor release`, async () => { + const { body: serviceInstalls } = await pineUser + .get({ + resource: 'service_install', + options: { + $expand: { + installs__service: { + $select: ['id', 'service_name'], + }, + }, + $filter: { + device: registeredDevice.id, + installs__service: { + $any: { + $alias: 'is', + $expr: { + is: { + application: ctx.amd64SupervisorApp.id, + }, + }, + }, + }, + }, + }, + }) + .expect(200); + expect(serviceInstalls).to.have.lengthOf(1); + const [service] = serviceInstalls[0].installs__service; + expect(service).to.have.property( + 'id', + ctx.fixtures.services.amd64_supervisor_app_service1.id, + ); + expect(service).to.have.property('service_name', 'main'); + }); + }); + }); + it('should allow upgrading to a logstream version', async () => { const patch = { should_be_managed_by__release: