mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-04 04:22:50 +00:00
feat: support docker swarm deployments
This commit is contained in:
56
package-lock.json
generated
56
package-lock.json
generated
@@ -649,6 +649,12 @@
|
||||
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/proxyquire": {
|
||||
"version": "1.3.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.28.tgz",
|
||||
"integrity": "sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "7.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
|
||||
@@ -1101,9 +1107,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001338",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001338.tgz",
|
||||
"integrity": "sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ==",
|
||||
"version": "1.0.30001339",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001339.tgz",
|
||||
"integrity": "sha512-Es8PiVqCe+uXdms0Gu5xP5PF2bxLR7OBp3wUzUnuO7OHzhOfCyg3hdiGWVPVxhiuniOzng+hTc1u3fEQ0TlkSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chai": {
|
||||
@@ -2061,6 +2067,16 @@
|
||||
"flat-cache": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||
"integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-object": "~1.0.1",
|
||||
"merge-descriptors": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -2571,6 +2587,12 @@
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||
"dev": true
|
||||
},
|
||||
"is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-plain-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
|
||||
@@ -3016,6 +3038,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -3229,6 +3257,12 @@
|
||||
"integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==",
|
||||
"dev": true
|
||||
},
|
||||
"module-not-found-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||
"integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||
@@ -3268,6 +3302,11 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-port-scanner": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-port-scanner/-/node-port-scanner-3.0.1.tgz",
|
||||
"integrity": "sha512-TuFGEWfye+1atB74v0Vm6myEjpq0L5Jo3UaOG9xgtYHxnFZN0fF9CnwCxp7ENWDerGbI1UXAgdRMIPz8TM73Hg=="
|
||||
},
|
||||
"node-preload": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
|
||||
@@ -3624,6 +3663,17 @@
|
||||
"fromentries": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"proxyquire": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
|
||||
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-keys": "^1.0.2",
|
||||
"module-not-found-error": "^1.0.1",
|
||||
"resolve": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"dockerode": "3.3.1",
|
||||
"is-cidr": "4.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"node-port-scanner": "3.0.1",
|
||||
"semver": "7.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -22,6 +23,7 @@
|
||||
"@types/chai": "4.3.1",
|
||||
"@types/chai-spies": "1.0.3",
|
||||
"@types/mustache": "4.1.2",
|
||||
"@types/proxyquire": "1.3.28",
|
||||
"@typescript-eslint/eslint-plugin": "5.22.0",
|
||||
"@typescript-eslint/parser": "5.22.0",
|
||||
"chai": "4.3.6",
|
||||
@@ -36,6 +38,7 @@
|
||||
"nyc": "15.1.0",
|
||||
"prepend-file-cli": "1.0.6",
|
||||
"prettier": "2.6.2",
|
||||
"proxyquire": "2.1.3",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-node": "10.7.0",
|
||||
"typedoc": "0.22.15",
|
||||
|
||||
@@ -30,6 +30,7 @@ process.on('unhandledRejection', async error => {
|
||||
|
||||
let containerHashCache = '';
|
||||
let configHashCache = '';
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Reads the container information from the docker socket and updates the nginx config if necessary
|
||||
@@ -51,6 +52,8 @@ async function updateNginxConfig() {
|
||||
// if containers changed -> write config file, reload nginx
|
||||
if (containerHash !== containerHashCache || configHash !== configHashCache) {
|
||||
Logger.log('Generating new NGINX configuration');
|
||||
Logger.log('Waiting for Docker network to settle...');
|
||||
await delay(10_000);
|
||||
|
||||
// render nginx config file
|
||||
const nginxConfig = render(
|
||||
|
||||
53
src/main.ts
53
src/main.ts
@@ -32,6 +32,9 @@ import {
|
||||
/* eslint-disable unicorn/prefer-module */
|
||||
/* eslint-disable unicorn/no-await-expression-member */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const nodePortScanner = require('node-port-scanner');
|
||||
|
||||
/**
|
||||
* Checks if a ContainerInfo matches a name and version regex
|
||||
*
|
||||
@@ -47,8 +50,11 @@ export function containerMatchesRegex(
|
||||
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
|
||||
((typeof container.Labels['com.docker.compose.service'] === 'string' &&
|
||||
container.Labels['com.docker.compose.service'] === name) ||
|
||||
(typeof container.Labels['com.docker.stack.namespace'] === 'string' &&
|
||||
container.Labels['com.docker.swarm.service.name'] ===
|
||||
`${container.Labels['com.docker.stack.namespace']}_${name}`))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,14 +68,43 @@ export function containerMatchesRegex(
|
||||
*/
|
||||
export async function getGatewayOfStAppsBackend(container: Dockerode.ContainerInfo): Promise<string> {
|
||||
if (container.Ports.length === 0) {
|
||||
await Logger.error(`Container ${container.Id} does not advertise any port.
|
||||
await Logger.error(`Container ${container.Names[0]} does not advertise any port.
|
||||
Please expose a port if the container should be accessible by NGINX.`);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// ip:port
|
||||
return `${container.Ports[0].IP}:${container.Ports[0].PublicPort}`;
|
||||
// Basic Docker network
|
||||
if (typeof container.Ports[0].IP !== 'undefined' && typeof container.Ports[0].PublicPort !== 'undefined') {
|
||||
// ip:port
|
||||
return `${container.Ports[0].IP}:${container.Ports[0].PublicPort}`;
|
||||
}
|
||||
|
||||
// Docker Swarm network
|
||||
if (
|
||||
typeof container.NetworkSettings?.Networks?.ingress?.IPAddress !== 'undefined' &&
|
||||
typeof container.Ports[0].PrivatePort !== 'undefined'
|
||||
) {
|
||||
const port = container.Ports[0].PrivatePort;
|
||||
|
||||
// Get a routable network connection
|
||||
for (const network in container.NetworkSettings.Networks) {
|
||||
const scan = await nodePortScanner(container.NetworkSettings.Networks[network].IPAddress, [port]);
|
||||
|
||||
if ((scan.ports.open as Array<number>).includes(port)) {
|
||||
Logger.info(
|
||||
`${container.Names[0]} reachable via ${container.NetworkSettings.Networks[network].IPAddress}:${port}`,
|
||||
);
|
||||
return `${container.NetworkSettings.Networks[network].IPAddress}:${port}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Logger.error(
|
||||
`Couldn't infer ${container.Names[0]} network reachability. It's possible your current Docker network setup isn't supported yet.`,
|
||||
);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +126,10 @@ export async function generateUpstreamMap(
|
||||
|
||||
let foundMatchingContainer = false;
|
||||
|
||||
// const backendContainer = containers.filter(container => container.Image.includes('backend'));
|
||||
// // eslint-disable-next-line no-console
|
||||
//console.log(JSON.stringify(backendContainer, undefined, 2));
|
||||
|
||||
// active versions
|
||||
result += (
|
||||
await Promise.all(
|
||||
@@ -137,7 +176,7 @@ export async function generateUpstreamMap(
|
||||
}
|
||||
}
|
||||
}
|
||||
await Logger.error('No backend for version', activeVersionRegex, 'found');
|
||||
Logger.warn('No backend for version', activeVersionRegex, 'found');
|
||||
|
||||
return ` \"~${activeVersionRegex}\" unavailable;\n`;
|
||||
}),
|
||||
@@ -149,7 +188,7 @@ export async function generateUpstreamMap(
|
||||
.map(outdatedVersionRegex => {
|
||||
return ` \"~${outdatedVersionRegex}\" outdated;`;
|
||||
})
|
||||
.join('');
|
||||
.join('\n');
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
result += '\n\}';
|
||||
|
||||
@@ -35,6 +35,9 @@ import {
|
||||
} 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);
|
||||
@@ -139,6 +142,56 @@ export class MainSpec {
|
||||
Status: 'Up 3 minutes',
|
||||
};
|
||||
|
||||
static swarmBackendContainerWithExposedPorts: 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: {
|
||||
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() {
|
||||
@@ -199,15 +252,54 @@ export class MainSpec {
|
||||
}
|
||||
|
||||
@test
|
||||
async 'upstream map calls logger error when no matching container is found'() {
|
||||
const spy = MainSpec.sandbox.on(console, 'warn', () => {
|
||||
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;
|
||||
delete backendContainer.Ports[0].PublicPort;
|
||||
delete backendContainer.Ports[0].PrivatePort;
|
||||
|
||||
const spy = MainSpec.sandbox.on(console, 'error', () => {
|
||||
// noop
|
||||
});
|
||||
|
||||
expect(await generateUpstreamMap(
|
||||
['0\\.8\\.\\d+'],
|
||||
['1\\.1\\.\\d+'],
|
||||
[MainSpec.backendContainerWithExposedPorts],
|
||||
)).to.be.equal(`map $http_x_stapps_version $proxyurl {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user