mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 21:57:54 +00:00
77333f1e11
Closes: #1476 Change-type: patch Signed-off-by: Miguel Casqueira <miguel@balena.io>
1096 lines
27 KiB
TypeScript
1096 lines
27 KiB
TypeScript
import * as _ from 'lodash';
|
|
import { expect } from 'chai';
|
|
|
|
import * as appMock from './lib/application-state-mock';
|
|
|
|
import * as applicationManager from '../src/compose/application-manager';
|
|
import * as deviceState from '../src/device-state';
|
|
import App from '../src/compose/app';
|
|
import * as config from '../src/config';
|
|
import * as dbFormat from '../src/device-state/db-format';
|
|
|
|
import Service from '../src/compose/service';
|
|
import Network from '../src/compose/network';
|
|
import Volume from '../src/compose/volume';
|
|
import {
|
|
CompositionStep,
|
|
CompositionStepAction,
|
|
} from '../src/compose/composition-steps';
|
|
import { ServiceComposeConfig } from '../src/compose/types/service';
|
|
import { Image } from '../src/compose/images';
|
|
import { inspect } from 'util';
|
|
|
|
const defaultContext = {
|
|
localMode: false,
|
|
availableImages: [],
|
|
containerIds: {},
|
|
downloading: [],
|
|
};
|
|
|
|
function createApp(
|
|
services: Service[],
|
|
networks: Network[],
|
|
volumes: Volume[],
|
|
target: boolean,
|
|
appId = 1,
|
|
) {
|
|
return new App(
|
|
{
|
|
appId,
|
|
services,
|
|
networks: _.keyBy(networks, 'name'),
|
|
volumes: _.keyBy(volumes, 'name'),
|
|
},
|
|
target,
|
|
);
|
|
}
|
|
|
|
async function createService(
|
|
conf: Partial<ServiceComposeConfig>,
|
|
appId = 1,
|
|
serviceName = 'test',
|
|
releaseId = 2,
|
|
serviceId = 3,
|
|
imageId = 4,
|
|
extraState?: Partial<Service>,
|
|
) {
|
|
const svc = await Service.fromComposeObject(
|
|
{
|
|
appId,
|
|
serviceName,
|
|
releaseId,
|
|
serviceId,
|
|
imageId,
|
|
...conf,
|
|
},
|
|
{} as any,
|
|
);
|
|
if (extraState != null) {
|
|
for (const k of Object.keys(extraState)) {
|
|
(svc as any)[k] = (extraState as any)[k];
|
|
}
|
|
}
|
|
return svc;
|
|
}
|
|
|
|
type ServicePredicate = string | ((service: Partial<Service>) => boolean);
|
|
type StepTest = Chai.Assertion & {
|
|
asStep?: CompositionStep;
|
|
forCurrent: (predicate: ServicePredicate) => Chai.Assertion;
|
|
forTarget: (predicate: ServicePredicate) => Chai.Assertion;
|
|
};
|
|
|
|
// tslint:disable: no-unused-expression-chai
|
|
function withSteps(steps: CompositionStep[]) {
|
|
return {
|
|
expectStep: (action: CompositionStepAction): StepTest => {
|
|
const matchingSteps = _.filter(steps, (step) => step.action === action);
|
|
|
|
const assertion: Partial<StepTest> = expect(
|
|
matchingSteps,
|
|
`Step for '${action}', not found`,
|
|
);
|
|
|
|
const forService = (
|
|
predicate: ServicePredicate,
|
|
property: 'current' | 'target',
|
|
) => {
|
|
const [firstMatch] = _.filter(matchingSteps, (s) => {
|
|
const t = (s as any)[property];
|
|
if (!t) {
|
|
throw new Error(`${property} is not defined for action ${action}`);
|
|
}
|
|
|
|
if (_.isFunction(predicate)) {
|
|
return predicate(t);
|
|
} else {
|
|
return t.serviceName! === predicate;
|
|
}
|
|
});
|
|
return expect(
|
|
firstMatch,
|
|
`Step for '${action}' matching predicate, not found`,
|
|
);
|
|
};
|
|
|
|
assertion.asStep = matchingSteps[0];
|
|
assertion.forCurrent = (service) => forService(service, 'current');
|
|
assertion.forTarget = (service) => forService(service, 'target');
|
|
|
|
return assertion as StepTest;
|
|
},
|
|
rejectStep: (action: CompositionStepAction) => expectNoStep(action, steps),
|
|
};
|
|
}
|
|
|
|
function expectStep(
|
|
action: CompositionStepAction,
|
|
steps: CompositionStep[],
|
|
): number {
|
|
const idx = _.findIndex(steps, { action });
|
|
if (idx === -1) {
|
|
console.log(inspect({ action, steps }, true, 3, true));
|
|
throw new Error(`Expected to find step with action: ${action}`);
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
function expectNoStep(action: CompositionStepAction, steps: CompositionStep[]) {
|
|
if (_.some(steps, { action })) {
|
|
console.log(inspect({ action, steps }, true, 3, true));
|
|
throw new Error(`Did not expect to find step with action: ${action}`);
|
|
}
|
|
}
|
|
|
|
const defaultNetwork = Network.fromComposeObject('default', 1, {});
|
|
|
|
describe('compose/app', () => {
|
|
before(async () => {
|
|
await config.initialized;
|
|
await applicationManager.initialized;
|
|
});
|
|
beforeEach(() => {
|
|
// Sane defaults
|
|
appMock.mockSupervisorNetwork(true);
|
|
appMock.mockManagers([], [], []);
|
|
appMock.mockImages([], false, []);
|
|
});
|
|
afterEach(() => {
|
|
appMock.unmockAll();
|
|
});
|
|
|
|
it('should correctly infer a volume create step', () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[],
|
|
[],
|
|
[Volume.fromComposeObject('test-volume', 1, {})],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const idx = expectStep('createVolume', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('name')
|
|
.that.equals('test-volume');
|
|
});
|
|
|
|
it('should correctly infer more than one volume create step', () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[],
|
|
[],
|
|
[
|
|
Volume.fromComposeObject('test-volume', 1, {}),
|
|
Volume.fromComposeObject('test-volume-2', 1, {}),
|
|
],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
let idx = expectStep('createVolume', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('name')
|
|
.that.equals('test-volume');
|
|
delete steps[idx];
|
|
idx = expectStep('createVolume', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('name')
|
|
.that.equals('test-volume-2');
|
|
});
|
|
|
|
// We don't remove volumes until the end
|
|
it('should correctly not infer a volume remove step when the app is still referenced', () => {
|
|
const current = createApp(
|
|
[],
|
|
[],
|
|
[
|
|
Volume.fromComposeObject('test-volume', 1, {}),
|
|
Volume.fromComposeObject('test-volume-2', 1, {}),
|
|
],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[],
|
|
[],
|
|
[Volume.fromComposeObject('test-volume-2', 1, {})],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
expect(() => {
|
|
expectStep('removeVolume', steps);
|
|
}).to.throw();
|
|
});
|
|
|
|
it('should correctly infer volume recreation steps', () => {
|
|
const current = createApp(
|
|
[],
|
|
[],
|
|
[Volume.fromComposeObject('test-volume', 1, {})],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[],
|
|
[],
|
|
[
|
|
Volume.fromComposeObject('test-volume', 1, {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
true,
|
|
);
|
|
|
|
let steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
let idx = expectStep('removeVolume', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('config')
|
|
.that.has.property('labels')
|
|
.that.deep.equals({ 'io.balena.supervised': 'true' });
|
|
|
|
current.volumes = {};
|
|
steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
idx = expectStep('createVolume', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('config')
|
|
.that.has.property('labels')
|
|
.that.deep.equals({ 'io.balena.supervised': 'true', test: 'test' });
|
|
});
|
|
|
|
it('should kill dependencies of a volume before changing config', async () => {
|
|
const current = createApp(
|
|
[await createService({ volumes: ['test-volume'] })],
|
|
[],
|
|
[Volume.fromComposeObject('test-volume', 1, {})],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ volumes: ['test-volume'] })],
|
|
[],
|
|
[
|
|
Volume.fromComposeObject('test-volume', 1, {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
|
|
const idx = expectStep('kill', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('serviceName')
|
|
.that.equals('test');
|
|
});
|
|
|
|
it('should correctly infer to remove an apps volumes when it is no longer referenced', async () => {
|
|
appMock.mockManagers(
|
|
[],
|
|
[Volume.fromComposeObject('test-volume', 1, {})],
|
|
[],
|
|
);
|
|
appMock.mockImages([], false, []);
|
|
|
|
const origFn = dbFormat.getApps;
|
|
// @ts-expect-error Assigning to a RO property
|
|
dbFormat.getApps = () => Promise.resolve({});
|
|
const target = await deviceState.getTarget();
|
|
|
|
try {
|
|
const steps = await applicationManager.getRequiredSteps(
|
|
target.local.apps,
|
|
);
|
|
expect(steps).to.have.length(1);
|
|
expect(steps[0]).to.have.property('action').that.equals('removeVolume');
|
|
} finally {
|
|
// @ts-expect-error Assigning to a RO property
|
|
dbFormat.getApps = origFn;
|
|
}
|
|
});
|
|
|
|
it('should correctly infer a network create step', () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[],
|
|
[Network.fromComposeObject('default', 1, {})],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('createNetwork', steps);
|
|
});
|
|
|
|
it('should correctly infer a network remove step', () => {
|
|
const current = createApp(
|
|
[],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp([], [], [], true);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const idx = expectStep('removeNetwork', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('name')
|
|
.that.equals('test-network');
|
|
});
|
|
|
|
it('should correctly infer a network recreation step', () => {
|
|
const current = createApp(
|
|
[],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[],
|
|
[
|
|
Network.fromComposeObject('test-network', 1, {
|
|
labels: { TEST: 'TEST' },
|
|
}),
|
|
],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
let steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
let idx = expectStep('removeNetwork', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('name')
|
|
.that.equals('test-network');
|
|
|
|
delete current.networks['test-network'];
|
|
steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
idx = expectStep('createNetwork', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('name')
|
|
.that.equals('test-network');
|
|
});
|
|
|
|
it('should kill dependencies of networks before removing', async () => {
|
|
const current = createApp(
|
|
[await createService({ networks: { 'test-network': {} } })],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp([await createService({})], [], [], true);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const idx = expectStep('kill', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('serviceName')
|
|
.that.equals('test');
|
|
});
|
|
|
|
it('should kill dependencies of networks before changing config', async () => {
|
|
const current = createApp(
|
|
[await createService({ networks: { 'test-network': {} } })],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ networks: { 'test-network': {} } })],
|
|
[
|
|
Network.fromComposeObject('test-network', 1, {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const idx = expectStep('kill', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('serviceName')
|
|
.that.equals('test');
|
|
// We shouldn't try to remove the network until we have gotten rid of the dependencies
|
|
expect(() => expectStep('removeNetwork', steps)).to.throw();
|
|
});
|
|
|
|
it('should not output a kill step for a service which is already stopping when changing a volume', async () => {
|
|
const service = await createService({ volumes: ['test-volume'] });
|
|
service.status = 'Stopping';
|
|
const current = createApp(
|
|
[service],
|
|
[],
|
|
[Volume.fromComposeObject('test-volume', 1, {})],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[service],
|
|
[],
|
|
[
|
|
Volume.fromComposeObject('test-volume', 1, {
|
|
labels: { test: 'test' },
|
|
}),
|
|
],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expect(() => expectStep('kill', steps)).to.throw();
|
|
});
|
|
|
|
it('should create the default network if it does not exist', () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp([], [], [], true);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const idx = expectStep('createNetwork', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('target')
|
|
.that.has.property('name')
|
|
.that.equals('default');
|
|
});
|
|
|
|
it('should create a kill step for service which is no longer referenced', async () => {
|
|
const current = createApp(
|
|
[
|
|
await createService({}, 1, 'main', 1, 1),
|
|
await createService({}, 1, 'aux', 1, 2),
|
|
],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 2, 1)],
|
|
[Network.fromComposeObject('test-network', 1, {})],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
const idx = expectStep('kill', steps);
|
|
expect(steps[idx])
|
|
.to.have.property('current')
|
|
.that.has.property('serviceName')
|
|
.that.equals('aux');
|
|
});
|
|
|
|
it('should emit a noop when a service which is no longer referenced is already stopping', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Stopping' })],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp([], [], [], true);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('noop', steps);
|
|
});
|
|
|
|
it('should remove a dead container that is still referenced in the target state', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('remove', steps);
|
|
});
|
|
|
|
it('should remove a dead container that is not referenced in the target state', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp([], [], [], true);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('remove', steps);
|
|
});
|
|
|
|
it('should emit a noop when a service has an image downloading', async () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(
|
|
{ ...defaultContext, ...{ downloading: [1] } },
|
|
target,
|
|
);
|
|
expectStep('noop', steps);
|
|
});
|
|
|
|
it('should emit an updateMetadata step when a service has not changed but the release has', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 2, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('updateMetadata', steps);
|
|
});
|
|
|
|
it('should stop a container which has stoppped as its target', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ running: false }, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
expectStep('stop', steps);
|
|
});
|
|
|
|
it('should not try to start a container which has exitted and has restart policy of no', async () => {
|
|
// Container is a "run once" type of service so it has exitted.
|
|
const current = createApp(
|
|
[
|
|
await createService(
|
|
{ restart: 'no', running: false },
|
|
1,
|
|
'main',
|
|
1,
|
|
1,
|
|
1,
|
|
{ containerId: 'run_once' },
|
|
),
|
|
],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
|
|
// Mark this container as previously being started
|
|
applicationManager.containerStarted['run_once'] = true;
|
|
|
|
// Now test that another start step is not added on this service
|
|
const target = createApp(
|
|
[
|
|
await createService(
|
|
{ restart: 'no', running: true },
|
|
1,
|
|
'main',
|
|
1,
|
|
1,
|
|
1,
|
|
{ containerId: 'run_once' },
|
|
),
|
|
],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).rejectStep('start');
|
|
});
|
|
|
|
it('should recreate a container if the target configuration changes', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
{
|
|
appId: 1,
|
|
dependent: 0,
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
serviceId: 1,
|
|
name: 'main-image',
|
|
serviceName: 'main',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
let current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1, {})],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ privileged: true }, 1, 'main', 1, 1, 1, {})],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
// should see a 'stop'
|
|
let steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps).expectStep('stop').to.exist;
|
|
|
|
// remove the service since it's stopped...
|
|
current = createApp([], [defaultNetwork], [], false);
|
|
|
|
// now should see a 'start'
|
|
steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps)
|
|
.expectStep('start')
|
|
.forTarget((t) => t.serviceName === 'main').to.exist;
|
|
});
|
|
|
|
it('should not start a container when it depends on a service which is being installed', async () => {
|
|
const mainImage: Image = {
|
|
appId: 1,
|
|
dependent: 0,
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
serviceId: 1,
|
|
name: 'main-image',
|
|
serviceName: 'main',
|
|
};
|
|
|
|
const depImage: Image = {
|
|
appId: 1,
|
|
dependent: 0,
|
|
imageId: 2,
|
|
releaseId: 1,
|
|
serviceId: 2,
|
|
name: 'dep-image',
|
|
serviceName: 'dep',
|
|
};
|
|
|
|
const availableImages = [mainImage, depImage];
|
|
const contextWithImages = { ...defaultContext, ...{ availableImages } };
|
|
|
|
try {
|
|
let current = createApp(
|
|
[
|
|
await createService({ running: false }, 1, 'dep', 1, 2, 2, {
|
|
status: 'Installing',
|
|
containerId: 'id',
|
|
}),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[
|
|
await createService({}, 1, 'main', 1, 1, 1, { dependsOn: ['dep'] }),
|
|
await createService({}, 1, 'dep', 1, 2, 2),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
let steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps)
|
|
.expectStep('start')
|
|
.forTarget((t) => t.serviceName === 'dep').to.exist;
|
|
|
|
withSteps(steps)
|
|
.expectStep('start')
|
|
.forTarget((t) => t.serviceName === 'main').to.not.exist;
|
|
|
|
// we now make our current state have the 'dep' service as started...
|
|
current = createApp(
|
|
[await createService({}, 1, 'dep', 1, 2, 2, { containerId: 'id' })],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
|
|
// We keep track of the containers that we've tried to start so that we
|
|
// dont spam start requests if the container hasn't started running
|
|
applicationManager.containerStarted['id'] = true;
|
|
|
|
// we should now see a start for the 'main' service...
|
|
steps = current.nextStepsForAppUpdate(
|
|
{ ...contextWithImages, ...{ containerIds: { dep: 'id' } } },
|
|
target,
|
|
);
|
|
withSteps(steps)
|
|
.expectStep('start')
|
|
.forTarget((t) => t.serviceName === 'main').to.exist;
|
|
} finally {
|
|
delete applicationManager.containerStarted['id'];
|
|
}
|
|
});
|
|
|
|
it('should emit a fetch step when an image has not been downloaded for a service', async () => {
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('fetch').to.exist;
|
|
});
|
|
|
|
it('should stop a container which has stoppped as its target', async () => {
|
|
const current = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ running: false }, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('stop');
|
|
});
|
|
|
|
it('should create a start step when all that changes is a running state', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
{
|
|
appId: 1,
|
|
dependent: 0,
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
serviceId: 1,
|
|
name: 'main-image',
|
|
serviceName: 'main',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const current = createApp(
|
|
[await createService({ running: false }, 1, 'main', 1, 1, 1, {})],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1, {})],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
// now should see a 'start'
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps)
|
|
.expectStep('start')
|
|
.forTarget((t) => t.serviceName === 'main').to.exist;
|
|
});
|
|
|
|
it('should not infer a fetch step when the download is already in progress', async () => {
|
|
const contextWithDownloading = {
|
|
...defaultContext,
|
|
...{
|
|
downloading: [1],
|
|
},
|
|
};
|
|
const current = createApp([], [], [], false);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'main', 1, 1, 1)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(contextWithDownloading, target);
|
|
withSteps(steps).expectStep('fetch').forTarget('main').to.not.exist;
|
|
});
|
|
|
|
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
{
|
|
appId: 1,
|
|
dependent: 0,
|
|
imageId: 1,
|
|
releaseId: 1,
|
|
serviceId: 1,
|
|
name: 'main-image',
|
|
serviceName: 'main',
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const labels = {
|
|
'io.balena.update.strategy': 'kill-then-download',
|
|
};
|
|
|
|
const current = createApp(
|
|
[
|
|
await createService(
|
|
{ labels, image: 'main-image' },
|
|
1,
|
|
'main',
|
|
1,
|
|
1,
|
|
1,
|
|
{},
|
|
),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[
|
|
await createService(
|
|
{ labels, image: 'main-image-2' },
|
|
1,
|
|
'main',
|
|
2,
|
|
1,
|
|
2,
|
|
{},
|
|
),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
let steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps).expectStep('kill').forCurrent('main').to.exist;
|
|
|
|
// next volatile state...
|
|
const afterKill = createApp([], [defaultNetwork], [], false);
|
|
|
|
steps = afterKill.nextStepsForAppUpdate(contextWithImages, target);
|
|
|
|
withSteps(steps).expectStep('fetch').to.exist;
|
|
|
|
const fetchStep = withSteps(steps).expectStep('fetch').asStep;
|
|
|
|
expect(fetchStep)
|
|
.to.have.property('image')
|
|
.that.has.property('name')
|
|
.that.equals('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: [4],
|
|
availableImages: [
|
|
{
|
|
appId: 1,
|
|
releaseId: 1,
|
|
dependent: 0,
|
|
name: 'main-image',
|
|
imageId: 1,
|
|
serviceName: 'main',
|
|
serviceId: 1,
|
|
},
|
|
{
|
|
appId: 1,
|
|
releaseId: 1,
|
|
dependent: 0,
|
|
name: 'dep-image',
|
|
imageId: 2,
|
|
serviceName: 'dep',
|
|
serviceId: 2,
|
|
},
|
|
{
|
|
appId: 1,
|
|
releaseId: 2,
|
|
dependent: 0,
|
|
name: 'main-image-2',
|
|
imageId: 3,
|
|
serviceName: 'main',
|
|
serviceId: 1,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp(
|
|
[
|
|
await createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {
|
|
dependsOn: ['dep'],
|
|
}),
|
|
await createService({ image: 'dep-image' }, 1, 'dep', 1, 2, 2, {}),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[
|
|
await createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 3, {
|
|
dependsOn: ['dep'],
|
|
}),
|
|
await createService({ image: 'dep-image-2' }, 1, 'dep', 2, 2, 4, {}),
|
|
],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps).expectStep('kill').forCurrent('main').to.not.exist;
|
|
});
|
|
|
|
it('should create several kill steps as long as there is no unmet dependencies', async () => {
|
|
const contextWithImages = {
|
|
...defaultContext,
|
|
...{
|
|
availableImages: [
|
|
{
|
|
appId: 1,
|
|
releaseId: 1,
|
|
dependent: 0,
|
|
name: 'main-image',
|
|
imageId: 1,
|
|
serviceName: 'main',
|
|
serviceId: 1,
|
|
},
|
|
{
|
|
appId: 1,
|
|
releaseId: 2,
|
|
dependent: 0,
|
|
name: 'main-image-2',
|
|
imageId: 2,
|
|
serviceName: 'main',
|
|
serviceId: 1,
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const current = createApp(
|
|
[await createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {})],
|
|
[defaultNetwork],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 2)],
|
|
[defaultNetwork],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
let steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps).expectStep('kill').forCurrent('main').to.exist;
|
|
|
|
// same states again...
|
|
steps = current.nextStepsForAppUpdate(contextWithImages, target);
|
|
withSteps(steps).expectStep('kill').forCurrent('main').to.exist;
|
|
});
|
|
|
|
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', async () => {
|
|
const labels = {
|
|
'io.balena.update.strategy': 'kill-then-download',
|
|
};
|
|
|
|
const current = createApp([await createService({ labels })], [], [], false);
|
|
const target = createApp(
|
|
[await createService({ privileged: true })],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('kill').forCurrent('main');
|
|
});
|
|
|
|
it('should not infer a kill step with the default strategy if a dependency is not downloaded', async () => {
|
|
const current = createApp(
|
|
[await createService({ image: 'image1' })],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({ image: 'image2' })],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('fetch');
|
|
withSteps(steps).rejectStep('kill');
|
|
});
|
|
|
|
it('should create several kill steps as long as there is no unmet dependencies', async () => {
|
|
const current = createApp(
|
|
[
|
|
await createService({}, 1, 'one', 1, 2),
|
|
await createService({}, 1, 'two', 1, 3),
|
|
await createService({}, 1, 'three', 1, 4),
|
|
],
|
|
[],
|
|
[],
|
|
false,
|
|
);
|
|
const target = createApp(
|
|
[await createService({}, 1, 'three', 1, 4)],
|
|
[],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('kill').to.have.length(2);
|
|
});
|
|
it('should not create a service when a network it depends on is not ready', async () => {
|
|
const current = createApp([], [defaultNetwork], [], false);
|
|
const target = createApp(
|
|
[await createService({ networks: ['test'] }, 1)],
|
|
[defaultNetwork, Network.fromComposeObject('test', 1, {})],
|
|
[],
|
|
true,
|
|
);
|
|
|
|
const steps = current.nextStepsForAppUpdate(defaultContext, target);
|
|
withSteps(steps).expectStep('createNetwork');
|
|
withSteps(steps).rejectStep('start');
|
|
}); // no create service, is create network
|
|
});
|