2021-05-05 20:32:31 +00:00
|
|
|
import { expect } from 'chai';
|
2022-08-17 23:35:08 +00:00
|
|
|
import App from '~/src/compose/app';
|
2021-05-05 20:32:31 +00:00
|
|
|
import {
|
|
|
|
CompositionStep,
|
|
|
|
CompositionStepAction,
|
2022-08-17 23:35:08 +00:00
|
|
|
} from '~/src/compose/composition-steps';
|
|
|
|
import { Image } from '~/src/compose/images';
|
|
|
|
import Network from '~/src/compose/network';
|
|
|
|
import Service from '~/src/compose/service';
|
|
|
|
import { ServiceComposeConfig } from '~/src/compose/types/service';
|
|
|
|
import Volume from '~/src/compose/volume';
|
2021-05-05 20:32:31 +00:00
|
|
|
|
|
|
|
const defaultContext = {
|
2023-04-06 17:50:10 +00:00
|
|
|
keepVolumes: false,
|
2021-07-23 16:48:27 +00:00
|
|
|
availableImages: [] as Image[],
|
2021-05-05 20:32:31 +00:00
|
|
|
containerIds: {},
|
2021-07-23 16:48:27 +00:00
|
|
|
downloading: [] as string[],
|
2021-05-05 20:32:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
function createApp({
|
|
|
|
services = [] as Service[],
|
|
|
|
networks = [] as Network[],
|
|
|
|
volumes = [] as Volume[],
|
|
|
|
isTarget = false,
|
|
|
|
appId = 1,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid = 'appuuid',
|
2021-05-05 20:32:31 +00:00
|
|
|
} = {}) {
|
|
|
|
return new App(
|
|
|
|
{
|
|
|
|
appId,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid,
|
2021-05-05 20:32:31 +00:00
|
|
|
services,
|
2023-02-10 19:13:38 +00:00
|
|
|
networks,
|
|
|
|
volumes,
|
2021-05-05 20:32:31 +00:00
|
|
|
},
|
|
|
|
isTarget,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createService(
|
|
|
|
{
|
|
|
|
appId = 1,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid = 'appuuid',
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName = 'test',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit = 'test-commit',
|
|
|
|
...conf
|
|
|
|
} = {} as Partial<ServiceComposeConfig>,
|
|
|
|
{ state = {} as Partial<Service>, options = {} as any } = {},
|
2021-05-05 20:32:31 +00:00
|
|
|
) {
|
|
|
|
const svc = await Service.fromComposeObject(
|
|
|
|
{
|
|
|
|
appId,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid,
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName,
|
2021-07-23 16:48:27 +00:00
|
|
|
commit,
|
2021-08-03 23:12:47 +00:00
|
|
|
running: true,
|
2021-05-05 20:32:31 +00:00
|
|
|
...conf,
|
|
|
|
},
|
2021-07-23 16:48:27 +00:00
|
|
|
options,
|
2021-05-05 20:32:31 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Add additonal configuration
|
|
|
|
for (const k of Object.keys(state)) {
|
|
|
|
(svc as any)[k] = (state as any)[k];
|
|
|
|
}
|
|
|
|
return svc;
|
|
|
|
}
|
|
|
|
|
2021-07-23 16:48:27 +00:00
|
|
|
function createImage(
|
|
|
|
{
|
|
|
|
appId = 1,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid = 'appuuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
name = 'test-image',
|
|
|
|
serviceName = 'test',
|
2021-08-25 23:25:47 +00:00
|
|
|
commit = 'test-commit',
|
2021-07-23 16:48:27 +00:00
|
|
|
...extra
|
|
|
|
} = {} as Partial<Image>,
|
|
|
|
) {
|
2021-08-27 19:22:04 +00:00
|
|
|
return {
|
|
|
|
appId,
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid,
|
|
|
|
commit,
|
2021-08-27 19:22:04 +00:00
|
|
|
name,
|
|
|
|
serviceName,
|
|
|
|
...extra,
|
|
|
|
} as Image;
|
2021-07-23 16:48:27 +00:00
|
|
|
}
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
const expectSteps = (
|
|
|
|
action: CompositionStepAction,
|
|
|
|
steps: CompositionStep[],
|
|
|
|
min = 1,
|
|
|
|
max = min,
|
|
|
|
message = `Expected to find ${min} step(s) with action '${action}', instead found ${JSON.stringify(
|
|
|
|
steps.map((s) => s.action),
|
|
|
|
)}`,
|
|
|
|
) => {
|
|
|
|
const filtered = steps.filter((s) => s.action === action);
|
|
|
|
|
|
|
|
if (filtered.length < min || filtered.length > max) {
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
return filtered;
|
|
|
|
};
|
|
|
|
|
|
|
|
function expectNoStep(action: CompositionStepAction, steps: CompositionStep[]) {
|
|
|
|
expectSteps(action, steps, 0, 0);
|
|
|
|
}
|
|
|
|
|
2021-08-25 23:25:47 +00:00
|
|
|
const defaultNetwork = Network.fromComposeObject('default', 1, 'appuuid', {});
|
2021-05-05 20:32:31 +00:00
|
|
|
|
|
|
|
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({
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
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: [
|
2021-08-25 21:50:34 +00:00
|
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef'),
|
|
|
|
Volume.fromComposeObject('test-volume-2', 1, 'deadbeef'),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
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: [
|
2021-08-25 21:50:34 +00:00
|
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef'),
|
|
|
|
Volume.fromComposeObject('test-volume-2', 1, 'deadbeef'),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume-2', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
expectNoStep('removeVolume', steps);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should correctly infer volume recreation steps', () => {
|
|
|
|
const current = createApp({
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
volumes: [
|
2021-08-25 21:50:34 +00:00
|
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
2021-05-05 20:32:31 +00:00
|
|
|
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')
|
2021-08-25 21:50:34 +00:00
|
|
|
.that.has.property('name')
|
|
|
|
.that.equals('test-volume');
|
|
|
|
expect(removalStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.has.property('appId')
|
|
|
|
.that.equals(1);
|
2021-05-05 20:32:31 +00:00
|
|
|
|
|
|
|
// 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({
|
2021-08-25 21:50:34 +00:00
|
|
|
labels: {
|
|
|
|
'io.balena.supervised': 'true',
|
|
|
|
'io.balena.app-uuid': 'deadbeef',
|
|
|
|
test: 'test',
|
|
|
|
},
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should kill dependencies of a volume before changing config', async () => {
|
|
|
|
const current = createApp({
|
2021-08-27 19:22:04 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: { volumes: ['test-volume:/data'] },
|
2021-08-27 19:22:04 +00:00
|
|
|
}),
|
|
|
|
],
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-08-27 19:22:04 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: { volumes: ['test-volume:/data'] },
|
2021-08-27 19:22:04 +00:00
|
|
|
}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
volumes: [
|
2021-08-25 21:50:34 +00:00
|
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
2021-05-05 20:32:31 +00:00
|
|
|
labels: { test: 'test' },
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Calculate steps
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
|
|
|
|
const [killStep] = expectSteps('kill', steps);
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'test' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should correctly infer to remove an app volumes when the app is being removed', async () => {
|
|
|
|
const current = createApp({
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await 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 () => {
|
2021-08-27 19:22:04 +00:00
|
|
|
const service = await createService({
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: { volumes: ['test-volume:/data'] },
|
2021-08-27 19:22:04 +00:00
|
|
|
});
|
2021-05-05 20:32:31 +00:00
|
|
|
service.status = 'Stopping';
|
|
|
|
const current = createApp({
|
|
|
|
services: [service],
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 1, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [service],
|
|
|
|
volumes: [
|
2021-08-25 21:50:34 +00:00
|
|
|
Volume.fromComposeObject('test-volume', 1, 'deadbeef', {
|
2021-05-05 20:32:31 +00:00
|
|
|
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({
|
2021-08-25 23:25:47 +00:00
|
|
|
appId: 1,
|
|
|
|
appUuid: 'deadbeef',
|
2021-05-05 20:32:31 +00:00
|
|
|
image: 'test-image',
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: { volumes: ['db-volume:/data'] },
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
2021-08-25 23:25:47 +00:00
|
|
|
const volume = Volume.fromComposeObject('db-volume', 1, 'deadbeef');
|
2021-05-05 20:32:31 +00:00
|
|
|
const contextWithImages = {
|
|
|
|
...defaultContext,
|
|
|
|
...{
|
|
|
|
availableImages: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: service.appId,
|
|
|
|
name: 'test-image',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
// Temporarily set target services & volumes to empty, as in doPurge
|
|
|
|
const intermediateTarget = createApp({
|
|
|
|
services: [],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Generate initial state with one service & one volume
|
|
|
|
const current = createApp({
|
|
|
|
services: [service],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
volumes: [volume],
|
|
|
|
});
|
|
|
|
|
|
|
|
// Step 1: kill
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
intermediateTarget,
|
|
|
|
);
|
|
|
|
expectSteps('kill', steps);
|
|
|
|
|
|
|
|
// Step 2: noop (service is stopping)
|
|
|
|
service.status = 'Stopping';
|
|
|
|
const secondStageSteps = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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: [defaultNetwork],
|
|
|
|
volumes: [volume],
|
|
|
|
});
|
|
|
|
expect(
|
|
|
|
currentWithServiceRemoved.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
intermediateTarget,
|
|
|
|
),
|
|
|
|
).to.have.length(0);
|
|
|
|
|
|
|
|
// Simulate volume removal
|
|
|
|
const currentWithVolumesRemoved = createApp({
|
|
|
|
services: [],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
volumes: [],
|
|
|
|
});
|
|
|
|
|
2021-08-27 19:22:04 +00:00
|
|
|
// Step 3: createVolume
|
2021-05-05 20:32:31 +00:00
|
|
|
service.status = 'Running';
|
|
|
|
const target = createApp({
|
|
|
|
services: [service],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
volumes: [volume],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
2022-09-19 15:33:52 +00:00
|
|
|
const recreateVolumeSteps =
|
|
|
|
currentWithVolumesRemoved.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
target,
|
|
|
|
);
|
2021-08-27 19:22:04 +00:00
|
|
|
|
|
|
|
expect(recreateVolumeSteps).to.have.length(1);
|
|
|
|
expectSteps('createVolume', recreateVolumeSteps);
|
|
|
|
|
|
|
|
// Final step: start service
|
|
|
|
const currentWithVolumeRecreated = createApp({
|
|
|
|
services: [],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
volumes: [volume],
|
|
|
|
});
|
|
|
|
|
2022-09-19 15:33:52 +00:00
|
|
|
const createServiceSteps =
|
|
|
|
currentWithVolumeRecreated.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
target,
|
|
|
|
);
|
2021-08-27 19:22:04 +00:00
|
|
|
expectSteps('start', createServiceSteps);
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('network state behavior', () => {
|
|
|
|
it('should correctly infer a network create step', () => {
|
|
|
|
const current = createApp({ networks: [] });
|
|
|
|
const target = createApp({
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
2021-05-05 20:32:31 +00:00
|
|
|
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({
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
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',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-02-10 19:13:38 +00:00
|
|
|
it('should correctly remove default duplicate networks', () => {
|
|
|
|
const current = createApp({
|
|
|
|
networks: [defaultNetwork, defaultNetwork],
|
|
|
|
});
|
|
|
|
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: [
|
|
|
|
defaultNetwork,
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
],
|
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
appUuid: 'deadbeef',
|
|
|
|
composition: { networks: ['test-network'] },
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
networks: [
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
],
|
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
appUuid: 'deadbeef',
|
|
|
|
composition: { networks: ['test-network'] },
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
|
|
|
|
const [removeNetworkStep] = expectSteps('kill', steps);
|
|
|
|
|
|
|
|
expect(removeNetworkStep).to.have.property('current').that.deep.includes({
|
|
|
|
serviceName: 'test',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
it('should correctly infer more than one network removal step', () => {
|
|
|
|
const current = createApp({
|
|
|
|
networks: [
|
2021-08-25 23:25:47 +00:00
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
Network.fromComposeObject('test-network-2', 1, 'deadbeef', {}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
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({
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
networks: [
|
2021-08-25 23:25:47 +00:00
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {
|
2021-05-05 20:32:31 +00:00
|
|
|
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')
|
2021-08-25 23:25:47 +00:00
|
|
|
.that.deep.includes({
|
|
|
|
labels: { TEST: 'TEST', 'io.balena.app-id': '1' },
|
|
|
|
});
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should kill dependencies of networks before removing', async () => {
|
|
|
|
const current = createApp({
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid: 'deadbeef',
|
2021-08-03 23:12:47 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
2021-08-25 23:25:47 +00:00
|
|
|
appId: 1,
|
|
|
|
appUuid: 'deadbeef',
|
|
|
|
composition: { networks: ['test-network'] },
|
2021-08-03 23:12:47 +00:00
|
|
|
}),
|
|
|
|
],
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [
|
|
|
|
Network.fromComposeObject('test-network', 1, 'deadbeef', {}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-08-25 23:25:47 +00:00
|
|
|
appUuid: 'deadbeef',
|
|
|
|
services: [await createService({ appUuid: 'deadbeef' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
|
|
|
|
const [killStep] = expectSteps('kill', steps);
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'test' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should kill dependencies of networks before changing config', async () => {
|
|
|
|
const current = createApp({
|
2021-08-03 23:12:47 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
2021-08-25 23:25:47 +00:00
|
|
|
composition: { networks: ['test-network'] },
|
2021-08-03 23:12:47 +00:00
|
|
|
}),
|
|
|
|
],
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [Network.fromComposeObject('test-network', 1, 'appuuid', {})],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-08-03 23:12:47 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
composition: { networks: { 'test-network': {} } },
|
|
|
|
}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [
|
2021-08-25 23:25:47 +00:00
|
|
|
Network.fromComposeObject('test-network', 1, 'appuuid', {
|
2021-05-05 20:32:31 +00:00
|
|
|
labels: { test: 'test' },
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [killStep] = expectSteps('kill', steps);
|
|
|
|
|
|
|
|
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
|
|
|
|
expectNoStep('removeNetwork', steps);
|
|
|
|
});
|
|
|
|
|
|
|
|
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({
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [Network.fromComposeObject('default', 1, 'deadbeef', {})],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({ networks: [], isTarget: true });
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
|
|
|
|
// The network should not be created again
|
|
|
|
expectNoStep('createNetwork', steps);
|
|
|
|
});
|
2022-11-15 04:22:45 +00:00
|
|
|
|
|
|
|
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', async () => {
|
|
|
|
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 });
|
|
|
|
});
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('service state behavior', () => {
|
2021-08-25 23:25:47 +00:00
|
|
|
it('should create a kill step for a service which is no longer referenced', async () => {
|
2021-05-05 20:32:31 +00:00
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ appId: 1, serviceName: 'main' }),
|
|
|
|
await createService({ appId: 1, serviceName: 'aux' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [defaultNetwork],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ appId: 1, serviceName: 'main' })],
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [defaultNetwork],
|
2021-05-05 20:32:31 +00:00
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [killStep] = expectSteps('kill', steps);
|
|
|
|
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(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ serviceName: 'main' },
|
|
|
|
{ state: { status: 'Stopping' } },
|
2021-05-05 20:32:31 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2022-08-02 21:34:25 +00:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
it('should remove a dead container that is still referenced in the target state', async () => {
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
|
|
|
await createService(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ serviceName: 'main' },
|
|
|
|
{ state: { status: 'Dead' } },
|
2021-05-05 20:32:31 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ serviceName: 'main' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
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(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ serviceName: 'main' },
|
|
|
|
{ state: { status: 'Dead' } },
|
2021-05-05 20:32:31 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
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: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ image: 'main-image', serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ ...defaultContext, ...{ downloading: ['main-image'] } },
|
2021-05-05 20:32:31 +00:00
|
|
|
target,
|
|
|
|
);
|
|
|
|
expectSteps('noop', steps);
|
|
|
|
expectNoStep('fetch', steps);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should emit an updateMetadata step when a service has not changed but the release has', async () => {
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ serviceName: 'main', commit: 'old-release' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ serviceName: 'main', commit: 'new-release' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [updateMetadataStep] = expectSteps('updateMetadata', steps);
|
|
|
|
|
|
|
|
expect(updateMetadataStep)
|
|
|
|
.to.have.property('current')
|
2021-07-23 16:48:27 +00:00
|
|
|
.to.deep.include({ serviceName: 'main', commit: 'old-release' });
|
2021-05-05 20:32:31 +00:00
|
|
|
|
|
|
|
expect(updateMetadataStep)
|
|
|
|
.to.have.property('target')
|
2021-07-23 16:48:27 +00:00
|
|
|
.to.deep.include({ serviceName: 'main', commit: 'new-release' });
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should stop a container which has `running: false` as its target', async () => {
|
|
|
|
const current = createApp({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ serviceName: 'main' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ running: false, serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [stopStep] = expectSteps('stop', steps);
|
|
|
|
expect(stopStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.to.deep.include({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not try to start a container which has exited and has restart policy of no', async () => {
|
|
|
|
// Container is a "run once" type of service so it has exitted.
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
|
|
|
await createService(
|
2021-08-03 23:12:47 +00:00
|
|
|
{ composition: { restart: 'no' }, running: false },
|
2021-05-05 20:32:31 +00:00
|
|
|
{ state: { containerId: 'run_once' } },
|
|
|
|
),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
// Now test that another start step is not added on this service
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
|
|
|
await createService(
|
2021-08-03 23:12:47 +00:00
|
|
|
{ composition: { restart: 'no' }, running: false },
|
2021-05-05 20:32:31 +00:00
|
|
|
{ state: { containerId: 'run_once' } },
|
|
|
|
),
|
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
expectNoStep('start', steps);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recreate a container if the target configuration changes', async () => {
|
|
|
|
const contextWithImages = {
|
|
|
|
...defaultContext,
|
|
|
|
...{
|
|
|
|
availableImages: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const current = createApp({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ appId: 1, serviceName: 'main' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
// Default network was already created
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: { privileged: true },
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// should see a 'stop'
|
|
|
|
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [defaultNetwork],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// now should see a 'start'
|
|
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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 availableImages = [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, serviceName: 'main', name: 'main-image' }),
|
|
|
|
createImage({ appId: 1, serviceName: 'dep', name: 'dep-image' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
];
|
|
|
|
const contextWithImages = { ...defaultContext, ...{ availableImages } };
|
|
|
|
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
|
|
|
await createService(
|
|
|
|
{
|
2021-07-23 16:48:27 +00:00
|
|
|
running: false,
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
|
|
|
serviceName: 'dep',
|
2021-07-23 16:48:27 +00:00
|
|
|
},
|
|
|
|
{
|
2021-05-05 20:32:31 +00:00
|
|
|
state: {
|
|
|
|
status: 'Installing',
|
|
|
|
containerId: 'dep-id',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: {
|
|
|
|
depends_on: ['dep'],
|
|
|
|
},
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'dep',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
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')
|
|
|
|
.that.deep.includes({ serviceName: 'dep' });
|
|
|
|
|
|
|
|
// we now make our current state have the 'dep' service as started...
|
|
|
|
const intermediate = createApp({
|
|
|
|
services: [
|
|
|
|
await createService(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ appId: 1, serviceName: 'dep' },
|
|
|
|
{ state: { containerId: 'dep-id' } },
|
2021-05-05 20:32:31 +00:00
|
|
|
),
|
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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' });
|
|
|
|
});
|
|
|
|
|
2022-04-12 06:05:56 +00:00
|
|
|
it('should not create a start step when all that changes is a running state', async () => {
|
2021-05-05 20:32:31 +00:00
|
|
|
const contextWithImages = {
|
|
|
|
...defaultContext,
|
|
|
|
...{
|
|
|
|
availableImages: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ running: false, serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ serviceName: 'main' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
|
|
|
2022-04-12 06:05:56 +00:00
|
|
|
// There should be no steps since the engine manages restart policy for stopped containers
|
|
|
|
expect(steps.length).to.equal(0);
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const labels = {
|
|
|
|
'io.balena.update.strategy': 'kill-then-download',
|
|
|
|
};
|
|
|
|
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
labels,
|
|
|
|
image: 'main-image',
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'old-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
labels,
|
|
|
|
image: 'main-image-2',
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const stepsToIntermediate = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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: [defaultNetwork],
|
|
|
|
});
|
|
|
|
|
|
|
|
const stepsToTarget = intermediate.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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,
|
|
|
|
...{
|
2021-07-23 16:48:27 +00:00
|
|
|
downloading: ['dep-image-2'], // The depended service image is being downloaded
|
2021-05-05 20:32:31 +00:00
|
|
|
availableImages: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
|
|
createImage({ appId: 1, name: 'dep-image', serviceName: 'dep' }),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
|
|
|
name: 'main-image-2',
|
|
|
|
serviceName: 'main',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'old-release',
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: {
|
|
|
|
depends_on: ['dep'],
|
|
|
|
},
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
await createService({
|
|
|
|
image: 'dep-image',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'dep',
|
|
|
|
commit: 'old-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image-2',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'new-release',
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: {
|
|
|
|
depends_on: ['dep'],
|
|
|
|
},
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
await createService({
|
|
|
|
image: 'dep-image-2',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'dep',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
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: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({ appId: 1, name: 'main-image', serviceName: 'main' }),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
|
|
|
name: 'main-image-2',
|
|
|
|
serviceName: 'main',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const current = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'old-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image-2',
|
2021-05-05 20:32:31 +00:00
|
|
|
// new release as target
|
2021-07-23 16:48:27 +00:00
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const stepsFirstTry = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
target,
|
|
|
|
);
|
|
|
|
|
|
|
|
const [killStep] = expectSteps('kill', stepsFirstTry);
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
|
|
|
|
// if at first you don't succeed
|
|
|
|
const stepsSecondTry = current.nextStepsForAppUpdate(
|
|
|
|
contextWithImages,
|
|
|
|
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')
|
|
|
|
.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({ labels })],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
labels,
|
2021-08-03 23:12:47 +00:00
|
|
|
composition: {
|
|
|
|
privileged: true,
|
|
|
|
},
|
2021-05-05 20:32:31 +00:00
|
|
|
}),
|
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const [killStep] = expectSteps('kill', steps);
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'test' });
|
|
|
|
});
|
|
|
|
|
2021-11-10 20:22:40 +00:00
|
|
|
it('should not start a service when a network it depends on is not ready', async () => {
|
2021-05-05 20:32:31 +00:00
|
|
|
const current = createApp({ networks: [defaultNetwork] });
|
|
|
|
const target = createApp({
|
2021-08-03 23:12:47 +00:00
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
composition: { networks: ['test'] },
|
|
|
|
appId: 1,
|
|
|
|
}),
|
|
|
|
],
|
2021-08-25 23:25:47 +00:00
|
|
|
networks: [
|
|
|
|
defaultNetwork,
|
|
|
|
Network.fromComposeObject('test', 1, 'appuuid', {}),
|
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
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: [
|
2021-07-23 16:48:27 +00:00
|
|
|
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',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'three',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
expectSteps('kill', steps, 2);
|
|
|
|
});
|
2021-11-10 20:22:40 +00:00
|
|
|
|
|
|
|
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: [defaultNetwork],
|
|
|
|
});
|
|
|
|
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: [defaultNetwork],
|
|
|
|
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: [defaultNetwork],
|
|
|
|
});
|
|
|
|
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: [defaultNetwork],
|
|
|
|
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 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: [defaultNetwork],
|
|
|
|
});
|
|
|
|
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: [defaultNetwork],
|
|
|
|
isTarget: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
// No kill steps should be generated
|
|
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
|
|
expectSteps('start', steps, 2);
|
|
|
|
});
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ serviceName: 'main' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
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,
|
|
|
|
...{
|
2021-07-23 16:48:27 +00:00
|
|
|
downloading: ['image2'],
|
2021-05-05 20:32:31 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
const current = createApp({ services: [] });
|
|
|
|
const target = createApp({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({ image: 'image2', serviceName: 'main' }),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|