mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-20 17:52:51 +00:00
Merge pull request #1372 from balena-io/db-format
Move database app processing out to its own module
This commit is contained in:
commit
53658c1c13
4
src/application-manager.d.ts
vendored
4
src/application-manager.d.ts
vendored
@ -64,7 +64,7 @@ class ApplicationManager extends EventEmitter {
|
||||
|
||||
public init(): Promise<void>;
|
||||
|
||||
public getCurrentApp(appId: number): Bluebird<Application | null>;
|
||||
public getCurrentApp(appId: number): Promise<Application | null>;
|
||||
|
||||
// TODO: This actually returns an object, but we don't need the values just yet
|
||||
public setTargetVolatileForService(serviceId: number, opts: Options): void;
|
||||
@ -90,7 +90,7 @@ class ApplicationManager extends EventEmitter {
|
||||
public getTargetApps(): Promise<InstancedAppState>;
|
||||
public stopAll(opts: { force?: boolean; skipLock?: boolean }): Promise<void>;
|
||||
|
||||
public serviceNameFromId(serviceId: number): Bluebird<string>;
|
||||
public serviceNameFromId(serviceId: number): Promise<string>;
|
||||
public imageForService(svc: any): Image;
|
||||
public getDependentTargets(): Promise<any>;
|
||||
public getCurrentForComparison(): Promise<any>;
|
||||
|
@ -3,8 +3,6 @@ import * as _ from 'lodash';
|
||||
import * as EventEmitter from 'events';
|
||||
import * as express from 'express';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as constants from './lib/constants';
|
||||
import { log } from './lib/supervisor-console';
|
||||
@ -13,26 +11,21 @@ import * as logger from './logger';
|
||||
|
||||
import { validateTargetContracts } from './lib/contracts';
|
||||
import { docker } from './lib/docker-utils';
|
||||
import * as dockerUtils from './lib/docker-utils';
|
||||
import { LocalModeManager } from './local-mode';
|
||||
import * as updateLock from './lib/update-lock';
|
||||
import { checkTruthy, checkInt, checkString } from './lib/validation';
|
||||
import {
|
||||
ContractViolationError,
|
||||
NotFoundError,
|
||||
InternalInconsistencyError,
|
||||
} from './lib/errors';
|
||||
import { pathExistsOnHost } from './lib/fs-utils';
|
||||
|
||||
import { TargetStateAccessor } from './device-state/target-state-cache';
|
||||
import * as dbFormat from './device-state/db-format';
|
||||
|
||||
import { Network } from './compose/network';
|
||||
import { ServiceManager } from './compose/service-manager';
|
||||
import { Service } from './compose/service';
|
||||
import * as Images from './compose/images';
|
||||
import { NetworkManager } from './compose/network-manager';
|
||||
import { Network } from './compose/network';
|
||||
import { VolumeManager } from './compose/volume-manager';
|
||||
import { Volume } from './compose/volume';
|
||||
import * as compositionSteps from './compose/composition-steps';
|
||||
|
||||
import { Proxyvisor } from './proxyvisor';
|
||||
@ -43,9 +36,6 @@ import { serviceAction } from './device-api/common';
|
||||
|
||||
import * as db from './db';
|
||||
|
||||
/** @type {Function} */
|
||||
const readFileAsync = Promise.promisify(fs.readFile);
|
||||
|
||||
// TODO: move this to an Image class?
|
||||
const imageForService = (service) => ({
|
||||
name: service.imageName,
|
||||
@ -145,10 +135,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
this._nextStepsForNetwork = this._nextStepsForNetwork.bind(this);
|
||||
this._nextStepForService = this._nextStepForService.bind(this);
|
||||
this._nextStepsForAppUpdate = this._nextStepsForAppUpdate.bind(this);
|
||||
this.normaliseAppForDB = this.normaliseAppForDB.bind(this);
|
||||
this.normaliseAndExtendAppFromDB = this.normaliseAndExtendAppFromDB.bind(
|
||||
this,
|
||||
);
|
||||
this.setTargetVolatileForService = this.setTargetVolatileForService.bind(
|
||||
this,
|
||||
);
|
||||
@ -185,8 +171,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
this._targetVolatilePerImageId = {};
|
||||
this._containerStarted = {};
|
||||
|
||||
this.targetStateWrapper = new TargetStateAccessor(this);
|
||||
|
||||
this.actionExecutors = compositionSteps.getExecutors({
|
||||
lockFn: this._lockingIfNecessary,
|
||||
services: this.services,
|
||||
@ -225,34 +209,26 @@ export class ApplicationManager extends EventEmitter {
|
||||
return this.emit('change', data);
|
||||
}
|
||||
|
||||
init() {
|
||||
return Images.initialized
|
||||
.then(() => Images.cleanupDatabase())
|
||||
.then(() => {
|
||||
const cleanup = () => {
|
||||
return docker.listContainers({ all: true }).then((containers) => {
|
||||
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||
});
|
||||
};
|
||||
// Rather than relying on removing out of date database entries when we're no
|
||||
// longer using them, set a task that runs periodically to clear out the database
|
||||
// This has the advantage that if for some reason a container is removed while the
|
||||
// supervisor is down, we won't have zombie entries in the db
|
||||
|
||||
// Once a day
|
||||
setInterval(cleanup, 1000 * 60 * 60 * 24);
|
||||
// But also run it in on startup
|
||||
return cleanup();
|
||||
})
|
||||
.then(() => {
|
||||
return this.localModeManager.init();
|
||||
})
|
||||
.then(() => {
|
||||
return this.services.attachToRunning();
|
||||
})
|
||||
.then(() => {
|
||||
return this.services.listenToEvents();
|
||||
async init() {
|
||||
await Images.initialized;
|
||||
await Images.cleanupDatabase();
|
||||
const cleanup = () => {
|
||||
return docker.listContainers({ all: true }).then((containers) => {
|
||||
return logger.clearOutOfDateDBLogs(_.map(containers, 'Id'));
|
||||
});
|
||||
};
|
||||
// Rather than relying on removing out of date database entries when we're no
|
||||
// longer using them, set a task that runs periodically to clear out the database
|
||||
// This has the advantage that if for some reason a container is removed while the
|
||||
// supervisor is down, we won't have zombie entries in the db
|
||||
|
||||
// Once a day
|
||||
setInterval(cleanup, 1000 * 60 * 60 * 24);
|
||||
// But also run it in on startup
|
||||
await cleanup();
|
||||
await this.localModeManager.init();
|
||||
await this.services.attachToRunning();
|
||||
await this.services.listenToEvents();
|
||||
}
|
||||
|
||||
// Returns the status of applications and their services
|
||||
@ -409,12 +385,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
}
|
||||
|
||||
getTargetApp(appId) {
|
||||
return this.targetStateWrapper.getTargetApp(appId).then((app) => {
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
return this.normaliseAndExtendAppFromDB(app);
|
||||
});
|
||||
return dbFormat.getApp(appId);
|
||||
}
|
||||
|
||||
// Compares current and target services and returns a list of service pairs to be updated/removed/installed.
|
||||
@ -873,7 +844,7 @@ export class ApplicationManager extends EventEmitter {
|
||||
} else {
|
||||
// Create the default network for the target app
|
||||
if (targetApp.networks['default'] == null) {
|
||||
targetApp.networks['default'] = this.createTargetNetwork(
|
||||
targetApp.networks['default'] = Network.fromComposeObject(
|
||||
'default',
|
||||
targetApp.appId,
|
||||
{},
|
||||
@ -984,171 +955,24 @@ export class ApplicationManager extends EventEmitter {
|
||||
return _.map(steps, (step) => _.assign({}, step, { appId }));
|
||||
}
|
||||
|
||||
normaliseAppForDB(app) {
|
||||
const services = _.map(app.services, function (s, serviceId) {
|
||||
const service = _.clone(s);
|
||||
service.appId = app.appId;
|
||||
service.releaseId = app.releaseId;
|
||||
service.serviceId = checkInt(serviceId);
|
||||
service.commit = app.commit;
|
||||
return service;
|
||||
});
|
||||
return Promise.map(services, (service) => {
|
||||
service.image = Images.normalise(service.image);
|
||||
return Promise.props(service);
|
||||
}).then(function ($services) {
|
||||
const dbApp = {
|
||||
appId: app.appId,
|
||||
commit: app.commit,
|
||||
name: app.name,
|
||||
source: app.source,
|
||||
releaseId: app.releaseId,
|
||||
services: JSON.stringify($services),
|
||||
networks: JSON.stringify(app.networks ?? {}),
|
||||
volumes: JSON.stringify(app.volumes ?? {}),
|
||||
};
|
||||
return dbApp;
|
||||
});
|
||||
}
|
||||
async setTarget(apps, dependent, source, maybeTrx) {
|
||||
const setInTransaction = async (filtered, trx) => {
|
||||
await dbFormat.setApps(filtered, source, trx);
|
||||
await trx('app')
|
||||
.where({ source })
|
||||
.whereNotIn(
|
||||
'appId',
|
||||
// Use apps here, rather than filteredApps, to
|
||||
// avoid removing a release from the database
|
||||
// without an application to replace it.
|
||||
// Currently this will only happen if the release
|
||||
// which would replace it fails a contract
|
||||
// validation check
|
||||
_.map(apps, (_v, appId) => checkInt(appId)),
|
||||
)
|
||||
.del();
|
||||
|
||||
createTargetService(service, opts) {
|
||||
// The image class now returns a native promise, so wrap
|
||||
// this in a bluebird promise until we convert this to typescript
|
||||
return Promise.resolve(Images.inspectByName(service.image))
|
||||
.catchReturn(NotFoundError, undefined)
|
||||
.then(function (imageInfo) {
|
||||
const serviceOpts = {
|
||||
serviceName: service.serviceName,
|
||||
imageInfo,
|
||||
...opts,
|
||||
};
|
||||
service.imageName = service.image;
|
||||
if (imageInfo?.Id != null) {
|
||||
service.image = imageInfo.Id;
|
||||
}
|
||||
return Service.fromComposeObject(service, serviceOpts);
|
||||
});
|
||||
}
|
||||
|
||||
createTargetVolume(name, appId, volume) {
|
||||
return Volume.fromComposeObject(name, appId, volume);
|
||||
}
|
||||
|
||||
createTargetNetwork(name, appId, network) {
|
||||
return Network.fromComposeObject(name, appId, network);
|
||||
}
|
||||
|
||||
normaliseAndExtendAppFromDB(app) {
|
||||
return Promise.join(
|
||||
config.get('extendedEnvOptions'),
|
||||
dockerUtils
|
||||
.getNetworkGateway(constants.supervisorNetworkInterface)
|
||||
.catch(() => '127.0.0.1'),
|
||||
Promise.props({
|
||||
firmware: pathExistsOnHost('/lib/firmware'),
|
||||
modules: pathExistsOnHost('/lib/modules'),
|
||||
}),
|
||||
readFileAsync(
|
||||
path.join(constants.rootMountPoint, '/etc/hostname'),
|
||||
'utf8',
|
||||
).then(_.trim),
|
||||
(opts, supervisorApiHost, hostPathExists, hostnameOnHost) => {
|
||||
const configOpts = {
|
||||
appName: app.name,
|
||||
supervisorApiHost,
|
||||
hostPathExists,
|
||||
hostnameOnHost,
|
||||
};
|
||||
_.assign(configOpts, opts);
|
||||
|
||||
const volumes = _.mapValues(
|
||||
JSON.parse(app.volumes),
|
||||
(volumeConfig, volumeName) => {
|
||||
if (volumeConfig == null) {
|
||||
volumeConfig = {};
|
||||
}
|
||||
if (volumeConfig.labels == null) {
|
||||
volumeConfig.labels = {};
|
||||
}
|
||||
return this.createTargetVolume(volumeName, app.appId, volumeConfig);
|
||||
},
|
||||
);
|
||||
|
||||
const networks = _.mapValues(
|
||||
JSON.parse(app.networks),
|
||||
(networkConfig, networkName) => {
|
||||
if (networkConfig == null) {
|
||||
networkConfig = {};
|
||||
}
|
||||
return this.createTargetNetwork(
|
||||
networkName,
|
||||
app.appId,
|
||||
networkConfig,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Promise.map(JSON.parse(app.services), (service) =>
|
||||
this.createTargetService(service, configOpts),
|
||||
).then((services) => {
|
||||
// If a named volume is defined in a service but NOT in the volumes of the compose file, we add it app-wide so that we can track it and purge it
|
||||
// !! DEPRECATED, WILL BE REMOVED IN NEXT MAJOR RELEASE !!
|
||||
for (const s of services) {
|
||||
const serviceNamedVolumes = s.getNamedVolumes();
|
||||
for (const name of serviceNamedVolumes) {
|
||||
if (volumes[name] == null) {
|
||||
volumes[name] = this.createTargetVolume(name, app.appId, {
|
||||
labels: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const outApp = {
|
||||
appId: app.appId,
|
||||
name: app.name,
|
||||
commit: app.commit,
|
||||
releaseId: app.releaseId,
|
||||
services,
|
||||
networks,
|
||||
volumes,
|
||||
};
|
||||
return outApp;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setTarget(apps, dependent, source, maybeTrx) {
|
||||
const setInTransaction = (filtered, trx) => {
|
||||
return Promise.try(() => {
|
||||
const appsArray = _.map(filtered, function (app, appId) {
|
||||
const appClone = _.clone(app);
|
||||
appClone.appId = checkInt(appId);
|
||||
appClone.source = source;
|
||||
return appClone;
|
||||
});
|
||||
return Promise.map(appsArray, this.normaliseAppForDB)
|
||||
.then((appsForDB) => {
|
||||
return this.targetStateWrapper.setTargetApps(appsForDB, trx);
|
||||
})
|
||||
.then(() =>
|
||||
trx('app')
|
||||
.where({ source })
|
||||
.whereNotIn(
|
||||
'appId',
|
||||
// Use apps here, rather than filteredApps, to
|
||||
// avoid removing a release from the database
|
||||
// without an application to replace it.
|
||||
// Currently this will only happen if the release
|
||||
// which would replace it fails a contract
|
||||
// validation check
|
||||
_.map(apps, (_v, appId) => checkInt(appId)),
|
||||
)
|
||||
.del(),
|
||||
);
|
||||
}).then(() => {
|
||||
return this.proxyvisor.setTargetInTransaction(dependent, trx);
|
||||
});
|
||||
await this.proxyvisor.setTargetInTransaction(dependent, trx);
|
||||
};
|
||||
|
||||
// We look at the container contracts here, as if we
|
||||
@ -1193,15 +1017,14 @@ export class ApplicationManager extends EventEmitter {
|
||||
} else {
|
||||
promise = db.transaction(setInTransaction);
|
||||
}
|
||||
return promise
|
||||
.then(() => {
|
||||
this._targetVolatilePerImageId = {};
|
||||
})
|
||||
.finally(function () {
|
||||
if (!_.isEmpty(contractViolators)) {
|
||||
throw new ContractViolationError(contractViolators);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await promise;
|
||||
this._targetVolatilePerImageId = {};
|
||||
} finally {
|
||||
if (!_.isEmpty(contractViolators)) {
|
||||
throw new ContractViolationError(contractViolators);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTargetVolatileForService(imageId, target) {
|
||||
@ -1217,23 +1040,24 @@ export class ApplicationManager extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
getTargetApps() {
|
||||
return Promise.map(
|
||||
this.targetStateWrapper.getTargetApps(),
|
||||
this.normaliseAndExtendAppFromDB,
|
||||
)
|
||||
.map((app) => {
|
||||
if (!_.isEmpty(app.services)) {
|
||||
app.services = _.map(app.services, (service) => {
|
||||
if (this._targetVolatilePerImageId[service.imageId] != null) {
|
||||
_.merge(service, this._targetVolatilePerImageId[service.imageId]);
|
||||
}
|
||||
return service;
|
||||
});
|
||||
}
|
||||
return app;
|
||||
})
|
||||
.then((apps) => _.keyBy(apps, 'appId'));
|
||||
async getTargetApps() {
|
||||
const apps = await dbFormat.getApps();
|
||||
|
||||
_.each(apps, (app) => {
|
||||
if (!_.isEmpty(app.services)) {
|
||||
app.services = _.mapValues(app.services, (svc) => {
|
||||
if (this._targetVolatilePerImageId[svc.imageId] != null) {
|
||||
return {
|
||||
...svc,
|
||||
...this._targetVolatilePerImageId[svc.imageId],
|
||||
};
|
||||
}
|
||||
return svc;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
getDependentTargets() {
|
||||
@ -1308,7 +1132,6 @@ export class ApplicationManager extends EventEmitter {
|
||||
Images.isSameImage(availableImage, targetImage),
|
||||
),
|
||||
);
|
||||
|
||||
// Images that are available but we don't have them in the DB with the exact metadata:
|
||||
let imagesToSave = [];
|
||||
if (!localMode) {
|
||||
|
@ -107,8 +107,7 @@ export class Service {
|
||||
delete appConfig.releaseId;
|
||||
service.serviceId = intOrNull(appConfig.serviceId);
|
||||
delete appConfig.serviceId;
|
||||
service.imageName = appConfig.imageName;
|
||||
delete appConfig.imageName;
|
||||
service.imageName = appConfig.image;
|
||||
service.dependsOn = appConfig.dependsOn || null;
|
||||
delete appConfig.dependsOn;
|
||||
service.createdAt = appConfig.createdAt;
|
||||
@ -415,6 +414,12 @@ export class Service {
|
||||
running: true,
|
||||
});
|
||||
|
||||
// If we have the docker image ID, we replace the image
|
||||
// with that
|
||||
if (options.imageInfo?.Id != null) {
|
||||
config.image = options.imageInfo.Id;
|
||||
}
|
||||
|
||||
// Mutate service with extra features
|
||||
ComposeUtils.addFeaturesFromLabels(service, options);
|
||||
|
||||
|
185
src/device-state/db-format.ts
Normal file
185
src/device-state/db-format.ts
Normal file
@ -0,0 +1,185 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as _ from 'lodash';
|
||||
import type { ImageInspectInfo } from 'dockerode';
|
||||
|
||||
import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
import * as targetStateCache from '../device-state/target-state-cache';
|
||||
import constants = require('../lib/constants');
|
||||
import { pathExistsOnHost } from '../lib/fs-utils';
|
||||
import * as dockerUtils from '../lib/docker-utils';
|
||||
import { NotFoundError } from '../lib/errors';
|
||||
|
||||
import Service from '../compose/service';
|
||||
import Network from '../compose/network';
|
||||
import Volume from '../compose/volume';
|
||||
import type {
|
||||
DeviceMetadata,
|
||||
ServiceComposeConfig,
|
||||
} from '../compose/types/service';
|
||||
import * as images from '../compose/images';
|
||||
|
||||
import { InstancedAppState, TargetApplication } from '../types/state';
|
||||
import { checkInt } from '../lib/validation';
|
||||
|
||||
type InstancedApp = InstancedAppState[0];
|
||||
|
||||
// Fetch and instance an app from the db. Throws if the requested appId cannot be found.
|
||||
// Currently this function does quite a bit more than it needs to as it pulls in a bunch
|
||||
// of required information for the instances but we should think about a method of not
|
||||
// requiring that data here
|
||||
export async function getApp(id: number): Promise<InstancedApp> {
|
||||
const dbApp = await getDBEntry(id);
|
||||
return await buildApp(dbApp);
|
||||
}
|
||||
|
||||
export async function getApps(): Promise<InstancedAppState> {
|
||||
const dbApps = await getDBEntry();
|
||||
const apps: InstancedAppState = {};
|
||||
for (const app of dbApps) {
|
||||
apps[app.appId] = await buildApp(app);
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
async function buildApp(dbApp: targetStateCache.DatabaseApp) {
|
||||
const volumes = _.mapValues(JSON.parse(dbApp.volumes) ?? {}, (conf, name) => {
|
||||
if (conf == null) {
|
||||
conf = {};
|
||||
}
|
||||
if (conf.labels == null) {
|
||||
conf.labels = {};
|
||||
}
|
||||
return Volume.fromComposeObject(name, dbApp.appId, conf);
|
||||
});
|
||||
|
||||
const networks = _.mapValues(
|
||||
JSON.parse(dbApp.networks) ?? {},
|
||||
(conf, name) => {
|
||||
if (conf == null) {
|
||||
conf = {};
|
||||
}
|
||||
return Network.fromComposeObject(name, dbApp.appId, conf);
|
||||
},
|
||||
);
|
||||
|
||||
const opts = await config.get('extendedEnvOptions');
|
||||
const supervisorApiHost = dockerUtils
|
||||
.getNetworkGateway(constants.supervisorNetworkInterface)
|
||||
.catch(() => '127.0.0.1');
|
||||
const hostPathExists = {
|
||||
firmware: await pathExistsOnHost('/lib/firmware'),
|
||||
modules: await pathExistsOnHost('/lib/modules'),
|
||||
};
|
||||
const hostnameOnHost = _.trim(
|
||||
await fs.readFile(
|
||||
path.join(constants.rootMountPoint, '/etc/hostname'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
const svcOpts = {
|
||||
appName: dbApp.name,
|
||||
supervisorApiHost,
|
||||
hostPathExists,
|
||||
hostnameOnHost,
|
||||
...opts,
|
||||
};
|
||||
|
||||
// In the db, the services are an array, but here we switch them to an
|
||||
// object so that they are consistent
|
||||
const services = _.keyBy(
|
||||
await Promise.all(
|
||||
(JSON.parse(dbApp.services) ?? []).map(
|
||||
async (svc: ServiceComposeConfig) => {
|
||||
// Try to fill the image id if the image is downloaded
|
||||
let imageInfo: ImageInspectInfo | undefined;
|
||||
try {
|
||||
imageInfo = await images.inspectByName(svc.image);
|
||||
} catch (e) {
|
||||
if (NotFoundError(e)) {
|
||||
imageInfo = undefined;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const thisSvcOpts = {
|
||||
...svcOpts,
|
||||
imageInfo,
|
||||
serviceName: svc.serviceName,
|
||||
};
|
||||
// We force the casting here as we know that the UUID exists, but the typings do
|
||||
// not
|
||||
return Service.fromComposeObject(
|
||||
svc,
|
||||
(thisSvcOpts as unknown) as DeviceMetadata,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
'serviceId',
|
||||
) as Dictionary<Service>;
|
||||
|
||||
return {
|
||||
appId: dbApp.appId,
|
||||
commit: dbApp.commit,
|
||||
releaseId: dbApp.releaseId,
|
||||
name: dbApp.name,
|
||||
source: dbApp.source,
|
||||
|
||||
services,
|
||||
volumes,
|
||||
networks,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setApps(
|
||||
apps: { [appId: number]: TargetApplication },
|
||||
source: string,
|
||||
trx?: db.Transaction,
|
||||
) {
|
||||
const cloned = _.cloneDeep(apps);
|
||||
|
||||
const dbApps = await Promise.all(
|
||||
Object.keys(cloned).map(async (appIdStr) => {
|
||||
const appId = checkInt(appIdStr)!;
|
||||
|
||||
const app = cloned[appId];
|
||||
const services = await Promise.all(
|
||||
_.map(app.services, async (s, sId) => ({
|
||||
...s,
|
||||
appId,
|
||||
releaseId: app.releaseId,
|
||||
serviceId: checkInt(sId),
|
||||
commit: app.commit,
|
||||
image: await images.normalise(s.image),
|
||||
})),
|
||||
);
|
||||
|
||||
return {
|
||||
appId,
|
||||
source,
|
||||
commit: app.commit,
|
||||
name: app.name,
|
||||
releaseId: app.releaseId,
|
||||
services: JSON.stringify(services),
|
||||
networks: JSON.stringify(app.networks ?? {}),
|
||||
volumes: JSON.stringify(app.volumes ?? {}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
await targetStateCache.setTargetApps(dbApps, trx);
|
||||
}
|
||||
|
||||
function getDBEntry(): Promise<targetStateCache.DatabaseApp[]>;
|
||||
function getDBEntry(appId: number): Promise<targetStateCache.DatabaseApp>;
|
||||
async function getDBEntry(appId?: number) {
|
||||
await config.initialized;
|
||||
await targetStateCache.initialized;
|
||||
|
||||
return appId != null
|
||||
? targetStateCache.getTargetApp(appId)
|
||||
: targetStateCache.getTargetApps();
|
||||
}
|
@ -1,74 +1,80 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ApplicationManager } from '../application-manager';
|
||||
import * as config from '../config';
|
||||
import * as db from '../db';
|
||||
|
||||
// Once we have correct types for both applications and the
|
||||
// incoming target state this should be changed
|
||||
export type DatabaseApp = Dictionary<any>;
|
||||
// We omit the id (which does appear in the db) in this type, as we don't use it
|
||||
// at all, and we can use the below type for both insertion and retrieval.
|
||||
export interface DatabaseApp {
|
||||
name: string;
|
||||
releaseId: number;
|
||||
commit: string;
|
||||
appId: number;
|
||||
services: string;
|
||||
networks: string;
|
||||
volumes: string;
|
||||
source: string;
|
||||
}
|
||||
export type DatabaseApps = DatabaseApp[];
|
||||
|
||||
/*
|
||||
* This class is a wrapper around the database setting and
|
||||
* receiving of target state. Because the target state can
|
||||
* only be set from a single place, but several workflows
|
||||
* rely on getting the target state at one point or another,
|
||||
* we cache the values using this class. Accessing the
|
||||
* database is inherently expensive, and for example the
|
||||
* local log backend accesses the target state for every log
|
||||
* line. This can very quickly cause serious memory problems
|
||||
* and database connection timeouts.
|
||||
* This module is a wrapper around the database fetching and retrieving of
|
||||
* target state. Because the target state can only be set only be set from a
|
||||
* single place, but several workflows rely on getting the target state at one
|
||||
* point or another, we cache the values using this class. Accessing the
|
||||
* database is inherently expensive, and for example the local log backend
|
||||
* accesses the target state for every log line. This can very quickly cause
|
||||
* serious memory problems and database connection timeouts.
|
||||
*/
|
||||
export class TargetStateAccessor {
|
||||
private targetState?: DatabaseApps;
|
||||
let targetState: DatabaseApps | undefined;
|
||||
|
||||
public constructor(protected applications: ApplicationManager) {
|
||||
// If we switch backend, the target state also needs to
|
||||
// be invalidated (this includes switching to and from
|
||||
// local mode)
|
||||
config.on('change', (conf) => {
|
||||
if (conf.apiEndpoint != null || conf.localMode != null) {
|
||||
this.targetState = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getTargetApp(appId: number): Promise<DatabaseApp | undefined> {
|
||||
if (this.targetState == null) {
|
||||
// TODO: Perhaps only fetch a single application from
|
||||
// the DB, at the expense of repeating code
|
||||
await this.getTargetApps();
|
||||
export const initialized = (async () => {
|
||||
await db.initialized;
|
||||
await config.initialized;
|
||||
// If we switch backend, the target state also needs to
|
||||
// be invalidated (this includes switching to and from
|
||||
// local mode)
|
||||
config.on('change', (conf) => {
|
||||
if (conf.apiEndpoint != null || conf.localMode != null) {
|
||||
targetState = undefined;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
return _.find(this.targetState, (app) => app.appId === appId);
|
||||
export async function getTargetApp(
|
||||
appId: number,
|
||||
): Promise<DatabaseApp | undefined> {
|
||||
if (targetState == null) {
|
||||
// TODO: Perhaps only fetch a single application from
|
||||
// the DB, at the expense of repeating code
|
||||
await getTargetApps();
|
||||
}
|
||||
|
||||
public async getTargetApps(): Promise<DatabaseApps> {
|
||||
if (this.targetState == null) {
|
||||
const { apiEndpoint, localMode } = await config.getMany([
|
||||
'apiEndpoint',
|
||||
'localMode',
|
||||
]);
|
||||
|
||||
const source = localMode ? 'local' : apiEndpoint;
|
||||
this.targetState = await db.models('app').where({ source });
|
||||
}
|
||||
return this.targetState!;
|
||||
}
|
||||
|
||||
public async setTargetApps(
|
||||
apps: DatabaseApps,
|
||||
trx: db.Transaction,
|
||||
): Promise<void> {
|
||||
// We can't cache the value here, as it could be for a
|
||||
// different source
|
||||
this.targetState = undefined;
|
||||
|
||||
await Promise.all(
|
||||
apps.map((app) => db.upsertModel('app', app, { appId: app.appId }, trx)),
|
||||
);
|
||||
}
|
||||
return _.find(targetState, (app) => app.appId === appId);
|
||||
}
|
||||
|
||||
export default TargetStateAccessor;
|
||||
export async function getTargetApps(): Promise<DatabaseApps> {
|
||||
if (targetState == null) {
|
||||
const { apiEndpoint, localMode } = await config.getMany([
|
||||
'apiEndpoint',
|
||||
'localMode',
|
||||
]);
|
||||
|
||||
const source = localMode ? 'local' : apiEndpoint;
|
||||
targetState = await db.models('app').where({ source });
|
||||
}
|
||||
return targetState!;
|
||||
}
|
||||
|
||||
export async function setTargetApps(
|
||||
apps: DatabaseApps,
|
||||
trx?: db.Transaction,
|
||||
): Promise<void> {
|
||||
// We can't cache the value here, as it could be for a
|
||||
// different source
|
||||
targetState = undefined;
|
||||
|
||||
await Promise.all(
|
||||
apps.map((app) => db.upsertModel('app', app, { appId: app.appId }, trx)),
|
||||
);
|
||||
}
|
||||
|
@ -18,12 +18,12 @@ import { BackupError, DatabaseParseError, NotFoundError } from '../lib/errors';
|
||||
import { docker } from '../lib/docker-utils';
|
||||
import { pathExistsOnHost } from '../lib/fs-utils';
|
||||
import { log } from '../lib/supervisor-console';
|
||||
import {
|
||||
ApplicationDatabaseFormat,
|
||||
import type {
|
||||
AppsJsonFormat,
|
||||
TargetApplication,
|
||||
TargetState,
|
||||
} from '../types/state';
|
||||
import type { DatabaseApp } from '../device-state/target-state-cache';
|
||||
|
||||
export const defaultLegacyVolume = () => 'resin-data';
|
||||
|
||||
@ -113,7 +113,7 @@ export async function normaliseLegacyDatabase(
|
||||
// When legacy apps are present, we kill their containers and migrate their /data to a named volume
|
||||
log.info('Migrating ids for legacy app...');
|
||||
|
||||
const apps: ApplicationDatabaseFormat = await db.models('app').select();
|
||||
const apps: DatabaseApp[] = await db.models('app').select();
|
||||
|
||||
if (apps.length === 0) {
|
||||
log.debug('No app to migrate');
|
||||
|
@ -96,17 +96,6 @@ export type AppsJsonFormat = Omit<TargetState['local'], 'name'> & {
|
||||
pinDevice?: boolean;
|
||||
};
|
||||
|
||||
export type ApplicationDatabaseFormat = Array<{
|
||||
appId: number;
|
||||
commit: string;
|
||||
name: string;
|
||||
source: string;
|
||||
releaseId: number;
|
||||
services: string;
|
||||
networks: string;
|
||||
volumes: string;
|
||||
}>;
|
||||
|
||||
// This structure is the internal representation of both
|
||||
// target and current state. We create instances of compose
|
||||
// objects and these are what the state engine uses to
|
||||
@ -117,7 +106,8 @@ export interface InstancedAppState {
|
||||
commit: string;
|
||||
releaseId: number;
|
||||
name: string;
|
||||
services: Service[];
|
||||
source: string;
|
||||
services: Dictionary<Service>;
|
||||
volumes: Dictionary<Volume>;
|
||||
networks: Dictionary<Network>;
|
||||
};
|
||||
|
@ -62,8 +62,8 @@ const testTarget1 = {
|
||||
name: 'superapp',
|
||||
commit: 'abcdef',
|
||||
releaseId: 1,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
imageId: 12345,
|
||||
@ -74,7 +74,7 @@ const testTarget1 = {
|
||||
'io.resin.something': 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
@ -172,15 +172,15 @@ const testTargetInvalid = {
|
||||
config: {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: '1234',
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: '2',
|
||||
config: {},
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
serviceId: '23',
|
||||
serviceName: 'aservice',
|
||||
imageId: '12345',
|
||||
@ -191,7 +191,7 @@ const testTargetInvalid = {
|
||||
},
|
||||
labels: {},
|
||||
},
|
||||
{
|
||||
24: {
|
||||
serviceId: '24',
|
||||
serviceName: 'anotherService',
|
||||
imageId: '12346',
|
||||
@ -202,19 +202,22 @@ const testTargetInvalid = {
|
||||
},
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
|
||||
describe('deviceState', () => {
|
||||
let deviceState: DeviceState;
|
||||
let source: string;
|
||||
const originalImagesSave = images.save;
|
||||
const originalImagesInspect = images.inspectByName;
|
||||
before(async () => {
|
||||
await prepare();
|
||||
await config.initialized;
|
||||
source = await config.get('apiEndpoint');
|
||||
|
||||
stub(Service as any, 'extendEnvVars').callsFake((env) => {
|
||||
env['ADDITIONAL_ENV_VAR'] = 'foo';
|
||||
@ -257,6 +260,10 @@ describe('deviceState', () => {
|
||||
images.inspectByName = originalImagesInspect;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await prepare();
|
||||
});
|
||||
|
||||
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
|
||||
stub(deviceState.deviceConfig, 'getCurrent').returns(
|
||||
Promise.resolve(mockedInitialConfig),
|
||||
@ -270,13 +277,15 @@ describe('deviceState', () => {
|
||||
const targetState = await deviceState.getTarget();
|
||||
|
||||
const testTarget = _.cloneDeep(testTarget1);
|
||||
testTarget.local.apps['1234'].services = _.map(
|
||||
testTarget.local.apps['1234'].services = _.mapValues(
|
||||
testTarget.local.apps['1234'].services,
|
||||
(s: any) => {
|
||||
s.imageName = s.image;
|
||||
return Service.fromComposeObject(s, { appName: 'superapp' } as any);
|
||||
},
|
||||
) as any;
|
||||
// @ts-ignore
|
||||
testTarget.local.apps['1234'].source = source;
|
||||
|
||||
expect(JSON.parse(JSON.stringify(targetState))).to.deep.equal(
|
||||
JSON.parse(JSON.stringify(testTarget)),
|
||||
@ -321,7 +330,11 @@ describe('deviceState', () => {
|
||||
);
|
||||
}
|
||||
|
||||
(testTarget as any).local.apps['1234'].services = services;
|
||||
(testTarget as any).local.apps['1234'].services = _.keyBy(
|
||||
services,
|
||||
'serviceId',
|
||||
);
|
||||
(testTarget as any).local.apps['1234'].source = source;
|
||||
await deviceState.setTarget(testTarget2);
|
||||
const target = await deviceState.getTarget();
|
||||
expect(JSON.parse(JSON.stringify(target))).to.deep.equal(
|
||||
|
@ -12,6 +12,11 @@ import * as images from '../src/compose/images';
|
||||
|
||||
import chai = require('./lib/chai-config');
|
||||
import prepare = require('./lib/prepare');
|
||||
import * as db from '../src/db';
|
||||
import * as dbFormat from '../src/device-state/db-format';
|
||||
import * as targetStateCache from '../src/device-state/target-state-cache';
|
||||
import * as config from '../src/config';
|
||||
import { TargetApplication, TargetApplications } from '../src/types/state';
|
||||
|
||||
// tslint:disable-next-line
|
||||
chai.use(require('chai-events'));
|
||||
@ -20,71 +25,6 @@ const { expect } = chai;
|
||||
let availableImages: any[] | null;
|
||||
let targetState: any[] | null;
|
||||
|
||||
const appDBFormatNormalised = {
|
||||
appId: 1234,
|
||||
commit: 'bar',
|
||||
releaseId: 2,
|
||||
name: 'app',
|
||||
source: 'https://api.resin.io',
|
||||
services: JSON.stringify([
|
||||
{
|
||||
appId: 1234,
|
||||
serviceName: 'serv',
|
||||
imageId: 12345,
|
||||
environment: { FOO: 'var2' },
|
||||
labels: {},
|
||||
image: 'foo/bar:latest',
|
||||
releaseId: 2,
|
||||
serviceId: 4,
|
||||
commit: 'bar',
|
||||
},
|
||||
]),
|
||||
networks: '{}',
|
||||
volumes: '{}',
|
||||
};
|
||||
|
||||
const appStateFormat = {
|
||||
appId: 1234,
|
||||
commit: 'bar',
|
||||
releaseId: 2,
|
||||
name: 'app',
|
||||
// This technically is not part of the appStateFormat, but in general
|
||||
// usage is added before calling normaliseAppForDB
|
||||
source: 'https://api.resin.io',
|
||||
services: {
|
||||
'4': {
|
||||
appId: 1234,
|
||||
serviceName: 'serv',
|
||||
imageId: 12345,
|
||||
environment: { FOO: 'var2' },
|
||||
labels: {},
|
||||
image: 'foo/bar:latest',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const appStateFormatNeedsServiceCreate = {
|
||||
appId: 1234,
|
||||
commit: 'bar',
|
||||
releaseId: 2,
|
||||
name: 'app',
|
||||
services: [
|
||||
{
|
||||
appId: 1234,
|
||||
environment: {
|
||||
FOO: 'var2',
|
||||
},
|
||||
imageId: 12345,
|
||||
serviceId: 4,
|
||||
releaseId: 2,
|
||||
serviceName: 'serv',
|
||||
image: 'foo/bar:latest',
|
||||
},
|
||||
],
|
||||
networks: {},
|
||||
volumes: {},
|
||||
};
|
||||
|
||||
const dependentStateFormat = {
|
||||
appId: 1234,
|
||||
image: 'foo/bar',
|
||||
@ -157,76 +97,71 @@ describe('ApplicationManager', function () {
|
||||
env['ADDITIONAL_ENV_VAR'] = 'foo';
|
||||
return env;
|
||||
});
|
||||
this.normaliseCurrent = function (current: {
|
||||
local: { apps: Iterable<unknown> | PromiseLike<Iterable<unknown>> };
|
||||
|
||||
this.normaliseCurrent = async function (current: {
|
||||
local: { apps: Dictionary<TargetApplication> };
|
||||
}) {
|
||||
return Bluebird.Promise.map(current.local.apps, async (app: any) => {
|
||||
return Bluebird.Promise.map(app.services, (service) =>
|
||||
Service.fromComposeObject(service as any, { appName: 'test' } as any),
|
||||
).then((normalisedServices) => {
|
||||
const appCloned = _.cloneDeep(app);
|
||||
appCloned.services = normalisedServices;
|
||||
appCloned.networks = _.mapValues(
|
||||
appCloned.networks,
|
||||
(config, name) => {
|
||||
return Network.fromComposeObject(name, app.appId, config);
|
||||
},
|
||||
);
|
||||
return appCloned;
|
||||
});
|
||||
}).then(function (normalisedApps) {
|
||||
const currentCloned = _.cloneDeep(current);
|
||||
// @ts-ignore
|
||||
currentCloned.local.apps = _.keyBy(normalisedApps, 'appId');
|
||||
return currentCloned;
|
||||
const currentCloned: any = _.cloneDeep(current);
|
||||
currentCloned.local.apps = {};
|
||||
|
||||
_.each(current.local.apps, (app, appId) => {
|
||||
const appCloned = {
|
||||
...app,
|
||||
services: _.mapValues(app.services, (svc) =>
|
||||
// @ts-ignore
|
||||
Service.fromComposeObject(svc, { appName: 'test' }),
|
||||
),
|
||||
networks: _.mapValues(app.networks, (conf, name) =>
|
||||
Network.fromComposeObject(name, parseInt(appId, 10), conf),
|
||||
),
|
||||
volumes: _.mapValues(app.volumes, (conf, name) =>
|
||||
Volume.fromComposeObject(name, parseInt(appId, 10), conf),
|
||||
),
|
||||
};
|
||||
currentCloned.local.apps[parseInt(appId, 10)] = appCloned;
|
||||
});
|
||||
return currentCloned;
|
||||
};
|
||||
|
||||
this.normaliseTarget = (
|
||||
this.normaliseTarget = async (
|
||||
target: {
|
||||
local: { apps: Iterable<unknown> | PromiseLike<Iterable<unknown>> };
|
||||
local: { apps: TargetApplications };
|
||||
},
|
||||
available: any,
|
||||
) => {
|
||||
return Bluebird.Promise.map(target.local.apps, (app) => {
|
||||
return this.applications
|
||||
.normaliseAppForDB(app)
|
||||
.then((normalisedApp: any) => {
|
||||
return this.applications.normaliseAndExtendAppFromDB(normalisedApp);
|
||||
});
|
||||
}).then(function (apps) {
|
||||
const targetCloned = _.cloneDeep(target);
|
||||
// We mock what createTargetService does when an image is available
|
||||
targetCloned.local.apps = _.map(apps, function (app) {
|
||||
app.services = _.map(app.services, function (service) {
|
||||
const img = _.find(
|
||||
available,
|
||||
(i) => i.name === service.config.image,
|
||||
);
|
||||
if (img != null) {
|
||||
service.config.image = img.dockerImageId;
|
||||
}
|
||||
return service;
|
||||
});
|
||||
return app;
|
||||
const source = await config.get('apiEndpoint');
|
||||
const cloned: any = _.cloneDeep(target);
|
||||
|
||||
// @ts-ignore types don't quite match up
|
||||
await dbFormat.setApps(target.local.apps, source);
|
||||
|
||||
cloned.local.apps = await dbFormat.getApps();
|
||||
|
||||
// We mock what createTargetService does when an image is available
|
||||
_.each(cloned.local.apps, (app) => {
|
||||
_.each(app.services, (svc) => {
|
||||
const img = _.find(available, (i) => i.name === svc.config.image);
|
||||
if (img != null) {
|
||||
svc.config.image = img.dockerImageId;
|
||||
}
|
||||
});
|
||||
// @ts-ignore
|
||||
targetCloned.local.apps = _.keyBy(targetCloned.local.apps, 'appId');
|
||||
return targetCloned;
|
||||
});
|
||||
return cloned;
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(
|
||||
() =>
|
||||
({
|
||||
currentState,
|
||||
targetState,
|
||||
availableImages,
|
||||
} = require('./lib/application-manager-test-states')),
|
||||
);
|
||||
beforeEach(async () => {
|
||||
({
|
||||
currentState,
|
||||
targetState,
|
||||
availableImages,
|
||||
} = require('./lib/application-manager-test-states'));
|
||||
await db.models('app').del();
|
||||
// @ts-expect-error modification of a RO property
|
||||
targetStateCache.targetState = undefined;
|
||||
});
|
||||
|
||||
after(function () {
|
||||
after(async function () {
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
images.inspectByName = originalInspectByName;
|
||||
// @ts-expect-error restore on non-stubbed type
|
||||
@ -235,7 +170,12 @@ describe('ApplicationManager', function () {
|
||||
dockerUtils.docker.listContainers.restore();
|
||||
// @ts-expect-error restore on non-stubbed type
|
||||
dockerUtils.docker.listImages.restore();
|
||||
return (Service as any).extendEnvVars.restore();
|
||||
// @ts-expect-error use of private function
|
||||
Service.extendEnvVars.restore();
|
||||
|
||||
await db.models('app').del();
|
||||
// @ts-expect-error modification of a RO property
|
||||
targetStateCache.targetState = undefined;
|
||||
});
|
||||
|
||||
it('should init', function () {
|
||||
@ -264,8 +204,8 @@ describe('ApplicationManager', function () {
|
||||
return expect(steps).to.eventually.deep.equal([
|
||||
{
|
||||
action: 'start',
|
||||
current: current.local.apps['1234'].services[1],
|
||||
target: target.local.apps['1234'].services[1],
|
||||
current: current.local.apps['1234'].services[24],
|
||||
target: target.local.apps['1234'].services[24],
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -297,7 +237,7 @@ describe('ApplicationManager', function () {
|
||||
return expect(steps).to.eventually.deep.equal([
|
||||
{
|
||||
action: 'kill',
|
||||
current: current.local.apps['1234'].services[1],
|
||||
current: current.local.apps['1234'].services[24],
|
||||
target: undefined,
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
@ -331,7 +271,7 @@ describe('ApplicationManager', function () {
|
||||
{
|
||||
action: 'fetch',
|
||||
image: this.applications.imageForService(
|
||||
target.local.apps['1234'].services[1],
|
||||
target.local.apps['1234'].services[24],
|
||||
),
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
@ -353,7 +293,7 @@ describe('ApplicationManager', function () {
|
||||
false,
|
||||
// @ts-ignore
|
||||
availableImages[0],
|
||||
[target.local.apps['1234'].services[1].imageId],
|
||||
[target.local.apps['1234'].services[24].imageId],
|
||||
true,
|
||||
current,
|
||||
target,
|
||||
@ -390,8 +330,8 @@ describe('ApplicationManager', function () {
|
||||
return expect(steps).to.eventually.deep.equal([
|
||||
{
|
||||
action: 'kill',
|
||||
current: current.local.apps['1234'].services[1],
|
||||
target: target.local.apps['1234'].services[1],
|
||||
current: current.local.apps['1234'].services[24],
|
||||
target: target.local.apps['1234'].services[24],
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -424,7 +364,7 @@ describe('ApplicationManager', function () {
|
||||
{
|
||||
action: 'fetch',
|
||||
image: this.applications.imageForService(
|
||||
target.local.apps['1234'].services[0],
|
||||
target.local.apps['1234'].services[23],
|
||||
),
|
||||
serviceId: 23,
|
||||
appId: 1234,
|
||||
@ -458,16 +398,16 @@ describe('ApplicationManager', function () {
|
||||
return expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'kill',
|
||||
current: current.local.apps['1234'].services[0],
|
||||
target: target.local.apps['1234'].services[0],
|
||||
current: current.local.apps['1234'].services[23],
|
||||
target: target.local.apps['1234'].services[23],
|
||||
serviceId: 23,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
},
|
||||
{
|
||||
action: 'kill',
|
||||
current: current.local.apps['1234'].services[1],
|
||||
target: target.local.apps['1234'].services[1],
|
||||
current: current.local.apps['1234'].services[24],
|
||||
target: target.local.apps['1234'].services[24],
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -500,7 +440,7 @@ describe('ApplicationManager', function () {
|
||||
{
|
||||
action: 'start',
|
||||
current: null,
|
||||
target: target.local.apps['1234'].services[0],
|
||||
target: target.local.apps['1234'].services[23],
|
||||
serviceId: 23,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -534,7 +474,7 @@ describe('ApplicationManager', function () {
|
||||
{
|
||||
action: 'start',
|
||||
current: null,
|
||||
target: target.local.apps['1234'].services[1],
|
||||
target: target.local.apps['1234'].services[24],
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -566,7 +506,7 @@ describe('ApplicationManager', function () {
|
||||
return expect(steps).to.eventually.have.deep.members([
|
||||
{
|
||||
action: 'kill',
|
||||
current: current.local.apps['1234'].services[0],
|
||||
current: current.local.apps['1234'].services[23],
|
||||
target: undefined,
|
||||
serviceId: 23,
|
||||
appId: 1234,
|
||||
@ -575,7 +515,7 @@ describe('ApplicationManager', function () {
|
||||
{
|
||||
action: 'start',
|
||||
current: null,
|
||||
target: target.local.apps['1234'].services[1],
|
||||
target: target.local.apps['1234'].services[24],
|
||||
serviceId: 24,
|
||||
appId: 1234,
|
||||
options: {},
|
||||
@ -585,11 +525,6 @@ describe('ApplicationManager', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('converts an app from a state format to a db format, adding missing networks and volumes and normalising the image name', function () {
|
||||
const app = this.applications.normaliseAppForDB(appStateFormat);
|
||||
return expect(app).to.eventually.deep.equal(appDBFormatNormalised);
|
||||
});
|
||||
|
||||
it('converts a dependent app from a state format to a db format, normalising the image name', function () {
|
||||
const app = this.applications.proxyvisor.normaliseDependentAppForDB(
|
||||
dependentStateFormat,
|
||||
@ -597,32 +532,6 @@ describe('ApplicationManager', function () {
|
||||
return expect(app).to.eventually.deep.equal(dependentDBFormat);
|
||||
});
|
||||
|
||||
it('converts an app in DB format into state format, adding default and missing fields', function () {
|
||||
return this.applications
|
||||
.normaliseAndExtendAppFromDB(appDBFormatNormalised)
|
||||
.then(function (app: any) {
|
||||
const appStateFormatWithDefaults = _.cloneDeep(
|
||||
appStateFormatNeedsServiceCreate,
|
||||
);
|
||||
const opts = {
|
||||
imageInfo: {
|
||||
Config: { Cmd: ['someCommand'], Entrypoint: ['theEntrypoint'] },
|
||||
},
|
||||
};
|
||||
(appStateFormatWithDefaults.services as any) = _.map(
|
||||
appStateFormatWithDefaults.services,
|
||||
function (service) {
|
||||
// @ts-ignore
|
||||
service.imageName = service.image;
|
||||
return Service.fromComposeObject(service, opts as any);
|
||||
},
|
||||
);
|
||||
return expect(JSON.parse(JSON.stringify(app))).to.deep.equal(
|
||||
JSON.parse(JSON.stringify(appStateFormatWithDefaults)),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a dependent app in DB format into state format', function () {
|
||||
const app = this.applications.proxyvisor.normaliseDependentAppFromDB(
|
||||
dependentDBFormat,
|
||||
@ -678,7 +587,7 @@ describe('ApplicationManager', function () {
|
||||
);
|
||||
});
|
||||
|
||||
return it('should remove volumes from previous applications', function () {
|
||||
it('should remove volumes from previous applications', function () {
|
||||
return Bluebird.Promise.join(
|
||||
// @ts-ignore
|
||||
this.normaliseCurrent(currentState[5]),
|
||||
|
153
test/28-db-format.spec.ts
Normal file
153
test/28-db-format.spec.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { expect } from 'chai';
|
||||
import prepare = require('./lib/prepare');
|
||||
|
||||
import * as config from '../src/config';
|
||||
import * as dbFormat from '../src/device-state/db-format';
|
||||
import * as targetStateCache from '../src/device-state/target-state-cache';
|
||||
import * as images from '../src/compose/images';
|
||||
|
||||
import Service from '../src/compose/service';
|
||||
import { TargetApplication } from '../src/types/state';
|
||||
|
||||
describe('DB Format', () => {
|
||||
const originalInspect = images.inspectByName;
|
||||
let apiEndpoint: string;
|
||||
before(async () => {
|
||||
await prepare();
|
||||
await config.initialized;
|
||||
await targetStateCache.initialized;
|
||||
|
||||
apiEndpoint = await config.get('apiEndpoint');
|
||||
|
||||
// Setup some mocks
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
images.inspectByName = () => {
|
||||
const error = new Error();
|
||||
// @ts-ignore
|
||||
error.statusCode = 404;
|
||||
return Promise.reject(error);
|
||||
};
|
||||
await targetStateCache.setTargetApps([
|
||||
{
|
||||
appId: 1,
|
||||
commit: 'abcdef',
|
||||
name: 'test-app',
|
||||
source: apiEndpoint,
|
||||
releaseId: 123,
|
||||
services: '[]',
|
||||
networks: '{}',
|
||||
volumes: '{}',
|
||||
},
|
||||
{
|
||||
appId: 2,
|
||||
commit: 'abcdef2',
|
||||
name: 'test-app2',
|
||||
source: apiEndpoint,
|
||||
releaseId: 1232,
|
||||
services: JSON.stringify([
|
||||
{
|
||||
serviceName: 'test-service',
|
||||
image: 'test-image',
|
||||
imageId: 5,
|
||||
environment: {
|
||||
TEST_VAR: 'test-string',
|
||||
},
|
||||
tty: true,
|
||||
appId: 2,
|
||||
releaseId: 1232,
|
||||
serviceId: 567,
|
||||
commit: 'abcdef2',
|
||||
},
|
||||
]),
|
||||
networks: '{}',
|
||||
volumes: '{}',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await prepare();
|
||||
|
||||
// @ts-expect-error Assigning to a RO property
|
||||
images.inspectByName = originalInspect;
|
||||
});
|
||||
|
||||
it('should retrieve a single app from the database', async () => {
|
||||
const app = await dbFormat.getApp(1);
|
||||
expect(app).to.have.property('appId').that.equals(1);
|
||||
expect(app).to.have.property('commit').that.equals('abcdef');
|
||||
expect(app).to.have.property('releaseId').that.equals(123);
|
||||
expect(app).to.have.property('name').that.equals('test-app');
|
||||
expect(app)
|
||||
.to.have.property('source')
|
||||
.that.deep.equals(await config.get('apiEndpoint'));
|
||||
expect(app).to.have.property('services').that.deep.equals({});
|
||||
expect(app).to.have.property('volumes').that.deep.equals({});
|
||||
expect(app).to.have.property('networks').that.deep.equals({});
|
||||
});
|
||||
|
||||
it('should correctly build services from the database', async () => {
|
||||
const app = await dbFormat.getApp(2);
|
||||
expect(app).to.have.property('services').that.is.an('object');
|
||||
expect(Object.keys(app.services)).to.deep.equal(['567']);
|
||||
|
||||
const service = app.services['567'];
|
||||
expect(service).to.be.instanceof(Service);
|
||||
// Don't do a deep equals here as a bunch of other properties are added that are
|
||||
// tested elsewhere
|
||||
expect(service.config)
|
||||
.to.have.property('environment')
|
||||
.that.has.property('TEST_VAR')
|
||||
.that.equals('test-string');
|
||||
expect(service.config).to.have.property('tty').that.equals(true);
|
||||
expect(service).to.have.property('imageName').that.equals('test-image');
|
||||
expect(service).to.have.property('imageId').that.equals(5);
|
||||
});
|
||||
|
||||
it('should retrieve multiple apps from the database', async () => {
|
||||
const apps = await dbFormat.getApps();
|
||||
expect(Object.keys(apps)).to.have.length(2).and.deep.equal(['1', '2']);
|
||||
});
|
||||
|
||||
it('should write target states to the database', async () => {
|
||||
const target = await import('./data/state-endpoints/simple.json');
|
||||
const dbApps: { [appId: number]: TargetApplication } = {};
|
||||
dbApps[1234] = {
|
||||
...target.local.apps[1234],
|
||||
};
|
||||
|
||||
await dbFormat.setApps(dbApps, apiEndpoint);
|
||||
|
||||
const app = await dbFormat.getApp(1234);
|
||||
|
||||
expect(app).to.have.property('name').that.equals('pi4test');
|
||||
expect(app).to.have.property('services').that.is.an('object');
|
||||
expect(app.services)
|
||||
.to.have.property('482141')
|
||||
.that.has.property('serviceName')
|
||||
.that.equals('main');
|
||||
});
|
||||
|
||||
it('should add default and missing fields when retreiving from the database', async () => {
|
||||
const originalImagesInspect = images.inspectByName;
|
||||
try {
|
||||
// @ts-expect-error Assigning a RO property
|
||||
images.inspectByName = () =>
|
||||
Promise.resolve({
|
||||
Config: { Cmd: ['someCommand'], Entrypoint: ['theEntrypoint'] },
|
||||
});
|
||||
|
||||
const app = await dbFormat.getApp(2);
|
||||
const conf = app.services[Object.keys(app.services)[0]].config;
|
||||
expect(conf)
|
||||
.to.have.property('entrypoint')
|
||||
.that.deep.equals(['theEntrypoint']);
|
||||
expect(conf)
|
||||
.to.have.property('command')
|
||||
.that.deep.equals(['someCommand']);
|
||||
} finally {
|
||||
// @ts-expect-error Assigning a RO property
|
||||
images.inspectByName = originalImagesInspect;
|
||||
}
|
||||
});
|
||||
});
|
56
test/data/state-endpoints/simple.json
Normal file
56
test/data/state-endpoints/simple.json
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
{
|
||||
"local": {
|
||||
"name": "lingering-frost",
|
||||
"config": {
|
||||
"RESIN_SUPERVISOR_DELTA_VERSION": "3",
|
||||
"RESIN_SUPERVISOR_NATIVE_LOGGER": "true",
|
||||
"RESIN_HOST_CONFIG_arm_64bit": "1",
|
||||
"RESIN_HOST_CONFIG_disable_splash": "1",
|
||||
"RESIN_HOST_CONFIG_dtoverlay": "\"vc4-fkms-v3d\"",
|
||||
"RESIN_HOST_CONFIG_dtparam": "\"i2c_arm=on\",\"spi=on\",\"audio=on\"",
|
||||
"RESIN_HOST_CONFIG_enable_uart": "1",
|
||||
"RESIN_HOST_CONFIG_gpu_mem": "16",
|
||||
"RESIN_SUPERVISOR_DELTA": "1",
|
||||
"RESIN_SUPERVISOR_POLL_INTERVAL": "900000"
|
||||
},
|
||||
"apps": {
|
||||
"1234": {
|
||||
"name": "pi4test",
|
||||
"commit": "d0b7b1d5353c4a1d9d411614caf827f5",
|
||||
"releaseId": 1405939,
|
||||
"services": {
|
||||
"482141": {
|
||||
"privileged": true,
|
||||
"tty": true,
|
||||
"restart": "always",
|
||||
"network_mode": "host",
|
||||
"volumes": [
|
||||
"resin-data:/data"
|
||||
],
|
||||
"labels": {
|
||||
"io.resin.features.dbus": "1",
|
||||
"io.resin.features.firmware": "1",
|
||||
"io.resin.features.kernel-modules": "1",
|
||||
"io.resin.features.resin-api": "1",
|
||||
"io.resin.features.supervisor-api": "1"
|
||||
},
|
||||
"imageId": 2339002,
|
||||
"serviceName": "main",
|
||||
"image": "registry2.balena-cloud.com/v2/f5aff5560e1fb6740a868bfe2e8a4684@sha256:9cd1d09aad181b98067dac95e08f121c3af16426f078c013a485c41a63dc035c",
|
||||
"running": true,
|
||||
"environment": {}
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
"resin-data": {}
|
||||
},
|
||||
"networks": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependent": {
|
||||
"apps": {},
|
||||
"devices": {}
|
||||
}
|
||||
}
|
@ -13,8 +13,8 @@ targetState[0] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -52,7 +52,7 @@ targetState[0] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -64,8 +64,8 @@ targetState[1] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -90,7 +90,7 @@ targetState[1] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -102,8 +102,8 @@ targetState[2] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -144,7 +144,7 @@ targetState[2] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -156,8 +156,8 @@ targetState[3] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -199,7 +199,7 @@ targetState[3] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -211,8 +211,8 @@ targetState[4] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -253,7 +253,7 @@ targetState[4] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -265,8 +265,8 @@ targetState[5] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
@ -306,7 +306,7 @@ targetState[5] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -315,8 +315,8 @@ targetState[6] = {
|
||||
local: {
|
||||
name: 'volumeTest',
|
||||
config: {},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
12345: {
|
||||
appId: 12345,
|
||||
name: 'volumeApp',
|
||||
commit: 'asd',
|
||||
@ -325,7 +325,7 @@ targetState[6] = {
|
||||
volumes: {},
|
||||
networks: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -338,14 +338,14 @@ currentState[0] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
releaseId: 2,
|
||||
@ -377,7 +377,7 @@ currentState[0] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
{
|
||||
24: {
|
||||
appId: 1234,
|
||||
serviceId: 24,
|
||||
releaseId: 2,
|
||||
@ -409,11 +409,11 @@ currentState[0] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -425,17 +425,17 @@ currentState[1] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [],
|
||||
services: {},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -447,14 +447,14 @@ currentState[2] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
releaseId: 2,
|
||||
@ -488,11 +488,11 @@ currentState[2] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -504,14 +504,14 @@ currentState[3] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
serviceName: 'aservice',
|
||||
@ -545,7 +545,7 @@ currentState[3] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
{
|
||||
24: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
serviceName: 'aservice',
|
||||
@ -579,11 +579,11 @@ currentState[3] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -595,14 +595,14 @@ currentState[4] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
24: {
|
||||
appId: 1234,
|
||||
serviceId: 24,
|
||||
releaseId: 2,
|
||||
@ -634,11 +634,11 @@ currentState[4] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -647,28 +647,28 @@ currentState[5] = {
|
||||
local: {
|
||||
name: 'volumeTest',
|
||||
config: {},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
12345: {
|
||||
appId: 12345,
|
||||
name: 'volumeApp',
|
||||
commit: 'asd',
|
||||
releaseId: 3,
|
||||
services: [],
|
||||
services: {},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
{
|
||||
12: {
|
||||
appId: 12,
|
||||
name: 'previous-app',
|
||||
commit: '123',
|
||||
releaseId: 10,
|
||||
services: [],
|
||||
services: {},
|
||||
networks: {},
|
||||
volumes: {
|
||||
my_volume: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
@ -680,14 +680,14 @@ currentState[6] = {
|
||||
RESIN_HOST_CONFIG_gpu_mem: '512',
|
||||
RESIN_HOST_LOG_TO_DISPLAY: '1',
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
apps: {
|
||||
1234: {
|
||||
appId: 1234,
|
||||
name: 'superapp',
|
||||
commit: 'afafafa',
|
||||
releaseId: 2,
|
||||
services: [
|
||||
{
|
||||
services: {
|
||||
23: {
|
||||
appId: 1234,
|
||||
serviceId: 23,
|
||||
releaseId: 2,
|
||||
@ -719,7 +719,7 @@ currentState[6] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
{
|
||||
24: {
|
||||
appId: 1234,
|
||||
serviceId: 24,
|
||||
releaseId: 2,
|
||||
@ -751,11 +751,11 @@ currentState[6] = {
|
||||
command: ['someCommand'],
|
||||
entrypoint: ['theEntrypoint'],
|
||||
},
|
||||
],
|
||||
},
|
||||
volumes: {},
|
||||
networks: { default: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
dependent: { apps: [], devices: [] },
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user