mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-26 08:51:06 +00:00
e9a52e6786
This moves from throwing an error when an app is rejected due to unmet requirements (because of contracts) to storing the target with a `rejected` flag on the database. The application manager filters rejected apps when calculating steps to prevent them from affecting the current state. The state engine uses the rejection info to generate the state report. Change-type: minor
206 lines
4.6 KiB
TypeScript
206 lines
4.6 KiB
TypeScript
import { App } from '~/src/compose/app';
|
|
import * as imageManager from '~/src/compose/images';
|
|
import type { Image } from '~/src/compose/images';
|
|
import { Network } from '~/src/compose/network';
|
|
import { Service } from '~/src/compose/service';
|
|
import type { ServiceComposeConfig } from '~/src/compose/types/service';
|
|
import type { Volume } from '~/src/compose/volume';
|
|
import type {
|
|
CompositionStep,
|
|
CompositionStepAction,
|
|
} from '~/src/compose/composition-steps';
|
|
import type { InstancedAppState } from '~/src/compose/types';
|
|
|
|
export const DEFAULT_NETWORK = Network.fromComposeObject(
|
|
'default',
|
|
1,
|
|
'appuuid',
|
|
{},
|
|
);
|
|
|
|
export async function createService(
|
|
{
|
|
appId = 1,
|
|
appUuid = 'appuuid',
|
|
serviceName = 'main',
|
|
commit = 'main-commit',
|
|
...conf
|
|
} = {} as Partial<ServiceComposeConfig>,
|
|
{ state = {} as Partial<Service>, options = {} as any } = {},
|
|
) {
|
|
const svc = await Service.fromComposeObject(
|
|
{
|
|
appId,
|
|
appUuid,
|
|
serviceName,
|
|
commit,
|
|
// db ids should not be used for target state calculation, but images
|
|
// are compared using _.isEqual so leaving this here to have image comparisons
|
|
// match
|
|
serviceId: 1,
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
...conf,
|
|
},
|
|
options,
|
|
);
|
|
|
|
// Add additonal configuration
|
|
for (const k of Object.keys(state)) {
|
|
(svc as any)[k] = (state as any)[k];
|
|
}
|
|
return svc;
|
|
}
|
|
|
|
export function createImage(
|
|
{
|
|
appId = 1,
|
|
appUuid = 'appuuid',
|
|
name = 'test-image',
|
|
serviceName = 'main',
|
|
commit = 'main-commit',
|
|
...extra
|
|
} = {} as Partial<Image>,
|
|
) {
|
|
return {
|
|
appId,
|
|
appUuid,
|
|
name,
|
|
serviceName,
|
|
commit,
|
|
// db ids should not be used for target state calculation, but images
|
|
// are compared using _.isEqual so leaving this here to have image comparisons
|
|
// match
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
serviceId: 1,
|
|
...extra,
|
|
} as Image;
|
|
}
|
|
|
|
export function createApp({
|
|
services = [] as Service[],
|
|
networks = [] as Network[],
|
|
volumes = [] as Volume[],
|
|
isTarget = false,
|
|
appId = 1,
|
|
appUuid = 'appuuid',
|
|
isRejected = false,
|
|
} = {}) {
|
|
return new App(
|
|
{
|
|
appId,
|
|
appUuid,
|
|
services,
|
|
networks,
|
|
volumes,
|
|
isRejected,
|
|
},
|
|
isTarget,
|
|
);
|
|
}
|
|
|
|
export function createApps(
|
|
{
|
|
services = [] as Service[],
|
|
networks = [] as Network[],
|
|
volumes = [] as Volume[],
|
|
rejectedAppIds = [] as number[],
|
|
},
|
|
target = false,
|
|
) {
|
|
const servicesByAppId = services.reduce(
|
|
(svcs, s) => ({ ...svcs, [s.appId]: [s].concat(svcs[s.appId] || []) }),
|
|
{} as Dictionary<Service[]>,
|
|
);
|
|
const volumesByAppId = volumes.reduce(
|
|
(vols, v) => ({ ...vols, [v.appId]: [v].concat(vols[v.appId] || []) }),
|
|
{} as Dictionary<Volume[]>,
|
|
);
|
|
const networksByAppId = networks.reduce(
|
|
(nets, n) => ({ ...nets, [n.appId]: [n].concat(nets[n.appId] || []) }),
|
|
{} as Dictionary<Network[]>,
|
|
);
|
|
|
|
const allAppIds = [
|
|
...new Set([
|
|
...Object.keys(servicesByAppId),
|
|
...Object.keys(networksByAppId),
|
|
...Object.keys(volumesByAppId),
|
|
]),
|
|
].map((i) => parseInt(i, 10));
|
|
|
|
const apps: InstancedAppState = {};
|
|
for (const appId of allAppIds) {
|
|
const isRejected = rejectedAppIds.includes(appId);
|
|
apps[appId] = createApp({
|
|
services: servicesByAppId[appId] ?? [],
|
|
networks: networksByAppId[appId] ?? [],
|
|
volumes: volumesByAppId[appId] ?? [],
|
|
appId,
|
|
appUuid: servicesByAppId[appId]?.[0]?.appUuid ?? 'deadbeef',
|
|
isTarget: target,
|
|
isRejected,
|
|
});
|
|
}
|
|
|
|
return apps;
|
|
}
|
|
|
|
export function createCurrentState({
|
|
services = [] as Service[],
|
|
networks = [] as Network[],
|
|
volumes = [] as Volume[],
|
|
images = services.map((s) => ({
|
|
// Infer images from services by default
|
|
dockerImageId: s.dockerImageId,
|
|
...imageManager.imageFromService(s),
|
|
})) as Image[],
|
|
downloading = [] as string[],
|
|
}) {
|
|
const currentApps = createApps({ services, networks, volumes });
|
|
|
|
const containerIdsByAppId = services.reduce(
|
|
(ids, s) => ({
|
|
...ids,
|
|
[s.appId]: {
|
|
...ids[s.appId],
|
|
...(s.serviceName &&
|
|
s.containerId && { [s.serviceName]: s.containerId }),
|
|
},
|
|
}),
|
|
{} as { [appId: number]: Dictionary<string> },
|
|
);
|
|
|
|
return {
|
|
currentApps,
|
|
availableImages: images,
|
|
downloading,
|
|
containerIdsByAppId,
|
|
};
|
|
}
|
|
|
|
export const expectSteps = (
|
|
action: CompositionStepAction,
|
|
steps: CompositionStep[],
|
|
min = 1,
|
|
max = min,
|
|
message = `Expected to find ${min} step(s) with action '${action}', instead found ${JSON.stringify(
|
|
steps.map((s) => s.action),
|
|
)}`,
|
|
) => {
|
|
const filtered = steps.filter((s) => s.action === action);
|
|
|
|
if (filtered.length < min || filtered.length > max) {
|
|
throw new Error(message);
|
|
}
|
|
return filtered;
|
|
};
|
|
|
|
export function expectNoStep(
|
|
action: CompositionStepAction,
|
|
steps: CompositionStep[],
|
|
) {
|
|
expectSteps(action, steps, 0, 0);
|
|
}
|