mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
feat: add proxy
This commit is contained in:
65
src/cli.ts
Normal file
65
src/cli.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {execSync} from 'child_process';
|
||||
import * as config from 'config';
|
||||
import * as Dockerode from 'dockerode';
|
||||
import {readFile, writeFileSync} from 'fs-extra';
|
||||
import {render} from 'mustache';
|
||||
import {ConfigFile, logger} from './common';
|
||||
import {getContainers, getTemplateView} from './main';
|
||||
|
||||
// handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (error: Error) => {
|
||||
logger.error(error.message);
|
||||
logger.info(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
let containerHashCache = '';
|
||||
const configFile: ConfigFile = config.util.toObject();
|
||||
|
||||
/**
|
||||
* Reads the container information from the docker socket and updates
|
||||
* the nginx config if necessary
|
||||
*
|
||||
* The function will call itself again every 10s
|
||||
*/
|
||||
async function updateNginxConfig() {
|
||||
|
||||
const containers = await getContainers();
|
||||
|
||||
const containerHash = containers.map((container: Dockerode.ContainerInfo) => {
|
||||
return container.Id;
|
||||
}).join(',');
|
||||
|
||||
// if containers changed -> write config file, reload nginx
|
||||
if (containerHash !== containerHashCache) {
|
||||
logger.log('docker container changed');
|
||||
logger.log('Generating new NGINX configuration');
|
||||
|
||||
// render nginx config file
|
||||
const nginxConfig = render(await readFile('nginx.conf.template', 'utf8'), await getTemplateView(containers));
|
||||
|
||||
logger.log(`containers (${containerHash}) matched the configuration.`);
|
||||
|
||||
containerHashCache = containerHash;
|
||||
|
||||
logger.log(`Writing new config file "${configFile.output}"`);
|
||||
// overwrite nginx config file with our rendered one
|
||||
writeFileSync(configFile.output, nginxConfig, 'utf8');
|
||||
|
||||
logger.log('Executing "nginx -s reload" to tell nginx to reload the configuration file');
|
||||
execSync('nginx -s reload');
|
||||
}
|
||||
}
|
||||
|
||||
function forever() {
|
||||
// start the process of dynamic nginx configuration
|
||||
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();
|
||||
28
src/common.ts
Normal file
28
src/common.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {SMTP} from '@openstapps/logger/lib/SMTP';
|
||||
|
||||
// use SMTP as a default monitoring system for logger.error();
|
||||
export const logger = new Logger(SMTP.getInstance());
|
||||
|
||||
/**
|
||||
* A representation of the config file
|
||||
*/
|
||||
export interface ConfigFile {
|
||||
activeVersions: string[];
|
||||
hiddenRoutes: string[];
|
||||
outdatedVersions: string[];
|
||||
output: string;
|
||||
sslFiles: string[];
|
||||
visibleRoutes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A view object to render the nginx config template
|
||||
*/
|
||||
export interface TemplateView {
|
||||
dockerVersionMap: string;
|
||||
hiddenRoutes: string;
|
||||
listener: string;
|
||||
staticRoute: string;
|
||||
visibleRoutes: string;
|
||||
}
|
||||
196
src/main.ts
Normal file
196
src/main.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user