mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-18 21:27:54 +00:00
Use fatrw utility for writes to boot partition
This PR changes the way the supervisor reads and writes files from /mnt/boot. Reads will now use the [fatrw utility](https://github.com/balena-os/fatrw/) as a way to minimize corruption of files in the boot partition, and thus preventing possible bricking of the device. Since this basically changes the way a lot of configurations are read, this work was being blocked because of the way tests were being done. While there still remain a couple of legacy tests to be migrated, this PR disables test:legacy tests when running npm run test, as the work on refactoring those tests is in progress (see #2048) and fatrw integration is of higher priority. Change-type: minor
This commit is contained in:
parent
d14cca8e5d
commit
dade598737
@ -4,13 +4,18 @@ ARG ARCH=%%BALENA_ARCH%%
|
||||
# balenaOS with buggy platform support
|
||||
# see https://github.com/balena-os/balena-engine/issues/269
|
||||
ARG PREFIX=library
|
||||
ARG FATRW_VERSION=0.2.9
|
||||
|
||||
###################################################
|
||||
# Build the supervisor dependencies
|
||||
###################################################
|
||||
FROM balenalib/${ARCH}-alpine-node:16-run as build-base
|
||||
|
||||
ARG ARCH
|
||||
ARG PREFIX
|
||||
ARG FATRW_VERSION
|
||||
ARG FATRW_ARCHIVE="fatrw-${ARCH}.tar.gz"
|
||||
ARG FATRW_LOCATION="https://github.com/balena-os/fatrw/releases/download/v${FATRW_VERSION}/${FATRW_ARCHIVE}"
|
||||
# Sanity check to prevent a prefix for a non-official docker image being
|
||||
# inserted. Only 'library' and 'arm32v6' are allowed right now
|
||||
RUN for allowed in "library" "arm32v6"; do [ "${PREFIX}" = "${allowed}" ] && break; done
|
||||
@ -30,6 +35,13 @@ COPY package*.json ./
|
||||
|
||||
RUN strip /usr/local/bin/node
|
||||
|
||||
# Install fatrw
|
||||
RUN curl -SLO "${FATRW_LOCATION}" && \
|
||||
echo curl -SLO "${FATRW_LOCATION}" && \
|
||||
ls -la "${FATRW_ARCHIVE}" && \
|
||||
tar -xzf "${FATRW_ARCHIVE}" -C /usr/local/bin && \
|
||||
rm -f "${FATRW_ARCHIVE}"
|
||||
|
||||
# Just install dev dependencies first
|
||||
RUN npm ci --build-from-source --sqlite=/usr/lib
|
||||
|
||||
@ -52,6 +64,9 @@ WORKDIR /usr/src/app
|
||||
# We just need the node binary in the final image
|
||||
COPY --from=build-base /usr/local/bin/node /usr/local/bin/node
|
||||
|
||||
# Also copy the fatrw binary
|
||||
COPY --from=build-base /usr/local/bin/fatrw /usr/local/bin/fatrw
|
||||
|
||||
# Similarly, from the procmail package we just need the lockfile binary
|
||||
COPY --from=extra /usr/bin/lockfile /usr/bin/lockfile
|
||||
|
||||
@ -99,11 +114,9 @@ COPY typings ./typings
|
||||
COPY src ./src
|
||||
COPY test ./test
|
||||
|
||||
# Run type checking and unit/legacy tests here
|
||||
# Run type checking and unit tests here
|
||||
# to prevent setting up a test environment that will
|
||||
# most likely fail.
|
||||
# For now this also runs the legacy tests, that are slow, this is a
|
||||
# forcing function to finish the split between unit/integration
|
||||
RUN npm run test
|
||||
|
||||
# When running tests from a container built from this stage,
|
||||
|
@ -19,7 +19,7 @@
|
||||
"test:node": "npm run test:unit && npm run test:integration && npm run test:legacy",
|
||||
"test:env": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.test.yml -f docker-compose.dev.yml up --build; npm run compose:down",
|
||||
"test:compose": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.yml -f docker-compose.test.yml up --build --remove-orphans --exit-code-from=sut ; npm run compose:down",
|
||||
"test": "npm run lint && npm run test:build && npm run test:unit && npm run test:legacy",
|
||||
"test": "npm run lint && npm run test:build && npm run test:unit",
|
||||
"compose:down": "docker-compose -f docker-compose.test.yml down && docker volume rm $(docker volume ls -f name=balena-supervisor -q)",
|
||||
"prettify": "balena-lint -e ts -e js --fix src/ test/ typings/ build-utils/ webpack.config.js",
|
||||
"release": "tsc --project tsconfig.release.json && mv build/src/* build",
|
||||
|
@ -103,7 +103,10 @@ export class ConfigFs extends ConfigBackend {
|
||||
|
||||
// read the config file...
|
||||
try {
|
||||
const content = await fs.readFile(this.ConfigFilePath, 'utf8');
|
||||
const content = await hostUtils.readFromBoot(
|
||||
this.ConfigFilePath,
|
||||
'utf-8',
|
||||
);
|
||||
return JSON.parse(content);
|
||||
} catch (err) {
|
||||
log.error('Unable to deserialise ConfigFS configuration.', err);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { ConfigOptions, ConfigBackend } from './backend';
|
||||
import * as constants from '../../lib/constants';
|
||||
@ -64,7 +63,10 @@ export class ConfigTxt extends ConfigBackend {
|
||||
let configContents = '';
|
||||
|
||||
if (await exists(ConfigTxt.bootConfigPath)) {
|
||||
configContents = await fs.readFile(ConfigTxt.bootConfigPath, 'utf-8');
|
||||
configContents = await hostUtils.readFromBoot(
|
||||
ConfigTxt.bootConfigPath,
|
||||
'utf-8',
|
||||
);
|
||||
} else {
|
||||
await hostUtils.writeToBoot(ConfigTxt.bootConfigPath, '');
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { ConfigOptions, ConfigBackend } from './backend';
|
||||
@ -59,7 +58,10 @@ export class Extlinux extends ConfigBackend {
|
||||
let confContents: string;
|
||||
|
||||
try {
|
||||
confContents = await fs.readFile(Extlinux.bootConfigPath, 'utf-8');
|
||||
confContents = await hostUtils.readFromBoot(
|
||||
Extlinux.bootConfigPath,
|
||||
'utf-8',
|
||||
);
|
||||
} catch {
|
||||
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
||||
// We do not have any backup to fallback too; warn the user of a possible brick
|
||||
@ -97,7 +99,10 @@ export class Extlinux extends ConfigBackend {
|
||||
let confContents: string;
|
||||
|
||||
try {
|
||||
confContents = await fs.readFile(Extlinux.bootConfigPath, 'utf-8');
|
||||
confContents = await hostUtils.readFromBoot(
|
||||
Extlinux.bootConfigPath,
|
||||
'utf-8',
|
||||
);
|
||||
} catch {
|
||||
// In the rare case where the user might have deleted extlinux conf file between linux boot and supervisor boot
|
||||
// We do not have any backup to fallback too; warn the user of a possible brick
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { ConfigOptions, ConfigBackend } from './backend';
|
||||
import * as constants from '../../lib/constants';
|
||||
@ -197,7 +196,7 @@ export class ExtraUEnv extends ConfigBackend {
|
||||
|
||||
private static async readBootConfigPath(): Promise<string> {
|
||||
try {
|
||||
return await fs.readFile(ExtraUEnv.bootConfigPath, 'utf-8');
|
||||
return await hostUtils.readFromBoot(ExtraUEnv.bootConfigPath, 'utf-8');
|
||||
} catch {
|
||||
// In the rare case where the user might have deleted extra_uEnv conf file between linux boot and supervisor boot
|
||||
// We do not have any backup to fallback too; warn the user of a possible brick
|
||||
|
@ -71,7 +71,7 @@ export class SplashImage extends ConfigBackend {
|
||||
where = where ?? (await this.getSplashPath());
|
||||
|
||||
// read the image file...
|
||||
return (await fs.readFile(where)).toString('base64');
|
||||
return (await hostUtils.readFromBoot(where)).toString('base64');
|
||||
}
|
||||
|
||||
// Write a splash image provided as a base64 string
|
||||
|
@ -1,10 +1,9 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as constants from '../lib/constants';
|
||||
import { writeAndSyncFile } from '../lib/fs-utils';
|
||||
import * as hostUtils from '../lib/host-utils';
|
||||
import * as osRelease from '../lib/os-release';
|
||||
|
||||
import log from '../lib/supervisor-console';
|
||||
@ -87,10 +86,8 @@ export default class ConfigJsonConfigBackend {
|
||||
}
|
||||
|
||||
private async write(): Promise<void> {
|
||||
// We use writeAndSyncFile since /mnt/boot partition is a vfat
|
||||
// filesystem which dows not provide atomic file renames. The best
|
||||
// course of action on that case is to write and sync as soon as possible
|
||||
return writeAndSyncFile(
|
||||
// writeToBoot uses fatrw to safely write to the boot partition
|
||||
return hostUtils.writeToBoot(
|
||||
await this.pathOnHost(),
|
||||
JSON.stringify(this.cache),
|
||||
);
|
||||
@ -98,7 +95,7 @@ export default class ConfigJsonConfigBackend {
|
||||
|
||||
private async read(): Promise<string> {
|
||||
const filename = await this.pathOnHost();
|
||||
return JSON.parse(await fs.readFile(filename, 'utf-8'));
|
||||
return JSON.parse(await hostUtils.readFromBoot(filename, 'utf-8'));
|
||||
}
|
||||
|
||||
private async resolveConfigPath(): Promise<string> {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as memoizee from 'memoizee';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import supervisorVersion = require('../lib/supervisor-version');
|
||||
|
||||
@ -9,6 +8,7 @@ import * as config from '.';
|
||||
import * as constants from '../lib/constants';
|
||||
import * as osRelease from '../lib/os-release';
|
||||
import * as macAddress from '../lib/mac-address';
|
||||
import * as hostUtils from '../lib/host-utils';
|
||||
import log from '../lib/supervisor-console';
|
||||
|
||||
export const fnSchema = {
|
||||
@ -43,9 +43,9 @@ export const fnSchema = {
|
||||
try {
|
||||
// FIXME: We should be mounting the following file into the supervisor from the
|
||||
// start-balena-supervisor script, changed in meta-balena - but until then, hardcode it
|
||||
const data = await fs.readFile(
|
||||
`${constants.rootMountPoint}${constants.bootMountPoint}/device-type.json`,
|
||||
'utf8',
|
||||
const data = await hostUtils.readFromBoot(
|
||||
hostUtils.pathOnBoot('device-type.json'),
|
||||
'utf-8',
|
||||
);
|
||||
const deviceInfo = JSON.parse(data);
|
||||
|
||||
@ -62,9 +62,9 @@ export const fnSchema = {
|
||||
try {
|
||||
// FIXME: We should be mounting the following file into the supervisor from the
|
||||
// start-balena-supervisor script, changed in meta-balena - but until then, hardcode it
|
||||
const data = await fs.readFile(
|
||||
`${constants.rootMountPoint}${constants.bootMountPoint}/device-type.json`,
|
||||
'utf8',
|
||||
const data = await hostUtils.readFromBoot(
|
||||
hostUtils.pathOnBoot('device-type.json'),
|
||||
'utf-8',
|
||||
);
|
||||
const deviceInfo = JSON.parse(data);
|
||||
|
||||
|
@ -10,7 +10,7 @@ import * as constants from './lib/constants';
|
||||
import * as dbus from './lib/dbus';
|
||||
import { ENOENT } from './lib/errors';
|
||||
import { mkdirp, unlinkAll } from './lib/fs-utils';
|
||||
import { writeToBoot } from './lib/host-utils';
|
||||
import { writeToBoot, readFromBoot } from './lib/host-utils';
|
||||
import * as updateLock from './lib/update-lock';
|
||||
|
||||
const redsocksHeader = stripIndent`
|
||||
@ -66,7 +66,7 @@ async function readProxy(): Promise<ProxyConfig | undefined> {
|
||||
const conf: ProxyConfig = {};
|
||||
let redsocksConf: string;
|
||||
try {
|
||||
redsocksConf = await fs.readFile(redsocksConfPath, 'utf-8');
|
||||
redsocksConf = await readFromBoot(redsocksConfPath, 'utf-8');
|
||||
} catch (e: any) {
|
||||
if (!ENOENT(e)) {
|
||||
throw e;
|
||||
@ -91,8 +91,7 @@ async function readProxy(): Promise<ProxyConfig | undefined> {
|
||||
}
|
||||
|
||||
try {
|
||||
const noProxy = await fs
|
||||
.readFile(noProxyPath, 'utf-8')
|
||||
const noProxy = await readFromBoot(noProxyPath, 'utf-8')
|
||||
// Prevent empty newline from being reported as a noProxy address
|
||||
.then((addrs) => addrs.split('\n').filter((addr) => addr !== ''));
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as constants from './constants';
|
||||
import * as fsUtils from './fs-utils';
|
||||
import { exec } from './fs-utils';
|
||||
|
||||
// Returns an absolute path starting from the hostOS root partition
|
||||
// This path is accessible from within the Supervisor container
|
||||
@ -14,10 +15,51 @@ export function pathOnBoot(relPath: string) {
|
||||
return pathOnRoot(path.join(constants.bootMountPoint, relPath));
|
||||
}
|
||||
|
||||
// Receives an absolute path for a file under the boot partition (e.g. `/mnt/root/mnt/boot/config.txt`)
|
||||
// and writes the given data. This function uses the best effort to write a file trying to minimize corruption
|
||||
// due to a power cut. Given that the boot partition is a vfat filesystem, this means
|
||||
// using write + sync
|
||||
export async function writeToBoot(file: string, data: string | Buffer) {
|
||||
return await fsUtils.writeAndSyncFile(file, data);
|
||||
class CodedError extends Error {
|
||||
constructor(msg: string, readonly code: number) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Receives an absolute path for a file (assumed to be under the boot partition, e.g. `/mnt/root/mnt/boot/config.txt`)
|
||||
// and reads from the given location. This function uses fatrw to safely read from a FAT filesystem
|
||||
// https://github.com/balena-os/fatrw
|
||||
export async function readFromBoot(
|
||||
fileName: string,
|
||||
encoding: 'utf-8' | 'utf8',
|
||||
): Promise<string>;
|
||||
export async function readFromBoot(fileName: string): Promise<Buffer>;
|
||||
export async function readFromBoot(
|
||||
fileName: string,
|
||||
encoding?: 'utf8' | 'utf-8',
|
||||
): Promise<string | Buffer> {
|
||||
const cmd = ['fatrw', 'read', fileName].join(' ');
|
||||
const { stdout } = await exec(cmd, {
|
||||
encoding,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
// Receives an absolute path for a file (assumed to be under the boot partition, e.g. `/mnt/root/mnt/boot/config.txt`)
|
||||
// and writes the given data. This function uses fatrw to safely write from a FAT filesystem
|
||||
// https://github.com/balena-os/fatrw
|
||||
export async function writeToBoot(fileName: string, data: string | Buffer) {
|
||||
const fatrw = spawn('fatrw', ['write', fileName], { stdio: 'pipe' });
|
||||
|
||||
// Write to the process stdinput
|
||||
fatrw.stdin.write(data);
|
||||
fatrw.stdin.end();
|
||||
|
||||
// We only care about stderr
|
||||
let error = '';
|
||||
for await (const chunk of fatrw.stderr) {
|
||||
error += chunk;
|
||||
}
|
||||
const exitCode: number = await new Promise((resolve) => {
|
||||
fatrw.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode) {
|
||||
throw new CodedError(`Write failed with error: ${error}`, exitCode);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { SinonSpy, spy, SinonStub, stub } from 'sinon';
|
||||
import { SinonSpy, spy, stub } from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import { testfs, TestFs } from 'mocha-pod';
|
||||
import * as hostUtils from '~/lib/host-utils';
|
||||
|
||||
import constants = require('~/lib/constants');
|
||||
import { SchemaTypeKey } from '~/src/config/schema-type';
|
||||
import { fnSchema } from '~/src/config/functions';
|
||||
|
||||
import * as conf from '~/src/config';
|
||||
|
||||
describe('config', () => {
|
||||
describe('config poop', () => {
|
||||
const configJsonPath = path.join(
|
||||
constants.rootMountPoint,
|
||||
constants.bootMountPoint,
|
||||
@ -31,31 +29,32 @@ describe('config', () => {
|
||||
|
||||
let testFs: TestFs.Enabled;
|
||||
|
||||
before(async () => {
|
||||
await conf.initialized();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// This tells testfs to make a backup of config.json before each test
|
||||
// as some of the tests modify the file. This prevents any leaking between
|
||||
// tests
|
||||
testFs = await testfs({}, { keep: [configJsonPath] }).enable();
|
||||
testFs = await testfs({
|
||||
[configJsonPath]: testfs.from('test/data/testconfig.json'),
|
||||
[deviceTypeJsonPath]: testfs.from('test/data/mnt/boot/device-type.json'),
|
||||
}).enable();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testFs.restore();
|
||||
delete require.cache[require.resolve('~/src/config')];
|
||||
});
|
||||
|
||||
it('reads and exposes values from config.json', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const configJson = await readConfigJson();
|
||||
const id = await conf.get('applicationId');
|
||||
const id = await config.get('applicationId');
|
||||
return expect(id).to.equal(configJson.applicationId);
|
||||
});
|
||||
|
||||
it('allows reading several values in one getMany call', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const configJson = await readConfigJson();
|
||||
return expect(
|
||||
await conf.getMany(['applicationId', 'apiEndpoint']),
|
||||
await config.getMany(['applicationId', 'apiEndpoint']),
|
||||
).to.deep.equal({
|
||||
applicationId: configJson.applicationId,
|
||||
apiEndpoint: configJson.apiEndpoint,
|
||||
@ -63,36 +62,49 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
it('generates a uuid and stores it in config.json', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const configJson = await readConfigJson();
|
||||
const uuid = await conf.get('uuid');
|
||||
const uuid = await config.get('uuid');
|
||||
expect(uuid).to.be.a('string');
|
||||
expect(uuid).to.have.lengthOf(32);
|
||||
expect(uuid).to.equal(configJson.uuid);
|
||||
});
|
||||
|
||||
it('does not allow setting an immutable field', async () => {
|
||||
return expect(conf.set({ deviceType: 'a different device type' })).to.be
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
return expect(config.set({ deviceType: 'a different device type' })).to.be
|
||||
.rejected;
|
||||
});
|
||||
|
||||
it('allows setting both config.json and database fields transparently', async () => {
|
||||
await conf.set({ appUpdatePollInterval: 30000, name: 'a new device name' });
|
||||
const config = await conf.getMany(['appUpdatePollInterval', 'name']);
|
||||
return expect(config).to.deep.equal({
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await config.set({
|
||||
appUpdatePollInterval: 30000,
|
||||
name: 'a new device name',
|
||||
});
|
||||
const values = await config.getMany(['appUpdatePollInterval', 'name']);
|
||||
return expect(values).to.deep.equal({
|
||||
appUpdatePollInterval: 30000,
|
||||
name: 'a new device name',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows deleting a config.json key and returns a default value if none is set', async () => {
|
||||
await conf.remove('appUpdatePollInterval');
|
||||
const poll = await conf.get('appUpdatePollInterval');
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await config.remove('appUpdatePollInterval');
|
||||
const poll = await config.get('appUpdatePollInterval');
|
||||
return expect(poll).to.equal(900000);
|
||||
});
|
||||
|
||||
it('allows deleting a config.json key if it is null', async () => {
|
||||
await conf.set({ apiKey: null });
|
||||
const key = await conf.get('apiKey');
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await config.set({ apiKey: null });
|
||||
const key = await config.get('apiKey');
|
||||
|
||||
expect(key).to.be.undefined;
|
||||
|
||||
@ -102,30 +114,30 @@ describe('config', () => {
|
||||
});
|
||||
|
||||
it('does not allow modifying or removing a function value', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
// We have to cast to any below, as the type system will
|
||||
// not allow removing a function value
|
||||
await expect(conf.remove('version' as any)).to.be.rejected;
|
||||
await expect(conf.set({ version: '2.0' })).to.be.rejected;
|
||||
await expect(config.remove('version' as any)).to.be.rejected;
|
||||
await expect(config.set({ version: '2.0' })).to.be.rejected;
|
||||
});
|
||||
|
||||
it('throws when asked for an unknown key', () => {
|
||||
return expect(conf.get('unknownInvalidValue' as any)).to.be.rejected;
|
||||
it('throws when asked for an unknown key', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await expect(config.get('unknownInvalidValue' as any)).to.be.rejected;
|
||||
});
|
||||
|
||||
it('emits a change event when values change', (done) => {
|
||||
const listener = (val: conf.ConfigChangeMap<SchemaTypeKey>) => {
|
||||
try {
|
||||
if ('name' in val) {
|
||||
expect(val.name).to.equal('someValue');
|
||||
done();
|
||||
conf.removeListener('change', listener);
|
||||
}
|
||||
} catch (e) {
|
||||
done(e);
|
||||
}
|
||||
};
|
||||
conf.on('change', listener);
|
||||
conf.set({ name: 'someValue' });
|
||||
it('emits a change event when values change', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const listener = stub();
|
||||
config.on('change', listener);
|
||||
config.set({ name: 'someValue' });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
expect(listener).to.have.been.calledWith({ name: 'someValue' });
|
||||
});
|
||||
|
||||
// FIXME: this test illustrates the issue with the singleton approach and the
|
||||
@ -144,26 +156,34 @@ describe('config', () => {
|
||||
),
|
||||
}).enable();
|
||||
|
||||
await conf.set({ developmentMode: false });
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await config.set({ developmentMode: false });
|
||||
|
||||
const osVariant = await conf.get('osVariant');
|
||||
const osVariant = await config.get('osVariant');
|
||||
expect(osVariant).to.equal('prod');
|
||||
|
||||
await tFs.restore();
|
||||
});
|
||||
|
||||
it('reads and exposes MAC addresses', async () => {
|
||||
const macAddress = await conf.get('macAddress');
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const macAddress = await config.get('macAddress');
|
||||
expect(macAddress).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
||||
describe('Function config providers', () => {
|
||||
it('should throw if a non-mutable function provider is set', () => {
|
||||
expect(conf.set({ version: 'some-version' })).to.be.rejected;
|
||||
it('should throw if a non-mutable function provider is set', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await expect(config.set({ version: 'some-version' })).to.be.rejected;
|
||||
});
|
||||
|
||||
it('should throw if a non-mutable function provider is removed', () => {
|
||||
expect(conf.remove('version' as any)).to.be.rejected;
|
||||
it('should throw if a non-mutable function provider is removed', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
await expect(config.remove('version' as any)).to.be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
@ -176,65 +196,86 @@ describe('config', () => {
|
||||
|
||||
it('should obtain deviceArch from device-type.json', async () => {
|
||||
const dtJson = await readDeviceTypeJson();
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
|
||||
const deviceArch = await conf.get('deviceArch');
|
||||
const deviceArch = await config.get('deviceArch');
|
||||
expect(deviceArch).to.equal(dtJson.arch);
|
||||
});
|
||||
|
||||
it('should obtain deviceType from device-type.json', async () => {
|
||||
const dtJson = await readDeviceTypeJson();
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
|
||||
const deviceArch = await conf.get('deviceType');
|
||||
const deviceArch = await config.get('deviceType');
|
||||
expect(deviceArch).to.equal(dtJson.slug);
|
||||
});
|
||||
|
||||
it('should memoize values from device-type.json', async () => {
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
const dtJson = await readDeviceTypeJson();
|
||||
spy(fs, 'readFile');
|
||||
spy(hostUtils, 'readFromBoot');
|
||||
|
||||
// Make a first call to get the value to be memoized
|
||||
await conf.get('deviceType');
|
||||
await conf.get('deviceArch');
|
||||
expect(fs.readFile).to.be.called;
|
||||
(fs.readFile as SinonSpy).resetHistory();
|
||||
await config.get('deviceType');
|
||||
await config.get('deviceArch');
|
||||
expect(hostUtils.readFromBoot).to.be.called;
|
||||
(hostUtils.readFromBoot as SinonSpy).resetHistory();
|
||||
|
||||
const deviceArch = await conf.get('deviceArch');
|
||||
const deviceArch = await config.get('deviceArch');
|
||||
expect(deviceArch).to.equal(dtJson.arch);
|
||||
|
||||
// The result should still be memoized from the previous call
|
||||
expect(fs.readFile).to.not.be.called;
|
||||
expect(hostUtils.readFromBoot).to.not.be.called;
|
||||
|
||||
const deviceType = await conf.get('deviceType');
|
||||
const deviceType = await config.get('deviceType');
|
||||
expect(deviceType).to.equal(dtJson.slug);
|
||||
|
||||
// The result should still be memoized from the previous call
|
||||
expect(fs.readFile).to.not.be.called;
|
||||
expect(hostUtils.readFromBoot).to.not.be.called;
|
||||
|
||||
(fs.readFile as SinonSpy).restore();
|
||||
(hostUtils.readFromBoot as SinonSpy).restore();
|
||||
});
|
||||
|
||||
it('should not memoize errors when reading deviceArch', async () => {
|
||||
// File not found
|
||||
stub(fs, 'readFile').rejects('File not found');
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
|
||||
await expect(conf.get('deviceArch')).to.eventually.equal('unknown');
|
||||
expect(fs.readFile).to.be.calledOnce;
|
||||
(fs.readFile as SinonStub).restore();
|
||||
const tfs = await testfs({}, { keep: [deviceTypeJsonPath] }).enable();
|
||||
|
||||
// Remove the file before the test
|
||||
await fs.unlink(deviceTypeJsonPath).catch(() => {
|
||||
/* noop */
|
||||
});
|
||||
|
||||
await expect(config.get('deviceArch')).to.eventually.equal('unknown');
|
||||
|
||||
// Restore the file before trying again
|
||||
await tfs.restore();
|
||||
|
||||
const dtJson = await readDeviceTypeJson();
|
||||
await expect(conf.get('deviceArch')).to.eventually.equal(dtJson.arch);
|
||||
await expect(config.get('deviceArch')).to.eventually.equal(dtJson.arch);
|
||||
});
|
||||
|
||||
it('should not memoize errors when reading deviceType', async () => {
|
||||
// File not found
|
||||
stub(fs, 'readFile').rejects('File not found');
|
||||
const config = await import('~/src/config');
|
||||
await config.initialized();
|
||||
|
||||
await expect(conf.get('deviceType')).to.eventually.equal('unknown');
|
||||
expect(fs.readFile).to.be.calledOnce;
|
||||
(fs.readFile as SinonStub).restore();
|
||||
const tfs = await testfs({}, { keep: [deviceTypeJsonPath] }).enable();
|
||||
// Remove the file before the test
|
||||
await fs.unlink(deviceTypeJsonPath).catch(() => {
|
||||
/* noop */
|
||||
});
|
||||
|
||||
await expect(config.get('deviceType')).to.eventually.equal('unknown');
|
||||
|
||||
// Restore the file before trying again
|
||||
await tfs.restore();
|
||||
|
||||
const dtJson = await readDeviceTypeJson();
|
||||
await expect(conf.get('deviceType')).to.eventually.equal(dtJson.slug);
|
||||
await expect(config.get('deviceType')).to.eventually.equal(dtJson.slug);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -17,9 +17,16 @@ describe('config/utils', () => {
|
||||
it('gets list of supported backends', async () => {
|
||||
const tFs = await testfs({
|
||||
// This is only needed so config.get doesn't fail
|
||||
[hostUtils.pathOnBoot('config.json')]: JSON.stringify({
|
||||
deviceType: 'raspberrypi4',
|
||||
[hostUtils.pathOnBoot('device-type.json')]: JSON.stringify({
|
||||
slug: 'raspberrypi4-64',
|
||||
arch: 'aarch64',
|
||||
}),
|
||||
[hostUtils.pathOnBoot('splash')]: {
|
||||
'balena-logo.png': Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=',
|
||||
'base64',
|
||||
),
|
||||
},
|
||||
}).enable();
|
||||
|
||||
// Get list of backends
|
||||
|
@ -525,7 +525,10 @@ describe('device-config', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly load the configfs.json file', async () => {
|
||||
// This test is skipped because it depends on internal details of the config-fs
|
||||
// backend. We need to refactor that module to make this more testable
|
||||
// properly
|
||||
it.skip('should correctly load the configfs.json file', async () => {
|
||||
await configFsBackend.initialise();
|
||||
|
||||
stub(fsUtils, 'exec').resolves();
|
||||
|
Loading…
Reference in New Issue
Block a user