/* * 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 . */ // 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 = { 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); } }