Support "os" key with object values in ConfigJsonConfigBackend

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2024-11-12 17:10:41 -08:00
parent 9ec45a724b
commit 54fcfa22a7
4 changed files with 128 additions and 21 deletions

View File

@ -39,14 +39,10 @@ export default class ConfigJsonConfigBackend {
await Bluebird.using(this.writeLockConfigJson(), async () => {
let changed = false;
_.forOwn(keyVals, (value, key: T) => {
if (this.cache[key] !== value) {
if (this.schema[key] != null && !_.isEqual(this.cache[key], value)) {
this.cache[key] = value;
if (
value == null &&
this.schema[key] != null &&
this.schema[key].removeIfNull
) {
if (value == null && this.schema[key].removeIfNull) {
delete this.cache[key];
}
@ -59,7 +55,7 @@ export default class ConfigJsonConfigBackend {
});
}
public async get(key: string): Promise<unknown> {
public async get(key: Schema.SchemaKey): Promise<unknown> {
await this.init();
return Bluebird.using(
this.readLockConfigJson(),
@ -67,7 +63,7 @@ export default class ConfigJsonConfigBackend {
);
}
public async remove(key: string) {
public async remove(key: Schema.SchemaKey) {
await this.init();
return Bluebird.using(this.writeLockConfigJson(), async () => {
let changed = false;

View File

@ -84,6 +84,10 @@ export const schemaTypes = {
type: t.string,
default: NullOrUndefined,
},
os: {
type: t.union([t.record(t.string, t.any), t.undefined]),
default: NullOrUndefined,
},
// Database types
name: {

View File

@ -84,6 +84,11 @@ export const schema = {
mutable: false,
removeIfNull: false,
},
os: {
source: 'config.json',
mutable: true,
removeIfNull: false,
},
name: {
source: 'db',

View File

@ -1,4 +1,5 @@
import { expect } from 'chai';
import type { TestFs } from 'mocha-pod';
import { testfs } from 'mocha-pod';
import { promises as fs } from 'fs';
@ -7,46 +8,150 @@ import { schema } from '~/src/config/schema';
describe('ConfigJsonConfigBackend', () => {
const CONFIG_PATH = '/mnt/boot/config.json';
const os = {
power: {
mode: 'high',
},
fan: {
profile: 'cool',
},
network: {
connectivity: {
uri: 'https://api.balena-cloud.com/connectivity-check',
interval: '300',
response: 'optional value in the response',
},
wifi: {
randomMacAddressScan: false,
},
},
udevRules: {
'56': 'ENV{ID_FS_LABEL_ENC}=="resin-root*", IMPORT{program}="resin_update_state_probe $devnode", SYMLINK+="disk/by-state/$env{RESIN_UPDATE_STATE}"',
'64': 'ACTION!="add|change", GOTO="modeswitch_rules_end"\nKERNEL=="ttyACM*", ATTRS{idVendor}=="1546", ATTRS{idProduct}=="1146", TAG+="systemd", ENV{SYSTEMD_WANTS}="u-blox-switch@\'%E{DEVNAME}\'.service"\nLBEL="modeswitch_rules_end"\n',
},
sshKeys: [
'ssh-rsa AAAAB3Nza...M2JB balena@macbook-pro',
'ssh-rsa AAAAB3Nza...nFTQ balena@zenbook',
],
};
let configJsonConfigBackend: ConfigJsonConfigBackend;
let tfs: TestFs.Enabled;
beforeEach(() => {
configJsonConfigBackend = new ConfigJsonConfigBackend(schema);
});
it('should get value for config.json key', async () => {
await testfs({
afterEach(async () => {
await tfs.restore();
});
it('should get primitive values for config.json key', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo',
deviceId: 123,
persistentLogging: true,
}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
await testfs.restore();
expect(await configJsonConfigBackend.get('deviceId')).to.equal(123);
expect(await configJsonConfigBackend.get('persistentLogging')).to.equal(
true,
);
});
it('should set value for config.json key', async () => {
await testfs({
it('should get object values for config.json "os" key', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
os,
}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
expect(await configJsonConfigBackend.get('os')).to.deep.equal(os);
});
it('should get object values for config.json "os" key while "os" is empty', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
expect(await configJsonConfigBackend.get('os')).to.be.undefined;
});
it('should set primitive values for config.json key', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo',
deviceId: 123,
persistentLogging: true,
}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
await configJsonConfigBackend.set({
apiEndpoint: 'bar',
deviceId: 456,
persistentLogging: false,
});
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('bar');
expect(await configJsonConfigBackend.get('deviceId')).to.equal(456);
expect(await configJsonConfigBackend.get('persistentLogging')).to.equal(
false,
);
});
await testfs.restore();
it('should set object values for config.json "os" key', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
os,
}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
const newOs = {
power: {
mode: 'low',
},
network: {
wifi: {
randomMacAddressScan: true,
},
},
udevRules: {
'56': 'ENV{ID_FS_LABEL_ENC}=="resin-root*", IMPORT{program}="resin_update_state_probe $devnode", SYMLINK+="disk/by-state/$env{RESIN_UPDATE_STATE}"',
},
sshKeys: ['ssh-rsa AAAAB3Nza...M2JB balena@macbook-pro'],
};
await configJsonConfigBackend.set({
os: newOs,
});
expect(await configJsonConfigBackend.get('os')).to.deep.equal(newOs);
});
it('should set object values for config.json "os" key while "os" is empty', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({}),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable();
await configJsonConfigBackend.set({
os,
});
expect(await configJsonConfigBackend.get('os')).to.deep.equal(os);
});
// The following test cases may be unnecessary as they test cases where another party
// writes to config.json directly (instead of through setting config vars on the API).
it('should get cached value even if actual value has changed', async () => {
await testfs({
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo',
}),
@ -66,12 +171,10 @@ describe('ConfigJsonConfigBackend', () => {
// Unintended behavior: the cached value should not be overwritten
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
await testfs.restore();
});
it('should set value and refresh cache to equal new value', async () => {
await testfs({
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo',
}),
@ -88,6 +191,7 @@ describe('ConfigJsonConfigBackend', () => {
);
// Unintended behavior: cached value should not have been updated
// as the change was not written to config.json by the Supervisor
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
await configJsonConfigBackend.set({
@ -95,7 +199,5 @@ describe('ConfigJsonConfigBackend', () => {
});
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('baz');
await testfs.restore();
});
});