mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-22 10:21:01 +00:00
Merge pull request #1566 from balena-io/1360-boot-splash
Add support for `BALENA_HOST_SPLASH_IMAGE` config var
This commit is contained in:
commit
e5a1561bff
@ -12,7 +12,7 @@ export const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountP
|
|||||||
|
|
||||||
export async function remountAndWriteAtomic(
|
export async function remountAndWriteAtomic(
|
||||||
file: string,
|
file: string,
|
||||||
data: string,
|
data: string | Buffer,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Here's the dangerous part:
|
// Here's the dangerous part:
|
||||||
await child_process.exec(
|
await child_process.exec(
|
||||||
@ -64,4 +64,11 @@ export abstract class ConfigBackend {
|
|||||||
public async initialise(): Promise<ConfigBackend> {
|
public async initialise(): Promise<ConfigBackend> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that all required fields for device type are included in the
|
||||||
|
// provided configuration. It is expected to modify the configuration if
|
||||||
|
// necessary
|
||||||
|
public ensureRequiredConfig(_deviceType: string, conf: ConfigOptions) {
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,4 +148,29 @@ export class ConfigTxt extends ConfigBackend {
|
|||||||
public createConfigVarName(configName: string): string {
|
public createConfigVarName(configName: string): string {
|
||||||
return ConfigTxt.bootConfigVarPrefix + configName;
|
return ConfigTxt.bootConfigVarPrefix + configName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that the balena-fin overlay is defined in the target configuration
|
||||||
|
// overrides the parent
|
||||||
|
public ensureRequiredConfig(deviceType: string, conf: ConfigOptions) {
|
||||||
|
if (deviceType === 'fincm3') {
|
||||||
|
this.ensureDtoverlay(conf, 'balena-fin');
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifies conf
|
||||||
|
private ensureDtoverlay(conf: ConfigOptions, field: string) {
|
||||||
|
if (conf.dtoverlay == null) {
|
||||||
|
conf.dtoverlay = [];
|
||||||
|
} else if (_.isString(conf.dtoverlay)) {
|
||||||
|
conf.dtoverlay = [conf.dtoverlay];
|
||||||
|
}
|
||||||
|
if (!_.includes(conf.dtoverlay, field)) {
|
||||||
|
conf.dtoverlay.push(field);
|
||||||
|
}
|
||||||
|
conf.dtoverlay = conf.dtoverlay.filter((s) => !_.isEmpty(s));
|
||||||
|
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { ExtraUEnv } from './extra-uEnv';
|
|||||||
import { ConfigTxt } from './config-txt';
|
import { ConfigTxt } from './config-txt';
|
||||||
import { ConfigFs } from './config-fs';
|
import { ConfigFs } from './config-fs';
|
||||||
import { Odmdata } from './odmdata';
|
import { Odmdata } from './odmdata';
|
||||||
|
import { SplashImage } from './splash-image';
|
||||||
|
|
||||||
export const allBackends = [
|
export const allBackends = [
|
||||||
new Extlinux(),
|
new Extlinux(),
|
||||||
@ -12,6 +13,7 @@ export const allBackends = [
|
|||||||
new ConfigTxt(),
|
new ConfigTxt(),
|
||||||
new ConfigFs(),
|
new ConfigFs(),
|
||||||
new Odmdata(),
|
new Odmdata(),
|
||||||
|
new SplashImage(),
|
||||||
];
|
];
|
||||||
|
|
||||||
export function matchesAnyBootConfig(envVar: string): boolean {
|
export function matchesAnyBootConfig(envVar: string): boolean {
|
||||||
|
206
src/config/backends/splash-image.ts
Normal file
206
src/config/backends/splash-image.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import * as Bluebird from 'bluebird';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { fs } from 'mz';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import * as constants from '../../lib/constants';
|
||||||
|
import log from '../../lib/supervisor-console';
|
||||||
|
import {
|
||||||
|
bootMountPoint,
|
||||||
|
ConfigBackend,
|
||||||
|
ConfigOptions,
|
||||||
|
remountAndWriteAtomic,
|
||||||
|
} from './backend';
|
||||||
|
|
||||||
|
export class SplashImage extends ConfigBackend {
|
||||||
|
private static readonly BASEPATH = path.join(bootMountPoint, 'splash');
|
||||||
|
private static readonly DEFAULT = path.join(
|
||||||
|
SplashImage.BASEPATH,
|
||||||
|
'balena-logo-default.png',
|
||||||
|
);
|
||||||
|
private static readonly FILENAMES = ['balena-logo.png', 'resin-logo.png'];
|
||||||
|
private static readonly PREFIX = `${constants.hostConfigVarPrefix}SPLASH_`;
|
||||||
|
private static readonly CONFIGS = ['image'];
|
||||||
|
private static readonly DATA_URI_REGEX = /^data:(.+);base64,(.*)$/;
|
||||||
|
|
||||||
|
// Check the first 8 bytes of a buffer for a PNG header
|
||||||
|
// Source: https://github.com/sindresorhus/is-png/blob/master/index.js
|
||||||
|
private isPng(buffer: Buffer) {
|
||||||
|
if (!buffer || buffer.length < 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
buffer[0] === 0x89 &&
|
||||||
|
buffer[1] === 0x50 &&
|
||||||
|
buffer[2] === 0x4e &&
|
||||||
|
buffer[3] === 0x47 &&
|
||||||
|
buffer[4] === 0x0d &&
|
||||||
|
buffer[5] === 0x0a &&
|
||||||
|
buffer[6] === 0x1a &&
|
||||||
|
buffer[7] === 0x0a
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file path for the splash image for the underlying
|
||||||
|
// system
|
||||||
|
private async getSplashPath(): Promise<string> {
|
||||||
|
// TODO: this is not perfect, if the file was supposed to be
|
||||||
|
// resin-logo.png and for some reason is not present on the folder
|
||||||
|
// the supervisor will try to write new images from the API into
|
||||||
|
// balena-logo.png to no effect. Ideally this should be configurable
|
||||||
|
// as a constant that is provided on supervisor container launch.
|
||||||
|
const [
|
||||||
|
// If no logo is found, assume the file is `balena-logo.png`
|
||||||
|
splashFile = path.join(SplashImage.BASEPATH, 'balena-logo.png'),
|
||||||
|
] = (
|
||||||
|
await Bluebird.resolve(fs.readdir(SplashImage.BASEPATH))
|
||||||
|
// Read the splash dir (will throw if the path does not exist)
|
||||||
|
// And filter valid filenames
|
||||||
|
.filter((filename) => SplashImage.FILENAMES.includes(filename))
|
||||||
|
)
|
||||||
|
// Sort by name, so in case both files are defined, balena-logo will
|
||||||
|
// be chosen
|
||||||
|
.sort()
|
||||||
|
// Convert to full path
|
||||||
|
.map((filename) => path.join(SplashImage.BASEPATH, filename));
|
||||||
|
|
||||||
|
return splashFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the base64 contents of the splash image
|
||||||
|
private async readSplashImage(where?: string): Promise<string> {
|
||||||
|
// Read from defaultPath unless where is defined
|
||||||
|
where = where ?? (await this.getSplashPath());
|
||||||
|
|
||||||
|
// read the image file...
|
||||||
|
return (await fs.readFile(where)).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a splash image provided as a base64 string
|
||||||
|
private async writeSplashImage(image: string, where?: string): Promise<void> {
|
||||||
|
// Write to defaultPath unless where is defined
|
||||||
|
where = where ?? (await this.getSplashPath());
|
||||||
|
|
||||||
|
const buffer = Buffer.from(image, 'base64');
|
||||||
|
if (this.isPng(buffer)) {
|
||||||
|
// Write the buffer to the given location
|
||||||
|
await remountAndWriteAtomic(where, buffer);
|
||||||
|
} else {
|
||||||
|
throw new Error('Splash image should be a base64 encoded PNG image');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stripPrefix(name: string): string {
|
||||||
|
if (!name.startsWith(SplashImage.PREFIX)) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return name.substr(SplashImage.PREFIX.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createConfigVarName(name: string): string {
|
||||||
|
return `${SplashImage.PREFIX}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBootConfig(): Promise<ConfigOptions> {
|
||||||
|
try {
|
||||||
|
const defaultImg = await this.readSplashImage(SplashImage.DEFAULT);
|
||||||
|
const img = await this.readSplashImage();
|
||||||
|
|
||||||
|
// If the image is the same as the default image
|
||||||
|
// return nothing
|
||||||
|
if (img !== defaultImg) {
|
||||||
|
return {
|
||||||
|
image: `data:image/png;base64,${img}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.warn('Failed to read splash image:', e);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialise(): Promise<SplashImage> {
|
||||||
|
try {
|
||||||
|
await super.initialise();
|
||||||
|
|
||||||
|
// The default boot image file has already
|
||||||
|
// been created
|
||||||
|
if (await fs.exists(SplashImage.DEFAULT)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the existing image file...
|
||||||
|
const image = await this.readSplashImage();
|
||||||
|
|
||||||
|
// write the image to the DEFAULT path
|
||||||
|
await this.writeSplashImage(image, SplashImage.DEFAULT);
|
||||||
|
|
||||||
|
log.success('Initialised splash image backend');
|
||||||
|
} catch (error) {
|
||||||
|
log.warn('Could not initialise splash image backend', error);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ensureRequiredConfig(_deviceType: string, conf: ConfigOptions) {
|
||||||
|
// If the value from the cloud is empty, it is the same as no definition
|
||||||
|
if (
|
||||||
|
!_.isUndefined(conf.image) &&
|
||||||
|
_.isEmpty((conf.image as string).trim())
|
||||||
|
) {
|
||||||
|
delete conf.image;
|
||||||
|
}
|
||||||
|
return conf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSupportedConfig(name: string): boolean {
|
||||||
|
return SplashImage.CONFIGS.includes(this.stripPrefix(name).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public isBootConfigVar(name: string): boolean {
|
||||||
|
return SplashImage.CONFIGS.includes(this.stripPrefix(name).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async matches(_deviceType: string): Promise<boolean> {
|
||||||
|
// all device types
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public processConfigVarName(name: string): string {
|
||||||
|
return this.stripPrefix(name).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public processConfigVarValue(
|
||||||
|
_name: string,
|
||||||
|
value: string,
|
||||||
|
): string | string[] {
|
||||||
|
// check data url regex
|
||||||
|
const matches = value.match(SplashImage.DATA_URI_REGEX);
|
||||||
|
if (!_.isNull(matches)) {
|
||||||
|
const [, media, data] = matches;
|
||||||
|
const [type] = media.split(';'); // discard mediatype parameters
|
||||||
|
|
||||||
|
return `data:${type};base64,${data}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(value.trim())) {
|
||||||
|
// Assume data is base64 encoded. If is not, setBootConfig will fail
|
||||||
|
return `data:image/png;base64,${value}`;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setBootConfig(opts: ConfigOptions): Promise<void> {
|
||||||
|
// If no splash image is defined, revert to the default splash image
|
||||||
|
const value = !_.isEmpty(opts.image)
|
||||||
|
? (opts.image as string)
|
||||||
|
: await this.readSplashImage(SplashImage.DEFAULT);
|
||||||
|
|
||||||
|
// If it is a data URI get only the data part
|
||||||
|
const [, image] = value.startsWith('data:') ? value.split(',') : [, value];
|
||||||
|
|
||||||
|
// Rewrite the splash image
|
||||||
|
await this.writeSplashImage(image);
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { DeviceStatus } from './types/state';
|
|||||||
import * as configUtils from './config/utils';
|
import * as configUtils from './config/utils';
|
||||||
import { SchemaTypeKey } from './config/schema-type';
|
import { SchemaTypeKey } from './config/schema-type';
|
||||||
import { matchesAnyBootConfig } from './config/backends';
|
import { matchesAnyBootConfig } from './config/backends';
|
||||||
import { ConfigOptions, ConfigBackend } from './config/backends/backend';
|
import { ConfigBackend } from './config/backends/backend';
|
||||||
import { Odmdata } from './config/backends/odmdata';
|
import { Odmdata } from './config/backends/odmdata';
|
||||||
|
|
||||||
const vpnServiceName = 'openvpn';
|
const vpnServiceName = 'openvpn';
|
||||||
@ -321,9 +321,11 @@ export function formatConfigKeys(conf: {
|
|||||||
confFromLegacyNamespace,
|
confFromLegacyNamespace,
|
||||||
noNamespaceConf,
|
noNamespaceConf,
|
||||||
);
|
);
|
||||||
return _.pickBy(confWithoutNamespace, (_v, k) => {
|
|
||||||
return _.includes(validKeys, k) || matchesAnyBootConfig(k);
|
return _.pickBy(
|
||||||
});
|
confWithoutNamespace,
|
||||||
|
(_v, k) => _.includes(validKeys, k) || matchesAnyBootConfig(k),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaults() {
|
export function getDefaults() {
|
||||||
@ -351,7 +353,7 @@ export function bootConfigChangeRequired(
|
|||||||
const currentBootConfig = configUtils.envToBootConfig(configBackend, current);
|
const currentBootConfig = configUtils.envToBootConfig(configBackend, current);
|
||||||
|
|
||||||
// Some devices require specific overlays, here we apply them
|
// Some devices require specific overlays, here we apply them
|
||||||
ensureRequiredOverlay(deviceType, targetBootConfig);
|
configBackend.ensureRequiredConfig(deviceType, targetBootConfig);
|
||||||
|
|
||||||
// Search for any unsupported values
|
// Search for any unsupported values
|
||||||
_.each(targetBootConfig, (value, key) => {
|
_.each(targetBootConfig, (value, key) => {
|
||||||
@ -597,8 +599,8 @@ export async function setBootConfig(
|
|||||||
'Apply boot config in progress',
|
'Apply boot config in progress',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure devices already have required overlays
|
// Ensure the required target config is available
|
||||||
ensureRequiredOverlay(await config.get('deviceType'), conf);
|
backend.ensureRequiredConfig(await config.get('deviceType'), conf);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await backend.setBootConfig(conf);
|
await backend.setBootConfig(conf);
|
||||||
@ -662,28 +664,3 @@ function checkBoolChanged(
|
|||||||
): boolean {
|
): boolean {
|
||||||
return checkTruthy(current[key]) !== checkTruthy(target[key]);
|
return checkTruthy(current[key]) !== checkTruthy(target[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifies conf
|
|
||||||
// exported for tests
|
|
||||||
export function ensureRequiredOverlay(deviceType: string, conf: ConfigOptions) {
|
|
||||||
if (deviceType === 'fincm3') {
|
|
||||||
ensureDtoverlay(conf, 'balena-fin');
|
|
||||||
}
|
|
||||||
|
|
||||||
return conf;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modifies conf
|
|
||||||
function ensureDtoverlay(conf: ConfigOptions, field: string) {
|
|
||||||
if (conf.dtoverlay == null) {
|
|
||||||
conf.dtoverlay = [];
|
|
||||||
} else if (_.isString(conf.dtoverlay)) {
|
|
||||||
conf.dtoverlay = [conf.dtoverlay];
|
|
||||||
}
|
|
||||||
if (!_.includes(conf.dtoverlay, field)) {
|
|
||||||
conf.dtoverlay.push(field);
|
|
||||||
}
|
|
||||||
conf.dtoverlay = conf.dtoverlay.filter((s) => !_.isEmpty(s));
|
|
||||||
|
|
||||||
return conf;
|
|
||||||
}
|
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
|
import * as _ from 'lodash';
|
||||||
import { fs } from 'mz';
|
import { fs } from 'mz';
|
||||||
import * as Path from 'path';
|
import * as Path from 'path';
|
||||||
import * as constants from './constants';
|
import * as constants from './constants';
|
||||||
import { ENOENT } from './errors';
|
import { ENOENT } from './errors';
|
||||||
|
|
||||||
export function writeAndSyncFile(path: string, data: string): Bluebird<void> {
|
export function writeAndSyncFile(
|
||||||
|
path: string,
|
||||||
|
data: string | Buffer,
|
||||||
|
): Bluebird<void> {
|
||||||
return Bluebird.resolve(fs.open(path, 'w')).then((fd) => {
|
return Bluebird.resolve(fs.open(path, 'w')).then((fd) => {
|
||||||
fs.write(fd, data, 0, 'utf8')
|
_.isString(data)
|
||||||
|
? fs.write(fd, data, 0, 'utf8')
|
||||||
|
: fs
|
||||||
|
.write(fd, data, 0, data.length)
|
||||||
.then(() => fs.fsync(fd))
|
.then(() => fs.fsync(fd))
|
||||||
.then(() => fs.close(fd));
|
.then(() => fs.close(fd));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeFileAtomic(path: string, data: string): Bluebird<void> {
|
export function writeFileAtomic(
|
||||||
|
path: string,
|
||||||
|
data: string | Buffer,
|
||||||
|
): Bluebird<void> {
|
||||||
return Bluebird.resolve(writeAndSyncFile(`${path}.new`, data)).then(() =>
|
return Bluebird.resolve(writeAndSyncFile(`${path}.new`, data)).then(() =>
|
||||||
fs.rename(`${path}.new`, path),
|
fs.rename(`${path}.new`, path),
|
||||||
);
|
);
|
||||||
|
@ -10,7 +10,9 @@ import { Extlinux } from '../src/config/backends/extlinux';
|
|||||||
import { ConfigTxt } from '../src/config/backends/config-txt';
|
import { ConfigTxt } from '../src/config/backends/config-txt';
|
||||||
import { Odmdata } from '../src/config/backends/odmdata';
|
import { Odmdata } from '../src/config/backends/odmdata';
|
||||||
import { ConfigFs } from '../src/config/backends/config-fs';
|
import { ConfigFs } from '../src/config/backends/config-fs';
|
||||||
|
import { SplashImage } from '../src/config/backends/splash-image';
|
||||||
import * as constants from '../src/lib/constants';
|
import * as constants from '../src/lib/constants';
|
||||||
|
import * as config from '../src/config';
|
||||||
|
|
||||||
import prepare = require('./lib/prepare');
|
import prepare = require('./lib/prepare');
|
||||||
|
|
||||||
@ -18,6 +20,7 @@ const extlinuxBackend = new Extlinux();
|
|||||||
const configTxtBackend = new ConfigTxt();
|
const configTxtBackend = new ConfigTxt();
|
||||||
const odmdataBackend = new Odmdata();
|
const odmdataBackend = new Odmdata();
|
||||||
const configFsBackend = new ConfigFs();
|
const configFsBackend = new ConfigFs();
|
||||||
|
const splashImageBackend = new SplashImage();
|
||||||
|
|
||||||
describe('Device Backend Config', () => {
|
describe('Device Backend Config', () => {
|
||||||
let logSpy: SinonSpy;
|
let logSpy: SinonSpy;
|
||||||
@ -171,6 +174,54 @@ describe('Device Backend Config', () => {
|
|||||||
(child_process.exec as SinonStub).restore();
|
(child_process.exec as SinonStub).restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ensures required fields are written to config.txt', async () => {
|
||||||
|
stub(fsUtils, 'writeFileAtomic').resolves();
|
||||||
|
stub(child_process, 'exec').resolves();
|
||||||
|
stub(config, 'get').withArgs('deviceType').resolves('fincm3');
|
||||||
|
const current = {
|
||||||
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
||||||
|
HOST_CONFIG_dtparam: '"i2c=on","audio=on"',
|
||||||
|
HOST_CONFIG_dtoverlay:
|
||||||
|
'"ads7846","lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
||||||
|
HOST_CONFIG_foobar: 'baz',
|
||||||
|
};
|
||||||
|
const target = {
|
||||||
|
HOST_CONFIG_initramfs: 'initramf.gz 0x00800000',
|
||||||
|
HOST_CONFIG_dtparam: '"i2c=on","audio=off"',
|
||||||
|
HOST_CONFIG_dtoverlay: '"lirc-rpi,gpio_out_pin=17,gpio_in_pin=13"',
|
||||||
|
HOST_CONFIG_foobar: 'bat',
|
||||||
|
HOST_CONFIG_foobaz: 'bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
// @ts-ignore accessing private value
|
||||||
|
deviceConfig.bootConfigChangeRequired(configTxtBackend, current, target),
|
||||||
|
).to.equal(true);
|
||||||
|
|
||||||
|
// @ts-ignore accessing private value
|
||||||
|
await deviceConfig.setBootConfig(configTxtBackend, target);
|
||||||
|
expect(child_process.exec).to.be.calledOnce;
|
||||||
|
expect(logSpy).to.be.calledTwice;
|
||||||
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledWith(
|
||||||
|
'./test/data/mnt/boot/config.txt',
|
||||||
|
stripIndent`
|
||||||
|
initramfs initramf.gz 0x00800000
|
||||||
|
dtparam=i2c=on
|
||||||
|
dtparam=audio=off
|
||||||
|
dtoverlay=lirc-rpi,gpio_out_pin=17,gpio_in_pin=13
|
||||||
|
dtoverlay=balena-fin
|
||||||
|
foobar=bat
|
||||||
|
foobaz=bar
|
||||||
|
` + '\n', // add newline because stripIndent trims last newline
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore stubs
|
||||||
|
(fsUtils.writeFileAtomic as SinonStub).restore();
|
||||||
|
(child_process.exec as SinonStub).restore();
|
||||||
|
(config.get as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
it('accepts RESIN_ and BALENA_ variables', async () => {
|
it('accepts RESIN_ and BALENA_ variables', async () => {
|
||||||
return expect(
|
return expect(
|
||||||
deviceConfig.formatConfigKeys({
|
deviceConfig.formatConfigKeys({
|
||||||
@ -258,12 +309,14 @@ describe('Device Backend Config', () => {
|
|||||||
|
|
||||||
describe('Balena fin', () => {
|
describe('Balena fin', () => {
|
||||||
it('should always add the balena-fin dtoverlay', () => {
|
it('should always add the balena-fin dtoverlay', () => {
|
||||||
expect(deviceConfig.ensureRequiredOverlay('fincm3', {})).to.deep.equal({
|
expect(configTxtBackend.ensureRequiredConfig('fincm3', {})).to.deep.equal(
|
||||||
|
{
|
||||||
dtoverlay: ['balena-fin'],
|
dtoverlay: ['balena-fin'],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
deviceConfig.ensureRequiredOverlay('fincm3', {
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
||||||
test: '123',
|
test: '123',
|
||||||
test2: ['123'],
|
test2: ['123'],
|
||||||
test3: ['123', '234'],
|
test3: ['123', '234'],
|
||||||
@ -275,12 +328,12 @@ describe('Device Backend Config', () => {
|
|||||||
dtoverlay: ['balena-fin'],
|
dtoverlay: ['balena-fin'],
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
deviceConfig.ensureRequiredOverlay('fincm3', {
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
||||||
dtoverlay: 'test',
|
dtoverlay: 'test',
|
||||||
}),
|
}),
|
||||||
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
||||||
expect(
|
expect(
|
||||||
deviceConfig.ensureRequiredOverlay('fincm3', {
|
configTxtBackend.ensureRequiredConfig('fincm3', {
|
||||||
dtoverlay: ['test'],
|
dtoverlay: ['test'],
|
||||||
}),
|
}),
|
||||||
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
).to.deep.equal({ dtoverlay: ['test', 'balena-fin'] });
|
||||||
@ -473,4 +526,151 @@ describe('Device Backend Config', () => {
|
|||||||
).to.equal(false);
|
).to.equal(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Boot splash image', () => {
|
||||||
|
const defaultLogo =
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
|
||||||
|
const png =
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
|
||||||
|
const uri = `data:image/png;base64,${png}`;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup stubs
|
||||||
|
stub(fsUtils, 'writeFileAtomic').resolves();
|
||||||
|
stub(child_process, 'exec').resolves();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore stubs
|
||||||
|
(fsUtils.writeFileAtomic as SinonStub).restore();
|
||||||
|
(child_process.exec as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly write to resin-logo.png', async () => {
|
||||||
|
// Devices with balenaOS < 2.51 use resin-logo.png
|
||||||
|
stub(fs, 'readdir').resolves(['resin-logo.png']);
|
||||||
|
|
||||||
|
const current = {};
|
||||||
|
const target = {
|
||||||
|
HOST_SPLASH_image: png,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should work with every device type, but testing on a couple
|
||||||
|
// of options
|
||||||
|
expect(
|
||||||
|
deviceConfig.bootConfigChangeRequired(
|
||||||
|
splashImageBackend,
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
'fincm3',
|
||||||
|
),
|
||||||
|
).to.equal(true);
|
||||||
|
|
||||||
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
||||||
|
|
||||||
|
expect(child_process.exec).to.be.calledOnce;
|
||||||
|
expect(logSpy).to.be.calledTwice;
|
||||||
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore the stub
|
||||||
|
(fs.readdir as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly write to balena-logo.png', async () => {
|
||||||
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
||||||
|
stub(fs, 'readdir').resolves(['balena-logo.png']);
|
||||||
|
|
||||||
|
const current = {};
|
||||||
|
const target = {
|
||||||
|
HOST_SPLASH_image: png,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should work with every device type, but testing on a couple
|
||||||
|
// of options
|
||||||
|
expect(
|
||||||
|
deviceConfig.bootConfigChangeRequired(
|
||||||
|
splashImageBackend,
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
'raspberrypi4-64',
|
||||||
|
),
|
||||||
|
).to.equal(true);
|
||||||
|
|
||||||
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
||||||
|
|
||||||
|
expect(child_process.exec).to.be.calledOnce;
|
||||||
|
expect(logSpy).to.be.calledTwice;
|
||||||
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore the stub
|
||||||
|
(fs.readdir as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly write to balena-logo.png if no default logo is found', async () => {
|
||||||
|
// Devices with balenaOS >= 2.51 use balena-logo.png
|
||||||
|
stub(fs, 'readdir').resolves([]);
|
||||||
|
|
||||||
|
const current = {};
|
||||||
|
const target = {
|
||||||
|
HOST_SPLASH_image: png,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should work with every device type, but testing on a couple
|
||||||
|
// of options
|
||||||
|
expect(
|
||||||
|
deviceConfig.bootConfigChangeRequired(
|
||||||
|
splashImageBackend,
|
||||||
|
current,
|
||||||
|
target,
|
||||||
|
'raspberrypi3',
|
||||||
|
),
|
||||||
|
).to.equal(true);
|
||||||
|
|
||||||
|
await deviceConfig.setBootConfig(splashImageBackend, target);
|
||||||
|
|
||||||
|
expect(child_process.exec).to.be.calledOnce;
|
||||||
|
expect(logSpy).to.be.calledTwice;
|
||||||
|
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
|
||||||
|
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
// restore the stub
|
||||||
|
(fs.readdir as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly read the splash logo if different from the default', async () => {
|
||||||
|
stub(fs, 'readdir').resolves(['balena-logo.png']);
|
||||||
|
|
||||||
|
const readFileStub: SinonStub = stub(fs, 'readFile').resolves(
|
||||||
|
Buffer.from(png, 'base64') as any,
|
||||||
|
);
|
||||||
|
readFileStub
|
||||||
|
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
|
||||||
|
.resolves(Buffer.from(defaultLogo, 'base64') as any);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await deviceConfig.getBootConfig(splashImageBackend),
|
||||||
|
).to.deep.equal({
|
||||||
|
HOST_SPLASH_image: uri,
|
||||||
|
});
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore stubs
|
||||||
|
(fs.readdir as SinonStub).restore();
|
||||||
|
(fs.readFile as SinonStub).restore();
|
||||||
|
readFileStub.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ import { ExtraUEnv } from '../src/config/backends/extra-uEnv';
|
|||||||
import { Extlinux } from '../src/config/backends/extlinux';
|
import { Extlinux } from '../src/config/backends/extlinux';
|
||||||
import { ConfigTxt } from '../src/config/backends/config-txt';
|
import { ConfigTxt } from '../src/config/backends/config-txt';
|
||||||
import { ConfigFs } from '../src/config/backends/config-fs';
|
import { ConfigFs } from '../src/config/backends/config-fs';
|
||||||
|
import { SplashImage } from '../src/config/backends/splash-image';
|
||||||
import { ConfigBackend } from '../src/config/backends/backend';
|
import { ConfigBackend } from '../src/config/backends/backend';
|
||||||
|
|
||||||
describe('Config Utilities', () => {
|
describe('Config Utilities', () => {
|
||||||
@ -16,8 +17,9 @@ describe('Config Utilities', () => {
|
|||||||
const configStub = stub(config, 'get').resolves('raspberry');
|
const configStub = stub(config, 'get').resolves('raspberry');
|
||||||
// Get list of backends
|
// Get list of backends
|
||||||
const devices = await configUtils.getSupportedBackends();
|
const devices = await configUtils.getSupportedBackends();
|
||||||
expect(devices.length).to.equal(1);
|
expect(devices.length).to.equal(2);
|
||||||
expect(devices[0].constructor.name).to.equal('ConfigTxt');
|
expect(devices[0].constructor.name).to.equal('ConfigTxt');
|
||||||
|
expect(devices[1].constructor.name).to.equal('SplashImage');
|
||||||
// Restore stub
|
// Restore stub
|
||||||
configStub.restore();
|
configStub.restore();
|
||||||
// TO-DO: When we have a device that will match for multiple backends
|
// TO-DO: When we have a device that will match for multiple backends
|
||||||
@ -46,6 +48,7 @@ const BACKENDS: Record<string, ConfigBackend> = {
|
|||||||
extlinux: new Extlinux(),
|
extlinux: new Extlinux(),
|
||||||
configtxt: new ConfigTxt(),
|
configtxt: new ConfigTxt(),
|
||||||
configfs: new ConfigFs(),
|
configfs: new ConfigFs(),
|
||||||
|
splashImage: new SplashImage(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONFIGS = {
|
const CONFIGS = {
|
||||||
@ -88,6 +91,14 @@ const CONFIGS = {
|
|||||||
foobar: 'baz',
|
foobar: 'baz',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
splashImage: {
|
||||||
|
envVars: {
|
||||||
|
HOST_SPLASH_image: 'data:image/png;base64,aaa',
|
||||||
|
},
|
||||||
|
bootConfig: {
|
||||||
|
image: 'data:image/png;base64,aaa',
|
||||||
|
},
|
||||||
|
},
|
||||||
// TO-DO: Config-FS is commented out because it behaves differently and doesn't
|
// TO-DO: Config-FS is commented out because it behaves differently and doesn't
|
||||||
// add value to the Config Utilities if we make it work but would like to add it
|
// add value to the Config Utilities if we make it work but would like to add it
|
||||||
// configfs: {
|
// configfs: {
|
||||||
|
306
test/43-splash-image.spec.ts
Normal file
306
test/43-splash-image.spec.ts
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import { fs, child_process } from 'mz';
|
||||||
|
import { SinonStub, stub } from 'sinon';
|
||||||
|
|
||||||
|
import { expect } from './lib/chai-config';
|
||||||
|
import * as fsUtils from '../src/lib/fs-utils';
|
||||||
|
import { SplashImage } from '../src/config/backends/splash-image';
|
||||||
|
import log from '../src/lib/supervisor-console';
|
||||||
|
|
||||||
|
describe('Splash image configuration', () => {
|
||||||
|
const backend = new SplashImage();
|
||||||
|
const defaultLogo =
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
|
||||||
|
const logo =
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
|
||||||
|
const uri = `data:image/png;base64,${logo}`;
|
||||||
|
let readDirStub: SinonStub;
|
||||||
|
let readFileStub: SinonStub;
|
||||||
|
let writeFileAtomicStub: SinonStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Setup stubs
|
||||||
|
writeFileAtomicStub = stub(fsUtils, 'writeFileAtomic').resolves();
|
||||||
|
stub(child_process, 'exec').resolves();
|
||||||
|
readFileStub = stub(fs, 'readFile').resolves(
|
||||||
|
Buffer.from(logo, 'base64') as any,
|
||||||
|
);
|
||||||
|
readFileStub
|
||||||
|
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
|
||||||
|
.resolves(Buffer.from(defaultLogo, 'base64') as any);
|
||||||
|
readDirStub = stub(fs, 'readdir').resolves(['balena-logo.png']);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore stubs
|
||||||
|
writeFileAtomicStub.restore();
|
||||||
|
(child_process.exec as SinonStub).restore();
|
||||||
|
readFileStub.restore();
|
||||||
|
readDirStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialise', () => {
|
||||||
|
it('should make a copy of the existing boot image on initialise if not yet created', async () => {
|
||||||
|
stub(fs, 'exists').resolves(false);
|
||||||
|
|
||||||
|
// Do the initialization
|
||||||
|
await backend.initialise();
|
||||||
|
|
||||||
|
expect(fs.readFile).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should make a copy
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
Buffer.from(logo, 'base64'),
|
||||||
|
);
|
||||||
|
|
||||||
|
(fs.exists as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip initialization if the default image already exists', async () => {
|
||||||
|
stub(fs, 'exists').resolves(true);
|
||||||
|
|
||||||
|
// Do the initialization
|
||||||
|
await backend.initialise();
|
||||||
|
|
||||||
|
expect(fs.exists).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(fs.readFile).to.not.have.been.called;
|
||||||
|
|
||||||
|
(fs.exists as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail initialization if there is no default image on the device', async () => {
|
||||||
|
stub(fs, 'exists').resolves(false);
|
||||||
|
readDirStub.resolves([]);
|
||||||
|
readFileStub.rejects();
|
||||||
|
stub(log, 'warn');
|
||||||
|
|
||||||
|
// Do the initialization
|
||||||
|
await backend.initialise();
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(fs.readFile).to.have.been.calledOnce;
|
||||||
|
expect(log.warn).to.be.calledOnce;
|
||||||
|
|
||||||
|
(log.warn as SinonStub).restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBootConfig', () => {
|
||||||
|
it('should return an empty object if the current logo matches the default logo', async () => {
|
||||||
|
readDirStub.resolves(['resin-logo.png']);
|
||||||
|
|
||||||
|
// The default logo resolves to the same value as the current logo
|
||||||
|
readFileStub
|
||||||
|
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
|
||||||
|
.resolves(logo);
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({});
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledTwice;
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the splash image from resin-logo.png if available', async () => {
|
||||||
|
readDirStub.resolves(['resin-logo.png']);
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({
|
||||||
|
image: uri,
|
||||||
|
});
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledTwice;
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the splash image from balena-logo.png if available', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png']);
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({
|
||||||
|
image: uri,
|
||||||
|
});
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledTwice;
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read the splash image from balena-logo.png even if resin-logo.png exists', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png', 'resin-logo.png']);
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({
|
||||||
|
image: uri,
|
||||||
|
});
|
||||||
|
expect(readFileStub).to.be.calledTwice;
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(readFileStub).to.be.calledWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should catch readDir errors', async () => {
|
||||||
|
stub(log, 'warn');
|
||||||
|
readDirStub.rejects();
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({});
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(log.warn).to.be.calledOnce;
|
||||||
|
|
||||||
|
(log.warn as SinonStub).restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should catch readFile errors', async () => {
|
||||||
|
stub(log, 'warn');
|
||||||
|
readDirStub.resolves([]);
|
||||||
|
readFileStub.rejects();
|
||||||
|
|
||||||
|
expect(await backend.getBootConfig()).to.deep.equal({});
|
||||||
|
expect(log.warn).to.be.calledOnce;
|
||||||
|
|
||||||
|
(log.warn as SinonStub).restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setBootConfig', () => {
|
||||||
|
it('should write the given image to resin-logo.png if set', async () => {
|
||||||
|
readDirStub.resolves(['resin-logo.png']);
|
||||||
|
|
||||||
|
await backend.setBootConfig({ image: uri });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
Buffer.from(logo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write the given image to balena-logo.png if set', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png']);
|
||||||
|
|
||||||
|
await backend.setBootConfig({ image: uri });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
Buffer.from(logo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write the given image to balena-logo.png by default', async () => {
|
||||||
|
readDirStub.resolves([]);
|
||||||
|
|
||||||
|
await backend.setBootConfig({ image: uri });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
Buffer.from(logo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept just a base64 as an image', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png']);
|
||||||
|
|
||||||
|
await backend.setBootConfig({ image: logo });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
Buffer.from(logo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore balena-logo.png if image arg is unset', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png']);
|
||||||
|
await backend.setBootConfig({});
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
Buffer.from(defaultLogo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore resin-logo.png if image arg is unset', async () => {
|
||||||
|
readDirStub.resolves(['resin-logo.png']);
|
||||||
|
await backend.setBootConfig({});
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
Buffer.from(defaultLogo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore balena-logo.png if image arg is empty', async () => {
|
||||||
|
readDirStub.resolves(['balena-logo.png']);
|
||||||
|
await backend.setBootConfig({ image: '' });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo.png',
|
||||||
|
Buffer.from(defaultLogo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should restore resin-logo.png if image arg is empty', async () => {
|
||||||
|
readDirStub.resolves(['resin-logo.png']);
|
||||||
|
await backend.setBootConfig({ image: '' });
|
||||||
|
|
||||||
|
expect(readDirStub).to.be.calledOnce;
|
||||||
|
expect(readFileStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/balena-logo-default.png',
|
||||||
|
);
|
||||||
|
expect(writeFileAtomicStub).to.be.calledOnceWith(
|
||||||
|
'test/data/mnt/boot/splash/resin-logo.png',
|
||||||
|
Buffer.from(defaultLogo, 'base64'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if arg is not a valid base64 string', async () => {
|
||||||
|
expect(backend.setBootConfig({ image: 'somestring' })).to.be.rejected;
|
||||||
|
expect(writeFileAtomicStub).to.not.be.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if image is not a valid PNG file', async () => {
|
||||||
|
expect(backend.setBootConfig({ image: 'aGVsbG8=' })).to.be.rejected;
|
||||||
|
expect(writeFileAtomicStub).to.not.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isBootConfigVar', () => {
|
||||||
|
it('Accepts any case variable names', () => {
|
||||||
|
expect(backend.isBootConfigVar('HOST_SPLASH_IMAGE')).to.be.true;
|
||||||
|
expect(backend.isBootConfigVar('HOST_SPLASH_image')).to.be.true;
|
||||||
|
expect(backend.isBootConfigVar('HOST_SPLASH_Image')).to.be.true;
|
||||||
|
expect(backend.isBootConfigVar('HOST_SPLASH_ImAgE')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user