mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-10 03:32:52 +00:00
refactor: update dependencies
This commit is contained in:
committed by
Rainer Killinger
parent
9512bca329
commit
053a6ce23f
4331
package-lock.json
generated
4331
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@@ -1,56 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "@openstapps/proxy",
|
"name": "@openstapps/proxy",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Nginx proxy that is dynamically configured by a Node.js script",
|
"description": "NGINX proxy that is dynamically configured by a Node.js script",
|
||||||
"main": "./lib/cli.js",
|
"main": "./lib/cli.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openstapps/logger": "0.0.5",
|
"@openstapps/logger": "0.5.0",
|
||||||
"@types/config": "0.0.34",
|
"@types/config": "0.0.36",
|
||||||
"@types/dockerode": "2.5.12",
|
"@types/dockerode": "2.5.34",
|
||||||
"@types/sha1": "1.1.1",
|
"@types/node": "10.17.17",
|
||||||
"config": "3.0.1",
|
"@types/sha1": "1.1.2",
|
||||||
"fs-extra": "7.0.1",
|
"config": "3.3.2",
|
||||||
"mustache": "3.0.1",
|
"dockerode": "3.2.1",
|
||||||
"sha1": "1.1.1",
|
"mustache": "4.0.1"
|
||||||
"typescript": "3.3.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openstapps/configuration": "0.6.0",
|
"@openstapps/configuration": "0.25.0",
|
||||||
"@types/chai": "4.1.7",
|
"@testdeck/mocha": "0.1.2",
|
||||||
"@types/mustache": "0.8.32",
|
"@types/chai": "4.2.12",
|
||||||
"@types/sinon": "7.0.6",
|
"@types/chai-spies": "1.0.2",
|
||||||
|
"@types/mustache": "4.0.1",
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"conventional-changelog-cli": "2.0.12",
|
"chai-spies": "1.0.0",
|
||||||
"dockerode": "2.5.8",
|
"conventional-changelog-cli": "2.1.0",
|
||||||
"mocha": "5.2.0",
|
"mocha": "7.1.1",
|
||||||
"mocha-typescript": "1.1.17",
|
"nyc": "15.1.0",
|
||||||
"prepend-file-cli": "1.0.6",
|
"prepend-file-cli": "1.0.6",
|
||||||
"rimraf": "2.6.3",
|
"rimraf": "3.0.2",
|
||||||
"sinon": "7.2.4",
|
"ts-node": "8.6.2",
|
||||||
"ts-node": "8.0.2",
|
"tslint": "6.1.3",
|
||||||
"tslint": "5.12.1",
|
"typedoc": "0.18.0",
|
||||||
"typedoc": "0.14.2"
|
"typescript": "4.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run tslint && npm run compile && npm run documentation",
|
"build": "npm run tslint && npm run compile",
|
||||||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
|
||||||
"check-configuration": "openstapps-configuration",
|
"check-configuration": "openstapps-configuration",
|
||||||
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
|
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
|
||||||
"documentation": "typedoc --includeDeclarations --excludeExternals --mode modules --out docs src",
|
"documentation": "typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src",
|
||||||
"prepublishOnly": "npm run build",
|
"postversion": "npm run changelog",
|
||||||
"tslint": "tslint 'src/**/*.ts'",
|
"prepublishOnly": "npm ci && npm run build",
|
||||||
"test": "node_modules/.bin/mocha --opts test/mocha.opts"
|
"preversion": "npm run prepublishOnly",
|
||||||
|
"push": "git push && git push origin \"v$npm_package_version\"",
|
||||||
|
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'",
|
||||||
|
"test": "nyc mocha --require source-map-support/register --require ts-node/register 'test/**/*.spec.ts'"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@gitlab.com:openstapps/proxy.git"
|
"url": "git@gitlab.com:openstapps/proxy.git"
|
||||||
},
|
},
|
||||||
"author": "Anselm Stordeur <anselmstordeur@gmail.com>",
|
"author": "Anselm Rochus Stordeur <anselmstordeur@gmail.com>",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"André Bierlein <andre.mt.bierlein@gmail.com>",
|
"André Bierlein <andre.mt.bierlein@gmail.com>",
|
||||||
"Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
|
"Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
|
||||||
"Benjamin Joeckel",
|
"Benjamin Joeckel",
|
||||||
"Jovan Krunic <jovan.krunic@gmail.com>"
|
"Jovan Krunic <jovan.krunic@gmail.com>",
|
||||||
|
"Rainer Killinger"
|
||||||
],
|
],
|
||||||
"license": "AGPL-3.0-only"
|
"license": "AGPL-3.0-only",
|
||||||
|
"openstappsConfiguration": {
|
||||||
|
"forPackaging": false,
|
||||||
|
"ignoreCiEntries": [
|
||||||
|
"package"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nyc": {
|
||||||
|
"all": true,
|
||||||
|
"branches": 95,
|
||||||
|
"check-coverage": true,
|
||||||
|
"exclude": [
|
||||||
|
"src/cli.ts"
|
||||||
|
],
|
||||||
|
"extension": [
|
||||||
|
".ts"
|
||||||
|
],
|
||||||
|
"functions": 95,
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"lines": 95,
|
||||||
|
"per-file": true,
|
||||||
|
"reporter": [
|
||||||
|
"html",
|
||||||
|
"text-summary"
|
||||||
|
],
|
||||||
|
"statements": 95
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/cli.ts
59
src/cli.ts
@@ -13,68 +13,57 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import {Logger} from '@openstapps/logger';
|
||||||
import {execSync} from 'child_process';
|
import {execSync} from 'child_process';
|
||||||
import * as config from 'config';
|
|
||||||
import * as Dockerode from 'dockerode';
|
import * as Dockerode from 'dockerode';
|
||||||
import {readFile, writeFileSync} from 'fs-extra';
|
|
||||||
import {render} from 'mustache';
|
import {render} from 'mustache';
|
||||||
import {ConfigFile, logger} from './common';
|
import {asyncReadFile, asyncWriteFile, configFile} from './common';
|
||||||
import {getContainers, getTemplateView} from './main';
|
import {getContainers, getTemplateView} from './main';
|
||||||
|
|
||||||
// handle unhandled promise rejections
|
// handle unhandled promise rejections
|
||||||
process.on('unhandledRejection', (error: Error) => {
|
process.on('unhandledRejection', async (error) => {
|
||||||
logger.error(error.message);
|
await Logger.error(error);
|
||||||
logger.info(error.stack);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
let containerHashCache = '';
|
let containerHashCache = '';
|
||||||
const configFile: ConfigFile = config.util.toObject();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the container information from the docker socket and updates
|
* Reads the container information from the docker socket and updates the nginx config if necessary
|
||||||
* the nginx config if necessary
|
|
||||||
*
|
|
||||||
* The function will call itself again every 10s
|
|
||||||
*/
|
*/
|
||||||
async function updateNginxConfig() {
|
async function updateNginxConfig() {
|
||||||
|
|
||||||
const containers = await getContainers();
|
const containers = await getContainers();
|
||||||
|
|
||||||
const containerHash = containers.map((container: Dockerode.ContainerInfo) => {
|
const containerHash = containers
|
||||||
return container.Id;
|
.map((container: Dockerode.ContainerInfo) => {
|
||||||
}).join(',');
|
return container.Id;
|
||||||
|
})
|
||||||
|
.join(',');
|
||||||
|
|
||||||
// if containers changed -> write config file, reload nginx
|
// if containers changed -> write config file, reload nginx
|
||||||
if (containerHash !== containerHashCache) {
|
if (containerHash !== containerHashCache) {
|
||||||
logger.log('docker container changed');
|
Logger.log('Generating new NGINX configuration');
|
||||||
logger.log('Generating new NGINX configuration');
|
|
||||||
|
|
||||||
// render nginx config file
|
// render nginx config file
|
||||||
const nginxConfig = render(await readFile('nginx.conf.template', 'utf8'), await getTemplateView(containers));
|
const nginxConfig = render(await asyncReadFile('nginx.conf.template', 'utf8'), await getTemplateView(containers));
|
||||||
|
|
||||||
logger.log(`containers (${containerHash}) matched the configuration.`);
|
Logger.log(`containers (${containerHash}) matched the configuration.`);
|
||||||
|
|
||||||
containerHashCache = containerHash;
|
containerHashCache = containerHash;
|
||||||
|
|
||||||
logger.log(`Writing new config file "${configFile.output}"`);
|
Logger.log(`Writing new config file "${configFile.output}"`);
|
||||||
// overwrite nginx config file with our rendered one
|
|
||||||
writeFileSync(configFile.output, nginxConfig, 'utf8');
|
// overwrite nginx config file with our rendered one
|
||||||
|
await asyncWriteFile(configFile.output, nginxConfig, 'utf8');
|
||||||
|
|
||||||
|
Logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');
|
||||||
|
|
||||||
logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');
|
|
||||||
execSync('nginx -s reload');
|
execSync('nginx -s reload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-magic-numbers - set timeout to update configuration again in 30s
|
||||||
|
setTimeout(updateNginxConfig, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function forever() {
|
// tslint:disable-next-line:no-floating-promises - start the process that checks the docker socket periodically
|
||||||
// start the process of dynamic nginx configuration
|
updateNginxConfig();
|
||||||
updateNginxConfig().then(() => {
|
|
||||||
// check for changes again in 10 seconds
|
|
||||||
setTimeout(forever, 10000);
|
|
||||||
}).catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the process that checks the docker socket periodically
|
|
||||||
forever();
|
|
||||||
|
|||||||
105
src/common.ts
105
src/common.ts
@@ -14,48 +14,99 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Logger} from '@openstapps/logger';
|
import {Logger} from '@openstapps/logger';
|
||||||
import {SMTP} from '@openstapps/logger/lib/SMTP';
|
import {SMTP} from '@openstapps/logger/lib/smtp';
|
||||||
|
import config from 'config';
|
||||||
|
import {existsSync, readFile, writeFile} from 'fs';
|
||||||
|
import {promisify} from 'util';
|
||||||
|
|
||||||
// use SMTP as a default monitoring system for logger.error();
|
// set transport on logger
|
||||||
export const logger = new Logger(SMTP.getInstance());
|
Logger.setTransport(SMTP.getInstance());
|
||||||
|
|
||||||
|
export const asyncReadFile = promisify(readFile);
|
||||||
|
export const asyncWriteFile = promisify(writeFile);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of the file paths of the needed ssl certificates
|
* A representation of the file paths of the needed ssl certificates
|
||||||
*/
|
*/
|
||||||
export interface SSLFilePaths {
|
export interface SSLFilePaths {
|
||||||
certificate: string;
|
/**
|
||||||
certificateChain: string;
|
* Path to SSL certificate
|
||||||
certificateKey: string;
|
*/
|
||||||
dhparam: string;
|
certificate: string;
|
||||||
|
/**
|
||||||
|
* Path to SSL certificate chain
|
||||||
|
*/
|
||||||
|
certificateChain: string;
|
||||||
|
/**
|
||||||
|
* Path to SSL certificate key
|
||||||
|
*/
|
||||||
|
certificateKey: string;
|
||||||
|
/**
|
||||||
|
* Path to SSL DHParam
|
||||||
|
*/
|
||||||
|
dhparam: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A representation of the config file
|
* A representation of the config file
|
||||||
*/
|
*/
|
||||||
export interface ConfigFile {
|
export interface ConfigFile {
|
||||||
activeVersions: string[];
|
/**
|
||||||
hiddenRoutes: string[];
|
* List of active version
|
||||||
outdatedVersions: string[];
|
*/
|
||||||
output: string;
|
activeVersions: string[];
|
||||||
sslFilePaths: SSLFilePaths;
|
/**
|
||||||
visibleRoutes: string[];
|
* List of hidden routes
|
||||||
|
*/
|
||||||
|
hiddenRoutes: string[];
|
||||||
|
/**
|
||||||
|
* List of outdated versions
|
||||||
|
*/
|
||||||
|
outdatedVersions: string[];
|
||||||
|
/**
|
||||||
|
* Output?! TODO
|
||||||
|
*/
|
||||||
|
output: string;
|
||||||
|
/**
|
||||||
|
* SSL file paths
|
||||||
|
*/
|
||||||
|
sslFilePaths: SSLFilePaths;
|
||||||
|
/**
|
||||||
|
* List of visible routes
|
||||||
|
*/
|
||||||
|
visibleRoutes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view object to render the nginx config template
|
* A view object to render the nginx config template
|
||||||
*/
|
*/
|
||||||
export interface TemplateView {
|
export interface TemplateView {
|
||||||
dockerVersionMap: string;
|
/**
|
||||||
hiddenRoutes: string;
|
* Docker version map
|
||||||
listener: string;
|
*/
|
||||||
staticRoute: string;
|
dockerVersionMap: string;
|
||||||
visibleRoutes: string;
|
/**
|
||||||
|
* Hidden routes
|
||||||
|
*/
|
||||||
|
hiddenRoutes: string;
|
||||||
|
/**
|
||||||
|
* Listener
|
||||||
|
*/
|
||||||
|
listener: string;
|
||||||
|
/**
|
||||||
|
* Static route
|
||||||
|
*/
|
||||||
|
staticRoute: string;
|
||||||
|
/**
|
||||||
|
* Visible routes
|
||||||
|
*/
|
||||||
|
visibleRoutes: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nginx protocol parameters to harden serverside settings
|
* Nginx protocol parameters to harden serverside settings
|
||||||
*/
|
*/
|
||||||
export const protocolHardeningParameters: string = `
|
export const protocolHardeningParameters = `
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains;";
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains;";
|
||||||
add_header X-Frame-Options DENY;
|
add_header X-Frame-Options DENY;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
@@ -65,7 +116,7 @@ add_header X-XSS-Protection "1; mode=block";`;
|
|||||||
/**
|
/**
|
||||||
* Nginx ssl parameters to harden serverside settings
|
* Nginx ssl parameters to harden serverside settings
|
||||||
*/
|
*/
|
||||||
export const sslHardeningParameters: string = `
|
export const sslHardeningParameters = `
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
@@ -75,3 +126,17 @@ ssl_session_cache shared:SSL:10m;
|
|||||||
ssl_session_tickets off;
|
ssl_session_tickets off;
|
||||||
ssl_stapling on;
|
ssl_stapling on;
|
||||||
ssl_stapling_verify on;`;
|
ssl_stapling_verify on;`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config file
|
||||||
|
*/
|
||||||
|
export const configFile: ConfigFile = config.util.toObject();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if path is a specific file type
|
||||||
|
*/
|
||||||
|
export function isFileType(path: string, fileType: string) {
|
||||||
|
const regExp = new RegExp(`.*\.${fileType}$`);
|
||||||
|
|
||||||
|
return existsSync(path) && regExp.test(path);
|
||||||
|
}
|
||||||
207
src/main.ts
207
src/main.ts
@@ -13,111 +13,117 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* 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/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import * as config from 'config';
|
import {Logger} from '@openstapps/logger';
|
||||||
import * as Dockerode from 'dockerode';
|
import Dockerode from 'dockerode';
|
||||||
import {existsSync, readFile} from 'fs-extra';
|
|
||||||
import {render} from 'mustache';
|
import {render} from 'mustache';
|
||||||
import {join} from 'path';
|
import {join} from 'path';
|
||||||
import {
|
import {
|
||||||
ConfigFile,
|
asyncReadFile,
|
||||||
logger,
|
configFile,
|
||||||
|
isFileType,
|
||||||
protocolHardeningParameters,
|
protocolHardeningParameters,
|
||||||
SSLFilePaths,
|
SSLFilePaths,
|
||||||
sslHardeningParameters,
|
sslHardeningParameters,
|
||||||
TemplateView,
|
TemplateView,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
const configFile: ConfigFile = config.util.toObject();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a ContainerInfo matches a name and version regex
|
* Checks if a ContainerInfo matches a name and version regex
|
||||||
* @param {string} name
|
*
|
||||||
* @param {RegExp} versionRegex
|
* @param name Name to check
|
||||||
* @param {Dockerode.ContainerInfo} container
|
* @param versionRegex Version regex to check
|
||||||
* @return {boolean}
|
* @param container Container info for check
|
||||||
*/
|
*/
|
||||||
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean {
|
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean {
|
||||||
return typeof container.Labels['stapps.version'] === 'string' &&
|
return typeof container.Labels['stapps.version'] === 'string'
|
||||||
container.Labels['stapps.version'].match(versionRegex) !== null &&
|
&& container.Labels['stapps.version'].match(versionRegex) !== null
|
||||||
typeof container.Labels['com.docker.compose.service'] === 'string' &&
|
&& typeof container.Labels['com.docker.compose.service'] === 'string'
|
||||||
container.Labels['com.docker.compose.service'] === name;
|
&& container.Labels['com.docker.compose.service'] === name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets Gateway (ip:port) of given ContainerInfo. Returns an empty String if there is no Gateway.
|
* Gets Gateway (ip:port) of given ContainerInfo
|
||||||
|
*
|
||||||
|
* Returns an empty String if there is no Gateway.
|
||||||
* This assumes that a backend runs in the container and it exposes one port.
|
* This assumes that a backend runs in the container and it exposes one port.
|
||||||
* @param {Dockerode.ContainerInfo} container
|
*
|
||||||
* @return {string}
|
* @param container Container info of which to get address from
|
||||||
*/
|
*/
|
||||||
export function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): string {
|
export async function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): Promise<string> {
|
||||||
|
|
||||||
if (container.Ports.length === 0) {
|
if (container.Ports.length === 0) {
|
||||||
logger.error(
|
await Logger.error(`Container ${container.Id} does not advertise any port.
|
||||||
'Container',
|
Please expose a port if the container should be accessible by NGINX.`);
|
||||||
container.Id,
|
|
||||||
'does not advertise any Port. Please expose a Port if the container should be accessible by NGINX.',
|
|
||||||
);
|
|
||||||
return '';
|
return '';
|
||||||
} else {
|
|
||||||
// ip:port
|
|
||||||
return container.Ports[0].IP + ':' + container.Ports[0].PublicPort;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ip:port
|
||||||
|
return `${container.Ports[0].IP}:${container.Ports[0].PublicPort}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates an upstream map. It maps all stapps-backend-containers to an gateway
|
* Generates an upstream map
|
||||||
* @param {string[]} activeVersions
|
*
|
||||||
* @param {string[]} outdatedVersions
|
* It maps all stapps-backend-containers to an gateway.
|
||||||
* @param {Dockerode.ContainerInfo[]} containers
|
*
|
||||||
* @return {string}
|
* @param activeVersions List of active versions
|
||||||
|
* @param outdatedVersions List of outdated versions
|
||||||
|
* @param containers List of container infos
|
||||||
*/
|
*/
|
||||||
export function generateUpstreamMap(
|
export async function generateUpstreamMap(
|
||||||
activeVersions: string[],
|
activeVersions: string[],
|
||||||
outdatedVersions: string[],
|
outdatedVersions: string[],
|
||||||
containers: Dockerode.ContainerInfo[],
|
containers: Dockerode.ContainerInfo[],
|
||||||
): string {
|
): Promise<string> {
|
||||||
let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n';
|
let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n';
|
||||||
let upstreams = '';
|
let upstreams = '';
|
||||||
|
|
||||||
let foundMatchingContainer = false;
|
let foundMatchingContainer = false;
|
||||||
|
|
||||||
// active versions
|
// active versions
|
||||||
result += activeVersions.map((activeVersionRegex) => {
|
result += (await Promise.all(
|
||||||
const upstreamName = activeVersionRegex.replace(/[\\|\.|\+]/g, '_');
|
activeVersions
|
||||||
|
.map(async (activeVersionRegex) => {
|
||||||
|
const upstreamName = activeVersionRegex.replace(/[\\|.+]/g, '_');
|
||||||
|
|
||||||
const activeBackends = containers.filter((container) => {
|
const activeBackends = containers.filter((container) => {
|
||||||
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
|
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (activeBackends.length > 0) {
|
if (activeBackends.length > 0) {
|
||||||
|
foundMatchingContainer = true;
|
||||||
|
|
||||||
foundMatchingContainer = true;
|
if (activeBackends.length > 1) {
|
||||||
|
throw new Error('Multiple backends for one version found.');
|
||||||
|
}
|
||||||
|
|
||||||
if (activeBackends.length > 1) {
|
const gateWayOfContainer = await getGatewayOfStAppsBackend(activeBackends[0]);
|
||||||
throw new Error('Multiple backends for one version found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const gateWayOfContainer = getGatewayOfStAppsBackend(activeBackends[0]);
|
if (gateWayOfContainer.length !== 0) {
|
||||||
|
upstreams += `\nupstream ${upstreamName} {\n server ${gateWayOfContainer};\n}`;
|
||||||
|
|
||||||
|
return ` \"~${activeVersionRegex}\" ${upstreamName};\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
||||||
|
}
|
||||||
|
await Logger.error('No backend for version', activeVersionRegex, 'found');
|
||||||
|
|
||||||
if (gateWayOfContainer.length !== 0) {
|
|
||||||
upstreams += `\nupstream ${upstreamName} {\n server ${gateWayOfContainer};\n}`;
|
|
||||||
return ` \"~${activeVersionRegex}\" ${upstreamName};\n`;
|
|
||||||
} else {
|
|
||||||
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
||||||
}
|
}),
|
||||||
} else {
|
)).join('');
|
||||||
logger.error('No backend for version', activeVersionRegex, 'found');
|
|
||||||
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
|
||||||
}
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// outdated versions
|
// outdated versions
|
||||||
result += outdatedVersions.map((outdatedVersionRegex) => {
|
result += outdatedVersions
|
||||||
return ` \"~${outdatedVersionRegex}\" outdated;`;
|
.map((outdatedVersionRegex) => {
|
||||||
}).join('') + '\n\}';
|
return ` \"~${outdatedVersionRegex}\" outdated;`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
result += '\n\}';
|
||||||
|
|
||||||
if (!foundMatchingContainer) {
|
if (!foundMatchingContainer) {
|
||||||
logger.error(
|
await Logger.error(
|
||||||
'No container with matching version label found. Please start a container with a matching version Label.',
|
'No container with matching version label found. Please start a container with a matching version Label.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -127,74 +133,65 @@ export function generateUpstreamMap(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates http or https listener
|
* Generates http or https listener
|
||||||
* @param sslFiles
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
*/
|
||||||
function generateListener(sslFilePaths: SSLFilePaths) {
|
export function generateListener(sslFilePaths: SSLFilePaths) {
|
||||||
|
|
||||||
function isSSLCert(path: string) {
|
|
||||||
return existsSync(path) && /.*\.crt$/.test(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSSLKey(path: string) {
|
|
||||||
return existsSync(path) && /.*\.key$/.test(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPEMFile(path: string) {
|
|
||||||
return existsSync(path) && /.*\.pem$/.test(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let listener = '';
|
let listener = '';
|
||||||
|
|
||||||
if (typeof sslFilePaths !== 'undefined' &&
|
if (typeof sslFilePaths !== 'undefined' &&
|
||||||
typeof sslFilePaths.certificate === 'string' && isSSLCert(sslFilePaths.certificate) &&
|
typeof sslFilePaths.certificate !== 'undefined' && isFileType(sslFilePaths.certificate,'crt') &&
|
||||||
typeof sslFilePaths.certificateChain === 'string' && isSSLCert(sslFilePaths.certificate) &&
|
typeof sslFilePaths.certificateChain !== 'undefined' && isFileType(sslFilePaths.certificateChain,'crt') &&
|
||||||
typeof sslFilePaths.certificateKey === 'string' && isSSLKey(sslFilePaths.certificate) &&
|
typeof sslFilePaths.certificateKey !== 'undefined' && isFileType(sslFilePaths.certificateKey,'key') &&
|
||||||
typeof sslFilePaths.dhparam === 'string' && isPEMFile(sslFilePaths.dhparam)
|
typeof sslFilePaths.dhparam !== 'undefined' && isFileType(sslFilePaths.dhparam,'pem')
|
||||||
) {
|
) {
|
||||||
// https listener
|
// https listener
|
||||||
listener = 'listen 443 ssl default_server;\n' +
|
listener = `listen 443 ssl default_server;
|
||||||
`ssl_certificate ${sslFilePaths.certificate};\n` +
|
ssl_certificate ${sslFilePaths.certificate};
|
||||||
`ssl_certificate_key ${sslFilePaths.certificateKey};\n` +
|
ssl_certificate_key ${sslFilePaths.certificateKey};
|
||||||
`ssl_trusted_certificate ${sslFilePaths.certificateChain};\n` +
|
ssl_trusted_certificate ${sslFilePaths.certificateChain};
|
||||||
`ssl_dhparam ${sslFilePaths.dhparam};\n` +
|
ssl_dhparam ${sslFilePaths.dhparam};
|
||||||
`${sslHardeningParameters}`;
|
${sslHardeningParameters}`;
|
||||||
} else {
|
} else {
|
||||||
// default http listener
|
// default http listener
|
||||||
listener = 'listen 80 default_server;';
|
listener = 'listen 80 default_server;';
|
||||||
logger.warn('Https usage is not setup properly, falling back to http!');
|
|
||||||
|
Logger.warn('Https usage is not setup properly, falling back to http!');
|
||||||
}
|
}
|
||||||
listener = `${listener}\n${protocolHardeningParameters}\n`;
|
listener = `${listener}
|
||||||
|
|
||||||
|
${protocolHardeningParameters}
|
||||||
|
`;
|
||||||
|
|
||||||
return listener;
|
return listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a mustache template file with given view object
|
* Render a mustache template file with given view object
|
||||||
* @param path (path to file)
|
*
|
||||||
* @param view
|
* @param path Path to template file
|
||||||
* @param callback
|
* @param view Data to render template with
|
||||||
*/
|
*/
|
||||||
async function renderTemplate(path: string, view: any): Promise<string> {
|
async function renderTemplate(path: string, view: unknown): Promise<string> {
|
||||||
const content = await readFile(path, 'utf8');
|
const content = await asyncReadFile(path, 'utf8');
|
||||||
|
|
||||||
return render(content, view);
|
return render(content, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns view for nginx config file
|
* Returns view for nginx config file
|
||||||
* @param containers
|
*
|
||||||
|
* @param containers List of container info
|
||||||
*/
|
*/
|
||||||
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
|
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
|
||||||
|
const cors = await asyncReadFile('./fixtures/cors.template', 'utf8');
|
||||||
|
|
||||||
const cors = await readFile('./fixtures/cors.template', 'utf8');
|
const visibleRoutesPromises = configFile.visibleRoutes.map(async (route) => {
|
||||||
|
|
||||||
const visibleRoutesPromises = configFile.visibleRoutes.map((route) => {
|
|
||||||
return renderTemplate(join('fixtures', 'visibleRoute.template'), {
|
return renderTemplate(join('fixtures', 'visibleRoute.template'), {
|
||||||
cors,
|
cors,
|
||||||
route,
|
route,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hiddenRoutesPromises = configFile.hiddenRoutes.map((route) => {
|
const hiddenRoutesPromises = configFile.hiddenRoutes.map(async (route) => {
|
||||||
return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
|
return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
|
||||||
cors,
|
cors,
|
||||||
route,
|
route,
|
||||||
@@ -202,7 +199,7 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dockerVersionMap: generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
|
dockerVersionMap: await generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
|
||||||
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
|
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
|
||||||
listener: generateListener(configFile.sslFilePaths),
|
listener: generateListener(configFile.sslFilePaths),
|
||||||
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}),
|
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}),
|
||||||
@@ -212,7 +209,8 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the list of docker containers
|
* Read the list of docker containers
|
||||||
* @param pathToDockerSocket
|
*
|
||||||
|
* @param pathToDockerSocket Path to docker socket
|
||||||
*/
|
*/
|
||||||
export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
|
export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
|
||||||
const docker = new Dockerode({
|
const docker = new Dockerode({
|
||||||
@@ -221,12 +219,15 @@ export async function getContainers(pathToDockerSocket = '/var/run/docker.sock')
|
|||||||
|
|
||||||
const containers = await docker.listContainers();
|
const containers = await docker.listContainers();
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
if (containers.length === 0) {
|
if (containers.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No running docker containers found.' +
|
`No running docker containers found.
|
||||||
`Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
|
|
||||||
|
Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
return containers;
|
return containers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
import {Logger} from '@openstapps/logger';
|
|
||||||
import {expect} from 'chai';
|
|
||||||
import {ContainerInfo} from 'dockerode';
|
|
||||||
import {slow, suite, test, timeout} from 'mocha-typescript';
|
|
||||||
import * as sinon from 'sinon';
|
|
||||||
import {containerMatchesRegex, generateUpstreamMap, getGatewayOfStAppsBackend} from '../src/main';
|
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (err) => {
|
|
||||||
logger.error('UNHANDLED REJECTION', err.stack);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
@suite(timeout(1000), slow(500))
|
|
||||||
// @ts-ignore
|
|
||||||
class ContainerInfoParsing {
|
|
||||||
|
|
||||||
static anyContainerWithExposedPorts: ContainerInfo;
|
|
||||||
static backendContainerWithExposedPorts: ContainerInfo;
|
|
||||||
|
|
||||||
static before(done: () => void) {
|
|
||||||
// tslint:disable:object-literal-sort-keys
|
|
||||||
this.backendContainerWithExposedPorts = {
|
|
||||||
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
|
|
||||||
Names: [
|
|
||||||
'/deployment_backend_1',
|
|
||||||
],
|
|
||||||
Image: 'gitlab-registry.tubit.tu-berlin.de/stapps/backend/b-tu-typescript-refactor-for-new-tslint-config',
|
|
||||||
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
|
|
||||||
Command: 'node ./bin/www',
|
|
||||||
Created: 1524669882,
|
|
||||||
Ports: [
|
|
||||||
{
|
|
||||||
IP: '127.0.0.1',
|
|
||||||
PrivatePort: 3000,
|
|
||||||
PublicPort: 3000,
|
|
||||||
Type: 'tcp',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
State: 'running',
|
|
||||||
Status: 'Up 3 minutes',
|
|
||||||
HostConfig: {
|
|
||||||
NetworkMode: 'deployment_default',
|
|
||||||
},
|
|
||||||
NetworkSettings: {
|
|
||||||
Networks: {
|
|
||||||
deployment_default: {
|
|
||||||
IPAMConfig: null,
|
|
||||||
Links: null,
|
|
||||||
Aliases: null,
|
|
||||||
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
|
|
||||||
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
|
|
||||||
Gateway: '172.18.0.1',
|
|
||||||
IPAddress: '172.18.0.3',
|
|
||||||
IPPrefixLen: 16,
|
|
||||||
IPv6Gateway: '',
|
|
||||||
GlobalIPv6Address: '',
|
|
||||||
GlobalIPv6PrefixLen: 0,
|
|
||||||
MacAddress: '03:41:ac:11:00:23',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// tslint:disable:object-literal-sort-keys
|
|
||||||
this.anyContainerWithExposedPorts = {
|
|
||||||
Id: 'e3d3f4d18aceac2780bdb95523845d066ed25c04fc65168a5ddbd37a85671bb7',
|
|
||||||
Names: [
|
|
||||||
'/container_name_1',
|
|
||||||
],
|
|
||||||
Image: 'ubuntu:4',
|
|
||||||
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
|
|
||||||
Command: 'sh',
|
|
||||||
Created: 1524669882,
|
|
||||||
Ports: [
|
|
||||||
{
|
|
||||||
IP: '0.0.0.0',
|
|
||||||
PrivatePort: 80,
|
|
||||||
PublicPort: 80,
|
|
||||||
Type: 'tcp',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
Labels: {},
|
|
||||||
State: 'running',
|
|
||||||
Status: 'Up 3 minutes',
|
|
||||||
HostConfig: {
|
|
||||||
NetworkMode: 'default',
|
|
||||||
},
|
|
||||||
NetworkSettings: {
|
|
||||||
Networks: {
|
|
||||||
bridge: {
|
|
||||||
IPAMConfig: null,
|
|
||||||
Links: null,
|
|
||||||
Aliases: null,
|
|
||||||
NetworkID: '947ea5247cc7429e1fdebd5404fa4d15f7c05e6765f2b93ddb3bdb6aaffd1193',
|
|
||||||
EndpointID: 'da17549a086ff2c9f622e80de833e6f334afda52c8f07080428640c1716dcd14',
|
|
||||||
Gateway: '172.18.0.1',
|
|
||||||
IPAddress: '172.18.0.3',
|
|
||||||
IPPrefixLen: 16,
|
|
||||||
IPv6Gateway: '',
|
|
||||||
GlobalIPv6Address: '',
|
|
||||||
GlobalIPv6PrefixLen: 0,
|
|
||||||
MacAddress: '03:41:ac:11:00:23',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
// tslint:disable:no-unused-expression
|
|
||||||
@test
|
|
||||||
checkIfContainerDoesNotMatchAnyContainer(done: () => void) {
|
|
||||||
expect(containerMatchesRegex(
|
|
||||||
'anyName',
|
|
||||||
new RegExp('d+'),
|
|
||||||
ContainerInfoParsing.anyContainerWithExposedPorts),
|
|
||||||
).to.be.false;
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
checkIfContainerDoesNotMatchIfVersionIsIncorrect(done: () => void) {
|
|
||||||
expect(containerMatchesRegex(
|
|
||||||
'backend',
|
|
||||||
new RegExp('1\\.4\\.\\d+'),
|
|
||||||
ContainerInfoParsing.backendContainerWithExposedPorts),
|
|
||||||
).to.be.false;
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
checkIfContainerMatches(done: () => void) {
|
|
||||||
expect(containerMatchesRegex(
|
|
||||||
'backend',
|
|
||||||
new RegExp('1\\.0\\.\\d+'),
|
|
||||||
ContainerInfoParsing.backendContainerWithExposedPorts),
|
|
||||||
).to.be.true;
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
getGatewayOfAnyContainerWithExposedPorts(done: () => void) {
|
|
||||||
expect(getGatewayOfStAppsBackend(ContainerInfoParsing.anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
getGatewayOfBackendContainer(done: () => void) {
|
|
||||||
expect(getGatewayOfStAppsBackend(ContainerInfoParsing.backendContainerWithExposedPorts))
|
|
||||||
.to
|
|
||||||
.be
|
|
||||||
.equal('127.0.0.1:3000');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
upstreamMapCallsLoggerErrorWhenNoMatchingContainerIsFound(done: () => void) {
|
|
||||||
|
|
||||||
const stub = sinon.stub(console, 'error');
|
|
||||||
|
|
||||||
expect(generateUpstreamMap(
|
|
||||||
['0\\.8\\.\\d+'],
|
|
||||||
['1\\.1\\.\\d+'],
|
|
||||||
[ContainerInfoParsing.backendContainerWithExposedPorts],
|
|
||||||
)).to.be.equal('map $http_x_stapps_version $proxyurl {\n' +
|
|
||||||
' default unsupported;\n' +
|
|
||||||
' "~0\\.8\\.\\d+" unavailable;\n' +
|
|
||||||
' "~1\\.1\\.\\d+" outdated;\n' +
|
|
||||||
'}\n');
|
|
||||||
|
|
||||||
stub.restore();
|
|
||||||
expect(stub.args[0][0]).contains('[ERROR] No backend for version').and.contains('found');
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
@test
|
|
||||||
upstreamMapWithOneActiveVersionAndNoOutdatedOnes(done: () => void) {
|
|
||||||
expect(
|
|
||||||
generateUpstreamMap(
|
|
||||||
['1\\.0\\.\\d+'],
|
|
||||||
['0\\.8\\.\\d+'],
|
|
||||||
[ContainerInfoParsing.backendContainerWithExposedPorts],
|
|
||||||
),
|
|
||||||
).to.be.equal(
|
|
||||||
'map $http_x_stapps_version $proxyurl {\n' +
|
|
||||||
' default unsupported;\n' +
|
|
||||||
' "~1\\.0\\.\\d+" 1__0___d_;\n' +
|
|
||||||
' "~0\\.8\\.\\d+" outdated;\n' +
|
|
||||||
'}\n' +
|
|
||||||
'upstream 1__0___d_ {\n' +
|
|
||||||
' server 127.0.0.1:3000;\n' +
|
|
||||||
'}\n',
|
|
||||||
);
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
54
test/common.spec.ts
Normal file
54
test/common.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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 {suite, test} from '@testdeck/mocha';
|
||||||
|
import {Logger} from '@openstapps/logger';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { isFileType } from '../src/common';
|
||||||
|
|
||||||
|
process.on('unhandledRejection', async (error) => {
|
||||||
|
await Logger.error(error);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@suite
|
||||||
|
export class CommonSpec {
|
||||||
|
|
||||||
|
@test
|
||||||
|
async testSSLCert() {
|
||||||
|
const testCertDir = resolve(__dirname, 'certs');
|
||||||
|
mkdirSync(testCertDir);
|
||||||
|
const notAnExptectedFileTypeFilePath = resolve(testCertDir, 'notAnExptectedFileType.txt');
|
||||||
|
const anExptectedFileTypeFilePath = resolve(testCertDir, 'notARealCert.crt');
|
||||||
|
writeFileSync(notAnExptectedFileTypeFilePath,'Test');
|
||||||
|
writeFileSync(anExptectedFileTypeFilePath,'Test');
|
||||||
|
|
||||||
|
expect(isFileType(notAnExptectedFileTypeFilePath,'crt')).to.equal(false)
|
||||||
|
expect(isFileType(anExptectedFileTypeFilePath,'crt')).to.equal(true)
|
||||||
|
|
||||||
|
unlinkSync(notAnExptectedFileTypeFilePath);
|
||||||
|
unlinkSync(anExptectedFileTypeFilePath);
|
||||||
|
rmdirSync(testCertDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
322
test/main.spec.ts
Normal file
322
test/main.spec.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
/*
|
||||||
|
* 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, getContainers} from '../src/main';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'fs';
|
||||||
|
|
||||||
|
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: 1524669882,
|
||||||
|
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: 'gitlab-registry.tubit.tu-berlin.de/stapps/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 sandbox = chai.spy.sandbox();
|
||||||
|
|
||||||
|
before() {
|
||||||
|
MainSpec.sandbox.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
checkIfContainerDoesNotMatchAnyContainer() {
|
||||||
|
expect(containerMatchesRegex(
|
||||||
|
'anyName',
|
||||||
|
new RegExp('d+'),
|
||||||
|
MainSpec.anyContainerWithExposedPorts),
|
||||||
|
).to.be.equal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
checkIfContainerDoesNotMatchIfVersionIsIncorrect() {
|
||||||
|
expect(containerMatchesRegex(
|
||||||
|
'backend',
|
||||||
|
new RegExp('1\\.4\\.\\d+'),
|
||||||
|
MainSpec.backendContainerWithExposedPorts),
|
||||||
|
).to.be.equal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
checkIfContainerMatches() {
|
||||||
|
expect(containerMatchesRegex(
|
||||||
|
'backend',
|
||||||
|
new RegExp('1\\.0\\.\\d+'),
|
||||||
|
MainSpec.backendContainerWithExposedPorts),
|
||||||
|
).to.be.equal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
async getGatewayOfAnyContainerWithExposedPorts() {
|
||||||
|
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: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
|
||||||
|
expect(spy.__spy.calls[0][0]).to.contain('Container Foo 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 upstreamMapCallsLoggerErrorWhenNoMatchingContainerIsFound() {
|
||||||
|
const spy = MainSpec.sandbox.on(console, 'error', () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
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 upstreamMapWithOneActiveVersionAndNoOutdatedOnes() {
|
||||||
|
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 testGetContainers() {
|
||||||
|
try {
|
||||||
|
await getContainers();
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.startsWith('No')) {
|
||||||
|
// Result, if docker is installed
|
||||||
|
expect(e.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(e.message).to.equal(`connect ENOENT /var/run/docker.sock`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
async testGetTemplateView() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
let containersWithSameVersion = [MainSpec.backendContainerWithExposedPorts, MainSpec.backendContainerWithExposedPorts];
|
||||||
|
await getTemplateView(containersWithSameVersion);
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).to.equal(
|
||||||
|
`Multiple backends for one version found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
createListenerFaultyConfig() {
|
||||||
|
|
||||||
|
expect(generateListener({
|
||||||
|
certificate: 'faultyTest',
|
||||||
|
certificateChain: 'faultyTest',
|
||||||
|
certificateKey: 'faultyTest',
|
||||||
|
dhparam: 'faultyTest',
|
||||||
|
})).to
|
||||||
|
.equal(`listen 80 default_server;
|
||||||
|
|
||||||
|
${protocolHardeningParameters}
|
||||||
|
`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
createListenerCorrectConfig() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
--ui mocha-typescript
|
|
||||||
--require ts-node/register
|
|
||||||
--require source-map-support/register
|
|
||||||
test/*.ts
|
|
||||||
Reference in New Issue
Block a user