2021-05-05 20:32:31 +00:00
|
|
|
import { expect } from 'chai';
|
|
|
|
import * as sinon from 'sinon';
|
|
|
|
import { stub } from 'sinon';
|
|
|
|
import App from '../../../src/compose/app';
|
|
|
|
import * as applicationManager from '../../../src/compose/application-manager';
|
|
|
|
import * as imageManager from '../../../src/compose/images';
|
|
|
|
import { Image } from '../../../src/compose/images';
|
|
|
|
import Network from '../../../src/compose/network';
|
|
|
|
import * as networkManager from '../../../src/compose/network-manager';
|
|
|
|
import Service from '../../../src/compose/service';
|
|
|
|
import { ServiceComposeConfig } from '../../../src/compose/types/service';
|
|
|
|
import Volume from '../../../src/compose/volume';
|
|
|
|
import log from '../../../src/lib/supervisor-console';
|
|
|
|
import { InstancedAppState } from '../../../src/types/state';
|
|
|
|
|
2021-07-07 22:01:48 +00:00
|
|
|
import * as dbHelper from '../../lib/db-helper';
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
const DEFAULT_NETWORK = Network.fromComposeObject('default', 1, {});
|
|
|
|
|
|
|
|
async function createService(
|
|
|
|
{
|
|
|
|
appId = 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid = 'app-uuid',
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName = 'main',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit = 'main-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-03 23:12:47 +00:00
|
|
|
appUuid,
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName,
|
2021-07-23 16:48:27 +00:00
|
|
|
commit,
|
|
|
|
// db ids should not be used for target state calculation, but images
|
|
|
|
// are compared using _.isEqual so leaving this here to have image comparisons
|
|
|
|
// match
|
|
|
|
serviceId: 1,
|
|
|
|
imageId: 1,
|
|
|
|
releaseId: 1,
|
2021-05-05 20:32:31 +00:00
|
|
|
...conf,
|
|
|
|
},
|
|
|
|
options,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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-03 23:12:47 +00:00
|
|
|
appUuid = 'app-uuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
name = 'test-image',
|
2021-08-03 23:12:47 +00:00
|
|
|
serviceName = 'main',
|
|
|
|
commit = 'main-commit',
|
2021-07-23 16:48:27 +00:00
|
|
|
...extra
|
|
|
|
} = {} as Partial<Image>,
|
|
|
|
) {
|
2021-07-07 22:01:48 +00:00
|
|
|
return {
|
2021-07-23 16:48:27 +00:00
|
|
|
appId,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid,
|
2021-07-23 16:48:27 +00:00
|
|
|
name,
|
|
|
|
serviceName,
|
2021-08-03 23:12:47 +00:00
|
|
|
commit,
|
2021-07-23 16:48:27 +00:00
|
|
|
// db ids should not be used for target state calculation, but images
|
|
|
|
// are compared using _.isEqual so leaving this here to have image comparisons
|
|
|
|
// match
|
|
|
|
imageId: 1,
|
|
|
|
releaseId: 1,
|
|
|
|
serviceId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
dependent: 0,
|
2021-07-23 16:48:27 +00:00
|
|
|
...extra,
|
|
|
|
} as Image;
|
2021-07-07 22:01:48 +00:00
|
|
|
}
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
function createApps(
|
|
|
|
{
|
|
|
|
services = [] as Service[],
|
|
|
|
networks = [] as Network[],
|
|
|
|
volumes = [] as Volume[],
|
|
|
|
},
|
|
|
|
target = false,
|
|
|
|
) {
|
|
|
|
const servicesByAppId = services.reduce(
|
|
|
|
(svcs, s) => ({ ...svcs, [s.appId]: [s].concat(svcs[s.appId] || []) }),
|
|
|
|
{} as Dictionary<Service[]>,
|
|
|
|
);
|
|
|
|
const volumesByAppId = volumes.reduce(
|
|
|
|
(vols, v) => ({ ...vols, [v.appId]: [v].concat(vols[v.appId] || []) }),
|
|
|
|
{} as Dictionary<Volume[]>,
|
|
|
|
);
|
|
|
|
const networksByAppId = networks.reduce(
|
|
|
|
(nets, n) => ({ ...nets, [n.appId]: [n].concat(nets[n.appId] || []) }),
|
|
|
|
{} as Dictionary<Network[]>,
|
|
|
|
);
|
|
|
|
|
|
|
|
const allAppIds = [
|
|
|
|
...new Set([
|
|
|
|
...Object.keys(servicesByAppId),
|
|
|
|
...Object.keys(networksByAppId),
|
|
|
|
...Object.keys(volumesByAppId),
|
|
|
|
]),
|
|
|
|
].map((i) => parseInt(i, 10));
|
|
|
|
|
|
|
|
const apps: InstancedAppState = {};
|
|
|
|
for (const appId of allAppIds) {
|
|
|
|
apps[appId] = new App(
|
|
|
|
{
|
|
|
|
appId,
|
|
|
|
services: servicesByAppId[appId] ?? [],
|
|
|
|
networks: (networksByAppId[appId] ?? []).reduce(
|
|
|
|
(nets, n) => ({ ...nets, [n.name]: n }),
|
|
|
|
{},
|
|
|
|
),
|
|
|
|
volumes: (volumesByAppId[appId] ?? []).reduce(
|
|
|
|
(vols, v) => ({ ...vols, [v.name]: v }),
|
|
|
|
{},
|
|
|
|
),
|
|
|
|
},
|
|
|
|
target,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return apps;
|
|
|
|
}
|
|
|
|
|
|
|
|
function createCurrentState({
|
|
|
|
services = [] as Service[],
|
|
|
|
networks = [] as Network[],
|
|
|
|
volumes = [] as Volume[],
|
2021-07-23 16:48:27 +00:00
|
|
|
images = services.map((s) => ({
|
|
|
|
// Infer images from services by default
|
2021-08-03 23:12:47 +00:00
|
|
|
dockerImageId: s.dockerImageId,
|
2021-07-23 16:48:27 +00:00
|
|
|
...imageManager.imageFromService(s),
|
|
|
|
})) as Image[],
|
|
|
|
downloading = [] as string[],
|
2021-05-05 20:32:31 +00:00
|
|
|
}) {
|
|
|
|
const currentApps = createApps({ services, networks, volumes });
|
|
|
|
|
|
|
|
const containerIdsByAppId = services.reduce(
|
|
|
|
(ids, s) => ({
|
|
|
|
...ids,
|
|
|
|
[s.appId]: {
|
|
|
|
...ids[s.appId],
|
|
|
|
...(s.serviceName &&
|
|
|
|
s.containerId && { [s.serviceName]: s.containerId }),
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
{} as { [appId: number]: Dictionary<string> },
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
currentApps,
|
|
|
|
availableImages: images,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('compose/application-manager', () => {
|
2021-07-07 22:01:48 +00:00
|
|
|
let testDb: dbHelper.TestDatabase;
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
before(async () => {
|
2021-07-07 22:01:48 +00:00
|
|
|
testDb = await dbHelper.createDB();
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
// disable log output during testing
|
|
|
|
sinon.stub(log, 'debug');
|
|
|
|
sinon.stub(log, 'warn');
|
|
|
|
sinon.stub(log, 'info');
|
|
|
|
sinon.stub(log, 'event');
|
|
|
|
sinon.stub(log, 'success');
|
|
|
|
|
|
|
|
// Stub methods that depend on external dependencies
|
|
|
|
stub(imageManager, 'isCleanupNeeded');
|
|
|
|
stub(networkManager, 'supervisorNetworkReady');
|
|
|
|
});
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
// Do not check for cleanup images by default
|
|
|
|
(imageManager.isCleanupNeeded as sinon.SinonStub).resolves(false);
|
|
|
|
// Do not check for network
|
|
|
|
(networkManager.supervisorNetworkReady as sinon.SinonStub).resolves(true);
|
|
|
|
});
|
|
|
|
|
2021-07-07 22:01:48 +00:00
|
|
|
afterEach(async () => {
|
|
|
|
await testDb.reset();
|
|
|
|
});
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
after(async () => {
|
2021-07-07 22:01:48 +00:00
|
|
|
try {
|
|
|
|
await testDb.destroy();
|
|
|
|
} catch (e) {
|
|
|
|
/* noop */
|
|
|
|
}
|
2021-05-05 20:32:31 +00:00
|
|
|
// Restore stubbed methods
|
|
|
|
sinon.restore();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should init', async () => {
|
|
|
|
await applicationManager.initialized;
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO: missing tests for getCurrentApps
|
|
|
|
|
|
|
|
it('infers a start step when all that changes is a running state', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ running: true, appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ running: false, appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [startStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(startStep).to.have.property('action').that.equals('start');
|
|
|
|
expect(startStep)
|
|
|
|
.to.have.property('target')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers a kill step when a service has to be removed', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [await createService()],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [killStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(killStep).to.have.property('action').that.equals('kill');
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers a fetch step when a service has to be updated', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ image: 'image-new', appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
2021-07-07 22:01:48 +00:00
|
|
|
images: [],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const [fetchStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(fetchStep).to.have.property('action').that.equals('fetch');
|
|
|
|
expect(fetchStep)
|
|
|
|
.to.have.property('image')
|
|
|
|
.that.deep.includes({ name: 'image-new' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('does not infer a fetch step when the download is already in progress', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ image: 'image-new', appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ appId: 1 })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
2021-07-23 16:48:27 +00:00
|
|
|
downloading: ['image-new'],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const [noopStep, ...nextSteps] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(noopStep).to.have.property('action').that.equals('noop');
|
|
|
|
expect(nextSteps).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers 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 targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'image-new',
|
|
|
|
labels,
|
|
|
|
appId: 1,
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
2021-07-07 22:01:48 +00:00
|
|
|
services: [
|
2021-08-03 23:12:47 +00:00
|
|
|
await createService(
|
|
|
|
{
|
|
|
|
image: 'image-old',
|
|
|
|
labels,
|
|
|
|
appId: 1,
|
|
|
|
commit: 'old-release',
|
|
|
|
},
|
|
|
|
{ options: { imageInfo: { Id: 'sha256:image-old-id' } } },
|
|
|
|
),
|
2021-07-07 22:01:48 +00:00
|
|
|
],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [killStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(killStep).to.have.property('action').that.equals('kill');
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
2021-11-11 19:28:02 +00:00
|
|
|
it('infers a kill step when a service has to be updated but the strategy is delete-then-download', async () => {
|
|
|
|
const labels = {
|
|
|
|
'io.balena.update.strategy': 'delete-then-download',
|
|
|
|
};
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
image: 'image-new',
|
|
|
|
labels,
|
|
|
|
appId: 1,
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
2021-08-03 23:12:47 +00:00
|
|
|
await createService(
|
|
|
|
{
|
|
|
|
image: 'image-old',
|
|
|
|
labels,
|
|
|
|
appId: 1,
|
|
|
|
commit: 'old-release',
|
|
|
|
},
|
|
|
|
{ options: { imageInfo: { Id: 'sha256:image-old-id' } } },
|
|
|
|
),
|
2021-11-11 19:28:02 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [killStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(killStep).to.have.property('action').that.equals('kill');
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers a remove step when the current service has stopped and the strategy is delete-then-download', async () => {
|
|
|
|
const labels = {
|
|
|
|
'io.balena.update.strategy': 'delete-then-download',
|
|
|
|
};
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
|
|
|
await createService({
|
|
|
|
image: 'image-new',
|
|
|
|
labels,
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
images: [
|
|
|
|
createImage({
|
|
|
|
appId: 1,
|
|
|
|
name: 'image-old',
|
|
|
|
serviceName: 'main',
|
|
|
|
dockerImageId: 'image-old-id',
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [removeImage] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// First we should see a kill
|
|
|
|
expect(removeImage).to.have.property('action').that.equals('removeImage');
|
|
|
|
expect(removeImage)
|
|
|
|
.to.have.property('image')
|
|
|
|
.that.deep.includes({ name: 'image-old' });
|
|
|
|
});
|
|
|
|
|
2021-05-05 20:32:31 +00:00
|
|
|
it('does not infer to kill a service with default strategy if a dependency is not downloaded', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
appId: 1,
|
|
|
|
commit: 'new-release',
|
|
|
|
serviceName: 'main',
|
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,
|
|
|
|
commit: 'new-release',
|
|
|
|
serviceName: 'dep',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
appId: 1,
|
|
|
|
commit: 'old-release',
|
|
|
|
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,
|
|
|
|
commit: 'old-release',
|
|
|
|
serviceName: 'dep',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
2021-07-23 16:48:27 +00:00
|
|
|
downloading: ['dep-image'], // dep-image is still being downloaded
|
2021-05-05 20:32:31 +00:00
|
|
|
images: [
|
|
|
|
// main-image was already downloaded
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
|
|
|
name: 'main-image',
|
|
|
|
serviceName: 'main',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Only noop steps should be seen at this point
|
|
|
|
expect(steps.filter((s) => s.action !== 'noop')).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers to kill several services as long as there is no unmet dependency', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'new-release',
|
|
|
|
serviceName: 'main',
|
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,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'new-release',
|
|
|
|
serviceName: 'dep',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'old-release',
|
|
|
|
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,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'old-release',
|
|
|
|
serviceName: 'dep',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
|
|
|
// Both images have been downloaded
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image',
|
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-uuid',
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'dep-image',
|
|
|
|
serviceName: 'dep',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// We should see kill steps for both currently running services
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) => s.action === 'kill' && s.current.serviceName === 'dep',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) => s.action === 'kill' && s.current.serviceName === 'main',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers to start the dependency first', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
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',
|
|
|
|
serviceName: 'dep',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
|
|
|
// Both images have been downloaded
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image',
|
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'dep-image',
|
|
|
|
serviceName: 'dep',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [startStep, ...nextSteps] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2021-07-07 22:01:48 +00:00
|
|
|
// A start step should happen for the depended service first
|
2021-05-05 20:32:31 +00:00
|
|
|
expect(startStep).to.have.property('action').that.equals('start');
|
|
|
|
expect(startStep)
|
|
|
|
.to.have.property('target')
|
|
|
|
.that.deep.includes({ serviceName: 'dep' });
|
|
|
|
|
|
|
|
// No more steps until the first container has been started
|
|
|
|
expect(nextSteps).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers to start a service once its dependency has been met', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'main-image',
|
|
|
|
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',
|
|
|
|
serviceName: 'dep',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
image: 'dep-image',
|
|
|
|
serviceName: 'dep',
|
|
|
|
commit: 'new-release',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
|
|
|
// Both images have been downloaded
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image',
|
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'dep-image',
|
|
|
|
serviceName: 'dep',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'new-release',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [startStep, ...nextSteps] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// A start step shoud happen for the depended service first
|
|
|
|
expect(startStep).to.have.property('action').that.equals('start');
|
|
|
|
expect(startStep)
|
|
|
|
.to.have.property('target')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
|
|
|
|
expect(nextSteps).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('infers to remove spurious containers', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [await createService({ image: 'main-image' })],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
2021-07-23 16:48:27 +00:00
|
|
|
services: [await createService({ appId: 5, serviceName: 'old-service' })],
|
2021-05-05 20:32:31 +00:00
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
2021-07-23 16:48:27 +00:00
|
|
|
// Image has been downloaded
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image',
|
|
|
|
serviceName: 'main',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Start the new service
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) => s.action === 'start' && s.target.serviceName === 'main',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
|
|
|
|
// Remove the leftover service
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) =>
|
|
|
|
s.action === 'kill' && s.current.serviceName === 'old-service',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not remove an app volumes when they are no longer referenced', async () => {
|
|
|
|
const targetApps = createApps({ networks: [DEFAULT_NETWORK] }, true);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
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 applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(steps.filter((s) => s.action === 'removeVolume')).to.be.empty;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should remove volumes from previous applications', async () => {
|
|
|
|
const targetApps = createApps({ networks: [DEFAULT_NETWORK] }, true);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [],
|
|
|
|
// Volume with different id
|
2021-08-25 21:50:34 +00:00
|
|
|
volumes: [Volume.fromComposeObject('test-volume', 2, 'deadbeef')],
|
2021-05-05 20:32:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(steps.filter((s) => s.action === 'removeVolume')).to.not.be.empty;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should infer that we need to create the supervisor network if it does not exist', async () => {
|
|
|
|
// stub the networkManager method to fail on finding the supervisor network
|
|
|
|
(networkManager.supervisorNetworkReady as sinon.SinonStub).resolves(false);
|
|
|
|
|
|
|
|
const targetApps = createApps(
|
2021-07-23 16:48:27 +00:00
|
|
|
{ services: [await createService()], networks: [DEFAULT_NETWORK] },
|
2021-05-05 20:32:31 +00:00
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [
|
|
|
|
ensureNetworkStep,
|
|
|
|
...nextSteps
|
|
|
|
] = await applicationManager.inferNextSteps(currentApps, targetApps, {
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
});
|
|
|
|
expect(ensureNetworkStep).to.deep.include({
|
|
|
|
action: 'ensureSupervisorNetwork',
|
|
|
|
});
|
|
|
|
expect(nextSteps).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should kill a service which depends on the supervisor network, if we need to create the network', async () => {
|
|
|
|
// stub the networkManager method to fail on finding the supervisor network
|
|
|
|
(networkManager.supervisorNetworkReady as sinon.SinonStub).resolves(false);
|
|
|
|
|
|
|
|
const labels = { 'io.balena.features.supervisor-api': 'true' };
|
|
|
|
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
|
|
|
await createService({ labels }, { options: { listenPort: '48484' } }),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
|
|
|
await createService({ labels }, { options: { listenPort: '48484' } }),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [killStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// A start step shoud happen for the depended service first
|
|
|
|
expect(killStep).to.have.property('action').that.equals('kill');
|
|
|
|
expect(killStep)
|
|
|
|
.to.have.property('current')
|
|
|
|
.that.deep.includes({ serviceName: 'main' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should infer a cleanup step when a cleanup is required', async () => {
|
|
|
|
// Stub the image manager function
|
|
|
|
(imageManager.isCleanupNeeded as sinon.SinonStub).resolves(true);
|
|
|
|
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [await createService()],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [await createService()],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [cleanupStep, ...nextSteps] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Cleanup needs to happen first
|
|
|
|
expect(cleanupStep).to.deep.include({
|
|
|
|
action: 'cleanup',
|
|
|
|
});
|
|
|
|
expect(nextSteps).to.have.lengthOf(0);
|
|
|
|
});
|
|
|
|
|
2021-07-07 22:01:48 +00:00
|
|
|
it('should infer that an image should be removed if it is no longer referenced in current or target state (only target)', async () => {
|
2021-05-05 20:32:31 +00:00
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
|
|
|
await createService(
|
|
|
|
{ image: 'main-image' },
|
2021-07-07 22:01:48 +00:00
|
|
|
// Target has a matching image already
|
2021-05-05 20:32:31 +00:00
|
|
|
{ options: { imageInfo: { Id: 'sha256:bbbb' } } },
|
|
|
|
),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
2021-07-07 22:01:48 +00:00
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
|
|
|
// An image for a service that no longer exists
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-07-07 22:01:48 +00:00
|
|
|
name: 'old-image',
|
|
|
|
appId: 5,
|
|
|
|
serviceName: 'old-service',
|
|
|
|
dockerImageId: 'sha256:aaaa',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-07-07 22:01:48 +00:00
|
|
|
name: 'main-image',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
|
|
|
dockerImageId: 'sha256:bbbb',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-07-07 22:01:48 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [removeImageStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// A start step shoud happen for the depended service first
|
|
|
|
expect(removeImageStep)
|
|
|
|
.to.have.property('action')
|
|
|
|
.that.equals('removeImage');
|
|
|
|
expect(removeImageStep)
|
|
|
|
.to.have.property('image')
|
|
|
|
.that.deep.includes({ name: 'old-image' });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should infer that an image should be removed if it is no longer referenced in current or target state (only current)', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
2021-05-05 20:32:31 +00:00
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [
|
|
|
|
await createService(
|
|
|
|
{ image: 'main-image' },
|
2021-07-07 22:01:48 +00:00
|
|
|
// Target has a matching image already
|
2021-05-05 20:32:31 +00:00
|
|
|
{ options: { imageInfo: { Id: 'sha256:bbbb' } } },
|
|
|
|
),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [
|
|
|
|
// An image for a service that no longer exists
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'old-image',
|
|
|
|
appId: 5,
|
|
|
|
serviceName: 'old-service',
|
|
|
|
dockerImageId: 'sha256:aaaa',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image',
|
|
|
|
appId: 1,
|
|
|
|
serviceName: 'main',
|
|
|
|
dockerImageId: 'sha256:bbbb',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const [removeImageStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// A start step shoud happen for the depended service first
|
|
|
|
expect(removeImageStep)
|
|
|
|
.to.have.property('action')
|
|
|
|
.that.equals('removeImage');
|
|
|
|
expect(removeImageStep)
|
|
|
|
.to.have.property('image')
|
|
|
|
.that.deep.includes({ name: 'old-image' });
|
|
|
|
});
|
|
|
|
|
2021-07-07 22:01:48 +00:00
|
|
|
it('should infer that an image should be saved if it is not in the available image list but it can be found on disk', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
|
|
|
await createService(
|
|
|
|
{ image: 'main-image' },
|
|
|
|
// Target has image info
|
|
|
|
{ options: { imageInfo: { Id: 'sha256:bbbb' } } },
|
|
|
|
),
|
|
|
|
],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [DEFAULT_NETWORK],
|
|
|
|
images: [], // no available images exist
|
|
|
|
});
|
|
|
|
|
|
|
|
const [saveImageStep] = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// A start step shoud happen for the depended service first
|
|
|
|
expect(saveImageStep).to.have.property('action').that.equals('saveImage');
|
|
|
|
expect(saveImageStep)
|
|
|
|
.to.have.property('image')
|
|
|
|
.that.deep.includes({ name: 'main-image' });
|
|
|
|
});
|
2021-05-05 20:32:31 +00:00
|
|
|
|
|
|
|
it('should correctly generate steps for multiple apps', async () => {
|
|
|
|
const targetApps = createApps(
|
|
|
|
{
|
|
|
|
services: [
|
2021-07-23 16:48:27 +00:00
|
|
|
await createService({
|
|
|
|
running: true,
|
|
|
|
image: 'main-image-1',
|
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-one',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'commit-for-app-1',
|
|
|
|
}),
|
|
|
|
await createService({
|
|
|
|
running: true,
|
|
|
|
image: 'main-image-2',
|
|
|
|
appId: 2,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-two',
|
2021-07-23 16:48:27 +00:00
|
|
|
commit: 'commit-for-app-2',
|
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
networks: [
|
|
|
|
// Default networks for two apps
|
|
|
|
Network.fromComposeObject('default', 1, {}),
|
|
|
|
Network.fromComposeObject('default', 2, {}),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
true,
|
|
|
|
);
|
|
|
|
const {
|
|
|
|
currentApps,
|
|
|
|
availableImages,
|
|
|
|
downloading,
|
|
|
|
containerIdsByAppId,
|
|
|
|
} = createCurrentState({
|
|
|
|
services: [],
|
|
|
|
networks: [
|
|
|
|
// Default networks for two apps
|
|
|
|
Network.fromComposeObject('default', 1, {}),
|
|
|
|
Network.fromComposeObject('default', 2, {}),
|
|
|
|
],
|
|
|
|
images: [
|
2021-07-23 16:48:27 +00:00
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image-1',
|
|
|
|
appId: 1,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-one',
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'commit-for-app-1',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
|
|
|
createImage({
|
2021-05-05 20:32:31 +00:00
|
|
|
name: 'main-image-2',
|
|
|
|
appId: 2,
|
2021-08-03 23:12:47 +00:00
|
|
|
appUuid: 'app-two',
|
2021-05-05 20:32:31 +00:00
|
|
|
serviceName: 'main',
|
2021-08-03 23:12:47 +00:00
|
|
|
commit: 'commit-for-app-2',
|
2021-07-23 16:48:27 +00:00
|
|
|
}),
|
2021-05-05 20:32:31 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const steps = await applicationManager.inferNextSteps(
|
|
|
|
currentApps,
|
|
|
|
targetApps,
|
|
|
|
{
|
|
|
|
downloading,
|
|
|
|
availableImages,
|
|
|
|
containerIdsByAppId,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Expect a start step for both apps
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) =>
|
|
|
|
s.action === 'start' &&
|
|
|
|
s.target.appId === 1 &&
|
|
|
|
s.target.serviceName === 'main',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
expect(
|
|
|
|
steps.filter(
|
|
|
|
(s: any) =>
|
|
|
|
s.action === 'start' &&
|
|
|
|
s.target.appId === 2 &&
|
|
|
|
s.target.serviceName === 'main',
|
|
|
|
),
|
|
|
|
).to.have.lengthOf(1);
|
|
|
|
});
|
|
|
|
});
|