refactor: apply @openstapps/eslint-config rules

This commit is contained in:
Rainer Killinger
2022-05-09 11:14:36 +02:00
parent 1fcf7340d4
commit ac144095bf
8 changed files with 1264 additions and 231 deletions

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "@openstapps"
}

3
.gitignore vendored
View File

@@ -92,4 +92,5 @@ docs/
# Certificates # Certificates
*.crt *.crt
*.key *.key
test/certs

1297
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,33 +17,41 @@
}, },
"devDependencies": { "devDependencies": {
"@openstapps/configuration": "0.29.0", "@openstapps/configuration": "0.29.0",
"@openstapps/eslint-config": "1.0.0",
"@testdeck/mocha": "0.2.0", "@testdeck/mocha": "0.2.0",
"@types/chai": "4.3.1", "@types/chai": "4.3.1",
"@types/chai-spies": "1.0.3", "@types/chai-spies": "1.0.3",
"@types/mustache": "4.1.2", "@types/mustache": "4.1.2",
"@typescript-eslint/eslint-plugin": "5.22.0",
"@typescript-eslint/parser": "5.22.0",
"chai": "4.3.6", "chai": "4.3.6",
"chai-spies": "1.0.0", "chai-spies": "1.0.0",
"conventional-changelog-cli": "2.2.2", "conventional-changelog-cli": "2.2.2",
"eslint": "8.15.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-jsdoc": "39.2.9",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-unicorn": "42.0.0",
"mocha": "9.2.2", "mocha": "9.2.2",
"nyc": "15.1.0", "nyc": "15.1.0",
"prepend-file-cli": "1.0.6", "prepend-file-cli": "1.0.6",
"prettier": "2.6.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "10.7.0", "ts-node": "10.7.0",
"tslint": "6.1.3",
"typedoc": "0.22.15", "typedoc": "0.22.15",
"typescript": "4.4.4" "typescript": "4.4.4"
}, },
"scripts": { "scripts": {
"build": "npm run tslint && npm run compile", "build": "npm run lint && 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 --includeVersion --out docs --readme README.md --entryPointStrategy expand src", "documentation": "typedoc --includeVersion --out docs --readme README.md --entryPointStrategy expand src",
"lint": "eslint --ext .ts src/",
"postversion": "npm run changelog", "postversion": "npm run changelog",
"prepublishOnly": "npm ci && npm run build", "prepublishOnly": "npm ci && npm run build",
"preversion": "npm run prepublishOnly", "preversion": "npm run prepublishOnly",
"push": "git push && git push origin \"v$npm_package_version\"", "push": "git push && git push origin \"v$npm_package_version\"",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'",
"test": "nyc mocha --require ts-node/register 'test/**/*.spec.ts'" "test": "nyc mocha --require ts-node/register 'test/**/*.spec.ts'"
}, },
"repository": { "repository": {

View File

@@ -20,8 +20,10 @@ import {render} from 'mustache';
import {asyncReadFile, asyncWriteFile} from './common'; import {asyncReadFile, asyncWriteFile} from './common';
import {getContainers, getTemplateView} from './main'; import {getContainers, getTemplateView} from './main';
/* eslint-disable unicorn/prefer-module */
// handle unhandled promise rejections // handle unhandled promise rejections
process.on('unhandledRejection', async (error) => { process.on('unhandledRejection', async error => {
await Logger.error(error); await Logger.error(error);
process.exit(1); process.exit(1);
}); });
@@ -42,6 +44,7 @@ async function updateNginxConfig() {
.join(','); .join(',');
delete require.cache[require.resolve('config')]; delete require.cache[require.resolve('config')];
// eslint-disable-next-line @typescript-eslint/no-var-requires
const configFile = require('config'); const configFile = require('config');
const configHash = JSON.stringify(configFile); const configHash = JSON.stringify(configFile);
@@ -50,7 +53,10 @@ async function updateNginxConfig() {
Logger.log('Generating new NGINX configuration'); Logger.log('Generating new NGINX configuration');
// render nginx config file // render nginx config file
const nginxConfig = render(await asyncReadFile('nginx.conf.template', 'utf8'), await getTemplateView(containers)); const nginxConfig = render(
await asyncReadFile('nginx.conf.template', 'utf8'),
await getTemplateView(containers),
);
containerHashCache = containerHash; containerHashCache = containerHash;
configHashCache = configHash; configHashCache = configHash;
@@ -65,9 +71,9 @@ async function updateNginxConfig() {
execSync('nginx -s reload'); execSync('nginx -s reload');
} }
// tslint:disable-next-line:no-magic-numbers - set timeout to update configuration again in 30s // set timeout to update configuration again in 30s
setTimeout(updateNginxConfig, 30000); setTimeout(updateNginxConfig, 30_000);
} }
// tslint:disable-next-line:no-floating-promises - start the process that checks the docker socket periodically // start the process that checks the docker socket periodically
updateNginxConfig(); updateNginxConfig();

View File

@@ -17,7 +17,7 @@ import {Logger} from '@openstapps/logger';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import isCidr from 'is-cidr'; import isCidr from 'is-cidr';
import {render} from 'mustache'; import {render} from 'mustache';
import {join} from 'path'; import path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import { import {
asyncReadFile, asyncReadFile,
@@ -29,6 +29,9 @@ import {
TemplateView, TemplateView,
} from './common'; } from './common';
/* eslint-disable unicorn/prefer-module */
/* eslint-disable unicorn/no-await-expression-member */
/** /**
* Checks if a ContainerInfo matches a name and version regex * Checks if a ContainerInfo matches a name and version regex
* *
@@ -36,11 +39,17 @@ import {
* @param versionRegex Version regex to check * @param versionRegex Version regex to check
* @param container Container info for check * @param container Container info for check
*/ */
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): boolean { export function containerMatchesRegex(
return typeof container.Labels['stapps.version'] === 'string' name: string,
&& container.Labels['stapps.version'].match(versionRegex) !== null versionRegex: RegExp,
&& typeof container.Labels['com.docker.compose.service'] === 'string' container: Dockerode.ContainerInfo,
&& container.Labels['com.docker.compose.service'] === name; ): boolean {
return (
typeof container.Labels['stapps.version'] === 'string' &&
container.Labels['stapps.version'].match(versionRegex) !== null &&
typeof container.Labels['com.docker.compose.service'] === 'string' &&
container.Labels['com.docker.compose.service'] === name
);
} }
/** /**
@@ -83,37 +92,46 @@ export async function generateUpstreamMap(
let foundMatchingContainer = false; let foundMatchingContainer = false;
// active versions // active versions
result += (await Promise.all( result += (
activeVersions await Promise.all(
.map(async (activeVersionRegex) => { activeVersions.map(async activeVersionRegex => {
const upstreamName = activeVersionRegex.replace(/[\\|.+]/g, '_'); const upstreamName = activeVersionRegex.replace(/[\\|.+]/g, '_');
let activeBackends = containers.filter((container) => { let activeBackends = containers.filter(container => {
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container); return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
}); });
// .Labels['stapps.version'] is available // .Labels['stapps.version'] is available
if (activeBackends.length > 0) { if (activeBackends.length > 0) {
activeBackends = activeBackends.sort((a, b) =>
activeBackends = activeBackends.sort((a, b) => semver.rcompare(a.Labels['stapps.version'],b.Labels['stapps.version'])); semver.rcompare(a.Labels['stapps.version'], b.Labels['stapps.version']),
const activeBackendsVersions = activeBackends.map((container) => container.Labels['stapps.version']) );
// tslint:disable-next-line: strict-boolean-expressions const activeBackendsVersions = activeBackends
.reduce((map, e) => map.set(e, (map.get(e) || 0) + 1), new Map<string, number>()); .map(container => container.Labels['stapps.version'])
// eslint-disable-next-line unicorn/no-array-reduce
.reduce(
(map, element) => map.set(element, (map.get(element) || 0) + 1),
new Map<string, number>(),
);
for (const [version, occurrences] of activeBackendsVersions) { for (const [version, occurrences] of activeBackendsVersions) {
if (occurrences > 1) { if (occurrences > 1) {
await Logger.error(`Omitting running version ${version} ! Multiple backends with this exact version are running`); await Logger.error(
activeBackends = activeBackends.filter((container) => container.Labels['stapps.version'] !== version); `Omitting running version ${version} ! Multiple backends with this exact version are running.`,
);
activeBackends = activeBackends.filter(
container => container.Labels['stapps.version'] !== version,
);
} }
} }
if (activeBackends.length !== 0) { if (activeBackends.length > 0) {
// not only duplicates // not only duplicates
foundMatchingContainer = true; foundMatchingContainer = true;
const gateWayOfContainer = await getGatewayOfStAppsBackend(activeBackends[0]); const gatewayOfContainer = await getGatewayOfStAppsBackend(activeBackends[0]);
if (gateWayOfContainer.length !== 0) { if (gatewayOfContainer.length > 0) {
upstreams += `\nupstream ${upstreamName} {\n server ${gateWayOfContainer};\n}`; upstreams += `\nupstream ${upstreamName} {\n server ${gatewayOfContainer};\n}`;
return ` \"~${activeVersionRegex}\" ${upstreamName};\n`; return ` \"~${activeVersionRegex}\" ${upstreamName};\n`;
} }
@@ -123,15 +141,17 @@ export async function generateUpstreamMap(
return ` \"~${activeVersionRegex}\" unavailable;\n`; return ` \"~${activeVersionRegex}\" unavailable;\n`;
}), }),
)).join(''); )
).join('');
// outdated versions // outdated versions
result += outdatedVersions result += outdatedVersions
.map((outdatedVersionRegex) => { .map(outdatedVersionRegex => {
return ` \"~${outdatedVersionRegex}\" outdated;`; return ` \"~${outdatedVersionRegex}\" outdated;`;
}) })
.join(''); .join('');
// eslint-disable-next-line prettier/prettier
result += '\n\}'; result += '\n\}';
if (!foundMatchingContainer) { if (!foundMatchingContainer) {
@@ -149,11 +169,16 @@ export async function generateUpstreamMap(
export function generateListener(sslFilePaths: SSLFilePaths) { export function generateListener(sslFilePaths: SSLFilePaths) {
let listener = ''; let listener = '';
if (typeof sslFilePaths !== 'undefined' && if (
typeof sslFilePaths.certificate !== 'undefined' && isFileType(sslFilePaths.certificate,'crt') && typeof sslFilePaths !== 'undefined' &&
typeof sslFilePaths.certificateChain !== 'undefined' && isFileType(sslFilePaths.certificateChain,'crt') && typeof sslFilePaths.certificate !== 'undefined' &&
typeof sslFilePaths.certificateKey !== 'undefined' && isFileType(sslFilePaths.certificateKey,'key') && isFileType(sslFilePaths.certificate, 'crt') &&
typeof sslFilePaths.dhparam !== 'undefined' && isFileType(sslFilePaths.dhparam,'pem') typeof sslFilePaths.certificateChain !== 'undefined' &&
isFileType(sslFilePaths.certificateChain, 'crt') &&
typeof sslFilePaths.certificateKey !== 'undefined' &&
isFileType(sslFilePaths.certificateKey, 'key') &&
typeof sslFilePaths.dhparam !== 'undefined' &&
isFileType(sslFilePaths.dhparam, 'pem')
) { ) {
// https listener // https listener
listener = ` listen 443 ssl default_server; listener = ` listen 443 ssl default_server;
@@ -161,7 +186,7 @@ export function generateListener(sslFilePaths: SSLFilePaths) {
ssl_certificate_key ${sslFilePaths.certificateKey}; ssl_certificate_key ${sslFilePaths.certificateKey};
ssl_trusted_certificate ${sslFilePaths.certificateChain}; ssl_trusted_certificate ${sslFilePaths.certificateChain};
ssl_dhparam ${sslFilePaths.dhparam}; ssl_dhparam ${sslFilePaths.dhparam};
${sslHardeningParameters}`; ${sslHardeningParameters}`;
} else { } else {
// default http listener // default http listener
listener = 'listen 80 default_server;'; listener = 'listen 80 default_server;';
@@ -194,7 +219,8 @@ async function renderTemplate(path: string, view: unknown): Promise<string> {
* @param entries Allow list entries that should be in CIDR notation * @param entries Allow list entries that should be in CIDR notation
*/ */
function generateRateLimitAllowList(entries: string[]): string { function generateRateLimitAllowList(entries: string[]): string {
return entries.filter(entry => isCidr(entry)) return entries
.filter(entry => isCidr(entry))
.map(entry => `${entry} 0;`) .map(entry => `${entry} 0;`)
.join('\n'); .join('\n');
} }
@@ -206,31 +232,36 @@ function generateRateLimitAllowList(entries: string[]): string {
*/ */
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> { export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
delete require.cache[require.resolve('config')]; delete require.cache[require.resolve('config')];
// eslint-disable-next-line @typescript-eslint/no-var-requires
const config = require('config'); const config = require('config');
const configFile = config as ConfigFile; const configFile = config as ConfigFile;
const cors = await asyncReadFile('./fixtures/cors.template', 'utf8'); const cors = await asyncReadFile('./fixtures/cors.template', 'utf8');
const visibleRoutesPromises = ['/'].map(async (route) => { const visibleRoutesPromises = ['/'].map(async route => {
return renderTemplate(join('fixtures', 'visibleRoute.template'), { return renderTemplate(path.join('fixtures', 'visibleRoute.template'), {
cors, cors,
route, route,
}); });
}); });
const hiddenRoutesPromises = configFile.hiddenRoutes.map(async (route) => { const hiddenRoutesPromises = configFile.hiddenRoutes.map(async route => {
return renderTemplate(join('fixtures', 'hiddenRoute.template'), { return renderTemplate(path.join('fixtures', 'hiddenRoute.template'), {
cors, cors,
route, route,
}); });
}); });
return { return {
dockerVersionMap: await 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),
rateLimitAllowList: generateRateLimitAllowList(configFile.rateLimitAllowList), rateLimitAllowList: generateRateLimitAllowList(configFile.rateLimitAllowList),
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}), staticRoute: await renderTemplate(path.join('fixtures', 'staticRoute.template'), {cors}),
visibleRoutes: (await Promise.all(visibleRoutesPromises)).join(''), visibleRoutes: (await Promise.all(visibleRoutesPromises)).join(''),
}; };
} }
@@ -240,7 +271,9 @@ export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Pr
* *
* @param pathToDockerSocket Path to docker socket * @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({
socketPath: pathToDockerSocket, socketPath: pathToDockerSocket,
}); });

