mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-26 17:01:06 +00:00
480 lines
12 KiB
TypeScript
480 lines
12 KiB
TypeScript
|
import { expect } from 'chai';
|
||
|
import { SinonStub, stub } from 'sinon';
|
||
|
import Volume from '../../../src/compose/volume';
|
||
|
import * as logTypes from '../../../src/lib/log-types';
|
||
|
import * as logger from '../../../src/logger';
|
||
|
|
||
|
import { createVolume, withMockerode } from '../../lib/mockerode';
|
||
|
|
||
|
describe('compose/volume', () => {
|
||
|
describe('creating a volume from a compose object', () => {
|
||
|
it('should use proper defaults when no compose configuration is provided', () => {
|
||
|
const volume = Volume.fromComposeObject('my_volume', 1234, {});
|
||
|
|
||
|
expect(volume.name).to.equal('my_volume');
|
||
|
expect(volume.appId).to.equal(1234);
|
||
|
expect(volume.config).to.deep.equal({
|
||
|
driver: 'local',
|
||
|
driverOpts: {},
|
||
|
labels: {
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should correctly parse compose volumes without an explicit driver', () => {
|
||
|
const volume = Volume.fromComposeObject('one_volume', 1032480, {
|
||
|
driver_opts: {
|
||
|
opt1: 'test',
|
||
|
},
|
||
|
labels: {
|
||
|
'my-label': 'test-label',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(volume).to.have.property('appId').that.equals(1032480);
|
||
|
expect(volume).to.have.property('name').that.equals('one_volume');
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('labels')
|
||
|
.that.deep.equals({
|
||
|
'io.balena.supervised': 'true',
|
||
|
'my-label': 'test-label',
|
||
|
});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driverOpts')
|
||
|
.that.deep.equals({
|
||
|
opt1: 'test',
|
||
|
});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driver')
|
||
|
.that.equals('local');
|
||
|
});
|
||
|
|
||
|
it('should correctly parse compose volumes with an explicit driver', () => {
|
||
|
const volume = Volume.fromComposeObject('one_volume', 1032480, {
|
||
|
driver: 'other',
|
||
|
driver_opts: {
|
||
|
opt1: 'test',
|
||
|
},
|
||
|
labels: {
|
||
|
'my-label': 'test-label',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(volume).to.have.property('appId').that.equals(1032480);
|
||
|
expect(volume).to.have.property('name').that.equals('one_volume');
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('labels')
|
||
|
.that.deep.equals({
|
||
|
'io.balena.supervised': 'true',
|
||
|
'my-label': 'test-label',
|
||
|
});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driverOpts')
|
||
|
.that.deep.equals({
|
||
|
opt1: 'test',
|
||
|
});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driver')
|
||
|
.that.equals('other');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('creating a volume instance from a docker volume', () => {
|
||
|
it('should correctly parse app id from volume name', () => {
|
||
|
const volume = Volume.fromDockerVolume({
|
||
|
Driver: 'local',
|
||
|
Name: '1234_my_volume',
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Labels: {},
|
||
|
Options: {},
|
||
|
Scope: 'local',
|
||
|
});
|
||
|
|
||
|
expect(volume.name).to.equal('my_volume');
|
||
|
expect(volume.appId).to.equal(1234);
|
||
|
});
|
||
|
|
||
|
it('should fail if volume name is not properly formatted', () => {
|
||
|
expect(() =>
|
||
|
Volume.fromDockerVolume({
|
||
|
Driver: 'local',
|
||
|
Name: 'non_supervised_volume',
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Labels: {},
|
||
|
Options: {},
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
).to.throw;
|
||
|
});
|
||
|
|
||
|
it('should correctly parse docker volumes', () => {
|
||
|
const volume = Volume.fromDockerVolume({
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Name: '1032480_one_volume',
|
||
|
Options: {},
|
||
|
Scope: 'local',
|
||
|
});
|
||
|
|
||
|
expect(volume).to.have.property('appId').that.equals(1032480);
|
||
|
expect(volume).to.have.property('name').that.equals('one_volume');
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('labels')
|
||
|
.that.deep.equals({
|
||
|
'io.balena.supervised': 'true',
|
||
|
});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driverOpts')
|
||
|
.that.deep.equals({});
|
||
|
expect(volume)
|
||
|
.to.have.property('config')
|
||
|
.that.has.property('driver')
|
||
|
.that.equals('local');
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('creating a docker volume from options', () => {
|
||
|
before(() => {
|
||
|
stub(logger, 'logSystemEvent');
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
(logger.logSystemEvent as SinonStub).reset();
|
||
|
});
|
||
|
|
||
|
after(() => {
|
||
|
(logger.logSystemEvent as SinonStub).restore();
|
||
|
});
|
||
|
|
||
|
it('should use defaults to create the volume when no options are given', async () => {
|
||
|
await withMockerode(async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('one_volume', 1032480, {});
|
||
|
|
||
|
await volume.create();
|
||
|
|
||
|
expect(mockerode.createVolume).to.have.been.calledOnceWith({
|
||
|
Name: '1032480_one_volume',
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
DriverOpts: {},
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should pass configuration options to the engine', async () => {
|
||
|
await withMockerode(async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('one_volume', 1032480, {
|
||
|
driver_opts: {
|
||
|
opt1: 'test',
|
||
|
},
|
||
|
labels: {
|
||
|
'my-label': 'test-label',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
await volume.create();
|
||
|
|
||
|
expect(mockerode.createVolume).to.have.been.calledOnceWith({
|
||
|
Name: '1032480_one_volume',
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'my-label': 'test-label',
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
DriverOpts: {
|
||
|
opt1: 'test',
|
||
|
},
|
||
|
});
|
||
|
|
||
|
expect(logger.logSystemEvent).to.have.been.calledOnceWith(
|
||
|
logTypes.createVolume,
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
it('should log successful volume creation to the cloud', async () => {
|
||
|
await withMockerode(async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('one_volume', 1032480, {});
|
||
|
|
||
|
await volume.create();
|
||
|
|
||
|
expect(mockerode.createVolume).to.have.been.calledOnce;
|
||
|
expect(logger.logSystemEvent).to.have.been.calledOnceWith(
|
||
|
logTypes.createVolume,
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('comparing volume configuration', () => {
|
||
|
it('should ignore name and supervisor labels in the comparison', () => {
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromComposeObject('bbb', 4567, {
|
||
|
driver: 'local',
|
||
|
driver_opts: {},
|
||
|
}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromComposeObject('bbb', 4567, {}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromDockerVolume({
|
||
|
Name: '1234_aaa',
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Options: {},
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromDockerVolume({
|
||
|
Name: '4567_bbb',
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Options: {},
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromDockerVolume({
|
||
|
Name: '1234_aaa',
|
||
|
Driver: 'local',
|
||
|
Labels: {
|
||
|
'some.other.label': '123',
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Options: {},
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
),
|
||
|
).to.be.false;
|
||
|
});
|
||
|
|
||
|
it('should compare based on driver configuration and options', () => {
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver: 'other',
|
||
|
driver_opts: {},
|
||
|
}),
|
||
|
),
|
||
|
).to.be.false;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver: 'other',
|
||
|
}).isEqualConfig(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver: 'other',
|
||
|
driver_opts: {},
|
||
|
}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {}).isEqualConfig(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver_opts: { opt: '123' },
|
||
|
}),
|
||
|
),
|
||
|
).to.be.false;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver: 'other',
|
||
|
labels: { 'some.other.label': '123' },
|
||
|
driver_opts: { 'some-opt': '123' },
|
||
|
}).isEqualConfig(
|
||
|
Volume.fromDockerVolume({
|
||
|
Name: '1234_aaa',
|
||
|
Driver: 'other',
|
||
|
Labels: {
|
||
|
'some.other.label': '123',
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Options: {},
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
),
|
||
|
).to.be.false;
|
||
|
|
||
|
expect(
|
||
|
Volume.fromComposeObject('aaa', 1234, {
|
||
|
driver: 'other',
|
||
|
labels: { 'some.other.label': '123' },
|
||
|
driver_opts: { 'some-opt': '123' },
|
||
|
}).isEqualConfig(
|
||
|
Volume.fromDockerVolume({
|
||
|
Name: '1234_aaa',
|
||
|
Driver: 'other',
|
||
|
Labels: {
|
||
|
'some.other.label': '123',
|
||
|
'io.balena.supervised': 'true',
|
||
|
},
|
||
|
Options: { 'some-opt': '123' },
|
||
|
Mountpoint: '/var/lib/docker/volumes/1032480_one_volume/_data',
|
||
|
Scope: 'local',
|
||
|
}),
|
||
|
),
|
||
|
).to.be.true;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('removing volumes', () => {
|
||
|
before(() => {
|
||
|
stub(logger, 'logSystemEvent');
|
||
|
});
|
||
|
|
||
|
afterEach(() => {
|
||
|
(logger.logSystemEvent as SinonStub).reset();
|
||
|
});
|
||
|
|
||
|
after(() => {
|
||
|
(logger.logSystemEvent as SinonStub).restore();
|
||
|
});
|
||
|
|
||
|
it('should remove the volume from the engine if it exists', async () => {
|
||
|
const dockerVolume = createVolume({
|
||
|
Name: '1234_aaa',
|
||
|
});
|
||
|
|
||
|
await withMockerode(
|
||
|
async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('aaa', 1234, {});
|
||
|
|
||
|
// Check engine state before (this is really to test that mockerode is doing its job)
|
||
|
expect((await mockerode.listVolumes()).Volumes).to.have.lengthOf(1);
|
||
|
expect(await mockerode.getVolume('1234_aaa').inspect()).to.deep.equal(
|
||
|
dockerVolume.inspectInfo,
|
||
|
);
|
||
|
|
||
|
// Remove the volume
|
||
|
await volume.remove();
|
||
|
|
||
|
// Check that the remove method was called
|
||
|
expect(mockerode.removeVolume).to.have.been.calledOnceWith(
|
||
|
'1234_aaa',
|
||
|
);
|
||
|
},
|
||
|
{ volumes: [dockerVolume] },
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should report the volume removal as a system event', async () => {
|
||
|
const dockerVolume = createVolume({
|
||
|
Name: '1234_aaa',
|
||
|
});
|
||
|
|
||
|
await withMockerode(
|
||
|
async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('aaa', 1234, {});
|
||
|
|
||
|
// Check engine state before
|
||
|
expect((await mockerode.listVolumes()).Volumes).to.have.lengthOf(1);
|
||
|
|
||
|
// Remove the volume
|
||
|
await volume.remove();
|
||
|
|
||
|
// Check that the remove method was called
|
||
|
expect(mockerode.removeVolume).to.have.been.calledOnceWith(
|
||
|
'1234_aaa',
|
||
|
);
|
||
|
|
||
|
// Check that log entry was generated
|
||
|
expect(logger.logSystemEvent).to.have.been.calledOnceWith(
|
||
|
logTypes.removeVolume,
|
||
|
);
|
||
|
},
|
||
|
{ volumes: [dockerVolume] },
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should report an error if the volume does not exist', async () => {
|
||
|
const dockerVolume = createVolume({
|
||
|
Name: '4567_bbb',
|
||
|
});
|
||
|
await withMockerode(
|
||
|
async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('aaa', 1234, {});
|
||
|
|
||
|
// Check engine state before
|
||
|
expect((await mockerode.listVolumes()).Volumes).to.have.lengthOf(1);
|
||
|
|
||
|
// Remove the volume, this should not throw
|
||
|
await expect(volume.remove()).to.not.be.rejected;
|
||
|
|
||
|
// Check that the remove method was called
|
||
|
expect(mockerode.removeVolume).to.not.have.been.called;
|
||
|
|
||
|
// Check that log entry was generated
|
||
|
expect(logger.logSystemEvent).to.have.been.calledWith(
|
||
|
logTypes.removeVolumeError,
|
||
|
);
|
||
|
},
|
||
|
{ volumes: [dockerVolume] },
|
||
|
);
|
||
|
});
|
||
|
|
||
|
it('should report an error if a problem happens while removing the volume', async () => {
|
||
|
const dockerVolume = createVolume({
|
||
|
Name: '1234_aaa',
|
||
|
});
|
||
|
await withMockerode(
|
||
|
async (mockerode) => {
|
||
|
const volume = Volume.fromComposeObject('aaa', 1234, {});
|
||
|
|
||
|
// Stub the mockerode method to fail
|
||
|
mockerode.removeVolume.rejects('Something bad happened');
|
||
|
|
||
|
// Check engine state before
|
||
|
expect((await mockerode.listVolumes()).Volumes).to.have.lengthOf(1);
|
||
|
|
||
|
// Remove the volume, this should not throw
|
||
|
await expect(volume.remove()).to.not.be.rejected;
|
||
|
|
||
|
// Check that log entry was generated
|
||
|
expect(logger.logSystemEvent).to.have.been.calledWith(
|
||
|
logTypes.removeVolumeError,
|
||
|
);
|
||
|
},
|
||
|
{ volumes: [dockerVolume] },
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
});
|