mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-18 18:56:24 +00:00
Add support for BALENA_HOST_SPLASH_IMAGE
config
Setting this this variable to a base64 encoded string will replace the splash image on the device by rewriting `/mnt/boot/splash/balena-logo.png`. This will also make a copy of the default balena logo so the splash can be restored if the variable is removed. Change-type: minor Signed-off-by: Felipe Lalanne <felipe@balena.io>
This commit is contained in:
parent
e66a775c15
commit
4aa8090a56
@ -12,7 +12,7 @@ export const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountP
|
||||
|
||||
export async function remountAndWriteAtomic(
|
||||
file: string,
|
||||
data: string,
|
||||
data: string | Buffer,
|
||||
): Promise<void> {
|
||||
// Here's the dangerous part:
|
||||
await child_process.exec(
|
||||
|
@ -5,6 +5,7 @@ import { ExtraUEnv } from './extra-uEnv';
|
||||
import { ConfigTxt } from './config-txt';
|
||||
import { ConfigFs } from './config-fs';
|
||||
import { Odmdata } from './odmdata';
|
||||
import { SplashImage } from './splash-image';
|
||||
|
||||
export const allBackends = [
|
||||
new Extlinux(),
|
||||
@ -12,6 +13,7 @@ export const allBackends = [
|
||||
new ConfigTxt(),
|
||||
new ConfigFs(),
|
||||
new Odmdata(),
|
||||
new SplashImage(),
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -321,9 +321,11 @@ export function formatConfigKeys(conf: {
|
||||
confFromLegacyNamespace,
|
||||
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() {
|
||||
|
@ -1,18 +1,28 @@
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
import * as Path from 'path';
|
||||
import * as constants from './constants';
|
||||
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) => {
|
||||
fs.write(fd, data, 0, 'utf8')
|
||||
.then(() => fs.fsync(fd))
|
||||
.then(() => fs.close(fd));
|
||||
_.isString(data)
|
||||
? fs.write(fd, data, 0, 'utf8')
|
||||
: fs
|
||||
.write(fd, data, 0, data.length)
|
||||
.then(() => fs.fsync(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(() =>
|
||||
fs.rename(`${path}.new`, path),
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import { Extlinux } from '../src/config/backends/extlinux';
|
||||
import { ConfigTxt } from '../src/config/backends/config-txt';
|
||||
import { Odmdata } from '../src/config/backends/odmdata';
|
||||
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 config from '../src/config';
|
||||
|
||||
@ -19,6 +20,7 @@ const extlinuxBackend = new Extlinux();
|
||||
const configTxtBackend = new ConfigTxt();
|
||||
const odmdataBackend = new Odmdata();
|
||||
const configFsBackend = new ConfigFs();
|
||||
const splashImageBackend = new SplashImage();
|
||||
|
||||
describe('Device Backend Config', () => {
|
||||
let logSpy: SinonSpy;
|
||||
@ -524,4 +526,151 @@ describe('Device Backend Config', () => {
|
||||
).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 { ConfigTxt } from '../src/config/backends/config-txt';
|
||||
import { ConfigFs } from '../src/config/backends/config-fs';
|
||||
import { SplashImage } from '../src/config/backends/splash-image';
|
||||
import { ConfigBackend } from '../src/config/backends/backend';
|
||||
|
||||
describe('Config Utilities', () => {
|
||||
@ -16,8 +17,9 @@ describe('Config Utilities', () => {
|
||||
const configStub = stub(config, 'get').resolves('raspberry');
|
||||
// Get list of backends
|
||||
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[1].constructor.name).to.equal('SplashImage');
|
||||
// Restore stub
|
||||
configStub.restore();
|
||||
// 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(),
|
||||
configtxt: new ConfigTxt(),
|
||||
configfs: new ConfigFs(),
|
||||
splashImage: new SplashImage(),
|
||||
};
|
||||
|
||||
const CONFIGS = {
|
||||
@ -88,6 +91,14 @@ const CONFIGS = {
|
||||
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
|
||||
// add value to the Config Utilities if we make it work but would like to add it
|
||||
// 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…
Reference in New Issue
Block a user