diff --git a/src/config/configJson.ts b/src/config/configJson.ts index c1773cf3..335faf90 100644 --- a/src/config/configJson.ts +++ b/src/config/configJson.ts @@ -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 { + public async get(key: Schema.SchemaKey): Promise { 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; diff --git a/src/config/schema-type.ts b/src/config/schema-type.ts index 1cd19564..7e6a2d06 100644 --- a/src/config/schema-type.ts +++ b/src/config/schema-type.ts @@ -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: { diff --git a/src/config/schema.ts b/src/config/schema.ts index e3d8005b..2e79ed58 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -84,6 +84,11 @@ export const schema = { mutable: false, removeIfNull: false, }, + os: { + source: 'config.json', + mutable: true, + removeIfNull: false, + }, name: { source: 'db', diff --git a/test/integration/config/configJson.spec.ts b/test/integration/config/configJson.spec.ts index 39c8f1b9..92f85a06 100644 --- a/test/integration/config/configJson.spec.ts +++ b/test/integration/config/configJson.spec.ts @@ -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(); }); });