Merge pull request #1372 from balena-io/db-format

Move database app processing out to its own module
This commit is contained in:
bulldozer-balena[bot] 2020-06-11 11:45:04 +00:00 committed by GitHub
commit 53658c1c13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 706 additions and 566 deletions

View File

@ -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>;

View File

@ -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) {

View File

@ -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);

View 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();
}

View File

@ -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)),
);
}

View File

@ -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');

View File

@ -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>;
};

View File

@ -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(

View File

@ -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
View 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;
}
});
});

View 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": {}
}
}

View File

@ -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: [] },
};