Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the hostApp to the state GET v3 result #1479

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading