Skip to content

Commit

Permalink
Merge pull request #1479 from balena-io/state-get-v3-hostApp
Browse files Browse the repository at this point in the history
Add the hostApp to the state GET v3 result
  • Loading branch information
flowzone-app[bot] authored Jan 23, 2024
2 parents 250e272 + 5cf3ced commit 758e3f8
Show file tree
Hide file tree
Showing 8 changed files with 350 additions and 33 deletions.
90 changes: 59 additions & 31 deletions src/features/device-state/routes/state-get-v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export function buildAppFromRelease(
application: AnyObject,
release: AnyObject,
config: Dictionary<string>,
defaultLabels?: Dictionary<string>,
): NonNullable<LocalStateApp['releases']> {
let composition: AnyObject = {};
const services: NonNullable<LocalStateApp['releases']>[string]['services'] =
Expand Down Expand Up @@ -135,7 +136,9 @@ export function buildAppFromRelease(
varListInsert(si.device_service_environment_variable, environment);
}

const labels: Dictionary<string> = {};
const labels: Dictionary<string> = {
...defaultLabels,
};
for (const { label_name, value } of [
...ipr.image_label,
...svc.service_label,
Expand Down Expand Up @@ -264,6 +267,16 @@ const deviceExpand: Expand = {
},
},
},
should_be_operated_by__release: {
...releaseExpand,
$expand: {
...releaseExpand.$expand,
belongs_to__application: {
$select: ['id', 'uuid', 'app_name', 'is_host', 'is_of__class'],
$expand: appExpand,
},
},
},
};

const stateQuery = _.once(() =>
Expand Down Expand Up @@ -293,15 +306,26 @@ const getStateV3 = async (req: Request, uuid: string): Promise<StateV3> => {
storedDeviceFields: _.pick(device, getStateEventAdditionalFields),
});

let apps = getUserAppState(device, config);
// We use an empty config for the supervisor & hostApp as we don't want any labels applied to them due to user app config
const svAndHostAppConfig = {};
const apps = {
...getAppState(device, 'should_be_managed_by__release', svAndHostAppConfig),
...getAppState(
device,
'should_be_operated_by__release',
svAndHostAppConfig,
{
// This label is necessary for older supervisors to properly detect the hostApp
// and ignore it, sinc `is_host: true` wasn't enough. W/o this the device would
// try to install the hostApp container like a normal user app and restart it
// constantly b/c the image doesn't have a CMD specified.
// See: https://github.com/balena-os/balena-supervisor/blob/v15.2.0/src/compose/app.ts#L839
'io.balena.image.store': 'root',
},
),
...getAppState(device, 'should_be_running__release', config),
};

const supervisorRelease = device.should_be_managed_by__release[0];
if (supervisorRelease) {
apps = {
...getSupervisorAppState(device),
...apps,
};
}
const state: StateV3 = {
[uuid]: {
name: device.device_name,
Expand Down Expand Up @@ -343,37 +367,41 @@ const getDevice = getStateDelayingEmpty(

const getAppState = (
device: AnyObject,
application: AnyObject,
release: AnyObject | undefined,
targetReleaseField:
| 'should_be_running__release'
| 'should_be_managed_by__release'
| 'should_be_operated_by__release',
config: Dictionary<string>,
): StateV3[string]['apps'] => {
defaultLabels?: Dictionary<string>,
): StateV3[string]['apps'] | null => {
let application: AnyObject;
let release: AnyObject | undefined;
if (targetReleaseField === 'should_be_running__release') {
application = device.belongs_to__application[0];
release = getReleaseForDevice(device);
} else {
release = device[targetReleaseField][0];
if (!release) {
return null;
}
application = release.belongs_to__application[0];
}

return {
[application.uuid]: {
id: application.id,
name: application.app_name,
is_host: application.is_host,
class: application.is_of__class,
...(release != null && {
releases: buildAppFromRelease(device, application, release, config),
releases: buildAppFromRelease(
device,
application,
release,
config,
defaultLabels,
),
}),
},
};
};

const getUserAppState = (
device: AnyObject,
config: Dictionary<string>,
): StateV3[string]['apps'] => {
const userApp = device.belongs_to__application[0];
const userAppRelease = getReleaseForDevice(device);
return getAppState(device, userApp, userAppRelease, config);
};
const getSupervisorAppState = (device: AnyObject): StateV3[string]['apps'] => {
const supervisorRelease = device.should_be_managed_by__release[0];
if (!supervisorRelease) {
return {};
}
const supervisorApp = supervisorRelease.belongs_to__application[0];
// We use an empty config as we don't want any labels applied to the supervisor due to user app config
return getAppState(device, supervisorApp, supervisorRelease, {});
};
99 changes: 99 additions & 0 deletions test/19_apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,104 @@ versions.test((version, pineTest) => {
).to.not.have.property(supervisorApp.uuid);
});
});

