mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-20 17:52:51 +00:00
Add takeLock to state funnel
A takeLock step should be generated before any of the following steps: * kill * start * stop * updateMetadata * restart * handover ALL services in an app will be locked for any of the above actions, unless the action is generated through Supervisor API's `POST /v2/applications/:appId/(start|stop|restart)-service` endpoints, in which case only the target service will be locked. A lock will be taken for a service before it starts by creating the directory in /tmp before the Engine creates it through bind mounts. Also, the commit simplifies the generation of service kill steps from network/volume changes or removals. Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
parent
cf8d8cedd7
commit
10f294cf8e
@ -53,6 +53,10 @@ interface ChangingPair<T> {
|
||||
target?: T;
|
||||
}
|
||||
|
||||
export interface AppsToLockMap {
|
||||
[appId: number]: Set<string>;
|
||||
}
|
||||
|
||||
export class App {
|
||||
public appId: number;
|
||||
public appUuid?: string;
|
||||
@ -132,39 +136,60 @@ export class App {
|
||||
true,
|
||||
);
|
||||
|
||||
const { removePairs, installPairs, updatePairs } = this.compareServices(
|
||||
this.services,
|
||||
target.services,
|
||||
state.containerIds,
|
||||
);
|
||||
const { removePairs, installPairs, updatePairs, dependentServices } =
|
||||
this.compareServices(
|
||||
this.services,
|
||||
target.services,
|
||||
state.containerIds,
|
||||
networkChanges,
|
||||
volumeChanges,
|
||||
);
|
||||
|
||||
// For every service which needs to be updated, update via update strategy.
|
||||
const servicePairs = removePairs.concat(updatePairs, installPairs);
|
||||
steps = steps.concat(
|
||||
servicePairs
|
||||
.map((pair) =>
|
||||
this.generateStepsForService(pair, {
|
||||
...state,
|
||||
servicePairs,
|
||||
targetApp: target,
|
||||
networkPairs: networkChanges,
|
||||
volumePairs: volumeChanges,
|
||||
// generateStepsForService will populate appsToLock with services that
|
||||
// need to be locked, including services that need to be removed due to
|
||||
// network or volume changes.
|
||||
const appsToLock: AppsToLockMap = {
|
||||
// this.appId should always equal target.appId.
|
||||
[target.appId]: new Set<string>(),
|
||||
};
|
||||
const serviceSteps = servicePairs
|
||||
.flatMap((pair) =>
|
||||
this.generateStepsForService(pair, {
|
||||
...state,
|
||||
servicePairs,
|
||||
targetApp: target,
|
||||
networkPairs: networkChanges,
|
||||
volumePairs: volumeChanges,
|
||||
appsToLock,
|
||||
}),
|
||||
)
|
||||
.filter((step) => step != null);
|
||||
|
||||
// Generate lock steps from appsToLock
|
||||
for (const [appId, services] of Object.entries(appsToLock)) {
|
||||
if (services.size > 0) {
|
||||
steps.push(
|
||||
generateStep('takeLock', {
|
||||
appId: parseInt(appId, 10),
|
||||
services: Array.from(services),
|
||||
force: state.force,
|
||||
}),
|
||||
)
|
||||
.filter((step) => step != null) as CompositionStep[],
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach service steps
|
||||
steps = steps.concat(serviceSteps);
|
||||
|
||||
// Generate volume steps
|
||||
steps = steps.concat(
|
||||
this.generateStepsForComponent(volumeChanges, servicePairs, (v, svc) =>
|
||||
svc.hasVolume(v.name),
|
||||
),
|
||||
this.generateStepsForComponent(volumeChanges, dependentServices),
|
||||
);
|
||||
// Generate network steps
|
||||
steps = steps.concat(
|
||||
this.generateStepsForComponent(networkChanges, servicePairs, (n, svc) =>
|
||||
svc.hasNetwork(n.name),
|
||||
),
|
||||
this.generateStepsForComponent(networkChanges, dependentServices),
|
||||
);
|
||||
|
||||
if (steps.length === 0) {
|
||||
@ -176,14 +201,17 @@ export class App {
|
||||
appId: this.appId,
|
||||
}),
|
||||
);
|
||||
} else if (
|
||||
target.services.length > 0 &&
|
||||
target.services.some(({ appId, serviceName }) =>
|
||||
state.locksTaken.isLocked(appId, serviceName),
|
||||
}
|
||||
// Current & target should be the same appId, but one of either current
|
||||
// or target may not have any services, so we need to check both
|
||||
const allServices = this.services.concat(target.services);
|
||||
if (
|
||||
allServices.length > 0 &&
|
||||
allServices.some((s) =>
|
||||
state.locksTaken.isLocked(s.appId, s.serviceName),
|
||||
)
|
||||
) {
|
||||
// Release locks for current services before settling state.
|
||||
// Current services should be the same as target services at this point.
|
||||
// Release locks for all services before settling state
|
||||
steps.push(
|
||||
generateStep('releaseLock', {
|
||||
appId: target.appId,
|
||||
@ -198,6 +226,21 @@ export class App {
|
||||
state: Omit<UpdateState, 'availableImages'> & { keepVolumes: boolean },
|
||||
): CompositionStep[] {
|
||||
if (Object.keys(this.services).length > 0) {
|
||||
// Take all locks before killing
|
||||
if (
|
||||
this.services.some(
|
||||
(svc) => !state.locksTaken.isLocked(svc.appId, svc.serviceName),
|
||||
)
|
||||
) {
|
||||
return [
|
||||
generateStep('takeLock', {
|
||||
appId: this.appId,
|
||||
services: this.services.map((svc) => svc.serviceName),
|
||||
force: state.force,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
return Object.values(this.services).map((service) =>
|
||||
generateStep('kill', { current: service }),
|
||||
);
|
||||
@ -302,14 +345,24 @@ export class App {
|
||||
return outputs;
|
||||
}
|
||||
|
||||
private getDependentServices<T extends Volume | Network>(
|
||||
component: T,
|
||||
dependencyFn: (component: T, service: Service) => boolean,
|
||||
) {
|
||||
return this.services.filter((s) => dependencyFn(component, s));
|
||||
}
|
||||
|
||||
private compareServices(
|
||||
current: Service[],
|
||||
target: Service[],
|
||||
containerIds: UpdateState['containerIds'],
|
||||
networkChanges: Array<ChangingPair<Network>>,
|
||||
volumeChanges: Array<ChangingPair<Volume>>,
|
||||
): {
|
||||
installPairs: Array<ChangingPair<Service>>;
|
||||
removePairs: Array<ChangingPair<Service>>;
|
||||
updatePairs: Array<ChangingPair<Service>>;
|
||||
dependentServices: Service[];
|
||||
} {
|
||||
const currentByServiceName = _.keyBy(current, 'serviceName');
|
||||
const targetByServiceName = _.keyBy(target, 'serviceName');
|
||||
@ -317,8 +370,26 @@ export class App {
|
||||
const currentServiceNames = Object.keys(currentByServiceName);
|
||||
const targetServiceNames = Object.keys(targetByServiceName);
|
||||
|
||||
// For volume|network removal or config changes, we require dependent
|
||||
// services be killed first.
|
||||
const dependentServices: Service[] = [];
|
||||
for (const { current: c } of networkChanges) {
|
||||
if (c != null) {
|
||||
dependentServices.push(
|
||||
...this.getDependentServices(c, (n, svc) => svc.hasNetwork(n.name)),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const { current: c } of volumeChanges) {
|
||||
if (c != null) {
|
||||
dependentServices.push(
|
||||
...this.getDependentServices(c, (v, svc) => svc.hasVolume(v.name)),
|
||||
);
|
||||
}
|
||||
}
|
||||
const toBeRemoved = _(currentServiceNames)
|
||||
.difference(targetServiceNames)
|
||||
.union(dependentServices.map((s) => s.serviceName))
|
||||
.map((id) => ({ current: currentByServiceName[id] }))
|
||||
.value();
|
||||
|
||||
@ -426,6 +497,15 @@ export class App {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a service is destined for removal due to a network or volume change
|
||||
*/
|
||||
const shouldBeRemoved = (serviceCurrent: Service) => {
|
||||
return toBeRemoved.some(
|
||||
(pair) => pair.current.serviceName === serviceCurrent.serviceName,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter all the services which should be updated due to run state change, or config mismatch.
|
||||
*/
|
||||
@ -436,16 +516,18 @@ export class App {
|
||||
}))
|
||||
.filter(
|
||||
({ current: c, target: t }) =>
|
||||
!isEqualExceptForRunningState(c, t) ||
|
||||
shouldBeStarted(c, t) ||
|
||||
shouldBeStopped(c, t) ||
|
||||
shouldWaitForStop(c),
|
||||
!shouldBeRemoved(c) &&
|
||||
(!isEqualExceptForRunningState(c, t) ||
|
||||
shouldBeStarted(c, t) ||
|
||||
shouldBeStopped(c, t) ||
|
||||
shouldWaitForStop(c)),
|
||||
);
|
||||
|
||||
return {
|
||||
installPairs: toBeInstalled,
|
||||
removePairs: toBeRemoved,
|
||||
updatePairs: toBeUpdated,
|
||||
dependentServices,
|
||||
};
|
||||
}
|
||||
|
||||
@ -457,8 +539,7 @@ export class App {
|
||||
// it should be changed.
|
||||
private generateStepsForComponent<T extends Volume | Network>(
|
||||
components: Array<ChangingPair<T>>,
|
||||
changingServices: Array<ChangingPair<Service>>,
|
||||
dependencyFn: (component: T, service: Service) => boolean,
|
||||
dependentServices: Service[],
|
||||
): CompositionStep[] {
|
||||
if (components.length === 0) {
|
||||
return [];
|
||||
@ -466,36 +547,42 @@ export class App {
|
||||
|
||||
let steps: CompositionStep[] = [];
|
||||
|
||||
const componentIsVolume =
|
||||
(components[0].current ?? components[0].target) instanceof Volume;
|
||||
|
||||
const actions: {
|
||||
create: CompositionStepAction;
|
||||
remove: CompositionStepAction;
|
||||
} =
|
||||
(components[0].current ?? components[0].target) instanceof Volume
|
||||
? { create: 'createVolume', remove: 'removeVolume' }
|
||||
: { create: 'createNetwork', remove: 'removeNetwork' };
|
||||
} = componentIsVolume
|
||||
? { create: 'createVolume', remove: 'removeVolume' }
|
||||
: { create: 'createNetwork', remove: 'removeNetwork' };
|
||||
|
||||
for (const { current, target } of components) {
|
||||
// If a current exists, we're either removing it or updating the configuration. In
|
||||
// both cases, we must remove the component first, so we output those steps first.
|
||||
// both cases, we must remove the component before creating it to avoid
|
||||
// Engine conflicts. So we always emit a remove step first.
|
||||
// If we do remove the component, we first need to remove any services which depend
|
||||
// on the component
|
||||
// on the component. The service removal steps are generated in this.generateStepsForService
|
||||
// after their removal is calculated in this.compareServices.
|
||||
if (current != null) {
|
||||
// Find any services which are currently running which need to be killed when we
|
||||
// recreate this component
|
||||
const dependencies = _.filter(this.services, (s) =>
|
||||
dependencyFn(current, s),
|
||||
);
|
||||
if (dependencies.length > 0) {
|
||||
// We emit kill steps for these services, and wait to destroy the component in
|
||||
// the next state application loop
|
||||
// FIXME: We should add to the changingServices array, as we could emit several
|
||||
// kill steps for a service
|
||||
steps = steps.concat(
|
||||
dependencies.flatMap((svc) =>
|
||||
this.generateKillStep(svc, changingServices),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// If there are any dependent services which have the volume or network,
|
||||
// we cannot proceed to component removal.
|
||||
const dependentServicesOfComponent = dependentServices.filter((s) => {
|
||||
if (componentIsVolume) {
|
||||
return this.serviceHasNetworkOrVolume(
|
||||
s,
|
||||
[],
|
||||
[{ current: current as Volume, target: target as Volume }],
|
||||
);
|
||||
} else {
|
||||
return this.serviceHasNetworkOrVolume(
|
||||
s,
|
||||
[{ current: current as Network, target: target as Network }],
|
||||
[],
|
||||
);
|
||||
}
|
||||
});
|
||||
if (dependentServicesOfComponent.length === 0) {
|
||||
steps = steps.concat([generateStep(actions.remove, { current })]);
|
||||
}
|
||||
} else if (target != null) {
|
||||
@ -513,17 +600,21 @@ export class App {
|
||||
networkPairs: Array<ChangingPair<Network>>;
|
||||
volumePairs: Array<ChangingPair<Volume>>;
|
||||
servicePairs: Array<ChangingPair<Service>>;
|
||||
appsToLock: AppsToLockMap;
|
||||
} & UpdateState,
|
||||
): Nullable<CompositionStep> {
|
||||
): CompositionStep[] {
|
||||
const servicesLocked = this.services
|
||||
.concat(context.targetApp.services)
|
||||
.every((svc) => context.locksTaken.isLocked(svc.appId, svc.serviceName));
|
||||
if (current?.status === 'Stopping') {
|
||||
// There's a kill step happening already, emit a noop to ensure
|
||||
// we stay alive while this happens
|
||||
return generateStep('noop', {});
|
||||
return [generateStep('noop', {})];
|
||||
}
|
||||
if (current?.status === 'Dead') {
|
||||
// A remove step will already have been generated, so we let the state
|
||||
// application loop revisit this service, once the state has settled
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
const needsDownload =
|
||||
@ -540,7 +631,7 @@ export class App {
|
||||
) {
|
||||
// The image needs to be downloaded, and it's currently downloading.
|
||||
// We simply keep the application loop alive
|
||||
return generateStep('noop', {});
|
||||
return [generateStep('noop', {})];
|
||||
}
|
||||
|
||||
if (current == null) {
|
||||
@ -549,6 +640,8 @@ export class App {
|
||||
target!,
|
||||
context.targetApp,
|
||||
needsDownload,
|
||||
servicesLocked,
|
||||
context.appsToLock,
|
||||
context.availableImages,
|
||||
context.networkPairs,
|
||||
context.volumePairs,
|
||||
@ -573,8 +666,9 @@ export class App {
|
||||
return this.generateContainerStep(
|
||||
current,
|
||||
target,
|
||||
context.locksTaken,
|
||||
context.force,
|
||||
context.appsToLock,
|
||||
context.targetApp.services,
|
||||
servicesLocked,
|
||||
);
|
||||
}
|
||||
|
||||
@ -607,29 +701,13 @@ export class App {
|
||||
dependenciesMetForStart,
|
||||
dependenciesMetForKill,
|
||||
needsSpecialKill,
|
||||
servicesLocked,
|
||||
services: this.services.concat(context.targetApp.services),
|
||||
appsToLock: context.appsToLock,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We return an array from this function so the caller can just concatenate the arrays
|
||||
// without worrying if the step is skipped or not
|
||||
private generateKillStep(
|
||||
service: Service,
|
||||
changingServices: Array<ChangingPair<Service>>,
|
||||
): CompositionStep[] {
|
||||
if (
|
||||
service.status !== 'Stopping' &&
|
||||
!_.some(
|
||||
changingServices,
|
||||
({ current }) => current?.serviceName === service.serviceName,
|
||||
)
|
||||
) {
|
||||
return [generateStep('kill', { current: service })];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private serviceHasNetworkOrVolume(
|
||||
svc: Service,
|
||||
networkPairs: Array<ChangingPair<Network>>,
|
||||
@ -667,31 +745,36 @@ export class App {
|
||||
private generateContainerStep(
|
||||
current: Service,
|
||||
target: Service,
|
||||
locksTaken: LocksTakenMap,
|
||||
force: boolean,
|
||||
) {
|
||||
appsToLock: AppsToLockMap,
|
||||
targetServices: Service[],
|
||||
servicesLocked: boolean,
|
||||
): CompositionStep[] {
|
||||
// Update container metadata if service release has changed
|
||||
if (current.commit !== target.commit) {
|
||||
// QUESTION: Should updateMetadata only be allowed when
|
||||
// *all* services have locks taken by the Supervisor? Currently
|
||||
// it proceeds when the service it's updating has locks taken,
|
||||
// meaning the service could be on new release while another service
|
||||
// with a user-taken lock is still on old release.
|
||||
if (locksTaken.isLocked(target.appId, target.serviceName)) {
|
||||
return generateStep('updateMetadata', { current, target });
|
||||
}
|
||||
// Otherwise, take lock for service first
|
||||
return generateStep('takeLock', {
|
||||
appId: target.appId,
|
||||
services: [target.serviceName],
|
||||
force,
|
||||
});
|
||||
} else if (target.config.running !== current.config.running) {
|
||||
if (target.config.running) {
|
||||
return generateStep('start', { target });
|
||||
if (servicesLocked) {
|
||||
return [generateStep('updateMetadata', { current, target })];
|
||||
} else {
|
||||
return generateStep('stop', { current });
|
||||
// Otherwise, take lock for all services first
|
||||
this.services.concat(targetServices).forEach((s) => {
|
||||
appsToLock[target.appId].add(s.serviceName);
|
||||
});
|
||||
return [];
|
||||
}
|
||||
} else if (target.config.running !== current.config.running) {
|
||||
// Take lock for all services before starting/stopping container
|
||||
if (!servicesLocked) {
|
||||
this.services.concat(targetServices).forEach((s) => {
|
||||
appsToLock[target.appId].add(s.serviceName);
|
||||
});
|
||||
return [];
|
||||
}
|
||||
if (target.config.running) {
|
||||
return [generateStep('start', { target })];
|
||||
} else {
|
||||
return [generateStep('stop', { current })];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -699,21 +782,26 @@ export class App {
|
||||
target: Service,
|
||||
targetApp: App,
|
||||
needsDownload: boolean,
|
||||
servicesLocked: boolean,
|
||||
appsToLock: AppsToLockMap,
|
||||
availableImages: UpdateState['availableImages'],
|
||||
networkPairs: Array<ChangingPair<Network>>,
|
||||
volumePairs: Array<ChangingPair<Volume>>,
|
||||
servicePairs: Array<ChangingPair<Service>>,
|
||||
): CompositionStep | undefined {
|
||||
): CompositionStep[] {
|
||||
if (
|
||||
needsDownload &&
|
||||
this.dependenciesMetForServiceFetch(target, servicePairs)
|
||||
) {
|
||||
// We know the service name exists as it always does for targets
|
||||
return generateStep('fetch', {
|
||||
image: imageManager.imageFromService(target),
|
||||
serviceName: target.serviceName,
|
||||
});
|
||||
return [
|
||||
generateStep('fetch', {
|
||||
image: imageManager.imageFromService(target),
|
||||
serviceName: target.serviceName,
|
||||
}),
|
||||
];
|
||||
} else if (
|
||||
target != null &&
|
||||
this.dependenciesMetForServiceStart(
|
||||
target,
|
||||
targetApp,
|
||||
@ -723,7 +811,15 @@ export class App {
|
||||
servicePairs,
|
||||
)
|
||||
) {
|
||||
return generateStep('start', { target });
|
||||
if (!servicesLocked) {
|
||||
this.services
|
||||
.concat(targetApp.services)
|
||||
.forEach((svc) => appsToLock[target.appId].add(svc.serviceName));
|
||||
return [];
|
||||
}
|
||||
return [generateStep('start', { target })];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import * as imageManager from './images';
|
||||
import type Service from './service';
|
||||
import type { CompositionStep } from './composition-steps';
|
||||
import { generateStep } from './composition-steps';
|
||||
import type { AppsToLockMap } from './app';
|
||||
import { InternalInconsistencyError } from '../lib/errors';
|
||||
import { checkString } from '../lib/validation';
|
||||
|
||||
@ -12,43 +13,82 @@ export interface StrategyContext {
|
||||
dependenciesMetForStart: boolean;
|
||||
dependenciesMetForKill: boolean;
|
||||
needsSpecialKill: boolean;
|
||||
services: Service[];
|
||||
servicesLocked: boolean;
|
||||
appsToLock: AppsToLockMap;
|
||||
}
|
||||
|
||||
function generateLockThenKillStep(
|
||||
current: Service,
|
||||
currentServices: Service[],
|
||||
servicesLocked: boolean,
|
||||
appsToLock: AppsToLockMap,
|
||||
): CompositionStep[] {
|
||||
if (!servicesLocked) {
|
||||
currentServices.forEach((svc) =>
|
||||
appsToLock[svc.appId].add(svc.serviceName),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
return [generateStep('kill', { current })];
|
||||
}
|
||||
|
||||
export function getStepsFromStrategy(
|
||||
strategy: string,
|
||||
context: StrategyContext,
|
||||
): CompositionStep {
|
||||
): CompositionStep[] {
|
||||
switch (strategy) {
|
||||
case 'download-then-kill':
|
||||
if (context.needsDownload && context.target) {
|
||||
return generateStep('fetch', {
|
||||
image: imageManager.imageFromService(context.target),
|
||||
serviceName: context.target.serviceName,
|
||||
});
|
||||
return [
|
||||
generateStep('fetch', {
|
||||
image: imageManager.imageFromService(context.target),
|
||||
serviceName: context.target.serviceName,
|
||||
}),
|
||||
];
|
||||
} else if (context.dependenciesMetForKill) {
|
||||
// We only kill when dependencies are already met, so that we minimize downtime
|
||||
return generateStep('kill', { current: context.current });
|
||||
return generateLockThenKillStep(
|
||||
context.current,
|
||||
context.services,
|
||||
context.servicesLocked,
|
||||
context.appsToLock,
|
||||
);
|
||||
} else {
|
||||
return generateStep('noop', {});
|
||||
return [generateStep('noop', {})];
|
||||
}
|
||||
case 'kill-then-download':
|
||||
case 'delete-then-download':
|
||||
return generateStep('kill', { current: context.current });
|
||||
return generateLockThenKillStep(
|
||||
context.current,
|
||||
context.services,
|
||||
context.servicesLocked,
|
||||
context.appsToLock,
|
||||
);
|
||||
case 'hand-over':
|
||||
if (context.needsDownload && context.target) {
|
||||
return generateStep('fetch', {
|
||||
image: imageManager.imageFromService(context.target),
|
||||
serviceName: context.target.serviceName,
|
||||
});
|
||||
return [
|
||||
generateStep('fetch', {
|
||||
image: imageManager.imageFromService(context.target),
|
||||
serviceName: context.target.serviceName,
|
||||
}),
|
||||
];
|
||||
} else if (context.needsSpecialKill && context.dependenciesMetForKill) {
|
||||
return generateStep('kill', { current: context.current });
|
||||
return generateLockThenKillStep(
|
||||
context.current,
|
||||
context.services,
|
||||
context.servicesLocked,
|
||||
context.appsToLock,
|
||||
);
|
||||
} else if (context.dependenciesMetForStart && context.target) {
|
||||
return generateStep('handover', {
|
||||
current: context.current,
|
||||
target: context.target,
|
||||
});
|
||||
return [
|
||||
generateStep('handover', {
|
||||
current: context.current,
|
||||
target: context.target,
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return generateStep('noop', {});
|
||||
return [generateStep('noop', {})];
|
||||
}
|
||||
default:
|
||||
throw new InternalInconsistencyError(
|
||||
|
@ -112,6 +112,8 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock lock taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -222,6 +224,8 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock lock taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -272,6 +276,8 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock lock taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -403,6 +409,8 @@ describe('compose/application-manager', () => {
|
||||
downloading: c1.downloading,
|
||||
availableImages: c1.availableImages,
|
||||
containerIdsByAppId: c1.containerIdsByAppId,
|
||||
// Mock lock taken for `main` service which just needs metadata updated
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
// There should be two noop steps, one for target service which is still downloading,
|
||||
@ -449,6 +457,10 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken for all services in either current or target state
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['old', 'main', 'new'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
// Service `old` is safe to kill after download for `new` has completed
|
||||
@ -495,8 +507,10 @@ describe('compose/application-manager', () => {
|
||||
availableImages: [],
|
||||
containerIdsByAppId: c1.containerIdsByAppId,
|
||||
// Mock locks for service to be updated via updateMetadata
|
||||
// to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
// or kill to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['old', 'main', 'new'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
// Service `new` should be fetched
|
||||
@ -569,6 +583,10 @@ describe('compose/application-manager', () => {
|
||||
}),
|
||||
],
|
||||
containerIdsByAppId: c1.containerIdsByAppId,
|
||||
// Mock lock taken for all services in target state
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['old', 'main', 'new'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
// Service `new` should be started
|
||||
@ -610,8 +628,10 @@ describe('compose/application-manager', () => {
|
||||
availableImages: [],
|
||||
containerIdsByAppId: c1.containerIdsByAppId,
|
||||
// Mock locks for service to be updated via updateMetadata
|
||||
// to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
// or kill to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['old', 'main', 'new'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
// Service `new` should be fetched
|
||||
@ -684,6 +704,10 @@ describe('compose/application-manager', () => {
|
||||
}),
|
||||
],
|
||||
containerIdsByAppId: c1.containerIdsByAppId,
|
||||
// Mock lock taken for all services in target state
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'new'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
// Service `new` should be started
|
||||
@ -780,6 +804,10 @@ describe('compose/application-manager', () => {
|
||||
}),
|
||||
],
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken for all services in target state
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('start', steps3, 2);
|
||||
@ -848,6 +876,8 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock lock taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -928,6 +958,10 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'dep'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -993,6 +1027,10 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'dep'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1061,6 +1099,10 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'dep'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1103,6 +1145,11 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock lock already taken for the new and leftover services
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 5, services: ['old-service'] },
|
||||
{ appId: 1, services: ['main'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1608,6 +1655,11 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken to avoid takeLock step
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main'] },
|
||||
{ appId: 2, services: ['main'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
|
||||
@ -1630,6 +1682,699 @@ describe('compose/application-manager', () => {
|
||||
).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
describe('taking and releasing locks', () => {
|
||||
it('should take locks for all services in current state when they should be killed', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({ serviceName: 'one' }),
|
||||
await createService({ serviceName: 'two' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
images: [],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
// kill
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps2, 2);
|
||||
});
|
||||
|
||||
it('should take locks for all services in current state when they should be stopped', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'one',
|
||||
running: false,
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'two',
|
||||
running: false,
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({ serviceName: 'one' }),
|
||||
await createService({ serviceName: 'two' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
images: [],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
// stop
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('stop', steps2, 2);
|
||||
});
|
||||
|
||||
it('should take locks for all services in target state before they should be started', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({ serviceName: 'one', image: 'one-image' }),
|
||||
await createService({ serviceName: 'two', image: 'two-image' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
images: [
|
||||
createImage({
|
||||
serviceName: 'one',
|
||||
name: 'one-image',
|
||||
}),
|
||||
createImage({
|
||||
serviceName: 'two',
|
||||
name: 'two-image',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
// start
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('start', steps2, 2);
|
||||
});
|
||||
|
||||
it('should take locks for all services in current and target states when services should be stopped, started, or killed', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'to-stop',
|
||||
image: 'image-to-stop',
|
||||
running: false,
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'to-start',
|
||||
image: 'image-to-start',
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'unchanged',
|
||||
image: 'image-unchanged',
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'to-stop',
|
||||
image: 'image-to-stop',
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'to-kill',
|
||||
image: 'image-to-kill',
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'unchanged',
|
||||
image: 'image-unchanged',
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
images: [
|
||||
createImage({ serviceName: 'to-start', name: 'image-to-start' }),
|
||||
createImage({ serviceName: 'to-stop', name: 'image-to-stop' }),
|
||||
createImage({ serviceName: 'unchanged', name: 'image-unchanged' }),
|
||||
],
|
||||
});
|
||||
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
// No matter the number of services, we should see a single takeLock for all services
|
||||
// regardless if they're only in current or only in target
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members([
|
||||
'to-stop',
|
||||
'to-kill',
|
||||
'to-start',
|
||||
'unchanged',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should download images before taking locks & killing when update strategy is download-then-kill or handover', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'one',
|
||||
image: 'one',
|
||||
labels: {
|
||||
'io.balena.update.strategy': 'download-then-kill',
|
||||
'io.updated': 'true',
|
||||
},
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'two',
|
||||
image: 'two',
|
||||
labels: {
|
||||
'io.balena.update.strategy': 'handover',
|
||||
'io.updated': 'true',
|
||||
},
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'one',
|
||||
image: 'one',
|
||||
labels: { 'io.balena.update.strategy': 'download-then-kill' },
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'two',
|
||||
image: 'two',
|
||||
labels: { 'io.balena.update.strategy': 'handover' },
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
});
|
||||
|
||||
// fetch
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: [],
|
||||
availableImages: [],
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
expectSteps('fetch', steps, 2);
|
||||
|
||||
// noop while downloading
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: ['one', 'two'],
|
||||
availableImages: [],
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
expectSteps('noop', steps2, 2);
|
||||
|
||||
// takeLock after download complete
|
||||
const steps3 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps3, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
// kill
|
||||
const steps4 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps4, 2);
|
||||
});
|
||||
|
||||
it('should take locks & kill before downloading images when update strategy is kill|delete-then-download', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'one',
|
||||
labels: {
|
||||
'io.balena.update.strategy': 'kill-then-download',
|
||||
'io.updated': 'true',
|
||||
},
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'two',
|
||||
labels: {
|
||||
'io.balena.update.strategy': 'delete-then-download',
|
||||
'io.updated': 'true',
|
||||
},
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'one',
|
||||
labels: { 'io.balena.update.strategy': 'kill-then-download' },
|
||||
}),
|
||||
await createService({
|
||||
serviceName: 'two',
|
||||
labels: { 'io.balena.update.strategy': 'delete-then-download' },
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
// Images haven't finished downloading,
|
||||
// but kill steps should still be inferred
|
||||
downloading: ['one', 'two'],
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
// kill
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: ['one', 'two'],
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps2, 2);
|
||||
});
|
||||
|
||||
it('should infer takeLock & kill steps for dependent services before their network should be removed', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({ serviceName: 'main', appUuid: 'deadbeef' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'main',
|
||||
appUuid: 'deadbeef',
|
||||
composition: { networks: { test: {} } },
|
||||
}),
|
||||
],
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test', 1, 'deadbeef', {
|
||||
driver: 'bridge',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// kill
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps2);
|
||||
|
||||
// removeNetwork
|
||||
const intermediateCurrent = createCurrentState({
|
||||
services: [],
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test', 1, 'deadbeef', {
|
||||
driver: 'bridge',
|
||||
}),
|
||||
],
|
||||
});
|
||||
const steps3 = await applicationManager.inferNextSteps(
|
||||
intermediateCurrent.currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: intermediateCurrent.downloading,
|
||||
availableImages: intermediateCurrent.availableImages,
|
||||
containerIdsByAppId: intermediateCurrent.containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('removeNetwork', steps3);
|
||||
});
|
||||
|
||||
it('should infer takeLock & kill steps for dependent services before their network should have config changed', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'main',
|
||||
appUuid: 'deadbeef',
|
||||
composition: { networks: { test: {} } },
|
||||
}),
|
||||
],
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test', 1, 'deadbeef', {
|
||||
driver: 'local',
|
||||
}),
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'main',
|
||||
appUuid: 'deadbeef',
|
||||
composition: { networks: { test: {} } },
|
||||
}),
|
||||
],
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test', 1, 'deadbeef', {
|
||||
driver: 'bridge',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// kill
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps2, 1);
|
||||
|
||||
// removeNetwork
|
||||
const intermediateCurrent = createCurrentState({
|
||||
services: [],
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test', 1, 'deadbeef', {
|
||||
driver: 'bridge',
|
||||
}),
|
||||
],
|
||||
});
|
||||
const steps3 = await applicationManager.inferNextSteps(
|
||||
intermediateCurrent.currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: intermediateCurrent.downloading,
|
||||
availableImages: intermediateCurrent.availableImages,
|
||||
containerIdsByAppId: intermediateCurrent.containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('removeNetwork', steps3);
|
||||
});
|
||||
|
||||
it('should infer takeLock & kill steps for dependent services before their volume should have config changed', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'main',
|
||||
appUuid: 'deadbeef',
|
||||
composition: { volumes: ['test:/test'] },
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
volumes: [
|
||||
Volume.fromComposeObject('test', 1, 'deadbeef', {
|
||||
labels: { 'io.updated': 'true' },
|
||||
}),
|
||||
],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({
|
||||
serviceName: 'main',
|
||||
appUuid: 'deadbeef',
|
||||
composition: { volumes: ['test:/test'] },
|
||||
}),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
volumes: [Volume.fromComposeObject('test', 1, 'deadbeef')],
|
||||
});
|
||||
|
||||
// takeLock
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
},
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', steps, 1, 1);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// kill
|
||||
const steps2 = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('kill', steps2, 1);
|
||||
|
||||
// removeVolume
|
||||
const intermediateCurrent = createCurrentState({
|
||||
services: [],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
volumes: [Volume.fromComposeObject('test', 1, 'deadbeef')],
|
||||
});
|
||||
const steps3 = await applicationManager.inferNextSteps(
|
||||
intermediateCurrent.currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading: intermediateCurrent.downloading,
|
||||
availableImages: intermediateCurrent.availableImages,
|
||||
containerIdsByAppId: intermediateCurrent.containerIdsByAppId,
|
||||
// Mock locks taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
);
|
||||
expectSteps('removeVolume', steps3);
|
||||
});
|
||||
|
||||
it('should release locks before settling state', async () => {
|
||||
const targetApps = createApps(
|
||||
{
|
||||
services: [
|
||||
await createService({ serviceName: 'one' }),
|
||||
await createService({ serviceName: 'two' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
},
|
||||
true,
|
||||
);
|
||||
const { currentApps, availableImages, downloading, containerIdsByAppId } =
|
||||
createCurrentState({
|
||||
services: [
|
||||
await createService({ serviceName: 'one' }),
|
||||
await createService({ serviceName: 'two' }),
|
||||
],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
});
|
||||
const steps = await applicationManager.inferNextSteps(
|
||||
currentApps,
|
||||
targetApps,
|
||||
{
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
);
|
||||
const [releaseLockStep] = expectSteps('releaseLock', steps, 1, 1);
|
||||
expect(releaseLockStep).to.have.property('appId').that.equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getting application's current state", () => {
|
||||
let getImagesState: sinon.SinonStub;
|
||||
let getServicesState: sinon.SinonStub;
|
||||
@ -2032,6 +2777,10 @@ describe('compose/application-manager', () => {
|
||||
downloading,
|
||||
availableImages,
|
||||
containerIdsByAppId,
|
||||
// Mock locks taken for all services in target state
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two', 'three', 'four'] },
|
||||
]),
|
||||
});
|
||||
|
||||
[startStep1, startStep2, startStep3, startStep4].forEach((step) => {
|
||||
|
@ -170,13 +170,38 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// Calculate steps
|
||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||
const availableImages = [createImage({ serviceName: 'test' })];
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['test']);
|
||||
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
// Mock lock already taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'test' });
|
||||
|
||||
// No remove volume steps until dependencies are killed
|
||||
expectNoStep('removeVolume', steps);
|
||||
expectNoStep('removeVolume', steps2);
|
||||
});
|
||||
|
||||
it('should correctly infer to remove an app volumes when the app is being removed', () => {
|
||||
@ -250,17 +275,32 @@ describe('compose/app', () => {
|
||||
volumes: [volume],
|
||||
});
|
||||
|
||||
// Step 1: kill
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
// Step 1: takeLock
|
||||
const lockStep = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
intermediateTarget,
|
||||
);
|
||||
expectSteps('kill', steps);
|
||||
expectSteps('takeLock', lockStep, 1, 1);
|
||||
|
||||
// Step 2: noop (service is stopping)
|
||||
// Step 2: kill
|
||||
const killSteps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks already taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
intermediateTarget,
|
||||
);
|
||||
expectSteps('kill', killSteps);
|
||||
|
||||
// Step 3: noop (service is stopping)
|
||||
service.status = 'Stopping';
|
||||
const secondStageSteps = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks already taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
intermediateTarget,
|
||||
);
|
||||
expectSteps('noop', secondStageSteps);
|
||||
@ -286,7 +326,7 @@ describe('compose/app', () => {
|
||||
volumes: [],
|
||||
});
|
||||
|
||||
// Step 3: createVolume
|
||||
// Step 4: createVolume
|
||||
service.status = 'Running';
|
||||
const target = createApp({
|
||||
services: [service],
|
||||
@ -303,18 +343,29 @@ describe('compose/app', () => {
|
||||
expect(recreateVolumeSteps).to.have.length(1);
|
||||
expectSteps('createVolume', recreateVolumeSteps);
|
||||
|
||||
// Final step: start service
|
||||
// Step 5: takeLock
|
||||
const currentWithVolumeRecreated = createApp({
|
||||
services: [],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
volumes: [volume],
|
||||
});
|
||||
|
||||
const createServiceSteps =
|
||||
const lockStepAfterRecreate =
|
||||
currentWithVolumeRecreated.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
target,
|
||||
);
|
||||
expectSteps('takeLock', lockStepAfterRecreate, 1, 1);
|
||||
|
||||
// Final step: start service
|
||||
const createServiceSteps =
|
||||
currentWithVolumeRecreated.nextStepsForAppUpdate(
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks already taken
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
expectSteps('start', createServiceSteps);
|
||||
});
|
||||
});
|
||||
@ -441,6 +492,7 @@ describe('compose/app', () => {
|
||||
});
|
||||
const target = createApp({
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||
],
|
||||
services: [
|
||||
@ -454,14 +506,61 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
console.log(killStep);
|
||||
const availableImages = [createImage({ appUuid: 'deadbeef' })];
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['test']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep).to.have.property('current').that.deep.includes({
|
||||
serviceName: 'test',
|
||||
});
|
||||
|
||||
// removeNetwork should not be generated until after the kill
|
||||
expectNoStep('removeNetwork', steps);
|
||||
expectNoStep('removeNetwork', steps2);
|
||||
|
||||
// Then remove duplicate networks
|
||||
const current2 = createApp({
|
||||
appUuid: 'deadbeef',
|
||||
networks: [
|
||||
DEFAULT_NETWORK,
|
||||
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
||||
],
|
||||
services: [],
|
||||
});
|
||||
|
||||
const steps3 = current2.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [removeNetworkStep] = expectSteps('removeNetwork', steps3);
|
||||
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
||||
name: 'test-network',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly infer more than one network removal step', () => {
|
||||
@ -564,15 +663,33 @@ describe('compose/app', () => {
|
||||
});
|
||||
|
||||
const availableImages = [createImage({ appUuid: 'deadbeef' })];
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{ ...defaultContext, availableImages },
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['test']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'test' });
|
||||
|
||||
// Network should not be removed until after dependency kills
|
||||
expectNoStep('removeNetwork', steps);
|
||||
expectNoStep('removeNetwork', steps2);
|
||||
});
|
||||
|
||||
it('should kill dependencies of networks before changing config', async () => {
|
||||
@ -599,16 +716,37 @@ describe('compose/app', () => {
|
||||
],
|
||||
isTarget: true,
|
||||
});
|
||||
const availableImages = [createImage({ appId: 1, serviceName: 'test' })];
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['test']);
|
||||
|
||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'test' });
|
||||
|
||||
// We shouldn't try to remove the network until we have gotten rid of the dependencies
|
||||
// Network should not be removed until after dependency kills
|
||||
expectNoStep('removeNetwork', steps);
|
||||
expectNoStep('removeNetwork', steps2);
|
||||
});
|
||||
|
||||
it('should always kill dependencies of networks before removing', async () => {
|
||||
@ -655,19 +793,35 @@ describe('compose/app', () => {
|
||||
createImage({ appId: 1, serviceName: 'one', name: 'alpine' }),
|
||||
createImage({ appId: 1, serviceName: 'two', name: 'alpine' }),
|
||||
];
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{ ...defaultContext, availableImages },
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'one' });
|
||||
|
||||
// We shouldn't try to remove the network until we have gotten rid of the dependencies
|
||||
expectNoStep('removeNetwork', steps);
|
||||
expectNoStep('removeNetwork', steps2);
|
||||
});
|
||||
|
||||
it('should kill dependencies of networks before updating between releases', async () => {
|
||||
@ -718,20 +872,35 @@ describe('compose/app', () => {
|
||||
createImage({ appId: 1, serviceName: 'one', name: 'alpine' }),
|
||||
createImage({ appId: 1, serviceName: 'two', name: 'alpine' }),
|
||||
];
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{ ...defaultContext, availableImages },
|
||||
target,
|
||||
);
|
||||
expectSteps('kill', steps, 2);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two']);
|
||||
|
||||
expect(steps.map((s) => (s as any).current.serviceName)).to.have.members([
|
||||
'one',
|
||||
'two',
|
||||
]);
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages,
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two'] },
|
||||
]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
expectSteps('kill', steps2, 2);
|
||||
expect(steps2.map((s) => (s as any).current.serviceName)).to.have.members(
|
||||
['one', 'two'],
|
||||
);
|
||||
|
||||
// We shouldn't try to remove the network until we have gotten rid of the dependencies
|
||||
expectNoStep('removeNetwork', steps);
|
||||
expectNoStep('removeNetwork', steps2);
|
||||
});
|
||||
|
||||
it('should create the default network if it does not exist', () => {
|
||||
@ -847,6 +1016,7 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
@ -856,7 +1026,24 @@ describe('compose/app', () => {
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main', 'aux']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages: [createImage({ serviceName: 'main' })],
|
||||
// Mock locks already taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'aux'] },
|
||||
]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.to.deep.include({ serviceName: 'aux' });
|
||||
@ -1013,8 +1200,22 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||
const [stopStep] = expectSteps('stop', steps);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// Then stop
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [stopStep] = expectSteps('stop', steps2);
|
||||
expect(stopStep)
|
||||
.to.have.property('current')
|
||||
.to.deep.include({ serviceName: 'main' });
|
||||
@ -1073,9 +1274,19 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// should see a 'stop'
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// Then kill
|
||||
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
{
|
||||
...contextWithImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', stepsToIntermediate);
|
||||
@ -1090,9 +1301,12 @@ describe('compose/app', () => {
|
||||
networks: [DEFAULT_NETWORK],
|
||||
});
|
||||
|
||||
// now should see a 'start'
|
||||
// Then start
|
||||
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
{
|
||||
...contextWithImages,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [startStep] = expectSteps('start', stepsToTarget);
|
||||
@ -1106,12 +1320,6 @@ describe('compose/app', () => {
|
||||
});
|
||||
|
||||
it('should not start a container when it depends on a service which is being installed', async () => {
|
||||
const availableImages = [
|
||||
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
|
||||
createImage({ appId: 1, serviceName: 'dep', name: 'dep-image' }),
|
||||
];
|
||||
const contextWithImages = { ...defaultContext, ...{ availableImages } };
|
||||
|
||||
const current = createApp({
|
||||
services: [
|
||||
await createService(
|
||||
@ -1148,12 +1356,24 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
const availableImages = [
|
||||
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
|
||||
createImage({ appId: 1, serviceName: 'dep', name: 'dep-image' }),
|
||||
];
|
||||
// As service is already being installed, lock for target should have been taken
|
||||
const contextWithImages = {
|
||||
...defaultContext,
|
||||
...{ availableImages },
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'dep'] },
|
||||
]),
|
||||
};
|
||||
|
||||
// Only one start step and it should be that of the 'dep' service
|
||||
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
target,
|
||||
);
|
||||
|
||||
// Only one start step and it should be that of the 'dep' service
|
||||
const [startStep] = expectSteps('start', stepsToIntermediate);
|
||||
expect(startStep)
|
||||
.to.have.property('target')
|
||||
@ -1246,11 +1466,28 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
||||
// Take lock first
|
||||
const stepsToIntermediateBeforeLock = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
target,
|
||||
);
|
||||
const [takeLockStep] = expectSteps(
|
||||
'takeLock',
|
||||
stepsToIntermediateBeforeLock,
|
||||
);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// Then kill
|
||||
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks taken before kill
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', stepsToIntermediate);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
@ -1263,7 +1500,12 @@ describe('compose/app', () => {
|
||||
});
|
||||
|
||||
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks still taken after kill (releaseLock not
|
||||
// yet inferred as state is not yet settled)
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
|
||||
@ -1375,23 +1617,42 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
const stepsFirstTry = current.nextStepsForAppUpdate(
|
||||
// Take lock first
|
||||
const stepsBeforeLock = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
target,
|
||||
);
|
||||
const [takeLockStep] = expectSteps('takeLock', stepsBeforeLock);
|
||||
expect(takeLockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main']);
|
||||
|
||||
// Then kill
|
||||
const stepsFirstTry = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks taken from previous step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
|
||||
const [killStep] = expectSteps('kill', stepsFirstTry);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'main' });
|
||||
|
||||
// if at first you don't succeed
|
||||
// As long as a kill step has not succeeded (current state hasn't
|
||||
// changed), a kill step should be generated.
|
||||
const stepsSecondTry = current.nextStepsForAppUpdate(
|
||||
contextWithImages,
|
||||
{
|
||||
...contextWithImages,
|
||||
// Mock locks taken from previous step
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['main'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
|
||||
// Since current state has not changed, another kill step needs to be generated
|
||||
const [newKillStep] = expectSteps('kill', stepsSecondTry);
|
||||
expect(newKillStep)
|
||||
.to.have.property('current')
|
||||
@ -1419,8 +1680,22 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
||||
const [killStep] = expectSteps('kill', steps);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['test']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
locksTaken: new LocksTakenMap([{ appId: 1, services: ['test'] }]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [killStep] = expectSteps('kill', steps2);
|
||||
expect(killStep)
|
||||
.to.have.property('current')
|
||||
.that.deep.includes({ serviceName: 'test' });
|
||||
@ -1485,16 +1760,31 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
const contextWithImages = {
|
||||
...defaultContext,
|
||||
// With default download-then-kill strategy, target images
|
||||
// should all be available before a kill step is inferred
|
||||
availableImages: [createImage({ serviceName: 'three' })],
|
||||
};
|
||||
// Take lock first
|
||||
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
||||
const [lockStep] = expectSteps('takeLock', steps);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['one', 'two', 'three']);
|
||||
|
||||
// Then kill
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
// With default download-then-kill strategy, target images
|
||||
// should all be available before a kill step is inferred
|
||||
availableImages: [createImage({ serviceName: 'three' })],
|
||||
...contextWithImages,
|
||||
// Mock locks already taken
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['one', 'two', 'three'] },
|
||||
]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
expectSteps('kill', steps, 2);
|
||||
expectSteps('kill', steps2, 2);
|
||||
});
|
||||
|
||||
it('should not infer a kill step with the default strategy before all target images have been downloaded', async () => {
|
||||
@ -1599,21 +1889,6 @@ describe('compose/app', () => {
|
||||
});
|
||||
|
||||
it('should infer a start step only when target images have been downloaded', async () => {
|
||||
const contextWithImages = {
|
||||
...defaultContext,
|
||||
...{
|
||||
downloading: [], // One of the images is being downloaded
|
||||
availableImages: [
|
||||
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
||||
createImage({
|
||||
appId: 1,
|
||||
name: 'other-image',
|
||||
serviceName: 'other',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const current = createApp({
|
||||
services: [],
|
||||
networks: [DEFAULT_NETWORK],
|
||||
@ -1637,9 +1912,59 @@ describe('compose/app', () => {
|
||||
isTarget: true,
|
||||
});
|
||||
|
||||
// No kill steps should be generated
|
||||
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
||||
expectSteps('start', steps, 2);
|
||||
// No start steps should be generated as long as any target image is downloading
|
||||
const steps = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
downloading: ['other-image'],
|
||||
availableImages: [
|
||||
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
||||
],
|
||||
},
|
||||
target,
|
||||
);
|
||||
expectNoStep('start', steps);
|
||||
expectSteps('noop', steps, 1);
|
||||
|
||||
// Take lock before starting once downloads complete
|
||||
const steps2 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages: [
|
||||
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
||||
createImage({
|
||||
appId: 1,
|
||||
name: 'other-image',
|
||||
serviceName: 'other',
|
||||
}),
|
||||
],
|
||||
},
|
||||
target,
|
||||
);
|
||||
const [lockStep] = expectSteps('takeLock', steps2);
|
||||
expect(lockStep)
|
||||
.to.have.property('services')
|
||||
.that.deep.includes.members(['main', 'other']);
|
||||
|
||||
// Then start
|
||||
const steps3 = current.nextStepsForAppUpdate(
|
||||
{
|
||||
...defaultContext,
|
||||
availableImages: [
|
||||
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
||||
createImage({
|
||||
appId: 1,
|
||||
name: 'other-image',
|
||||
serviceName: 'other',
|
||||
}),
|
||||
],
|
||||
locksTaken: new LocksTakenMap([
|
||||
{ appId: 1, services: ['main', 'other'] },
|
||||
]),
|
||||
},
|
||||
target,
|
||||
);
|
||||
expectSteps('start', steps3, 2);
|
||||
});
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user