mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-07 11:50:27 +00:00
Extract loadTargetFromFile function to preload module
Change-type: patch Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
parent
fea80c5205
commit
09a8231fde
5
src/application-manager.d.ts
vendored
5
src/application-manager.d.ts
vendored
@ -6,7 +6,7 @@ import { EventTracker } from './event-tracker';
|
|||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
import { DeviceApplicationState } from './types/state';
|
import { DeviceApplicationState } from './types/state';
|
||||||
|
|
||||||
import Images from './compose/images';
|
import ImageManager, { Image } from './compose/images';
|
||||||
import ServiceManager from './compose/service-manager';
|
import ServiceManager from './compose/service-manager';
|
||||||
import DB from './db';
|
import DB from './db';
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
public networks: NetworkManager;
|
public networks: NetworkManager;
|
||||||
public config: Config;
|
public config: Config;
|
||||||
public db: DB;
|
public db: DB;
|
||||||
public images: Images;
|
public images: ImageManager;
|
||||||
|
|
||||||
public proxyvisor: any;
|
public proxyvisor: any;
|
||||||
|
|
||||||
@ -81,6 +81,7 @@ export class ApplicationManager extends EventEmitter {
|
|||||||
public stopAll(opts: { force?: boolean; skipLock?: boolean }): Promise<void>;
|
public stopAll(opts: { force?: boolean; skipLock?: boolean }): Promise<void>;
|
||||||
|
|
||||||
public serviceNameFromId(serviceId: number): Bluebird<string>;
|
public serviceNameFromId(serviceId: number): Bluebird<string>;
|
||||||
|
public imageForService(svc: any): Promise<Image>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationManager;
|
export default ApplicationManager;
|
||||||
|
@ -476,7 +476,7 @@ export class Images extends (EventEmitter as new () => ImageEventEmitter) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalise(imageName: string): Bluebird<string> {
|
public normalise(imageName: string): Bluebird<string> {
|
||||||
return this.docker.normaliseImageName(imageName);
|
return this.docker.normaliseImageName(imageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,10 +16,9 @@ constants = require './lib/constants'
|
|||||||
validation = require './lib/validation'
|
validation = require './lib/validation'
|
||||||
systemd = require './lib/systemd'
|
systemd = require './lib/systemd'
|
||||||
updateLock = require './lib/update-lock'
|
updateLock = require './lib/update-lock'
|
||||||
|
{ loadTargetFromFile } = require './device-state/preload'
|
||||||
{ singleToMulticontainerApp } = require './lib/migration'
|
{ singleToMulticontainerApp } = require './lib/migration'
|
||||||
{
|
{
|
||||||
ENOENT,
|
|
||||||
EISDIR,
|
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
UpdatesLockedError
|
UpdatesLockedError
|
||||||
} = require './lib/errors'
|
} = require './lib/errors'
|
||||||
@ -301,7 +300,7 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
@applications.getTargetApps()
|
@applications.getTargetApps()
|
||||||
.then (targetApps) =>
|
.then (targetApps) =>
|
||||||
if !conf.provisioned or (_.isEmpty(targetApps) and !conf.targetStateSet)
|
if !conf.provisioned or (_.isEmpty(targetApps) and !conf.targetStateSet)
|
||||||
@loadTargetFromFile()
|
loadTargetFromFile(null, this)
|
||||||
.finally =>
|
.finally =>
|
||||||
@config.set({ targetStateSet: 'true' })
|
@config.set({ targetStateSet: 'true' })
|
||||||
else
|
else
|
||||||
@ -423,14 +422,6 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
_.assign(@_currentVolatile, newState)
|
_.assign(@_currentVolatile, newState)
|
||||||
@emitAsync('change')
|
@emitAsync('change')
|
||||||
|
|
||||||
_convertLegacyAppsJson: (appsArray) ->
|
|
||||||
Promise.try ->
|
|
||||||
deviceConf = _.reduce(appsArray, (conf, app) ->
|
|
||||||
return _.merge({}, conf, app.config)
|
|
||||||
, {})
|
|
||||||
apps = _.keyBy(_.map(appsArray, singleToMulticontainerApp), 'appId')
|
|
||||||
return { apps, config: deviceConf }
|
|
||||||
|
|
||||||
restoreBackup: (targetState) =>
|
restoreBackup: (targetState) =>
|
||||||
@setTarget(targetState)
|
@setTarget(targetState)
|
||||||
.then =>
|
.then =>
|
||||||
@ -469,73 +460,6 @@ module.exports = class DeviceState extends EventEmitter
|
|||||||
.then ->
|
.then ->
|
||||||
rimraf(path.join(constants.rootMountPoint, 'mnt/data', constants.migrationBackupFile))
|
rimraf(path.join(constants.rootMountPoint, 'mnt/data', constants.migrationBackupFile))
|
||||||
|
|
||||||
loadTargetFromFile: (appsPath) ->
|
|
||||||
log.info('Attempting to load preloaded apps...')
|
|
||||||
appsPath ?= constants.appsJsonPath
|
|
||||||
fs.readFileAsync(appsPath, 'utf8')
|
|
||||||
.then(JSON.parse)
|
|
||||||
.then (stateFromFile) =>
|
|
||||||
if _.isArray(stateFromFile)
|
|
||||||
# This is a legacy apps.json
|
|
||||||
log.debug('Legacy apps.json detected')
|
|
||||||
return @_convertLegacyAppsJson(stateFromFile)
|
|
||||||
else
|
|
||||||
return stateFromFile
|
|
||||||
.then (stateFromFile) =>
|
|
||||||
commitToPin = null
|
|
||||||
appToPin = null
|
|
||||||
if !_.isEmpty(stateFromFile)
|
|
||||||
images = _.flatMap stateFromFile.apps, (app, appId) =>
|
|
||||||
# multi-app warning!
|
|
||||||
# The following will need to be changed once running multiple applications is possible
|
|
||||||
commitToPin = app.commit
|
|
||||||
appToPin = appId
|
|
||||||
_.map app.services, (service, serviceId) =>
|
|
||||||
svc = {
|
|
||||||
imageName: service.image
|
|
||||||
serviceName: service.serviceName
|
|
||||||
imageId: service.imageId
|
|
||||||
serviceId
|
|
||||||
releaseId: app.releaseId
|
|
||||||
appId
|
|
||||||
}
|
|
||||||
return @applications.imageForService(svc)
|
|
||||||
Promise.map images, (img) =>
|
|
||||||
@applications.images.normalise(img.name)
|
|
||||||
.then (name) =>
|
|
||||||
img.name = name
|
|
||||||
@applications.images.save(img)
|
|
||||||
.then =>
|
|
||||||
@deviceConfig.getCurrent()
|
|
||||||
.then (deviceConf) =>
|
|
||||||
@deviceConfig.formatConfigKeys(stateFromFile.config)
|
|
||||||
.then (formattedConf) =>
|
|
||||||
stateFromFile.config = _.defaults(formattedConf, deviceConf)
|
|
||||||
stateFromFile.name ?= ''
|
|
||||||
@setTarget({
|
|
||||||
local: stateFromFile
|
|
||||||
})
|
|
||||||
.then =>
|
|
||||||
log.success('Preloading complete')
|
|
||||||
if stateFromFile.pinDevice
|
|
||||||
# multi-app warning!
|
|
||||||
# The following will need to be changed once running multiple applications is possible
|
|
||||||
log.debug('Device will be pinned')
|
|
||||||
if commitToPin? and appToPin?
|
|
||||||
@config.set
|
|
||||||
pinDevice: {
|
|
||||||
commit: commitToPin,
|
|
||||||
app: parseInt(appToPin, 10),
|
|
||||||
}
|
|
||||||
# Ensure that this is actually a file, and not an empty path
|
|
||||||
# It can be an empty path because if the file does not exist
|
|
||||||
# on host, the docker daemon creates an empty directory when
|
|
||||||
# the bind mount is added
|
|
||||||
.catch ENOENT, EISDIR, ->
|
|
||||||
log.debug('No apps.json file present, skipping preload')
|
|
||||||
.catch (err) =>
|
|
||||||
@eventTracker.track('Loading preloaded apps failed', { error: err })
|
|
||||||
|
|
||||||
reboot: (force, skipLock) =>
|
reboot: (force, skipLock) =>
|
||||||
@applications.stopAll({ force, skipLock })
|
@applications.stopAll({ force, skipLock })
|
||||||
.then =>
|
.then =>
|
||||||
|
2
src/device-state.d.ts
vendored
2
src/device-state.d.ts
vendored
@ -14,6 +14,8 @@ class DeviceState extends EventEmitter {
|
|||||||
public applications: ApplicationManager;
|
public applications: ApplicationManager;
|
||||||
public router: Router;
|
public router: Router;
|
||||||
public deviceConfig: DeviceConfig;
|
public deviceConfig: DeviceConfig;
|
||||||
|
public config: Config;
|
||||||
|
public eventTracker: EventTracker;
|
||||||
|
|
||||||
public constructor(args: {
|
public constructor(args: {
|
||||||
config: Config;
|
config: Config;
|
||||||
|
115
src/device-state/preload.ts
Normal file
115
src/device-state/preload.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
import { fs } from 'mz';
|
||||||
|
|
||||||
|
import { Image } from '../compose/images';
|
||||||
|
import DeviceState = require('../device-state');
|
||||||
|
|
||||||
|
import constants = require('../lib/constants');
|
||||||
|
import { AppsJsonParseError, EISDIR, ENOENT } from '../lib/errors';
|
||||||
|
import log from '../lib/supervisor-console';
|
||||||
|
|
||||||
|
import { convertLegacyAppsJson } from '../lib/migration';
|
||||||
|
import { AppsJsonFormat } from '../types/state';
|
||||||
|
|
||||||
|
export async function loadTargetFromFile(
|
||||||
|
appsPath: Nullable<string>,
|
||||||
|
deviceState: DeviceState,
|
||||||
|
): Promise<void> {
|
||||||
|
log.info('Attempting to load any preloaded applications');
|
||||||
|
if (!appsPath) {
|
||||||
|
appsPath = constants.appsJsonPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(appsPath, 'utf8');
|
||||||
|
|
||||||
|
// It's either a target state or it's a list of legacy
|
||||||
|
// style application definitions, we reconcile this below
|
||||||
|
let stateFromFile: AppsJsonFormat | any[];
|
||||||
|
try {
|
||||||
|
stateFromFile = JSON.parse(content);
|
||||||
|
} catch (e) {
|
||||||
|
throw new AppsJsonParseError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isArray(stateFromFile)) {
|
||||||
|
log.debug('Detected a legacy apps.json, converting...');
|
||||||
|
stateFromFile = convertLegacyAppsJson(stateFromFile);
|
||||||
|
}
|
||||||
|
const preloadState = stateFromFile as AppsJsonFormat;
|
||||||
|
|
||||||
|
let commitToPin: string | undefined;
|
||||||
|
let appToPin: string | undefined;
|
||||||
|
|
||||||
|
if (_.isEmpty(preloadState)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const images: Image[] = [];
|
||||||
|
const appIds = _.keys(preloadState.apps);
|
||||||
|
for (const appId of appIds) {
|
||||||
|
const app = preloadState.apps[appId];
|
||||||
|
// Multi-app warning!
|
||||||
|
// The following will need to be changed once running
|
||||||
|
// multiple applications is possible
|
||||||
|
commitToPin = app.commit;
|
||||||
|
appToPin = appId;
|
||||||
|
const serviceIds = _.keys(app.services);
|
||||||
|
for (const serviceId of serviceIds) {
|
||||||
|
const service = app.services[serviceId];
|
||||||
|
const svc = {
|
||||||
|
imageName: service.image,
|
||||||
|
serviceName: service.serviceName,
|
||||||
|
imageId: service.imageId,
|
||||||
|
serviceId,
|
||||||
|
releaseId: app.releaseId,
|
||||||
|
appId,
|
||||||
|
};
|
||||||
|
images.push(await deviceState.applications.imageForService(svc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
const name = await deviceState.applications.images.normalise(image.name);
|
||||||
|
image.name = name;
|
||||||
|
await deviceState.applications.images.save(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceConf = await deviceState.deviceConfig.getCurrent();
|
||||||
|
const formattedConf = await deviceState.deviceConfig.formatConfigKeys(
|
||||||
|
preloadState.config,
|
||||||
|
);
|
||||||
|
preloadState.config = { ...formattedConf, ...deviceConf };
|
||||||
|
const localState = { local: { name: '', ...preloadState } };
|
||||||
|
|
||||||
|
await deviceState.setTarget(localState);
|
||||||
|
|
||||||
|
log.success('Preloading complete');
|
||||||
|
if (stateFromFile.pinDevice) {
|
||||||
|
// Multi-app warning!
|
||||||
|
// The following will need to be changed once running
|
||||||
|
// multiple applications is possible
|
||||||
|
if (commitToPin != null && appToPin != null) {
|
||||||
|
log.debug('Device will be pinned');
|
||||||
|
await deviceState.config.set({
|
||||||
|
pinDevice: {
|
||||||
|
commit: commitToPin,
|
||||||
|
app: parseInt(appToPin, 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ensure that this is actually a file, and not an empty path
|
||||||
|
// It can be an empty path because if the file does not exist
|
||||||
|
// on host, the docker daemon creates an empty directory when
|
||||||
|
// the bind mount is added
|
||||||
|
if (ENOENT(e) || EISDIR(e)) {
|
||||||
|
log.debug('No apps.json file present, skipping preload');
|
||||||
|
} else {
|
||||||
|
deviceState.eventTracker.track('Loading preloaded apps failed', {
|
||||||
|
error: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -101,3 +101,5 @@ export class ContractViolationError extends TypedError {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AppsJsonParseError extends TypedError {}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { Application } from '../types/application';
|
import { AppsJsonFormat, TargetApplication } from '../types/state';
|
||||||
|
|
||||||
export const defaultLegacyVolume = () => 'resin-data';
|
export const defaultLegacyVolume = () => 'resin-data';
|
||||||
|
|
||||||
export function singleToMulticontainerApp(app: Dictionary<any>): Application {
|
export function singleToMulticontainerApp(
|
||||||
|
app: Dictionary<any>,
|
||||||
|
): TargetApplication & { appId: string } {
|
||||||
const environment: Dictionary<string> = {};
|
const environment: Dictionary<string> = {};
|
||||||
for (const key in app.env) {
|
for (const key in app.env) {
|
||||||
if (!/^RESIN_/.test(key)) {
|
if (!/^RESIN_/.test(key)) {
|
||||||
@ -14,15 +16,15 @@ export function singleToMulticontainerApp(app: Dictionary<any>): Application {
|
|||||||
|
|
||||||
const { appId } = app;
|
const { appId } = app;
|
||||||
const conf = app.config != null ? app.config : {};
|
const conf = app.config != null ? app.config : {};
|
||||||
const newApp = new Application();
|
const newApp: TargetApplication & { appId: string } = {
|
||||||
_.assign(newApp, {
|
appId: appId.toString(),
|
||||||
appId,
|
|
||||||
commit: app.commit,
|
commit: app.commit,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
releaseId: 1,
|
releaseId: 1,
|
||||||
networks: {},
|
networks: {},
|
||||||
volumes: {},
|
volumes: {},
|
||||||
});
|
services: {},
|
||||||
|
};
|
||||||
const defaultVolume = exports.defaultLegacyVolume();
|
const defaultVolume = exports.defaultLegacyVolume();
|
||||||
newApp.volumes[defaultVolume] = {};
|
newApp.volumes[defaultVolume] = {};
|
||||||
const updateStrategy =
|
const updateStrategy =
|
||||||
@ -67,3 +69,16 @@ export function singleToMulticontainerApp(app: Dictionary<any>): Application {
|
|||||||
};
|
};
|
||||||
return newApp;
|
return newApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertLegacyAppsJson(appsArray: any[]): AppsJsonFormat {
|
||||||
|
const deviceConfig = _.reduce(
|
||||||
|
appsArray,
|
||||||
|
(conf, app) => {
|
||||||
|
return _.merge({}, conf, app.config);
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const apps = _.keyBy(_.map(appsArray, singleToMulticontainerApp), 'appId');
|
||||||
|
return { apps, config: deviceConfig } as AppsJsonFormat;
|
||||||
|
}
|
||||||
|
@ -14,6 +14,8 @@ import { RPiConfigBackend } from '../src/config/backend';
|
|||||||
import DB from '../src/db';
|
import DB from '../src/db';
|
||||||
import DeviceState = require('../src/device-state');
|
import DeviceState = require('../src/device-state');
|
||||||
|
|
||||||
|
import { loadTargetFromFile } from '../src/device-state/preload';
|
||||||
|
|
||||||
import Service from '../src/compose/service';
|
import Service from '../src/compose/service';
|
||||||
|
|
||||||
const mockedInitialConfig = {
|
const mockedInitialConfig = {
|
||||||
@ -260,8 +262,9 @@ describe('deviceState', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deviceState.loadTargetFromFile(
|
await loadTargetFromFile(
|
||||||
process.env.ROOT_MOUNTPOINT + '/apps.json',
|
process.env.ROOT_MOUNTPOINT + '/apps.json',
|
||||||
|
deviceState,
|
||||||
);
|
);
|
||||||
const targetState = await deviceState.getTarget();
|
const targetState = await deviceState.getTarget();
|
||||||
|
|
||||||
@ -288,21 +291,22 @@ describe('deviceState', () => {
|
|||||||
stub(deviceState.deviceConfig, 'getCurrent').returns(
|
stub(deviceState.deviceConfig, 'getCurrent').returns(
|
||||||
Promise.resolve(mockedInitialConfig),
|
Promise.resolve(mockedInitialConfig),
|
||||||
);
|
);
|
||||||
deviceState
|
loadTargetFromFile(
|
||||||
.loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json')
|
process.env.ROOT_MOUNTPOINT + '/apps-pin.json',
|
||||||
.then(() => {
|
deviceState,
|
||||||
(deviceState as any).applications.images.save.restore();
|
).then(() => {
|
||||||
(deviceState as any).deviceConfig.getCurrent.restore();
|
(deviceState as any).applications.images.save.restore();
|
||||||
|
(deviceState as any).deviceConfig.getCurrent.restore();
|
||||||
|
|
||||||
config.get('pinDevice').then(pinned => {
|
config.get('pinDevice').then(pinned => {
|
||||||
expect(pinned)
|
expect(pinned)
|
||||||
.to.have.property('app')
|
.to.have.property('app')
|
||||||
.that.equals(1234);
|
.that.equals(1234);
|
||||||
expect(pinned)
|
expect(pinned)
|
||||||
.to.have.property('commit')
|
.to.have.property('commit')
|
||||||
.that.equals('abcdef');
|
.that.equals('abcdef');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits a change event when a new state is reported', () => {
|
it('emits a change event when a new state is reported', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user