mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-21 10:01:55 +00:00
This fixes a regression where dependencies would only be started in
order and would start the dependent service if its dependency had been
started at some point in the past, regardless of the running condition.
This makes the behavior more consistent with docker compose where the
[dependency needs to be
running or healthy](69a83d1303/pkg/compose/convergence.go (L441)
) for the service to be started.
Change-type: patch
2280 lines
60 KiB
TypeScript
2280 lines
60 KiB
TypeScript
import { expect } from 'chai';
|
|
import type { Image } from '~/src/compose/images';
|
|
import { Network } from '~/src/compose/network';
|
|
import { Volume } from '~/src/compose/volume';
|
|
import { type Lock } from '~/lib/update-lock';
|
|
|
|
import {
|
|
createService,
|
|
createImage,
|
|
createApp,
|
|
DEFAULT_NETWORK,
|
|
expectSteps,
|
|
expectNoStep,
|
|
} from '~/test-lib/state-helper';
|
|
|
|
const defaultContext = {
|
|
keepVolumes: false,
|
|
force: false,
|
|
availableImages: [] as Image[],
|
|
containerIds: {},
|
|
downloading: [] as string[],
|
|
lock: null,
|
|
hasLeftoverLocks: false,
|
|
};
|
|
|
|
const mockLock: Lock = {
|
|
async unlock() {
|
|
/* noop */
|
|
},
|
|
};
|
|
|
|
describe('compose/app', () => {
|
|
describe('volume state behavior', () => {
|
|
it('should correctly infer a volume create step', () => {
|
|
// Setup current and target apps
|
|
const current = createApp();
|
|
const target = createApp({
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Calculate the steps
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
// Check that a createVolume step has been created
|
|
const [createVolumeStep] = expectSteps('createVolume', steps);
|
|
expect(createVolumeStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ name: 'test-volume' });
|
|
});
|
|
|
|
it('should correctly infer more than one volume create step', () => {
|
|
const current = createApp();
|
|
const target = createApp({
|
|
volumes: [
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef'),
|
|
Volume.fromComposeObject('test-volume-2', 1, 'deadbeef'),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
// Check that 2 createVolume steps are found
|
|
const createVolumeSteps = expectSteps('createVolume', steps, 2);
|
|
|
|
// Check that the steps contain the volumes without any order
|
|
// expectation
|
|
expect(
|
|
createVolumeSteps.filter(
|
|
(step: any) => step.target && step.target.name === 'test-volume',
|
|
),
|
|
).to.have.lengthOf(1);
|
|
|
|
expect(
|
|
createVolumeSteps.filter(
|
|
(step: any) => step.target && step.target.name === 'test-volume-2',
|
|
),
|
|
).to.have.lengthOf(1);
|
|
});
|
|
|
|
// We don't remove volumes until the end
|
|
it('should not infer a volume remove step when the app is still referenced', () => {
|
|
const current = createApp({
|
|
volumes: [
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef'),
|
|
Volume.fromComposeObject('test-volume-2', 1, 'deadbeef'),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
volumes: [Volume.fromComposeObject('test-volume-2', 1, 'deadbeef')],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectNoStep('removeVolume', steps);
|
|
});
|
|
|
|
it('should correctly infer volume recreation steps', () => {
|
|
const current = createApp({
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
|
});
|
|
const target = createApp({
|
|
volumes: [
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
// First step should create a volume removal step
|
|
const stepsForRemoval = current.nextStepsForAppUpdate(
|
|
defaultContext,
|
|
target,
|
|
);
|
|
|
|
const [removalStep] = expectSteps('removeVolume', stepsForRemoval);
|
|
expect(removalStep)
|
|
.to.have.property('current')
|
|
.that.has.property('name')
|
|
.that.equals('test-volume');
|
|
expect(removalStep)
|
|
.to.have.property('current')
|
|
.that.has.property('appId')
|
|
.that.equals(1);
|
|
|
|
// we are assuming that after the execution steps the current state of the
|
|
// app will look like this
|
|
const intermediate = createApp({
|
|
volumes: [],
|
|
});
|
|
|
|
// This test is extra since we have already tested that the volume gets created
|
|
const stepsForCreation = intermediate.nextStepsForAppUpdate(
|
|
defaultContext,
|
|
target,
|
|
);
|
|
const [creationStep] = expectSteps('createVolume', stepsForCreation);
|
|
|
|
expect(creationStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({
|
|
labels: {
|
|
'io.balena.supervised': 'true',
|
|
'io.balena.app-uuid': 'deadbeef',
|
|
test: 'test',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should kill dependencies of a volume before changing config', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'test',
|
|
composition: { volumes: ['test-volume:/data'] },
|
|
}),
|
|
],
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'test',
|
|
composition: { volumes: ['test-volume:/data'] },
|
|
}),
|
|
],
|
|
volumes: [
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
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']);
|
|
|
|
// Then kill
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
availableImages,
|
|
// Mock lock already taken
|
|
lock: mockLock,
|
|
},
|
|
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', () => {
|
|
const current = createApp({
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
|
});
|
|
|
|
const steps = current.stepsToRemoveApp(defaultContext);
|
|
const [removeVolumeStep] = expectSteps('removeVolume', steps);
|
|
|
|
expect(removeVolumeStep).to.have.property('current').that.deep.includes({
|
|
name: 'test-volume',
|
|
});
|
|
});
|
|
|
|
it('should not output a kill step for a service which is already stopping when changing a volume', async () => {
|
|
const service = await createService({
|
|
serviceName: 'test',
|
|
composition: { volumes: ['test-volume:/data'] },
|
|
});
|
|
service.status = 'Stopping';
|
|
const current = createApp({
|
|
services: [service],
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
|
});
|
|
const target = createApp({
|
|
services: [service],
|
|
volumes: [
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectNoStep('kill', steps);
|
|
});
|
|
|
|
it('should generate the correct step sequence for a volume purge request', async () => {
|
|
const service = await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
image: 'test-image',
|
|
composition: { volumes: ['db-volume:/data'] },
|
|
});
|
|
const volume = Volume.fromComposeObject('db-volume', 1, 'deadbeef');
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
createImage({
|
|
appId: service.appId,
|
|
name: 'test-image',
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
|
|
// Temporarily set target services & volumes to empty, as in doPurge
|
|
const intermediateTarget = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Generate initial state with one service & one volume
|
|
const current = createApp({
|
|
services: [service],
|
|
networks: [DEFAULT_NETWORK],
|
|
volumes: [volume],
|
|
});
|
|
|
|
// Step 1: takeLock
|
|
const lockStep = current.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
intermediateTarget,
|
|
);
|
|
expectSteps('takeLock', lockStep, 1, 1);
|
|
|
|
// Step 2: kill
|
|
const killSteps = current.nextStepsForAppUpdate(
|
|
{
|
|
...contextWithImages,
|
|
// Mock locks already taken
|
|
lock: mockLock,
|
|
},
|
|
intermediateTarget,
|
|
);
|
|
expectSteps('kill', killSteps);
|
|
|
|
// Step 3: noop (service is stopping)
|
|
service.status = 'Stopping';
|
|
const secondStageSteps = current.nextStepsForAppUpdate(
|
|
{
|
|
...contextWithImages,
|
|
// Mock locks already taken
|
|
lock: mockLock,
|
|
},
|
|
intermediateTarget,
|
|
);
|
|
expectSteps('noop', secondStageSteps);
|
|
expect(secondStageSteps).to.have.length(1);
|
|
|
|
// No steps, simulate container removal & explicit volume removal as in doPurge
|
|
const currentWithServiceRemoved = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
volumes: [volume],
|
|
});
|
|
|
|
expect(
|
|
currentWithServiceRemoved.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
intermediateTarget,
|
|
),
|
|
).to.have.length(0);
|
|
|
|
// Simulate volume removal
|
|
const currentWithVolumesRemoved = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
volumes: [],
|
|
});
|
|
|
|
// Step 4: createVolume
|
|
service.status = 'Running';
|
|
const target = createApp({
|
|
services: [service],
|
|
networks: [DEFAULT_NETWORK],
|
|
volumes: [volume],
|
|
isTarget: true,
|
|
});
|
|
const recreateVolumeSteps =
|
|
currentWithVolumesRemoved.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
target,
|
|
);
|
|
|
|
expectSteps('createVolume', recreateVolumeSteps);
|
|
|
|
// Step 5: takeLock
|
|
const currentWithVolumeRecreated = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
volumes: [volume],
|
|
});
|
|
const lockStepAfterRecreate =
|
|
currentWithVolumeRecreated.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
target,
|
|
);
|
|
expectSteps('takeLock', lockStepAfterRecreate, 1, 1);
|
|
|
|
// Final step: start service
|
|
const createServiceSteps =
|
|
currentWithVolumeRecreated.nextStepsForAppUpdate(
|
|
{
|
|
...contextWithImages,
|
|
// Mock locks already taken
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
expectSteps('start', createServiceSteps);
|
|
});
|
|
});
|
|
|
|
describe('network state behavior', () => {
|
|
it('should correctly infer a network create step', () => {
|
|
const current = createApp({ networks: [] });
|
|
const target = createApp({
|
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep).to.have.property('target').that.deep.includes({
|
|
name: 'default',
|
|
});
|
|
});
|
|
|
|
it('should correctly infer a network remove step', () => {
|
|
const current = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
});
|
|
const target = createApp({ networks: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [removeNetworkStep] = expectSteps('removeNetwork', steps);
|
|
|
|
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
|
name: 'test-network',
|
|
});
|
|
});
|
|
|
|
it('should correctly remove default duplicate networks', () => {
|
|
const current = createApp({
|
|
networks: [DEFAULT_NETWORK, DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
networks: [],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [removeNetworkStep] = expectSteps('removeNetwork', steps);
|
|
|
|
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
|
name: 'default',
|
|
});
|
|
});
|
|
|
|
it('should correctly remove duplicate networks', () => {
|
|
const current = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
networks: [
|
|
// The target is a single network
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [removeNetworkStep] = expectSteps('removeNetwork', steps);
|
|
|
|
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
|
name: 'test-network',
|
|
});
|
|
});
|
|
|
|
it('should ignore the duplicates if there are changes already', () => {
|
|
const current = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
networks: [
|
|
// The target is a single network
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {
|
|
config_only: true,
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [removeNetworkStep] = expectSteps('removeNetwork', steps);
|
|
|
|
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
|
name: 'test-network',
|
|
});
|
|
});
|
|
|
|
// This should never happen because there can never be a service that is refencing
|
|
// a network that has a duplicate
|
|
it('should generate service kill steps if there are duplicate networks', async () => {
|
|
const current = createApp({
|
|
appUuid: 'deadbeef',
|
|
networks: [
|
|
DEFAULT_NETWORK,
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
serviceName: 'test',
|
|
composition: { networks: ['test-network'] },
|
|
}),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
networks: [
|
|
DEFAULT_NETWORK,
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
serviceName: 'test',
|
|
composition: { networks: ['test-network'] },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
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,
|
|
lock: mockLock,
|
|
},
|
|
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,
|
|
lock: mockLock,
|
|
},
|
|
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', () => {
|
|
const current = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
Network.fromComposeObject('test-network-2', 1, 'deadbeef', {}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
const target = createApp({ networks: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [first, second] = expectSteps('removeNetwork', steps, 2);
|
|
|
|
expect(first).to.have.property('current').that.deep.includes({
|
|
name: 'test-network',
|
|
});
|
|
expect(second).to.have.property('current').that.deep.includes({
|
|
name: 'test-network-2',
|
|
});
|
|
});
|
|
|
|
it('should correctly infer a network recreation step', () => {
|
|
const current = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {
|
|
labels: { TEST: 'TEST' },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const stepsForRemoval = current.nextStepsForAppUpdate(
|
|
defaultContext,
|
|
target,
|
|
);
|
|
|
|
const [removeStep] = expectSteps('removeNetwork', stepsForRemoval);
|
|
expect(removeStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ name: 'test-network' });
|
|
|
|
// We assume that the intermediate state looks like this
|
|
const intermediate = createApp({
|
|
networks: [],
|
|
});
|
|
|
|
const stepsForCreation = intermediate.nextStepsForAppUpdate(
|
|
defaultContext,
|
|
target,
|
|
);
|
|
|
|
const [createNetworkStep] = expectSteps(
|
|
'createNetwork',
|
|
stepsForCreation,
|
|
1,
|
|
2, // The update will also generate a step for the default network but we don't care about that
|
|
);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ name: 'test-network' });
|
|
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({
|
|
labels: { TEST: 'TEST', 'io.balena.app-id': '1' },
|
|
});
|
|
});
|
|
|
|
it('should kill dependencies of networks before removing', async () => {
|
|
const current = createApp({
|
|
appUuid: 'deadbeef',
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
serviceName: 'test',
|
|
composition: { networks: ['test-network'] },
|
|
}),
|
|
],
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
appUuid: 'deadbeef',
|
|
services: [
|
|
await createService({ appUuid: 'deadbeef', serviceName: 'test' }),
|
|
],
|
|
networks: [],
|
|
isTarget: true,
|
|
});
|
|
|
|
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,
|
|
lock: mockLock,
|
|
},
|
|
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 perform fetch steps first even for services that reference deleted networks', async () => {
|
|
const current = createApp({
|
|
appUuid: 'deadbeef',
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
image: 'test-image',
|
|
serviceName: 'test',
|
|
composition: { networks: ['test-network'] },
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
image: 'other-image',
|
|
serviceName: 'other',
|
|
composition: { network_mode: 'host' },
|
|
}),
|
|
],
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
DEFAULT_NETWORK,
|
|
],
|
|
});
|
|
const target = createApp({
|
|
appUuid: 'deadbeef',
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
serviceName: 'test',
|
|
image: 'new-test-image',
|
|
commit: 'new-commit',
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
appUuid: 'deadbeef',
|
|
serviceName: 'other',
|
|
image: 'new-other-image',
|
|
commit: 'new-commit',
|
|
composition: { network_mode: 'host' },
|
|
}),
|
|
],
|
|
networks: [],
|
|
isTarget: true,
|
|
});
|
|
|
|
const availableImages = [
|
|
createImage({
|
|
appUuid: 'deadbeef',
|
|
name: 'test-image',
|
|
serviceName: 'test',
|
|
}),
|
|
createImage({
|
|
appUuid: 'deadbeef',
|
|
name: 'other-image',
|
|
serviceName: 'other',
|
|
}),
|
|
];
|
|
// Take lock first
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{ ...defaultContext, availableImages },
|
|
target,
|
|
);
|
|
|
|
const fetchSteps = expectSteps('fetch', steps, 2);
|
|
expect(fetchSteps.map((s) => (s as any).image.name)).to.have.deep.members(
|
|
['new-test-image', 'new-other-image'],
|
|
);
|
|
});
|
|
|
|
it('should kill dependencies of networks before changing config', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'test',
|
|
composition: { networks: { 'test-network': {} } },
|
|
}),
|
|
],
|
|
networks: [Network.fromComposeObject('test-network', 1, 'appuuid', {})],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'test',
|
|
composition: { networks: { 'test-network': {} } },
|
|
}),
|
|
],
|
|
networks: [
|
|
Network.fromComposeObject('test-network', 1, 'appuuid', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
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']);
|
|
|
|
// Then kill
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
availableImages,
|
|
lock: mockLock,
|
|
},
|
|
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 always kill dependencies of networks before removing', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
// The device for some reason is already running some services
|
|
// of the new release, but we need to kill it anyways
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'one',
|
|
commit: 'deadca1f',
|
|
composition: { command: 'sleep infinity', networks: ['default'] },
|
|
}),
|
|
],
|
|
networks: [Network.fromComposeObject('default', 1, 'appuuid', {})],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'one',
|
|
commit: 'deadca1f',
|
|
composition: { command: 'sleep infinity', networks: ['default'] },
|
|
}),
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'two',
|
|
commit: 'deadca1f',
|
|
composition: {
|
|
command: 'sh -c "echo two && sleep infinity"',
|
|
networks: ['default'],
|
|
},
|
|
}),
|
|
],
|
|
networks: [
|
|
Network.fromComposeObject('default', 1, 'appuuid', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const availableImages = [
|
|
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 [lockStep] = expectSteps('takeLock', steps);
|
|
expect(lockStep)
|
|
.to.have.property('services')
|
|
.that.deep.includes.members(['one']);
|
|
|
|
// Then kill
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
availableImages,
|
|
lock: mockLock,
|
|
},
|
|
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 () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'one',
|
|
commit: 'deadbeef',
|
|
composition: { command: 'sleep infinity', networks: ['default'] },
|
|
}),
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'two',
|
|
commit: 'deadbeef',
|
|
composition: { command: 'sleep infinity', networks: ['default'] },
|
|
}),
|
|
],
|
|
networks: [Network.fromComposeObject('default', 1, 'appuuid', {})],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'one',
|
|
commit: 'deadca1f',
|
|
composition: { command: 'sleep infinity', networks: ['default'] },
|
|
}),
|
|
await createService({
|
|
image: 'alpine',
|
|
serviceName: 'two',
|
|
commit: 'deadca1f',
|
|
composition: {
|
|
command: 'sh -c "echo two && sleep infinity"',
|
|
networks: ['default'],
|
|
},
|
|
}),
|
|
],
|
|
networks: [
|
|
Network.fromComposeObject('default', 1, 'appuuid', {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const availableImages = [
|
|
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 [lockStep] = expectSteps('takeLock', steps);
|
|
expect(lockStep)
|
|
.to.have.property('services')
|
|
.that.deep.includes.members(['one', 'two']);
|
|
|
|
// Then kill
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
availableImages,
|
|
lock: mockLock,
|
|
},
|
|
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', () => {
|
|
const current = createApp({ networks: [] });
|
|
const target = createApp({ networks: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
// A default network should always be created
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ name: 'default' });
|
|
});
|
|
|
|
it('should not create the default network if it already exists', () => {
|
|
const current = createApp({
|
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
|
});
|
|
const target = createApp({ networks: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
// The network should not be created again
|
|
expectNoStep('createNetwork', steps);
|
|
});
|
|
|
|
it('should create a config-only network if network_mode is host for all services', async () => {
|
|
const svcOne = await createService({
|
|
appId: 1,
|
|
serviceName: 'one',
|
|
composition: { network_mode: 'host' },
|
|
});
|
|
const svcTwo = await createService({
|
|
appId: 1,
|
|
serviceName: 'two',
|
|
composition: { network_mode: 'host' },
|
|
});
|
|
const current = createApp({
|
|
services: [svcOne, svcTwo],
|
|
networks: [],
|
|
});
|
|
const target = createApp({
|
|
services: [svcOne, svcTwo],
|
|
networks: [],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({ configOnly: true });
|
|
});
|
|
|
|
it('should not create a config-only network if network_mode: host is not specified for any service', async () => {
|
|
const svcOne = await createService({
|
|
appId: 1,
|
|
serviceName: 'one',
|
|
composition: { network_mode: 'host' },
|
|
});
|
|
const svcTwo = await createService({
|
|
appId: 1,
|
|
serviceName: 'two',
|
|
});
|
|
const current = createApp({
|
|
services: [svcOne, svcTwo],
|
|
networks: [],
|
|
});
|
|
const target = createApp({
|
|
services: [svcOne, svcTwo],
|
|
networks: [],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({ configOnly: false });
|
|
});
|
|
|
|
it('should create a config-only network if there are no services in the app', () => {
|
|
const current = createApp({});
|
|
const target = createApp({ isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({ configOnly: true });
|
|
});
|
|
});
|
|
|
|
describe('service state behavior', () => {
|
|
it('should create a kill step for a service which is no longer referenced', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({ appId: 1, serviceName: 'main' }),
|
|
await createService({ appId: 1, serviceName: 'aux' }),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [await createService({ appId: 1, serviceName: 'main' })],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Take lock first
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
// With default download-then-kill, a kill should be inferred
|
|
// once all images in target state are available
|
|
availableImages: [createImage({ serviceName: 'main' })],
|
|
},
|
|
target,
|
|
);
|
|
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
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [killStep] = expectSteps('kill', steps2);
|
|
expect(killStep)
|
|
.to.have.property('current')
|
|
.to.deep.include({ serviceName: 'aux' });
|
|
});
|
|
|
|
it('should emit a noop when a service which is no longer referenced is already stopping', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{ serviceName: 'main' },
|
|
{ state: { status: 'Stopping' } },
|
|
),
|
|
],
|
|
});
|
|
const target = createApp({ services: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectSteps('noop', steps);
|
|
|
|
// Kill was already emitted for this service
|
|
expectNoStep('kill', steps);
|
|
});
|
|
|
|
it('should emit a noop while waiting on a stopping service', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{ serviceName: 'main', running: true },
|
|
{ state: { status: 'Stopping' } },
|
|
),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
services: [await createService({ serviceName: 'main', running: true })],
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectSteps('noop', steps);
|
|
});
|
|
|
|
it('should remove a dead container that is still referenced in the target state', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{ serviceName: 'main' },
|
|
{ state: { status: 'Dead' } },
|
|
),
|
|
],
|
|
});
|
|
const target = createApp({
|
|
services: [await createService({ serviceName: 'main' })],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [removeStep] = expectSteps('remove', steps);
|
|
|
|
expect(removeStep)
|
|
.to.have.property('current')
|
|
.to.deep.include({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should remove a dead container that is not referenced in the target state', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{ serviceName: 'main' },
|
|
{ state: { status: 'Dead' } },
|
|
),
|
|
],
|
|
});
|
|
const target = createApp({ services: [], isTarget: true });
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [removeStep] = expectSteps('remove', steps);
|
|
|
|
expect(removeStep)
|
|
.to.have.property('current')
|
|
.to.deep.include({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should emit a noop when a service has an image downloading', async () => {
|
|
const current = createApp({ services: [] });
|
|
const target = createApp({
|
|
services: [
|
|
await createService({ image: 'main-image', serviceName: 'main' }),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{ ...defaultContext, ...{ downloading: ['main-image'] } },
|
|
target,
|
|
);
|
|
expectSteps('noop', steps);
|
|
expectNoStep('fetch', steps);
|
|
});
|
|
|
|
it('should emit a takeLock followed by an updateMetadata step when a service has not changed but the release has', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'main',
|
|
appId: 1,
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'main',
|
|
appId: 1,
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Take lock before updating metadata
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [takeLockStep] = expectSteps('takeLock', steps);
|
|
expect(takeLockStep)
|
|
.to.have.property('services')
|
|
.that.deep.equals(['main']);
|
|
|
|
// Infer updateMetadata after locks are taken
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [updateMetadataStep] = expectSteps('updateMetadata', steps2);
|
|
expect(updateMetadataStep)
|
|
.to.have.property('current')
|
|
.to.deep.include({ serviceName: 'main', commit: 'old-release' });
|
|
expect(updateMetadataStep)
|
|
.to.have.property('target')
|
|
.to.deep.include({ serviceName: 'main', commit: 'new-release' });
|
|
});
|
|
|
|
it('should stop a container which has `running: false` as its target', async () => {
|
|
const current = createApp({
|
|
services: [await createService({ serviceName: 'main' })],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({ running: false, serviceName: 'main' }),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Take lock first
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [lockStep] = expectSteps('takeLock', steps);
|
|
expect(lockStep)
|
|
.to.have.property('services')
|
|
.that.deep.includes.members(['main']);
|
|
|
|
// Then stop
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [stopStep] = expectSteps('stop', steps2);
|
|
expect(stopStep)
|
|
.to.have.property('current')
|
|
.to.deep.include({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should not try to start a container which has exited', async () => {
|
|
// Container is a "run once" type of service so it has exitted.
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{ composition: { restart: 'yes' }, running: false },
|
|
{ state: { containerId: 'run_once' } },
|
|
),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
|
|
// Now test that another start step is not added on this service
|
|
const target = createApp({
|
|
services: [
|
|
await createService(
|
|
{ composition: { restart: 'always' }, running: false },
|
|
{ state: { containerId: 'run_once' } },
|
|
),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expect(steps.length).to.equal(0);
|
|
expectNoStep('start', steps);
|
|
});
|
|
|
|
it('should recreate a container if the target configuration changes', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [await createService({ appId: 1, serviceName: 'main' })],
|
|
// Default network was already created
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
composition: { privileged: true },
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// 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,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [killStep] = expectSteps('kill', stepsToIntermediate);
|
|
expect(killStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
// Assume the intermediate step has already removed the app
|
|
const intermediate = createApp({
|
|
services: [],
|
|
// Default network was already created
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
|
|
// Then start
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
{
|
|
...contextWithImages,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [startStep] = expectSteps('start', stepsToTarget);
|
|
expect(startStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
expect(startStep)
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.deep.includes({ privileged: true });
|
|
});
|
|
|
|
it('should not start a container when it depends on a service which is being installed', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{
|
|
running: false,
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
},
|
|
{
|
|
state: {
|
|
status: 'Installing',
|
|
containerId: 'dep-id',
|
|
},
|
|
},
|
|
),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
composition: {
|
|
depends_on: ['dep'],
|
|
},
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
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 },
|
|
lock: mockLock,
|
|
};
|
|
|
|
// Only one start step and it should be that of the 'dep' service
|
|
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
target,
|
|
);
|
|
const [startStep] = expectSteps('start', stepsToIntermediate);
|
|
expect(startStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ serviceName: 'dep' });
|
|
|
|
// we now make our current state have the 'dep' service as started...
|
|
const intermediate = createApp({
|
|
services: [
|
|
await createService(
|
|
{ appId: 1, serviceName: 'dep' },
|
|
{ state: { containerId: 'dep-id' } },
|
|
),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
|
|
// we should now see a start for the 'main' service...
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
{ ...contextWithImages, ...{ containerIds: { dep: 'dep-id' } } },
|
|
target,
|
|
);
|
|
const [startMainStep] = expectSteps('start', stepsToTarget);
|
|
expect(startMainStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should not start a container when it depends on a service that is not running', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService(
|
|
{
|
|
running: false,
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
},
|
|
{
|
|
state: {
|
|
containerId: 'dep-id',
|
|
},
|
|
},
|
|
),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
composition: {
|
|
depends_on: ['dep'],
|
|
},
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
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 },
|
|
lock: mockLock,
|
|
};
|
|
|
|
// Only one start step and it should be that of the 'dep' service
|
|
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
|
contextWithImages,
|
|
target,
|
|
);
|
|
expectNoStep('start', stepsToIntermediate);
|
|
expectSteps('noop', stepsToIntermediate);
|
|
|
|
// we now make our current state have the 'dep' service as started...
|
|
const intermediate = createApp({
|
|
services: [
|
|
await createService(
|
|
{ appId: 1, serviceName: 'dep' },
|
|
{ state: { containerId: 'dep-id' } },
|
|
),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
|
|
// we should now see a start for the 'main' service...
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
{ ...contextWithImages, ...{ containerIds: { dep: 'dep-id' } } },
|
|
target,
|
|
);
|
|
const [startMainStep] = expectSteps('start', stepsToTarget);
|
|
expect(startMainStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should not create a start step when all that changes is a running state', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
],
|
|
},
|
|
};
|
|
const current = createApp({
|
|
services: [
|
|
await createService({ running: false, serviceName: 'main' }),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [await createService({ serviceName: 'main' })],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
|
|
// There should be no steps since the engine manages restart policy for stopped containers
|
|
expect(steps.length).to.equal(0);
|
|
});
|
|
|
|
it('should create a kill step when a service release has to be updated but the strategy is kill-then-download', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
],
|
|
},
|
|
};
|
|
|
|
const labels = {
|
|
'io.balena.update.strategy': 'kill-then-download',
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
labels,
|
|
image: 'main-image',
|
|
serviceName: 'main',
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
labels,
|
|
image: 'main-image-2',
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// 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
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [killStep] = expectSteps('kill', stepsToIntermediate);
|
|
expect(killStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
// assume steps were applied
|
|
const intermediate = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
{
|
|
...contextWithImages,
|
|
// Mock locks still taken after kill (releaseLock not
|
|
// yet inferred as state is not yet settled)
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
|
|
const [fetchStep] = expectSteps('fetch', stepsToTarget);
|
|
expect(fetchStep)
|
|
.to.have.property('image')
|
|
.that.deep.includes({ name: 'main-image-2' });
|
|
});
|
|
|
|
it('should not infer a kill step with the default strategy if a dependency is not downloaded', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
downloading: ['dep-image-2'], // The depended service image is being downloaded
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
createImage({ appId: 1, name: 'dep-image', serviceName: 'dep' }),
|
|
createImage({
|
|
appId: 1,
|
|
name: 'main-image-2',
|
|
serviceName: 'main',
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'old-release',
|
|
composition: {
|
|
depends_on: ['dep'],
|
|
},
|
|
}),
|
|
await createService({
|
|
image: 'dep-image',
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image-2',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
composition: {
|
|
depends_on: ['dep'],
|
|
},
|
|
}),
|
|
await createService({
|
|
image: 'dep-image-2',
|
|
appId: 1,
|
|
serviceName: 'dep',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// No kill steps should be generated
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
expectNoStep('kill', steps);
|
|
});
|
|
|
|
it('should create several kill steps as long as there are unmet dependencies', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
createImage({
|
|
appId: 1,
|
|
name: 'main-image-2',
|
|
serviceName: 'main',
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image',
|
|
serviceName: 'main',
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image-2',
|
|
// new release as target
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// 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
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
|
|
const [killStep] = expectSteps('kill', stepsFirstTry);
|
|
expect(killStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
// 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,
|
|
// Mock locks taken from previous step
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
|
|
const [newKillStep] = expectSteps('kill', stepsSecondTry);
|
|
expect(newKillStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should create a kill step when a service config has to be updated but the strategy is kill-then-download', async () => {
|
|
const labels = {
|
|
'io.balena.update.strategy': 'kill-then-download',
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [await createService({ serviceName: 'test', labels })],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
serviceName: 'test',
|
|
labels,
|
|
composition: {
|
|
privileged: true,
|
|
},
|
|
}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
// Take lock first
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [lockStep] = expectSteps('takeLock', steps);
|
|
expect(lockStep)
|
|
.to.have.property('services')
|
|
.that.deep.includes.members(['test']);
|
|
|
|
// Then kill
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [killStep] = expectSteps('kill', steps2);
|
|
expect(killStep)
|
|
.to.have.property('current')
|
|
.that.deep.includes({ serviceName: 'test' });
|
|
});
|
|
|
|
it('should not start a service when a network it depends on is not ready', async () => {
|
|
const current = createApp({ networks: [DEFAULT_NETWORK] });
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
composition: { networks: ['test'] },
|
|
appId: 1,
|
|
}),
|
|
],
|
|
networks: [
|
|
DEFAULT_NETWORK,
|
|
Network.fromComposeObject('test', 1, 'appuuid', {}),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [createNetworkStep] = expectSteps('createNetwork', steps);
|
|
expect(createNetworkStep)
|
|
.to.have.property('target')
|
|
.that.deep.includes({ name: 'test' });
|
|
|
|
// service should not be created yet
|
|
expectNoStep('start', steps);
|
|
});
|
|
|
|
it('should create several kill steps as long as there are no unmet dependencies', async () => {
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'one',
|
|
commit: 'old-release',
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'two',
|
|
commit: 'old-release',
|
|
}),
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'three',
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
appId: 1,
|
|
serviceName: 'three',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
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(
|
|
{
|
|
...contextWithImages,
|
|
// Mock locks already taken
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
expectSteps('kill', steps2, 2);
|
|
});
|
|
|
|
it('should not infer a kill step with the default strategy before all target images have been downloaded', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
downloading: ['other-image-2'], // One of the images is being downloaded
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
createImage({
|
|
appId: 1,
|
|
name: 'other-image',
|
|
serviceName: 'other',
|
|
}),
|
|
createImage({
|
|
appId: 1,
|
|
name: 'main-image-2',
|
|
serviceName: 'main',
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'old-release',
|
|
}),
|
|
await createService({
|
|
image: 'other-image',
|
|
appId: 1,
|
|
serviceName: 'other',
|
|
commit: 'old-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image-2',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
}),
|
|
await createService({
|
|
image: 'other-image-2',
|
|
appId: 1,
|
|
serviceName: 'other',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// No kill steps should be generated
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
expectNoStep('kill', steps);
|
|
});
|
|
|
|
it('should not infer a start step before all target images have been downloaded', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
downloading: ['other-image'], // One of the images is being downloaded
|
|
availableImages: [
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
}),
|
|
await createService({
|
|
image: 'other-image',
|
|
appId: 1,
|
|
serviceName: 'other',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// No kill steps should be generated
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
expectNoStep('start', steps);
|
|
});
|
|
|
|
it('should infer a start step only when target images have been downloaded', async () => {
|
|
const current = createApp({
|
|
services: [],
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services: [
|
|
await createService({
|
|
image: 'main-image',
|
|
appId: 1,
|
|
serviceName: 'main',
|
|
commit: 'new-release',
|
|
}),
|
|
await createService({
|
|
image: 'other-image',
|
|
appId: 1,
|
|
serviceName: 'other',
|
|
commit: 'new-release',
|
|
}),
|
|
],
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
// 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, Infinity);
|
|
|
|
// 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',
|
|
}),
|
|
],
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
expectSteps('start', steps3, 2);
|
|
});
|
|
});
|
|
|
|
describe('image state behavior', () => {
|
|
it('should emit a fetch step when an image has not been downloaded for a service', async () => {
|
|
const current = createApp({ services: [] });
|
|
const target = createApp({
|
|
services: [await createService({ serviceName: 'main' })],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const [fetchStep] = expectSteps('fetch', steps);
|
|
expect(fetchStep)
|
|
.to.have.property('image')
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
});
|
|
|
|
it('should not infer a fetch step when the download is already in progress', async () => {
|
|
const contextWithDownloading = {
|
|
...defaultContext,
|
|
...{
|
|
downloading: ['image2'],
|
|
},
|
|
};
|
|
const current = createApp({ services: [] });
|
|
const target = createApp({
|
|
services: [
|
|
await createService({ image: 'image2', serviceName: 'main' }),
|
|
],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
contextWithDownloading,
|
|
target,
|
|
);
|
|
expectNoStep('fetch', steps);
|
|
});
|
|
|
|
it('should not infer a kill step with the default strategy if a dependency is not downloaded', async () => {
|
|
const current = createApp({
|
|
services: [await createService({ image: 'image1' })],
|
|
});
|
|
const target = createApp({
|
|
services: [await createService({ image: 'image2' })],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [fetchStep] = expectSteps('fetch', steps);
|
|
expect(fetchStep)
|
|
.to.have.property('image')
|
|
.that.deep.includes({ name: 'image2' });
|
|
|
|
expectNoStep('kill', steps);
|
|
});
|
|
});
|
|
|
|
describe('update lock state behavior', () => {
|
|
it('should not infer a releaseLock step if there are locks to be released before settling target state', async () => {
|
|
const services = [
|
|
await createService({ serviceName: 'server' }),
|
|
await createService({ serviceName: 'client' }),
|
|
];
|
|
const current = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [releaseLockStep] = expectSteps('releaseLock', steps, 1);
|
|
expect(releaseLockStep).to.have.property('appId').that.equals(1);
|
|
|
|
// Even if not all the locks are taken, releaseLock should be inferred
|
|
const steps2 = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [releaseLockStep2] = expectSteps('releaseLock', steps2, 1);
|
|
expect(releaseLockStep2).to.have.property('appId').that.equals(1);
|
|
});
|
|
|
|
it('should infer a releaseLock step if there are leftover locks', async () => {
|
|
const services = [
|
|
await createService({ serviceName: 'server' }),
|
|
await createService({ serviceName: 'client' }),
|
|
];
|
|
const current = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{ ...defaultContext, hasLeftoverLocks: true },
|
|
target,
|
|
);
|
|
expect(steps).to.have.length(1);
|
|
expectSteps('releaseLock', steps);
|
|
});
|
|
|
|
it('should not infer a releaseLock step if there are no locks to be released', async () => {
|
|
const services = [
|
|
await createService({ serviceName: 'server' }),
|
|
await createService({ serviceName: 'client' }),
|
|
];
|
|
const current = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expect(steps).to.have.length(0);
|
|
});
|
|
|
|
it('should infer a releaseLock step for the current appId only', async () => {
|
|
const services = [
|
|
await createService({ serviceName: 'server' }),
|
|
await createService({ serviceName: 'client' }),
|
|
];
|
|
const current = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
});
|
|
const target = createApp({
|
|
services,
|
|
networks: [DEFAULT_NETWORK],
|
|
isTarget: true,
|
|
});
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{
|
|
...defaultContext,
|
|
lock: mockLock,
|
|
},
|
|
target,
|
|
);
|
|
const [releaseLockStep] = expectSteps('releaseLock', steps, 1);
|
|
expect(releaseLockStep).to.have.property('appId').that.equals(1);
|
|
});
|
|
});
|
|
});
|