Merge pull request #2040 from balena-os/docker-utils

Migrate some more legacy tests to integration
This commit is contained in:
bulldozer-balena[bot] 2022-10-20 14:41:11 +00:00 committed by GitHub
commit 156aac4b02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 271 additions and 286 deletions

38
package-lock.json generated
View File

@ -96,7 +96,7 @@
"rimraf": "^2.7.1",
"rwlock": "^5.0.0",
"shell-quote": "^1.7.2",
"sinon": "^11.1.2",
"sinon": "^14.0.1",
"sinon-chai": "^3.7.0",
"strict-event-emitter-types": "^2.0.0",
"supertest": "^6.1.3",
@ -1128,9 +1128,9 @@
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
"integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz",
"integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^1.7.0"
@ -10770,16 +10770,16 @@
"dev": true
},
"node_modules/sinon": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
"integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz",
"integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^1.8.3",
"@sinonjs/fake-timers": "^7.1.2",
"@sinonjs/samsam": "^6.0.2",
"@sinonjs/fake-timers": "^9.1.2",
"@sinonjs/samsam": "^6.1.1",
"diff": "^5.0.0",
"nise": "^5.1.0",
"nise": "^5.1.1",
"supports-color": "^7.2.0"
},
"funding": {
@ -14170,9 +14170,9 @@
}
},
"@sinonjs/fake-timers": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
"integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz",
"integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.7.0"
@ -21907,16 +21907,16 @@
}
},
"sinon": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz",
"integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==",
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz",
"integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.8.3",
"@sinonjs/fake-timers": "^7.1.2",
"@sinonjs/samsam": "^6.0.2",
"@sinonjs/fake-timers": "^9.1.2",
"@sinonjs/samsam": "^6.1.1",
"diff": "^5.0.0",
"nise": "^5.1.0",
"nise": "^5.1.1",
"supports-color": "^7.2.0"
},
"dependencies": {

View File

@ -122,7 +122,7 @@
"rimraf": "^2.7.1",
"rwlock": "^5.0.0",
"shell-quote": "^1.7.2",
"sinon": "^11.1.2",
"sinon": "^14.0.1",
"sinon-chai": "^3.7.0",
"strict-event-emitter-types": "^2.0.0",
"supertest": "^6.1.3",

View File

@ -1,13 +1,11 @@
import { expect } from 'chai';
import { isRight } from 'fp-ts/lib/Either';
import * as sinon from 'sinon';
import App from '~/src/compose/app';
import Network from '~/src/compose/network';
import * as config from '~/src/config';
import * as testDb from '~/src/db';
import * as dbFormat from '~/src/device-state/db-format';
import log from '~/lib/supervisor-console';
import { TargetApps } from '~/src/types/state';
import * as dbHelper from '~/test-lib/db-helper';
function getDefaultNetwork(appId: number) {
return {
@ -15,15 +13,10 @@ function getDefaultNetwork(appId: number) {
};
}
describe('db-format', () => {
let testDb: dbHelper.TestDatabase;
describe('device-state/db-format', () => {
let apiEndpoint: string;
before(async () => {
testDb = await dbHelper.createDB();
await config.initialized();
// Prevent side effects from changes in config
sinon.stub(config, 'on');
// TargetStateCache checks the API endpoint to
// store and invalidate the cache
@ -31,26 +24,14 @@ describe('db-format', () => {
// should not be part of the test suite. We need to change
// the target state architecture for this
apiEndpoint = await config.get('apiEndpoint');
// 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');
});
after(async () => {
try {
await testDb.destroy();
} catch {
/* noop */
}
sinon.restore();
});
afterEach(async () => {
await testDb.reset();
// Delete all apps between calls to prevent leaking tests
await testDb.models('app').del();
// Disable local mode by default
await config.set({ localMode: false });
});
it('converts target apps into the database format', async () => {

View File

@ -0,0 +1,173 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import * as dockerUtils from '~/lib/docker-utils';
import { createDockerImage } from '~/test-lib/docker-helper';
import * as Docker from 'dockerode';
describe('lib/docker-utils', () => {
const docker = new Docker();
describe('getNetworkGateway', async () => {
before(async () => {
await docker.createNetwork({
Name: 'supervisor0',
Options: {
'com.docker.network.bridge.name': 'supervisor0',
},
IPAM: {
Driver: 'default',
Config: [
{
Gateway: '10.0.105.1',
Subnet: '10.0.105.0/16',
},
],
},
});
});
after(async () => {
const allNetworks = await docker.listNetworks();
// Delete any remaining networks
await Promise.all(
allNetworks
.filter(({ Name }) => !['bridge', 'host', 'none'].includes(Name)) // exclude docker default network from the cleanup
.map(({ Name }) => docker.getNetwork(Name).remove()),
);
});
// test using existing data...
it('should return the correct gateway address for supervisor0', async () => {
const gateway = await dockerUtils.getNetworkGateway('supervisor0');
expect(gateway).to.equal('10.0.105.1');
});
it('should return the correct gateway address for host', async () => {
const gateway = await dockerUtils.getNetworkGateway('host');
expect(gateway).to.equal('127.0.0.1');
});
});
describe('getImageEnv', () => {
before(async () => {
await createDockerImage('test-image', ['io.balena.testing=1'], docker, [
'ENV TEST_VAR_1=1234',
'ENV TEST_VAR_2=5678',
]);
});
after(async () => {
await docker.pruneImages({ filters: { dangling: { false: true } } });
});
// test using existing data...
it('should return the correct image environment', async () => {
const obj = await dockerUtils.getImageEnv('test-image');
expect(obj).to.have.property('TEST_VAR_1').equal('1234');
expect(obj).to.have.property('TEST_VAR_2').equal('5678');
});
});
describe('isV2DeltaImage', () => {
it('should correctly detect a V2 delta', async () => {
// INFO: we still use the stub here. V2 deltas should eventually go away and there is no
// really easy way to create a real image that simulates a v2 delta.
const imageStub = stub(dockerUtils.docker, 'getImage').returns({
inspect: () => {
return Promise.resolve({
Id: 'sha256:34ec91fe6e08cb0f867bbc069c5f499d39297eb8e874bb8ce9707537d983bcbc',
RepoTags: [],
RepoDigests: [],
Parent: '',
Comment: '',
Created: '2019-12-05T10:20:51.516Z',
Container: '',
ContainerConfig: {
Hostname: '',
Domainname: '',
User: '',
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: null,
Cmd: null,
Image: '',
Volumes: null,
WorkingDir: '',
Entrypoint: null,
OnBuild: null,
Labels: null,
},
DockerVersion: '',
Author: '',
Config: {
Hostname: '7675a23f4fdc',
Domainname: '',
User: '',
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: [
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'TINI_VERSION=0.14.0',
'LC_ALL=C.UTF-8',
'DEBIAN_FRONTEND=noninteractive',
'UDEV=on',
'container=docker',
'test=123',
],
Cmd: [
'/bin/sh',
'-c',
"while true; do echo 'hello'; sleep 10; done;",
],
ArgsEscaped: true,
Image:
'sha256:b24946093df7157727b20934d11a7287359d8de42d8a80030f51f46a73d645ec',
Volumes: {
'/sys/fs/cgroup': {},
},
WorkingDir: '',
Entrypoint: ['/usr/bin/entry.sh'],
OnBuild: [],
Labels: {
'io.resin.architecture': 'amd64',
'io.resin.device-type': 'intel-nuc',
},
StopSignal: '37',
},
Architecture: '',
Os: 'linux',
Size: 17,
VirtualSize: 17,
GraphDriver: {
Data: null,
Name: 'aufs',
},
RootFS: {
Type: 'layers',
Layers: [
'sha256:c6e6cd4f95ef00e62f5c9df5798393470c991ca0148cb1e434b28101ed4219d3',
],
},
Metadata: {
LastTagTime: '0001-01-01T00:00:00Z',
},
});
},
} as any);
expect(await dockerUtils.isV2DeltaImage('test')).to.be.true;
expect(imageStub.callCount).to.equal(1);
imageStub.restore();
});
});
});

View File

@ -1,46 +1,32 @@
import _ = require('lodash');
import { expect } from 'chai';
import * as Docker from 'dockerode';
import { docker } from '~/lib/docker-utils';
import * as sinon from 'sinon';
import * as config from '~/src/config';
import * as firewall from '~/lib/firewall';
import * as logger from '~/src/logger';
import * as iptablesMock from '~/test-lib/mocked-iptables';
import * as targetStateCache from '~/src/device-state/target-state-cache';
import * as dbFormat from '~/src/device-state/db-format';
import constants = require('~/lib/constants');
import { RuleAction, Rule } from '~/lib/iptables';
import { log } from '~/lib/supervisor-console';
describe('Host Firewall', function () {
const dockerStubs: Dictionary<sinon.SinonStub> = {};
describe('lib/firewall', function () {
let loggerSpy: sinon.SinonSpy;
let logSpy: sinon.SinonSpy;
let logSpy: sinon.SinonStub;
let apiEndpoint: string;
let listenPort: number;
before(async () => {
await config.initialized();
// spy the logs...
loggerSpy = sinon.spy(logger, 'logSystemMessage');
logSpy = sinon.spy(log, 'error');
logSpy = log.error as sinon.SinonStub;
// stub the docker calls...
dockerStubs.listContainers = sinon
.stub(docker, 'listContainers')
.resolves([]);
dockerStubs.listImages = sinon.stub(docker, 'listImages').resolves([]);
dockerStubs.getImage = sinon.stub(docker, 'getImage').returns({
id: 'abcde',
inspect: async () => {
return {};
},
} as Docker.Image);
await targetStateCache.initialized();
await firewall.initialised();
apiEndpoint = await config.get('apiEndpoint');
@ -48,11 +34,7 @@ describe('Host Firewall', function () {
});
after(async () => {
for (const stub of _.values(dockerStubs)) {
stub.restore();
}
loggerSpy.restore();
logSpy.restore();
});
describe('Basic On/Off operation', () => {
@ -158,35 +140,38 @@ describe('Host Firewall', function () {
it('should respect the HOST_FIREWALL_MODE configuration value: auto (no services in host-network)', async function () {
await iptablesMock.whilstMocked(
async ({ hasAppliedRules, expectRule }) => {
await targetStateCache.setTargetApps([
await dbFormat.setApps(
{
appId: 2,
uuid: 'myapp',
commit: 'abcdef2',
name: 'test-app2',
class: 'fleet',
source: apiEndpoint,
releaseId: 1232,
isHost: false,
services: JSON.stringify([
{
serviceName: 'test-service',
image: 'test-image',
imageId: 5,
environment: {
TEST_VAR: 'test-string',
myapp: {
id: 2,
name: 'test-app2',
class: 'fleet',
is_host: false,
releases: {
abcdef2: {
id: 1232,
services: {
'test-service': {
id: 567,
image: 'test-image',
image_id: 5,
environment: {
TEST_VAR: 'test-string',
},
labels: {},
composition: {
tty: true,
},
},
},
networks: {},
volumes: {},
},
tty: true,
appId: 2,
releaseId: 1232,
serviceId: 567,
commit: 'abcdef2',
},
]),
networks: '[]',
volumes: '[]',
},
},
]);
apiEndpoint,
);
// set the firewall to be in auto mode...
await config.set({ firewallMode: 'auto' });
@ -213,38 +198,39 @@ describe('Host Firewall', function () {
it('should respect the HOST_FIREWALL_MODE configuration value: auto (service in host-network)', async function () {
await iptablesMock.whilstMocked(
async ({ hasAppliedRules, expectRule, expectNoRule }) => {
await targetStateCache.setTargetApps([
await dbFormat.setApps(
{
appId: 2,
uuid: 'myapp',
commit: 'abcdef2',
name: 'test-app2',
source: apiEndpoint,
class: 'fleet',
releaseId: 1232,
isHost: false,
services: JSON.stringify([
{
serviceName: 'test-service',
image: 'test-image',
imageId: 5,
environment: {
TEST_VAR: 'test-string',
},
appId: 2,
releaseId: 1232,
serviceId: 567,
commit: 'abcdef2',
composition: {
tty: true,
network_mode: 'host',
myapp: {
id: 2,
name: 'test-app2',
class: 'fleet',
is_host: false,
releases: {
abcdef2: {
id: 1232,
services: {
'test-service': {
id: 567,
image: 'test-image',
image_id: 5,
environment: {
TEST_VAR: 'test-string',
},
labels: {},
composition: {
tty: true,
network_mode: 'host',
},
},
},
networks: {},
volumes: {},
},
},
]),
networks: '[]',
volumes: '[]',
},
},
]);
apiEndpoint,
);
// set the firewall to be in auto mode...
await config.set({ firewallMode: 'auto' });

View File

@ -1,103 +0,0 @@
import { expect } from 'chai';
import { stub } from 'sinon';
import * as dockerUtils from '~/lib/docker-utils';
describe('Deltas', () => {
it('should correctly detect a V2 delta', async () => {
const imageStub = stub(dockerUtils.docker, 'getImage').returns({
inspect: () => {
return Promise.resolve({
Id: 'sha256:34ec91fe6e08cb0f867bbc069c5f499d39297eb8e874bb8ce9707537d983bcbc',
RepoTags: [],
RepoDigests: [],
Parent: '',
Comment: '',
Created: '2019-12-05T10:20:51.516Z',
Container: '',
ContainerConfig: {
Hostname: '',
Domainname: '',
User: '',
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: null,
Cmd: null,
Image: '',
Volumes: null,
WorkingDir: '',
Entrypoint: null,
OnBuild: null,
Labels: null,
},
DockerVersion: '',
Author: '',
Config: {
Hostname: '7675a23f4fdc',
Domainname: '',
User: '',
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Tty: false,
OpenStdin: false,
StdinOnce: false,
Env: [
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'TINI_VERSION=0.14.0',
'LC_ALL=C.UTF-8',
'DEBIAN_FRONTEND=noninteractive',
'UDEV=on',
'container=docker',
'test=123',
],
Cmd: [
'/bin/sh',
'-c',
"while true; do echo 'hello'; sleep 10; done;",
],
ArgsEscaped: true,
Image:
'sha256:b24946093df7157727b20934d11a7287359d8de42d8a80030f51f46a73d645ec',
Volumes: {
'/sys/fs/cgroup': {},
},
WorkingDir: '',
Entrypoint: ['/usr/bin/entry.sh'],
OnBuild: [],
Labels: {
'io.resin.architecture': 'amd64',
'io.resin.device-type': 'intel-nuc',
},
StopSignal: '37',
},
Architecture: '',
Os: 'linux',
Size: 17,
VirtualSize: 17,
GraphDriver: {
Data: null,
Name: 'aufs',
},
RootFS: {
Type: 'layers',
Layers: [
'sha256:c6e6cd4f95ef00e62f5c9df5798393470c991ca0148cb1e434b28101ed4219d3',
],
},
Metadata: {
LastTagTime: '0001-01-01T00:00:00Z',
},
});
},
} as any);
expect(await dockerUtils.isV2DeltaImage('test')).to.be.true;
expect(imageStub.callCount).to.equal(1);
imageStub.restore();
});
});

View File

@ -1,56 +0,0 @@
import { expect } from 'chai';
import { testWithData } from '~/test-lib/mocked-dockerode';
import * as dockerUtils from '~/lib/docker-utils';
describe('Docker Utils', () => {
describe('Supervisor Address', () => {
// setup some fake data...
const networks = {
supervisor0: {
IPAM: {
Config: [
{
Gateway: '10.0.105.1',
Subnet: '10.0.105.0/16',
},
],
},
},
};
// test using existing data...
it('should return the correct gateway address for supervisor0', async () => {
await testWithData({ networks }, async () => {
const gateway = await dockerUtils.getNetworkGateway('supervisor0');
expect(gateway).to.equal('10.0.105.1');
});
});
it('should return the correct gateway address for host', async () => {
await testWithData({ networks }, async () => {
const gateway = await dockerUtils.getNetworkGateway('host');
expect(gateway).to.equal('127.0.0.1');
});
});
});
describe('Image Environment', () => {
const images = {
['test-image']: {
Config: {
Env: ['TEST_VAR_1=1234', 'TEST_VAR_2=5678'],
},
},
};
// test using existing data...
it('should return the correct image environment', async () => {
await testWithData({ images }, async () => {
const obj = await dockerUtils.getImageEnv('test-image');
expect(obj).to.have.property('TEST_VAR_1').equal('1234');
expect(obj).to.have.property('TEST_VAR_2').equal('5678');
});
});
});
});

View File

@ -8,11 +8,15 @@ export async function createDockerImage(
name: string,
labels: [string, ...string[]],
docker = new Docker(),
extra = [] as string[], // Additional instructions to add to the dockerfile
): Promise<string> {
const pack = tar.pack(); // pack is a streams2 stream
pack.entry(
{ name: 'Dockerfile' },
['FROM scratch'].concat(labels.map((l) => `LABEL ${l}`)).join('\n'),
['FROM scratch']
.concat(labels.map((l) => `LABEL ${l}`))
.concat(extra)
.join('\n'),
(err) => {
if (err) {
throw err;

View File

@ -136,11 +136,11 @@ export const whilstMocked = async (
const stdin = new Writable();
stdin._write = (
chunk: Buffer,
_chunk: Buffer,
_encoding: string,
callback: (err?: Error) => void,
) => {
console.log(chunk.toString('utf8'));
// console.log(chunk.toString('utf8'));
callback();
fakeProc.emit('close', 1);
};

View File

@ -6,7 +6,7 @@ import { NetworkInspectInfo } from 'dockerode';
import { log } from '~/lib/supervisor-console';
describe('compose/network: unit tests', () => {
describe('compose/network', () => {
describe('creating a network from a compose object', () => {
it('creates a default network configuration if no config is given', () => {
const network = Network.fromComposeObject(