diff --git a/src/features/hostapp/hooks/target-hostapp.ts b/src/features/hostapp/hooks/target-hostapp.ts index e19c82209..f96614094 100644 --- a/src/features/hostapp/hooks/target-hostapp.ts +++ b/src/features/hostapp/hooks/target-hostapp.ts @@ -63,7 +63,8 @@ hooks.addPureHook('PATCH', 'resin', 'device', { /** * When a device checks in with it's initial OS version, set the corresponding should_be_operated_by__release resource - * using its current reported version. + * using its current reported version only if should_be_operated_by__release is null or older than the current OS version. + * This ensures the target hostapp field isn't overwritten by an older OS release when a device reports OS info on provision. */ hooks.addPureHook('PATCH', 'resin', 'device', { async PRERUN(args) { @@ -121,7 +122,7 @@ async function setOSReleaseResource( id: { $in: deviceIds }, os_version: null, }, - $select: ['id', 'is_of__device_type'], + $select: ['id', 'should_be_operated_by__release', 'is_of__device_type'], }, }); @@ -147,8 +148,6 @@ async function setOSReleaseResource( return Promise.all( Array.from(devicesByDeviceTypeId.entries()).map( async ([deviceTypeId, affectedDevices]) => { - const affectedDeviceIds = affectedDevices.map((d) => d.id); - const osRelease = await getOSReleaseResource( api, osVersion, @@ -160,6 +159,19 @@ async function setOSReleaseResource( return; } + // Only patch should_be_operated_by__release if it's null or older than the reported OS release + const affectedDeviceIds = affectedDevices + .filter( + (d) => + d.should_be_operated_by__release == null || + d.should_be_operated_by__release.__id < osRelease.id, + ) + .map((d) => d.id); + + if (affectedDeviceIds.length === 0) { + return; + } + await rootApi.patch({ resource: 'device', options: { diff --git a/test/15_target-hostapp.ts b/test/15_target-hostapp.ts index 27233202d..eddaed09b 100644 --- a/test/15_target-hostapp.ts +++ b/test/15_target-hostapp.ts @@ -633,6 +633,109 @@ export default () => { .expect(200); expect(dev.d[0].should_be_operated_by__release).to.be.null; }); + + it('should overwrite existing should_be_operated_by__release when device reports OS info if should_be_operated_by__release is null', async () => { + // First set up a device with a specific target release + const device = await fakeDevice.provisionDevice(admin, applicationId); + await supertest(admin) + .patch(`/${version}/device(${device.id})`) + .send({ + should_be_operated_by__release: null, + }) + .expect(200); + + // Verify initial state + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: null, + }); + + // Now patch the device with OS info + await device.patchStateV2({ + local: { + os_version: 'balenaOS 2.50.0+rev1', + os_variant: 'prod', + }, + }); + + // Verify the should_be_operated_by__release was updated with OS info + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: { + __id: nuc2_50_0_rev1prodId, + }, + os_version: 'balenaOS 2.50.0+rev1', + os_variant: 'prod', + }); + }); + + it('should not overwrite existing should_be_operated_by__release when device reports OS info if should_be_operated_by__release is newer', async () => { + // First set up a device with a specific target release + const device = await fakeDevice.provisionDevice(admin, applicationId); + await supertest(admin) + .patch(`/${version}/device(${device.id})`) + .send({ + should_be_operated_by__release: nuc2_51_0_rev1prodTagAndSemverId, + }) + .expect(200); + + // Verify initial state + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: { + __id: nuc2_51_0_rev1prodTagAndSemverId, + }, + }); + + // Now patch the device with OS info that would normally map to a different & older release + await device.patchStateV2({ + local: { + os_version: 'balenaOS 2.50.0+rev1', + os_variant: 'prod', + }, + }); + + // Verify the should_be_operated_by__release was not changed + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: { + __id: nuc2_51_0_rev1prodTagAndSemverId, + }, + os_version: 'balenaOS 2.50.0+rev1', + os_variant: 'prod', + }); + }); + + it('should overwrite existing should_be_operated_by__release when device reports OS info if should_be_operated_by__release is older', async () => { + // First set up a device with a specific target release + const device = await fakeDevice.provisionDevice(admin, applicationId); + await supertest(admin) + .patch(`/${version}/device(${device.id})`) + .send({ + should_be_operated_by__release: unifiedSemverOnlyHostAppReleaseId, + }) + .expect(200); + + // Verify initial state + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: { + __id: unifiedSemverOnlyHostAppReleaseId, + }, + }); + + // Now patch the device with OS info that would normally map to a different & newer release + await device.patchStateV2({ + local: { + os_version: 'balenaOS 2.88.5+rev1', + os_variant: 'prod', + }, + }); + + // Verify the should_be_operated_by__release was updated with newer release + await expectResourceToMatch(pineUser, 'device', device.id, { + should_be_operated_by__release: { + __id: unifiedSemverRevHostAppReleaseId, + }, + os_version: 'balenaOS 2.88.5+rev1', + os_variant: 'prod', + }); + }); }); }); };