View File

@@ -24,12 +24,19 @@ import {expect} from 'chai';
import chaiSpies from 'chai-spies'; import chaiSpies from 'chai-spies';
import {ContainerInfo} from 'dockerode'; import {ContainerInfo} from 'dockerode';
import {slow, suite, test, timeout} from '@testdeck/mocha'; import {slow, suite, test, timeout} from '@testdeck/mocha';
import {sslHardeningParameters, protocolHardeningParameters, SSLFilePaths } from './../src/common'; import {sslHardeningParameters, protocolHardeningParameters, SSLFilePaths} from './../src/common';
import {containerMatchesRegex, generateUpstreamMap, getGatewayOfStAppsBackend, getTemplateView, generateListener, getContainers} from '../src/main'; import {
import { resolve } from 'path'; containerMatchesRegex,
import { mkdirSync, writeFileSync, unlinkSync, rmdirSync } from 'fs'; generateUpstreamMap,
getGatewayOfStAppsBackend,
getTemplateView,
generateListener,
getContainers,
} from '../src/main';
import {resolve} from 'path';
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
process.on('unhandledRejection', async (error) => { process.on('unhandledRejection', async error => {
await Logger.error(error); await Logger.error(error);
process.exit(1); process.exit(1);
@@ -42,7 +49,7 @@ chai.use(chaiSpies);
export class MainSpec { export class MainSpec {
static anyContainerWithExposedPorts: ContainerInfo = { static anyContainerWithExposedPorts: ContainerInfo = {
Command: 'sh', Command: 'sh',
Created: 1524669882, Created: 1_524_669_882,
HostConfig: { HostConfig: {
NetworkMode: 'default', NetworkMode: 'default',
}, },
@@ -51,9 +58,7 @@ export class MainSpec {
ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9', ImageID: 'sha256:ef9f0c8c4b6f99dd208948c7aae1d042590aa18e05ebeae4f586e4b4beebeac9',
Labels: {}, Labels: {},
Mounts: [], Mounts: [],
Names: [ Names: ['/container_name_1'],
'/container_name_1',
],
NetworkSettings: { NetworkSettings: {
Networks: { Networks: {
bridge: { bridge: {
@@ -103,9 +108,7 @@ export class MainSpec {
'stapps.version': '1.0.0', 'stapps.version': '1.0.0',
}, },
Mounts: [], Mounts: [],
Names: [ Names: ['/deployment_backend_1'],
'/deployment_backend_1',
],
NetworkSettings: { NetworkSettings: {
Networks: { Networks: {
deployment_default: { deployment_default: {
@@ -183,10 +186,11 @@ export class MainSpec {
const containerWithoutPorts: Partial<ContainerInfo> = { const containerWithoutPorts: Partial<ContainerInfo> = {
Id: 'Foo', Id: 'Foo',
Ports: [], Ports: [],
Names: ['/container_name_1'],
}; };
expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal(''); expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
expect(spy.__spy.calls[0][0]).to.contain('Container Foo does not advertise any port.'); expect(spy.__spy.calls[0][0]).to.contain('Container /container_name_1 does not advertise any port.');
} }
@test @test
@@ -196,7 +200,7 @@ export class MainSpec {
@test @test
async 'upstream map calls logger error when no matching container is found'() { async 'upstream map calls logger error when no matching container is found'() {
const spy = MainSpec.sandbox.on(console, 'error', () => { const spy = MainSpec.sandbox.on(console, 'warn', () => {
}); });
expect(await generateUpstreamMap( expect(await generateUpstreamMap(
@@ -294,10 +298,10 @@ ${protocolHardeningParameters}
const certificateChainFile = resolve(testCertDir, 'chain.crt'); const certificateChainFile = resolve(testCertDir, 'chain.crt');
const dhparamFile = resolve(testCertDir, 'dhparam.pem'); const dhparamFile = resolve(testCertDir, 'dhparam.pem');
writeFileSync(certificateFile,'Test'); writeFileSync(certificateFile, 'Test');
writeFileSync(certificateKeyFile,'Test'); writeFileSync(certificateKeyFile, 'Test');
writeFileSync(certificateChainFile,'Test'); writeFileSync(certificateChainFile, 'Test');
writeFileSync(dhparamFile,'Test'); writeFileSync(dhparamFile, 'Test');
const sslFilePaths: SSLFilePaths = { const sslFilePaths: SSLFilePaths = {
certificate: certificateFile, certificate: certificateFile,
@@ -306,12 +310,12 @@ ${protocolHardeningParameters}
dhparam: dhparamFile, dhparam: dhparamFile,
}; };
expect(generateListener(sslFilePaths)).to.equal(`listen 443 ssl default_server; expect(generateListener(sslFilePaths)).to.equal(` listen 443 ssl default_server;
ssl_certificate ${sslFilePaths.certificate}; ssl_certificate ${sslFilePaths.certificate};
ssl_certificate_key ${sslFilePaths.certificateKey}; ssl_certificate_key ${sslFilePaths.certificateKey};
ssl_trusted_certificate ${sslFilePaths.certificateChain}; ssl_trusted_certificate ${sslFilePaths.certificateChain};
ssl_dhparam ${sslFilePaths.dhparam}; ssl_dhparam ${sslFilePaths.dhparam};
${sslHardeningParameters} ${sslHardeningParameters}
${protocolHardeningParameters} ${protocolHardeningParameters}
`); `);

View File

@@ -1,3 +0,0 @@
{
"extends": "./node_modules/@openstapps/configuration/tslint.json"
}