mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-13 01:36:22 +00:00
467 lines
14 KiB
TypeScript
467 lines
14 KiB
TypeScript
/*
|
|
* Copyright (C) 2019 StApps
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
// tslint:disable:no-implicit-dependencies
|
|
// tslint:disable:no-magic-numbers
|
|
// tslint:disable:completed-docs
|
|
// tslint:disable:prefer-function-over-method
|
|
// tslint:disable:newline-per-chained-call
|
|
import {Logger} from '@openstapps/logger';
|
|
import chai from 'chai';
|
|
import {expect} from 'chai';
|
|
import chaiSpies from 'chai-spies';
|
|
import {ContainerInfo} from 'dockerode';
|
|
import {slow, suite, test, timeout} from '@testdeck/mocha';
|
|
import {sslHardeningParameters, protocolHardeningParameters, SSLFilePaths} from './../src/common';
|
|
import {
|
|
containerMatchesRegex,
|
|
generateUpstreamMap,
|
|
getGatewayOfStAppsBackend,
|
|
getTemplateView,
|
|
generateListener,
|
|
generateMetricsServer,
|
|
getContainers,
|
|
} from '../src/main';
|
|
import {resolve} from 'path';
|
|
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
|
|
import proxyquire from 'proxyquire';
|
|
|
|
proxyquire.callThru().preserveCache();
|
|
|
|
process.on('unhandledRejection', async error => {
|
|
await Logger.error(error);
|
|
|
|
process.exit(1);
|
|
});
|
|
|
|
chai.should();
|
|
chai.use(chaiSpies);
|
|
|
|
@suite(timeout(1000), slow(500))
|
|
export class MainSpec {
|
|
static 'anyContainerWithExposedPorts': ContainerInfo = {
|
|
Command: 'sh',
|
|
Created: 1_524_669_882,
|
|
HostConfig: {
|
|
NetworkMode: 'default',
|
|
},
|
|
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
|
|
Image: 'ubuntu:4',
|
|
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
|
|
Labels: {},
|
|
Mounts: [],
|
|
Names: ['/container_name_1'],
|
|
NetworkSettings: {
|
|
Networks: {
|
|
bridge: {
|
|
Aliases: null,
|
|
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
|
|
Gateway: '172.18.0.1',
|
|
GlobalIPv6Address: '',
|
|
GlobalIPv6PrefixLen: 0,
|
|
IPAMConfig: null,
|
|
IPAddress: '172.18.0.3',
|
|
IPPrefixLen: 16,
|
|
IPv6Gateway: '',
|
|
Links: null,
|
|
MacAddress: '03:41:ac:11:00:23',
|
|
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
|
|
},
|
|
},
|
|
},
|
|
Ports: [
|
|
{
|
|
IP: '0.0.0.0',
|
|
PrivatePort: 80,
|
|
PublicPort: 80,
|
|
Type: 'tcp',
|
|
},
|
|
],
|
|
State: 'running',
|
|
Status: 'Up 3 minutes',
|
|
};
|
|
|
|
static 'backendContainerWithExposedPorts': ContainerInfo = {
|
|
Command: 'node ./bin/www',
|
|
Created: 1524669882,
|
|
HostConfig: {
|
|
NetworkMode: 'deployment_default',
|
|
},
|
|
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
|
|
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
|
|
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
|
|
Labels: {
|
|
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
|
|
'com.docker.compose.container-number': '1',
|
|
'com.docker.compose.oneoff': 'False',
|
|
'com.docker.compose.project': 'deployment',
|
|
'com.docker.compose.service': 'backend',
|
|
'com.docker.compose.version': '1.21.0',
|
|
'stapps.version': '1.0.0',
|
|
},
|
|
Mounts: [],
|
|
Names: ['/deployment_backend_1'],
|
|
NetworkSettings: {
|
|
Networks: {
|
|
deployment_default: {
|
|
Aliases: null,
|
|
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
|
|
Gateway: '172.18.0.1',
|
|
GlobalIPv6Address: '',
|
|
GlobalIPv6PrefixLen: 0,
|
|
IPAMConfig: null,
|
|
IPAddress: '172.18.0.3',
|
|
IPPrefixLen: 16,
|
|
IPv6Gateway: '',
|
|
Links: null,
|
|
MacAddress: '03:41:ac:11:00:23',
|
|
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
|
|
},
|
|
},
|
|
},
|
|
Ports: [
|
|
{
|
|
IP: '127.0.0.1',
|
|
PrivatePort: 3000,
|
|
PublicPort: 3000,
|
|
Type: 'tcp',
|
|
},
|
|
],
|
|
State: 'running',
|
|
Status: 'Up 3 minutes',
|
|
};
|
|
|
|
static 'swarmBackendContainerWithExposedPorts': ContainerInfo = {
|
|
Command: 'node ./bin/www',
|
|
Created: 1524669882,
|
|
HostConfig: {
|
|
NetworkMode: 'swarm_default',
|
|
},
|
|
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
|
|
Image: 'registry.gitlab.com/openstapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
|
|
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
|
|
Labels: {
|
|
'com.docker.compose.config-hash': '91c6e0cebad15951824162c93392b6880b69599692f07798ae8de659c1616a03',
|
|
'com.docker.compose.container-number': '1',
|
|
'com.docker.compose.oneoff': 'False',
|
|
'com.docker.stack.namespace': 'deployment',
|
|
'com.docker.swarm.service.name': 'deployment_backend',
|
|
'com.docker.compose.version': '1.21.0',
|
|
'stapps.version': '1.0.0',
|
|
},
|
|
Mounts: [],
|
|
Names: ['/deployment_backend_1'],
|
|
NetworkSettings: {
|
|
Networks: {
|
|
ingress: {
|
|
Aliases: null,
|
|
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
|
|
Gateway: '172.18.0.1',
|
|
GlobalIPv6Address: '',
|
|
GlobalIPv6PrefixLen: 0,
|
|
IPAMConfig: null,
|
|
IPAddress: '172.18.0.3',
|
|
IPPrefixLen: 16,
|
|
IPv6Gateway: '',
|
|
Links: null,
|
|
MacAddress: '03:41:ac:11:00:23',
|
|
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
|
|
},
|
|
},
|
|
},
|
|
Ports: [
|
|
{
|
|
IP: 'delete me',
|
|
PrivatePort: 3000,
|
|
PublicPort: 3000,
|
|
Type: 'tcp',
|
|
},
|
|
],
|
|
State: 'running',
|
|
Status: 'Up 3 minutes',
|
|
};
|
|
|
|
static 'sandbox' = chai.spy.sandbox();
|
|
|
|
'before'() {
|
|
MainSpec.sandbox.restore();
|
|
}
|
|
|
|
@test
|
|
'check if container does not match any container'() {
|
|
expect(
|
|
containerMatchesRegex('anyName', new RegExp('d+'), MainSpec.anyContainerWithExposedPorts),
|
|
).to.be.equal(false);
|
|
}
|
|
|
|
@test
|
|
'check if container does not match if version is incorrect'() {
|
|
expect(
|
|
containerMatchesRegex('backend', new RegExp('1\\.4\\.\\d+'), MainSpec.backendContainerWithExposedPorts),
|
|
).to.be.equal(false);
|
|
}
|
|
|
|
@test
|
|
'check if container matches'() {
|
|
expect(
|
|
containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), MainSpec.backendContainerWithExposedPorts),
|
|
).to.be.equal(true);
|
|
expect(
|
|
containerMatchesRegex(
|
|
'backend',
|
|
new RegExp('1\\.0\\.\\d+'),
|
|
MainSpec.swarmBackendContainerWithExposedPorts,
|
|
),
|
|
).to.be.equal(true);
|
|
}
|
|
|
|
@test
|
|
async 'get gateway of any container with exposed ports'() {
|
|
expect(await getGatewayOfStAppsBackend(MainSpec.anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
|
|
}
|
|
|
|
@test
|
|
async 'get gateway of backend container'() {
|
|
const spy = MainSpec.sandbox.on(console, 'error', () => {
|
|
// noop
|
|
});
|
|
|
|
const containerWithoutPorts: Partial<ContainerInfo> = {
|
|
Id: 'Foo',
|
|
Ports: [],
|
|
Names: ['/container_name_1'],
|
|
};
|
|
|
|
expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
|
|
expect(spy.__spy.calls[0][0]).to.contain('Container /container_name_1 does not advertise any port.');
|
|
}
|
|
|
|
@test
|
|
async 'get gateway of backend container without ports'() {
|
|
expect(await getGatewayOfStAppsBackend(MainSpec.backendContainerWithExposedPorts)).to.be.equal(
|
|
'127.0.0.1:3000',
|
|
);
|
|
}
|
|
|
|
@test
|
|
async 'get gateway of backend container within docker swarm'() {
|
|
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
|
|
delete backendContainer.Ports[0].IP;
|
|
|
|
const main = proxyquire('../src/main', {
|
|
'node-port-scanner': (_host: unknown, _ports: unknown) => {
|
|
return new Promise((resolve, _reject) => {
|
|
resolve({
|
|
ports: {
|
|
open: [3000],
|
|
},
|
|
});
|
|
});
|
|
},
|
|
});
|
|
expect(await main.getGatewayOfStAppsBackend(backendContainer)).to.be.equal('172.18.0.3:3000');
|
|
}
|
|
|
|
@test
|
|
async 'fail to get gateway of backend container if unreachable'() {
|
|
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
|
|
delete backendContainer.Ports[0].IP;
|
|
|
|
const spy = MainSpec.sandbox.on(console, 'error', () => {
|
|
// noop
|
|
});
|
|
|
|
const main = proxyquire('../src/main', {
|
|
'node-port-scanner': (_host: unknown, _ports: unknown) => {
|
|
return new Promise((resolve, _reject) => {
|
|
resolve({
|
|
ports: {
|
|
open: [],
|
|
},
|
|
});
|
|
});
|
|
},
|
|
});
|
|
|
|
expect(await main.getGatewayOfStAppsBackend(MainSpec.swarmBackendContainerWithExposedPorts)).to.be.equal(
|
|
'',
|
|
);
|
|
expect(spy.__spy.calls[0][0]).to.contain(
|
|
"It's possible your current Docker network setup isn't supported yet.",
|
|
);
|
|
}
|
|
|
|
@test
|
|
async 'fail to get gateway of backend container network mode is unsupported'() {
|
|
const backendContainer = MainSpec.swarmBackendContainerWithExposedPorts as any;
|
|
delete backendContainer.Ports[0].IP;
|
|
delete backendContainer.Ports[0].PublicPort;
|
|
delete backendContainer.Ports[0].PrivatePort;
|
|
|
|
const spy = MainSpec.sandbox.on(console, 'error', () => {
|
|
// noop
|
|
});
|
|
|
|
expect(await getGatewayOfStAppsBackend(MainSpec.swarmBackendContainerWithExposedPorts)).to.be.equal('');
|
|
expect(spy.__spy.calls[0][0]).to.contain(
|
|
"It's possible your current Docker network setup isn't supported yet.",
|
|
);
|
|
}
|
|
|
|
@test
|
|
async 'upstream map calls logger error when no matching container is found'() {
|
|
const spy = MainSpec.sandbox.on(console, 'warn', () => {
|
|
// noop
|
|
});
|
|
|
|
expect(
|
|
await generateUpstreamMap(
|
|
['0\\.8\\.\\d+'],
|
|
['1\\.1\\.\\d+'],
|
|
[MainSpec.backendContainerWithExposedPorts],
|
|
),
|
|
).to.be.equal(`map $http_x_stapps_version $proxyurl {
|
|
default unsupported;
|
|
"~0\\.8\\.\\d+" unavailable;
|
|
"~1\\.1\\.\\d+" outdated;
|
|
}
|
|
`);
|
|
|
|
expect(spy.__spy.calls[0][0]).to.contain('No backend for version');
|
|
}
|
|
|
|
@test
|
|
async 'upstream map with one active version and no outdated ones'() {
|
|
expect(
|
|
await generateUpstreamMap(
|
|
['1\\.0\\.\\d+'],
|
|
['0\\.8\\.\\d+'],
|
|
[MainSpec.backendContainerWithExposedPorts],
|
|
),
|
|
).to.be.equal(`map $http_x_stapps_version $proxyurl {
|
|
default unsupported;
|
|
"~1\\.0\\.\\d+" 1__0___d_;
|
|
"~0\\.8\\.\\d+" outdated;
|
|
}
|
|
upstream 1__0___d_ {
|
|
server 127.0.0.1:3000;
|
|
}
|
|
`);
|
|
}
|
|
|
|
@test
|
|
async 'get containers'() {
|
|
try {
|
|
await getContainers();
|
|
return false;
|
|
} catch (e) {
|
|
if ((e as Error).message.startsWith('No')) {
|
|
// Result, if docker is installed
|
|
expect((e as Error).message).to.equal(`No running docker containers found.
|
|
|
|
Please check if docker is running and Node.js can access the docker socket (/var/run/docker.sock)`);
|
|
} else {
|
|
// Result, if docker is not installed
|
|
expect([
|
|
new Error(`connect ENOENT /var/run/docker.sock`).message,
|
|
new Error('connect EACCES /var/run/docker.sock').message,
|
|
]).to.include((e as Error).message);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@test
|
|
async 'get template view'() {
|
|
try {
|
|
let containersWithSameVersion = [
|
|
MainSpec.backendContainerWithExposedPorts,
|
|
MainSpec.backendContainerWithExposedPorts,
|
|
];
|
|
await getTemplateView(containersWithSameVersion);
|
|
return false;
|
|
} catch (e) {
|
|
expect((e as Error).message).to.equal(`Multiple backends for one version found.`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@test
|
|
async 'include metrics config'() {
|
|
expect(await generateMetricsServer('test', true)).length.to.be.greaterThan(1);
|
|
}
|
|
|
|
@test
|
|
async 'omit metrics config'() {
|
|
expect(await generateMetricsServer('test', false)).to.equal('');
|
|
}
|
|
|
|
@test
|
|
'create listener faulty config'() {
|
|
expect(
|
|
generateListener({
|
|
certificate: 'faultyTest',
|
|
certificateChain: 'faultyTest',
|
|
certificateKey: 'faultyTest',
|
|
dhparam: 'faultyTest',
|
|
}),
|
|
).to.equal(`listen 80 default_server;
|
|
|
|
${protocolHardeningParameters}
|
|
`);
|
|
}
|
|
|
|
@test
|
|
'create listener correct config'() {
|
|
const testCertDir = resolve(__dirname, 'certs');
|
|
mkdirSync(testCertDir);
|
|
|
|
const certificateFile = resolve(testCertDir, 'ssl.crt');
|
|
const certificateKeyFile = resolve(testCertDir, 'ssl.key');
|
|
const certificateChainFile = resolve(testCertDir, 'chain.crt');
|
|
const dhparamFile = resolve(testCertDir, 'dhparam.pem');
|
|
|
|
writeFileSync(certificateFile, 'Test');
|
|
writeFileSync(certificateKeyFile, 'Test');
|
|
writeFileSync(certificateChainFile, 'Test');
|
|
writeFileSync(dhparamFile, 'Test');
|
|
|
|
const sslFilePaths: SSLFilePaths = {
|
|
certificate: certificateFile,
|
|
certificateKey: certificateKeyFile,
|
|
certificateChain: certificateChainFile,
|
|
dhparam: dhparamFile,
|
|
};
|
|
|
|
expect(generateListener(sslFilePaths)).to.equal(` listen 443 ssl default_server;
|
|
ssl_certificate ${sslFilePaths.certificate};
|
|
ssl_certificate_key ${sslFilePaths.certificateKey};
|
|
ssl_trusted_certificate ${sslFilePaths.certificateChain};
|
|
ssl_dhparam ${sslFilePaths.dhparam};
|
|
${sslHardeningParameters}
|
|
|
|
${protocolHardeningParameters}
|
|
`);
|
|
|
|
unlinkSync(certificateFile);
|
|
unlinkSync(certificateKeyFile);
|
|
unlinkSync(certificateChainFile);
|
|
unlinkSync(dhparamFile);
|
|
rmdirSync(testCertDir);
|
|
}
|
|
}
|