feat: add proxy

This commit is contained in:
Anselm Stordeur
2019-01-16 19:54:30 +01:00
committed by Rainer Killinger
commit fbe1a65cd1
23 changed files with 3656 additions and 0 deletions

196
src/main.ts Normal file
View File

@@ -0,0 +1,196 @@
import * as config from 'config';
import * as Dockerode from 'dockerode';
import {existsSync, readFile} from 'fs-extra';
import {render} from 'mustache';
import {join} from 'path';
import {ConfigFile, logger, TemplateView} from './common';
const configFile: ConfigFile = config.util.toObject();
/**
* Checks if a ContainerInfo matches a name and version regex
* @param {string} name
* @param {RegExp} versionRegex
* @param {Dockerode.ContainerInfo} container
* @return {boolean}
*/
export function containerMatchesRegex(name: string, versionRegex: RegExp, container: Dockerode.ContainerInfo): 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;
}
/**
* 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.
* @param {Dockerode.ContainerInfo} container
* @return {string}
*/
export function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): string {
if (container.Ports.length === 0) {
logger.error(
'Container',
container.Id,
'does not advertise any Port. Please expose a Port if the container should be accessible by NGINX.',
);
return '';
} else {
// ip:port
return container.Ports[0].IP + ':' + container.Ports[0].PublicPort;
}
}
/**
* Generates an upstream map. It maps all stapps-backend-containers to an gateway
* @param {string[]} activeVersions
* @param {string[]} outdatedVersions
* @param {Dockerode.ContainerInfo[]} containers
* @return {string}
*/
export function generateUpstreamMap(
activeVersions: string[],
outdatedVersions: string[],
containers: Dockerode.ContainerInfo[],
): string {
let result = 'map $http_x_stapps_version $proxyurl {\n default unsupported;\n';
let upstreams = '';
let foundMatchingContainer = false;
// active versions
result += activeVersions.map((activeVersionRegex) => {
const upstreamName = activeVersionRegex.replace(/[\\|\.|\+]/g, '_');
const activeBackends = containers.filter((container) => {
return containerMatchesRegex('backend', new RegExp(activeVersionRegex), container);
});
if (activeBackends.length > 0) {
foundMatchingContainer = true;
if (activeBackends.length > 1) {
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`;
} else {
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
} else {
logger.error('No backend for version', activeVersionRegex, 'found');
return ` \"~${activeVersionRegex}\" unavailable;\n`;
}
}).join('');
// outdated versions
result += outdatedVersions.map((outdatedVersionRegex) => {
return ` \"~${outdatedVersionRegex}\" outdated;`;
}).join('') + '\n\}';
if (!foundMatchingContainer) {
logger.error(
'No container with matching version label found. Please start a container with a matching version Label.',
);
}
return `${result}${upstreams}\n`;
}
/**
* Generates http or https listener
* @param sslFiles
* @returns {string}
*/
function generateListener(sslFiles: string[]) {
function isSSLCert(path: string) {
return existsSync(path) && /.*\.crt$/.test(path);
}
function isSSLKey(path: string) {
return existsSync(path) && /.*\.key$/.test(path);
}
let listener = '';
if (Array.isArray(sslFiles) && sslFiles.length === 2 && sslFiles.some(isSSLCert) && sslFiles.some(isSSLKey)) {
// https listener
listener = 'listen 443 ssl default_server;\n' +
`ssl_certificate ${sslFiles.find(isSSLCert)};\n` +
`ssl_certificate_key ${sslFiles.find(isSSLKey)};\n`;
} else {
// default http listener
listener = 'listen 80 default_server;';
}
return listener;
}
/**
* Render a mustache template file with given view object
* @param path (path to file)
* @param view
* @param callback
*/
async function renderTemplate(path: string, view: any): Promise<string> {
const content = await readFile(path, 'utf8');
return render(content, view);
}
/**
* Returns view for nginx config file
* @param containers
*/
export async function getTemplateView(containers: Dockerode.ContainerInfo[]): Promise<TemplateView> {
const cors = await readFile('./fixtures/cors.template', 'utf8');
const visibleRoutesPromises = configFile.visibleRoutes.map((route) => {
return renderTemplate(join('fixtures', 'visibleRoute.template'), {
cors,
route,
});
});
const hiddenRoutesPromises = configFile.hiddenRoutes.map((route) => {
return renderTemplate(join('fixtures', 'hiddenRoute.template'), {
cors,
route,
});
});
return {
dockerVersionMap: generateUpstreamMap(configFile.activeVersions, configFile.outdatedVersions, containers),
hiddenRoutes: (await Promise.all(hiddenRoutesPromises)).join(''),
listener: generateListener(configFile.sslFiles),
staticRoute: await renderTemplate(join('fixtures', 'staticRoute.template'), {cors}),
visibleRoutes: (await Promise.all(visibleRoutesPromises)).join(''),
};
}
/**
* Read the list of docker containers
* @param pathToDockerSocket
*/
export async function getContainers(pathToDockerSocket = '/var/run/docker.sock'): Promise<Dockerode.ContainerInfo[]> {
const docker = new Dockerode({
socketPath: pathToDockerSocket,
});
const containers = await docker.listContainers();
if (containers.length === 0) {
throw new Error(
'No running docker containers found.' +
`Please check if docker is running and Node.js can access the docker socket (${pathToDockerSocket})`,
);
}
return containers;
}