mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 05:37:53 +00:00
Migrate volume-manager tests to integration
Now the tests are ran against the actual docker engine instead of against mockerode. The new tests actually caught a bug in `volumeManager.removeOrphanedVolumes`, where that function would try to remove volumes for stopped containers, causing an exception. This commit also fixes that bug.
This commit is contained in:
parent
18c2f8cec9
commit
a69fbf6eac
@ -117,7 +117,7 @@ export async function removeOrphanedVolumes(
|
|||||||
// *all* containers. This means we don't remove
|
// *all* containers. This means we don't remove
|
||||||
// something that's part of a sideloaded container
|
// something that's part of a sideloaded container
|
||||||
const [dockerContainers, dockerVolumes] = await Promise.all([
|
const [dockerContainers, dockerVolumes] = await Promise.all([
|
||||||
docker.listContainers(),
|
docker.listContainers({ all: true }),
|
||||||
docker.listVolumes(),
|
docker.listVolumes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
305
test/integration/compose/volume-manager.spec.ts
Normal file
305
test/integration/compose/volume-manager.spec.ts
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import * as volumeManager from '~/src/compose/volume-manager';
|
||||||
|
import Volume from '~/src/compose/volume';
|
||||||
|
import { createDockerImage } from '~/test-lib/docker-helper';
|
||||||
|
|
||||||
|
import * as Docker from 'dockerode';
|
||||||
|
|
||||||
|
describe('compose/volume-manager', () => {
|
||||||
|
const docker = new Docker();
|
||||||
|
after(async () => {
|
||||||
|
await docker.pruneContainers();
|
||||||
|
await docker.pruneVolumes();
|
||||||
|
await docker.pruneImages();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Retrieving volumes from the engine', () => {
|
||||||
|
it('gets all supervised Volumes', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await Promise.all([
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(1, 'redis'),
|
||||||
|
// Recently created volumes contain io.balena.supervised label
|
||||||
|
Labels: { 'io.balena.supervised': '1' },
|
||||||
|
}),
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(1, 'mysql'),
|
||||||
|
// Recently created volumes contain io.balena.supervised label and app-uuid
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
'io.balena.app-uuid': 'deadbeef',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(2, 'backend'),
|
||||||
|
// Old Volumes will not have labels
|
||||||
|
}),
|
||||||
|
// Volume not created by the Supervisor
|
||||||
|
docker.createVolume({ Name: 'user_created_volume' }),
|
||||||
|
docker.createVolume({
|
||||||
|
Name: 'decoy',
|
||||||
|
// Added decoy to really test the inference (should not return)
|
||||||
|
Labels: { 'io.balena.supervised': '1' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Perform test
|
||||||
|
await expect(volumeManager.getAll()).to.eventually.have.deep.members([
|
||||||
|
{
|
||||||
|
appId: 1,
|
||||||
|
appUuid: undefined,
|
||||||
|
config: {
|
||||||
|
driver: 'local',
|
||||||
|
driverOpts: {},
|
||||||
|
labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'redis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 1,
|
||||||
|
appUuid: 'deadbeef',
|
||||||
|
config: {
|
||||||
|
driver: 'local',
|
||||||
|
driverOpts: {},
|
||||||
|
labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
'io.balena.app-uuid': 'deadbeef',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'mysql',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appId: 2,
|
||||||
|
appUuid: undefined,
|
||||||
|
config: {
|
||||||
|
driver: 'local',
|
||||||
|
driverOpts: {},
|
||||||
|
labels: {},
|
||||||
|
},
|
||||||
|
name: 'backend',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cleanup volumes
|
||||||
|
await Promise.all([
|
||||||
|
docker.getVolume(Volume.generateDockerName(1, 'redis')).remove(),
|
||||||
|
docker.getVolume(Volume.generateDockerName(1, 'mysql')).remove(),
|
||||||
|
docker.getVolume(Volume.generateDockerName(2, 'backend')).remove(),
|
||||||
|
docker.getVolume('user_created_volume').remove(),
|
||||||
|
docker.getVolume('decoy').remove(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can parse null Volumes', async () => {
|
||||||
|
// Perform test with no volumes
|
||||||
|
await expect(volumeManager.getAll()).to.eventually.deep.equal([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gets the volume for specific application', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await Promise.all([
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(111, 'app'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(222, 'otherApp'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Perform test
|
||||||
|
await expect(volumeManager.getAllByAppId(111)).to.eventually.deep.equal([
|
||||||
|
{
|
||||||
|
appId: 111,
|
||||||
|
appUuid: undefined,
|
||||||
|
config: {
|
||||||
|
driver: 'local',
|
||||||
|
driverOpts: {},
|
||||||
|
labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'app',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Cleanup volumes
|
||||||
|
await Promise.all([
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'app')).remove(),
|
||||||
|
docker.getVolume(Volume.generateDockerName(222, 'otherApp')).remove(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Creating volumes', () => {
|
||||||
|
it('creates a volume if it does not exist', async () => {
|
||||||
|
// The volume does not exist on the engine before
|
||||||
|
await expect(
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
|
||||||
|
).to.be.rejected;
|
||||||
|
|
||||||
|
// Volume to create
|
||||||
|
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
||||||
|
|
||||||
|
// Create volume
|
||||||
|
await volumeManager.create(volume);
|
||||||
|
|
||||||
|
// Check the volume should have been created
|
||||||
|
await expect(
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
|
||||||
|
).to.not.be.rejected;
|
||||||
|
|
||||||
|
// Cleanup volumes
|
||||||
|
await Promise.all([
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not try to create a volume that already exists', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(111, 'main'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create compose object for volume already set up in mock engine
|
||||||
|
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
||||||
|
sinon.spy(volume, 'create');
|
||||||
|
|
||||||
|
// Create volume
|
||||||
|
await volumeManager.create(volume);
|
||||||
|
|
||||||
|
// Check volume was not created
|
||||||
|
expect(volume.create).to.not.have.been.called;
|
||||||
|
|
||||||
|
// Cleanup volumes
|
||||||
|
await Promise.all([
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Removing volumes', () => {
|
||||||
|
it('removes a volume if it exists', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await Promise.all([
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(111, 'main'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Volume to remove
|
||||||
|
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
||||||
|
|
||||||
|
// Remove volume
|
||||||
|
await volumeManager.remove(volume);
|
||||||
|
|
||||||
|
// Check volume was removed
|
||||||
|
await expect(
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
|
||||||
|
).to.be.rejected;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing on removal if the volume does not exist', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await Promise.all([
|
||||||
|
docker.createVolume({
|
||||||
|
Name: 'decoy-volume',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Volume to remove
|
||||||
|
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
||||||
|
|
||||||
|
// Remove volume
|
||||||
|
await expect(volumeManager.remove(volume)).to.not.be.rejected;
|
||||||
|
|
||||||
|
// Cleanup volumes
|
||||||
|
await Promise.all([docker.getVolume('decoy-volume').remove()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Removing orphaned volumes', () => {
|
||||||
|
it('removes any remaining unreferenced volumes after services have been deleted', async () => {
|
||||||
|
// Setup volume data
|
||||||
|
await Promise.all([
|
||||||
|
docker.createVolume({
|
||||||
|
Name: 'some-volume',
|
||||||
|
}),
|
||||||
|
// This volume is still referenced in the target state
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(111, 'main'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
docker.createVolume({
|
||||||
|
Name: Volume.generateDockerName(222, 'old'),
|
||||||
|
Labels: {
|
||||||
|
'io.balena.supervised': '1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// This volume is referenced by a container
|
||||||
|
docker.createVolume({
|
||||||
|
Name: 'other-volume',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create an empty image
|
||||||
|
await createDockerImage('hello', ['io.balena.testing=1'], docker);
|
||||||
|
|
||||||
|
// Create a container from the image
|
||||||
|
const { id: containerId } = await docker.createContainer({
|
||||||
|
Image: 'hello',
|
||||||
|
Cmd: ['true'],
|
||||||
|
HostConfig: {
|
||||||
|
Binds: ['other-volume:/data'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
volumeManager.removeOrphanedVolumes([
|
||||||
|
// Keep any volumes in the target state
|
||||||
|
Volume.generateDockerName(111, 'main'),
|
||||||
|
]),
|
||||||
|
).to.not.be.rejected;
|
||||||
|
|
||||||
|
// All volumes should have been deleted
|
||||||
|
expect(await docker.listVolumes())
|
||||||
|
.to.have.property('Volumes')
|
||||||
|
.that.has.lengthOf(2);
|
||||||
|
|
||||||
|
// Reference volume should have been kept
|
||||||
|
await expect(
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
|
||||||
|
).to.not.be.rejected;
|
||||||
|
await expect(docker.getVolume('other-volume').inspect()).to.not.be
|
||||||
|
.rejected;
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await Promise.all([
|
||||||
|
docker.getVolume(Volume.generateDockerName(111, 'main')).remove(),
|
||||||
|
docker.getContainer(containerId).remove(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
docker.getImage('hello').remove(),
|
||||||
|
docker.getVolume('other-volume').remove(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,360 +0,0 @@
|
|||||||
import { expect } from 'chai';
|
|
||||||
|
|
||||||
import * as sinon from 'sinon';
|
|
||||||
import {
|
|
||||||
createVolume,
|
|
||||||
createContainer,
|
|
||||||
withMockerode,
|
|
||||||
} from '~/test-lib/mockerode';
|
|
||||||
import * as volumeManager from '~/src/compose/volume-manager';
|
|
||||||
import log from '~/lib/supervisor-console';
|
|
||||||
import Volume from '~/src/compose/volume';
|
|
||||||
|
|
||||||
describe('compose/volume-manager', () => {
|
|
||||||
describe('Retrieving volumes from the engine', () => {
|
|
||||||
let logDebug: sinon.SinonStub;
|
|
||||||
before(() => {
|
|
||||||
logDebug = sinon.stub(log, 'debug');
|
|
||||||
});
|
|
||||||
after(() => {
|
|
||||||
logDebug.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
logDebug.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gets all supervised Volumes', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumeData = [
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(1, 'redis'),
|
|
||||||
// Recently created volumes contain io.balena.supervised label
|
|
||||||
Labels: { 'io.balena.supervised': '1' },
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(1, 'mysql'),
|
|
||||||
// Recently created volumes contain io.balena.supervised label and app-uuid
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
'io.balena.app-uuid': 'deadbeef',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(1, 'backend'),
|
|
||||||
// Old Volumes will not have labels
|
|
||||||
}),
|
|
||||||
// Volume not created by the Supervisor
|
|
||||||
createVolume({ Name: 'user_created_volume' }),
|
|
||||||
createVolume({
|
|
||||||
Name: 'decoy',
|
|
||||||
// Added decoy to really test the inference (should not return)
|
|
||||||
Labels: { 'io.balena.supervised': '1' },
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(
|
|
||||||
async () => {
|
|
||||||
await expect(volumeManager.getAll()).to.eventually.deep.equal([
|
|
||||||
{
|
|
||||||
appId: 1,
|
|
||||||
appUuid: undefined,
|
|
||||||
config: {
|
|
||||||
driver: 'local',
|
|
||||||
driverOpts: {},
|
|
||||||
labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: 'redis',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
appId: 1,
|
|
||||||
appUuid: 'deadbeef',
|
|
||||||
config: {
|
|
||||||
driver: 'local',
|
|
||||||
driverOpts: {},
|
|
||||||
labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
'io.balena.app-uuid': 'deadbeef',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: 'mysql',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
appId: 1,
|
|
||||||
appUuid: undefined,
|
|
||||||
config: {
|
|
||||||
driver: 'local',
|
|
||||||
driverOpts: {},
|
|
||||||
labels: {},
|
|
||||||
},
|
|
||||||
name: 'backend',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
// Check that debug message was logged saying we found a Volume not created by us
|
|
||||||
expect(logDebug.lastCall.lastArg).to.equal(
|
|
||||||
'Found unmanaged or anonymous Volume: decoy',
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ volumes: volumeData },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can parse null Volumes', async () => {
|
|
||||||
// Perform test with no volumes
|
|
||||||
await withMockerode(async () => {
|
|
||||||
await expect(volumeManager.getAll()).to.eventually.deep.equal([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('gets the volume for specific application', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'app'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(222, 'otherApp'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(
|
|
||||||
async () => {
|
|
||||||
await expect(
|
|
||||||
volumeManager.getAllByAppId(111),
|
|
||||||
).to.eventually.deep.equal([
|
|
||||||
{
|
|
||||||
appId: 111,
|
|
||||||
appUuid: undefined,
|
|
||||||
config: {
|
|
||||||
driver: 'local',
|
|
||||||
driverOpts: {},
|
|
||||||
labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: 'app',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Creating volumes', () => {
|
|
||||||
it('creates a volume if it does not exist', async () => {
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(async (mockerode) => {
|
|
||||||
// The volume does not exist on the engine before
|
|
||||||
expect(
|
|
||||||
mockerode.getVolume(Volume.generateDockerName(111, 'main')).inspect(),
|
|
||||||
).to.be.rejected;
|
|
||||||
|
|
||||||
// Volume to create
|
|
||||||
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
|
||||||
sinon.spy(volume, 'create');
|
|
||||||
|
|
||||||
// Create volume
|
|
||||||
await volumeManager.create(volume);
|
|
||||||
|
|
||||||
// Check that the creation function was called
|
|
||||||
expect(volume.create).to.have.been.calledOnce;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not try to create a volume that already exists', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'main'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(
|
|
||||||
async () => {
|
|
||||||
// Create compose object for volume already set up in mock engine
|
|
||||||
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
|
||||||
sinon.spy(volume, 'create');
|
|
||||||
|
|
||||||
// Create volume
|
|
||||||
await volumeManager.create(volume);
|
|
||||||
|
|
||||||
// Check volume was not created
|
|
||||||
expect(volume.create).to.not.have.been.called;
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Removing volumes', () => {
|
|
||||||
it('removes a volume if it exists', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'main'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(
|
|
||||||
async (mockerode) => {
|
|
||||||
// Volume to remove
|
|
||||||
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
|
||||||
sinon.spy(volume, 'remove');
|
|
||||||
|
|
||||||
// Remove volume
|
|
||||||
await volumeManager.remove(volume);
|
|
||||||
|
|
||||||
// Check volume was removed
|
|
||||||
expect(volume.remove).to.be.calledOnce;
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledOnceWith(
|
|
||||||
Volume.generateDockerName(111, 'main'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing on removal if the volume does not exist', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: 'decoy-volume',
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Perform test
|
|
||||||
await withMockerode(
|
|
||||||
async (mockerode) => {
|
|
||||||
// Volume to remove
|
|
||||||
const volume = Volume.fromComposeObject('main', 111, 'deadbeef', {});
|
|
||||||
sinon.spy(volume, 'remove');
|
|
||||||
|
|
||||||
// Remove volume
|
|
||||||
await expect(volumeManager.remove(volume)).to.not.be.rejected;
|
|
||||||
expect(mockerode.removeVolume).to.not.have.been.called;
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Removing orphaned volumes', () => {
|
|
||||||
it('removes any remaining unreferenced volumes after services have been deleted', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: 'some-volume',
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'main'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
await withMockerode(
|
|
||||||
async (mockerode) => {
|
|
||||||
await volumeManager.removeOrphanedVolumes([]);
|
|
||||||
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledTwice;
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledWith('some-volume');
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledWith(
|
|
||||||
Volume.generateDockerName(111, 'main'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps volumes still referenced in target state', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: 'some-volume',
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'main'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(222, 'old'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
await withMockerode(
|
|
||||||
async (mockerode) => {
|
|
||||||
await volumeManager.removeOrphanedVolumes([
|
|
||||||
Volume.generateDockerName(111, 'main'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledTwice;
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledWith('some-volume');
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledWith(
|
|
||||||
Volume.generateDockerName(222, 'old'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ volumes },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps volumes still referenced by a container', async () => {
|
|
||||||
// Setup volume data
|
|
||||||
const volumes = [
|
|
||||||
createVolume({
|
|
||||||
Name: 'some-volume',
|
|
||||||
}),
|
|
||||||
createVolume({
|
|
||||||
Name: Volume.generateDockerName(111, 'main'),
|
|
||||||
Labels: {
|
|
||||||
'io.balena.supervised': '1',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
const containers = [
|
|
||||||
createContainer({
|
|
||||||
Id: 'some-service',
|
|
||||||
Mounts: [
|
|
||||||
{
|
|
||||||
Name: 'some-volume',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
await withMockerode(
|
|
||||||
async (mockerode) => {
|
|
||||||
await volumeManager.removeOrphanedVolumes([]);
|
|
||||||
|
|
||||||
// Container that has a volume should not be removed
|
|
||||||
expect(mockerode.removeVolume).to.have.been.calledOnceWith(
|
|
||||||
Volume.generateDockerName(111, 'main'),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ volumes, containers },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
28
test/lib/docker-helper.ts
Normal file
28
test/lib/docker-helper.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as Docker from 'dockerode';
|
||||||
|
import * as tar from 'tar-stream';
|
||||||
|
|
||||||
|
// Creates an image from scratch with just some labels
|
||||||
|
export async function createDockerImage(
|
||||||
|
name: string,
|
||||||
|
labels: [string, ...string[]],
|
||||||
|
docker = new Docker(),
|
||||||
|
) {
|
||||||
|
const pack = tar.pack(); // pack is a streams2 stream
|
||||||
|
pack.entry(
|
||||||
|
{ name: 'Dockerfile' },
|
||||||
|
['FROM scratch'].concat(labels.map((l) => `LABEL ${l}`)).join('\n'),
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
pack.finalize();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Create an empty image
|
||||||
|
const stream = await docker.buildImage(pack, { t: name });
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
docker.modem.followProgress(stream, (err: any, res: any) =>
|
||||||
|
err ? reject(err) : resolve(res),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user