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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import { expect } from 'chai'; import { expect } from 'chai';
import type { TestFs } from 'mocha-pod';
import { testfs } from 'mocha-pod'; import { testfs } from 'mocha-pod';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
@ -7,46 +8,150 @@ import { schema } from '~/src/config/schema';
describe('ConfigJsonConfigBackend', () => { describe('ConfigJsonConfigBackend', () => {
const CONFIG_PATH = '/mnt/boot/config.json'; 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 configJsonConfigBackend: ConfigJsonConfigBackend;
let tfs: TestFs.Enabled;
beforeEach(() => { beforeEach(() => {
configJsonConfigBackend = new ConfigJsonConfigBackend(schema); configJsonConfigBackend = new ConfigJsonConfigBackend(schema);
}); });
it('should get value for config.json key', async () => { afterEach(async () => {
await testfs({ await tfs.restore();
});
it('should get primitive values for config.json key', async () => {
tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({ [CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo', apiEndpoint: 'foo',
deviceId: 123,
persistentLogging: true,
}), }),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'), '/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable(); }).enable();
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo'); expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
expect(await configJsonConfigBackend.get('deviceId')).to.equal(123);
await testfs.restore(); expect(await configJsonConfigBackend.get('persistentLogging')).to.equal(
true,
);
}); });
it('should set value for config.json key', async () => { it('should get object values for config.json "os" key', async () => {
await testfs({ 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({ [CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo', apiEndpoint: 'foo',
deviceId: 123,
persistentLogging: true,
}), }),
'/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'), '/mnt/root/etc/os-release': testfs.from('test/data/etc/os-release'),
}).enable(); }).enable();
await configJsonConfigBackend.set({ await configJsonConfigBackend.set({
apiEndpoint: 'bar', apiEndpoint: 'bar',
deviceId: 456,
persistentLogging: false,
}); });
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('bar'); 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 // 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). // 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 () => { it('should get cached value even if actual value has changed', async () => {
await testfs({ tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({ [CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo', apiEndpoint: 'foo',
}), }),
@ -66,12 +171,10 @@ describe('ConfigJsonConfigBackend', () => {
// Unintended behavior: the cached value should not be overwritten // Unintended behavior: the cached value should not be overwritten
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo'); expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
await testfs.restore();
}); });
it('should set value and refresh cache to equal new value', async () => { it('should set value and refresh cache to equal new value', async () => {
await testfs({ tfs = await testfs({
[CONFIG_PATH]: JSON.stringify({ [CONFIG_PATH]: JSON.stringify({
apiEndpoint: 'foo', apiEndpoint: 'foo',
}), }),
@ -88,6 +191,7 @@ describe('ConfigJsonConfigBackend', () => {
); );
// Unintended behavior: cached value should not have been updated // 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'); expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('foo');
await configJsonConfigBackend.set({ await configJsonConfigBackend.set({
@ -95,7 +199,5 @@ describe('ConfigJsonConfigBackend', () => {
}); });
expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('baz'); expect(await configJsonConfigBackend.get('apiEndpoint')).to.equal('baz');
await testfs.restore();
}); });
}); });