describe('HostApps', () => {
let fx: fixtures.Fixtures;
let admin: UserObjectParam;
let pineAdmin: PineTest;
let userApp: Application;
let intelNucHostApp: Application;
let intelNucHostAppRelease1: Release;
let deviceWithHostApp: fakeDevice.Device;
let deviceWithoutHostApp: fakeDevice.Device;

before(async () => {
mockery.registerMock('../src/lib/config', configMock);
fx = await fixtures.load('19-apps');

admin = fx.users.admin;
pineAdmin = pineTest.clone({
passthrough: {
user: admin,
},
});
userApp = fx.applications.app1;
intelNucHostApp = fx.applications['intel-nuc'];
intelNucHostAppRelease1 = fx.releases.intelNucHostAppRelease1;

deviceWithHostApp = await fakeDevice.provisionDevice(
admin,
userApp.id,
'balenaOS 2.50.1+rev1',
);
await expectResourceToMatch(pineAdmin, 'device', deviceWithHostApp.id, {
should_be_operated_by__release: { __id: intelNucHostAppRelease1.id },
});
deviceWithoutHostApp = await fakeDevice.provisionDevice(
admin,
userApp.id,
'balenaOS 2.3.0+rev1',
);
await expectResourceToMatch(
pineAdmin,
'device',
deviceWithoutHostApp.id,
{
should_be_operated_by__release: null,
},
);
});

after(async () => {
await fixtures.clean({ devices: [deviceWithHostApp] });
await fixtures.clean(fx);
mockery.deregisterMock('../src/lib/config');
});

it('should have a host app if operated by a release', async () => {
const state = await deviceWithHostApp.getStateV3();
expect(
Object.keys(state[deviceWithHostApp.uuid].apps).sort(),
'wrong number of apps',
).to.deep.equal([intelNucHostApp.uuid, userApp.uuid].sort());

expect(state[deviceWithHostApp.uuid].apps)
.to.have.property(intelNucHostApp.uuid)
.that.is.an('object');
const stateGetHostApp =
state[deviceWithHostApp.uuid].apps?.[intelNucHostApp.uuid];
expect(stateGetHostApp).to.have.property(
'name',
intelNucHostApp.app_name,
);
expect(stateGetHostApp).to.have.property('is_host', true);
expect(stateGetHostApp).to.have.property('class', 'app');
expect(stateGetHostApp)
.to.have.nested.property('releases')
.that.has.property(intelNucHostAppRelease1.commit)
.that.is.an('object');
expect(stateGetHostApp.releases?.[intelNucHostAppRelease1.commit])
.to.have.property('services')
.that.has.property('main')
.that.is.an('object')
.and.has.property('labels')
.that.has.property('io.balena.image.store', 'root');

expect(stateGetHostApp, 'hostApp is undefined').to.not.be.undefined;
});

it('should not have a host app if not operated by a release', async () => {
const state = await deviceWithoutHostApp.getStateV3();
expect(
Object.keys(state[deviceWithoutHostApp.uuid].apps),
'wrong number of apps',
).to.deep.equal([userApp.uuid]);

expect(
state[deviceWithoutHostApp.uuid].apps,
'host app should not be included',
).to.not.have.property(intelNucHostApp.uuid);
});
});
});
});
27 changes: 27 additions & 0 deletions test/fixtures/19-apps/applications.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@
"device_type": "intel-nuc",
"app_name": "test-app-1"
},
"raspberrypi3": {
"user": "admin",
"is_host": true,
"is_public": true,
"is_of__class": "app",
"device_type": "raspberrypi3",
"app_name": "raspberrypi3",
"organization": "balena_os"
},
"intel-nuc": {
"user": "admin",
"is_host": true,
"is_public": true,
"is_of__class": "app",
"device_type": "intel-nuc",
"app_name": "intel-nuc",
"organization": "balena_os"
},
"intel-nuc-esr": {
"user": "admin",
"is_host": true,
"is_public": true,
"is_of__class": "app",
"device_type": "intel-nuc",
"app_name": "intel-nuc-esr",
"organization": "balena_os"
},
"supervisorApp": {
"uuid": "031f48d8f47b4062ad2d67b8de933711",
"user": "admin",
Expand Down
36 changes: 36 additions & 0 deletions test/fixtures/19-apps/images.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,41 @@
"project_type": "project type goes here",
"build_log": "This is also the build log",
"status": "success"
},
"rpi3NucHostAppImage0": {
"user": "admin",
"service": "rpi3HostAppService",
"releases": [ "rpi3HostAppRelease0" ],
"image_size": 2048,
"project_type": "project type goes here",
"build_log": "This is also the build log",
"status": "success"
},
"intelNucHostAppImage0": {
"user": "admin",
"service": "intelNucHostAppService",
"releases": [ "intelNucHostAppRelease0" ],
"image_size": 2048,
"project_type": "project type goes here",
"build_log": "This is also the build log",
"status": "success"
},
"intelNucHostAppImage1": {
"user": "admin",
"service": "intelNucHostAppService",
"releases": [ "intelNucHostAppRelease1" ],
"image_size": 2048,
"project_type": "project type goes here",
"build_log": "This is also the build log",
"status": "success"
},
"intelNucHostAppImageUnified1": {
"user": "admin",
"service": "intelNucHostAppService",
"releases": [ "intelNucHostAppUnifiedRelease1" ],
"image_size": 2048,
"project_type": "project type goes here",
"build_log": "This is also the build log",
"status": "success"
}
}
66 changes: 66 additions & 0 deletions test/fixtures/19-apps/release_tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,71 @@
"release": "supervisorRelease2",
"tag_key": "BALENA_REQUIRED_SUPERVISOR_v1.0.1",
"value": "v1.0.1"
},
"rpi3HostAppRelease0tag-variant": {
"user": "admin",
"release": "rpi3HostAppRelease0",
"tag_key": "variant",
"value": "production"
},
"rpi3HostAppRelease0tag-version": {
"user": "admin",
"release": "rpi3HostAppRelease0",
"tag_key": "version",
"value": "2.50.0+rev1"
},
"intelNucHostAppRelease0Tag-variant": {
"user": "admin",
"release": "intelNucHostAppRelease0",
"tag_key": "variant",
"value": "production"
},
"intelNucHostAppRelease0Tag-version": {
"user": "admin",
"release": "intelNucHostAppRelease0",
"tag_key": "version",
"value": "2.50.0+rev1"
},
"intelNucHostAppRelease1Tag-variant": {
"user": "admin",
"release": "intelNucHostAppRelease1",
"tag_key": "variant",
"value": "production"
},
"intelNucHostAppRelease1Tag-version": {
"user": "admin",
"release": "intelNucHostAppRelease1",
"tag_key": "version",
"value": "2.50.1+rev1"
},
"intelNucHostAppReleaseFailedTag-variant": {
"user": "admin",
"release": "intelNucHostAppReleaseFailed",
"tag_key": "variant",
"value": "production"
},
"intelNucHostAppReleaseFailedTag-version": {
"user": "admin",
"release": "intelNucHostAppReleaseFailed",
"tag_key": "version",
"value": "2.50.1+rev1"
},
"intelNucHostAppUnifiedRelease1Tag-version": {
"user": "admin",
"release": "intelNucHostAppUnifiedRelease1",
"tag_key": "version",
"value": "2.88.4"
},
"intelNucEsrHostAppRelease0Tag-variant": {
"user": "admin",
"release": "intelNucEsrHostAppRelease0",
"tag_key": "variant",
"value": "production"
},
"intelNucEsrHostAppRelease0Tag-version": {
"user": "admin",
"release": "intelNucEsrHostAppRelease0",
"tag_key": "version",
"value": "2021.01.0"
}
}
Loading

0 comments on commit 758e3f8

Please sign in to comment.