Skip to content

Commit

Permalink
Docker services
Browse files Browse the repository at this point in the history
  • Loading branch information
willnode committed Sep 18, 2024
1 parent af0cb1e commit cba0022
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 10 deletions.
23 changes: 20 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "domcloud-bridge",
"version": "0.58.0",
"version": "0.59.0",
"description": "Deployment runner for DOM Cloud",
"main": "app.js",
"engines": {
Expand All @@ -25,7 +25,8 @@
"fast-xml-parser": "^4.3.6",
"nginx-conf": "^2.1.0",
"proper-lockfile": "^4.1.2",
"shelljs": "^0.8.5"
"shelljs": "^0.8.5",
"yaml": "^2.5.1"
},
"devDependencies": {
"@types/cli": "^0.11.25",
Expand Down
117 changes: 116 additions & 1 deletion src/executor/docker.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {
cat,
executeLock,
spawnSudoUtil,
} from '../util.js';
import { existsSync } from 'fs';
import { nginxExec } from './nginx.js';
import path from 'path';
import * as yaml from 'yaml';
import { ShellString } from 'shelljs';

const tmpFile = path.join(process.cwd(), '/.tmp/compose')

class DockerExecutor {
LOGINLINGERDIR = '/var/lib/systemd/linger';
LOGINLINGERDIR = '/var/lib/systemd/linger';
constructor() {
if (process.env.LOGINLINGERDIR) {
this.LOGINLINGERDIR = process.env.LOGINLINGERDIR;
Expand Down Expand Up @@ -49,6 +56,114 @@ class DockerExecutor {
return "Updated for docker";
});
}
generateRandomIPv4() {
// Generate random octets (numbers between 0 and 255)
const octet1 = Math.trunc(Math.random() * 256);
const octet2 = Math.trunc(Math.random() * 256);
const octet3 = Math.trunc(Math.random() * 256);

// Construct the address with the "10." prefix
const ipAddress = `10.${octet1}.${octet2}.${octet3}`;
return ipAddress;
}
/**
*
* @param {any} services
* @param {string} domain
*/
async rewriteServices(services, domain) {
// get IP data from nginx
const nginxNode = await nginxExec.get(domain);
const nginx = nginxExec.extractInfo(nginxNode, domain);
let nginxChanged = false;
if (!nginx.docker_ip) {
nginx.docker_ip = this.generateRandomIPv4();
nginxChanged = true;
}
var exposedPorts = [];
// rewrite all ips
for (const [name, service] of Object.entries(services)) {
if (typeof service !== 'object' || !service.ports) continue;
if (!Array.isArray(service.ports) || service.ports.length == 0) throw new Error("Invalid ports format in service: " + name);
for (let i = 0; i < service.ports.length; i++) {
var port = service.ports[i];
var conf = {
target: 0,
host_ip: nginx.docker_ip,
protocol: 'tcp',
published: "" + Math.trunc(Math.random() * 30000 + 1025),
}
if (typeof port === 'string') {
if (/^\d+$/.test(port)) {
conf.target = parseInt(port);
} else if (/^\d+:\d+$/.test(port)) {
const [src, dst] = port.split(":");
conf.target = parseInt(dst);
conf.published = src;
} else if (/^127.0.0.1:\d+:\d+$/.test(port)) {
const [_, src, dst] = port.split(":");
conf.target = parseInt(dst);
conf.published = src;
} else {
throw new Error("Unknown ports format: " + name);
}
}
exposedPorts.push(conf.published);
service.ports[i] = conf;
}
}
// nginx replace port docker
let matchedConf = nginx.config.locations?.find(x => x.match == '/');
if (!matchedConf) {
if (!nginx.config.locations)
nginx.config.locations = [];
matchedConf = { match: '/' }
nginx.config.locations.push(matchedConf);
nginxChanged = true;
}
let proxyPass = matchedConf.proxy_pass + "";
if (!proxyPass || !proxyPass.startsWith('docker:') || exposedPorts.includes(proxyPass.replace(/^docker:/, ''))) {
if (exposedPorts.length == 0) {
throw new Error("There are no exposed ports! Need atleast one to forward it into NGINX");
}
matchedConf.proxy_pass = "docker:" + exposedPorts[exposedPorts.length - 1];
nginxChanged = true;
}
if (nginxChanged) {
await nginxExec.setDirect(domain, nginx);
}
return services;
}
/**
*
* @param {any} services
* @param {string} home
* @param {string} domain
* @return {Promise<string>}
*/
async executeServices(services, home, domain) {
let filename = path.join(home, 'docker-compose.yml');
if (typeof services === 'string') {
filename = path.join(home, services);
// cat from file
services = yaml.parse(await executeLock('compose', () => {
return new Promise((resolve, reject) => {
spawnSudoUtil('COMPOSE_GET', [filename]).then(() => {
resolve(cat(tmpFile));
}).catch(reject);
});
})).services;
}
services = this.rewriteServices(services, domain);
let composeFile = yaml.stringify({ services });
await executeLock('compose', () => {
return new Promise((resolve, reject) => {
ShellString(composeFile).to(filename)
spawnSudoUtil('COMPOSE_SET', [filename]).then(resolve).catch(reject);
});
});
return composeFile;
}
}

export const dockerExec = new DockerExecutor();
26 changes: 24 additions & 2 deletions src/executor/nginx.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ class NginxExecutor {
} else if (key === "proxy_pass") {
if (config[key] === "unit") {
node._add(key, unitProxy);
} else if (/^http:\/\/(10|127)\.\d+\.\d+\.\d+:\d+(\$\d+|\/.+)?$/) {
} else if (/^docker:(\d+)$/.test(config[key]) && info.docker_ip) {
let port = parseInt(config[key].substring(7));
if (port > 0x400 && port < 0xFFFF) {
node._add(key, `http://${info.docker_ip}:${port}`);
}
} else if (/^http:\/\/(10|127)\.\d+\.\d+\.\d+:\d+(\$.+|\/.+)?$/.test(config[key])) {
node._add(key, config[key]);
}
} else {
Expand Down Expand Up @@ -93,7 +98,7 @@ class NginxExecutor {
}
}
}
if (config.fastcgi) {
if (config.fastcgi && info.fcgi) {
node._add("location", "~ \\.php" + (config.fastcgi == 'always' ? "(/|$)" : "$"));
var n = node.location[node.location.length - 1];
switch (config.fastcgi) {
Expand Down Expand Up @@ -261,6 +266,21 @@ class NginxExecutor {
}
return null;
}
const findDockerIp = (l) => {
if (l.proxy_pass && l.proxy_pass[0]) {
if (/^http:\/\/10\.\d+\.\d+\.\d+:\d+$/.test(l.proxy_pass[0]._value)) {
return new URL(l.proxy_pass[0]._value).hostname;
}
}
if (l.location) {
for (const ll of l.location) {
var r = findFastCgi(ll);
if (r)
return r;
}
}
return null;
}
const data = {
ssl: 0, // binary of 1 = HTTP, 2 = HTTPS
http: 1, // http version (1 or 2)
Expand All @@ -270,6 +290,7 @@ class NginxExecutor {
root: null,
user: null,
fcgi: null,
docker_ip: null,
free: false,
access_log: null,
error_log: null,
Expand Down Expand Up @@ -301,6 +322,7 @@ class NginxExecutor {
data.ssl_certificate_key = node.ssl_certificate_key ? node.ssl_certificate_key[0]._value : `/home/${data.user}/ssl.key`;

data.fcgi = findFastCgi(node);
data.docker_ip = findDockerIp(node);
data.config = extractLocations(node, `/home/${data.user}/`);
delete data.config.match;
delete data.config.alias;
Expand Down
15 changes: 13 additions & 2 deletions src/executor/runnersub.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import { namedExec } from "./named.js";
import { nginxExec } from "./nginx.js";
import { virtualminExec } from "./virtualmin.js";
import { unitExec } from "./unit.js";
import { dockerExec } from "./docker.js";

/**
* @param {{source: any;features: any;commands: any;nginx: any;unit: any;envs: any,directory:any, root:any}} config
* @param {{source: any;features: any;commands: any;services: any;nginx: any;unit: any;envs: any,directory:any, root:any}} config
* @param {{[x: string]: any}} domaindata
* @param {string} subdomain
* @param {{(cmd: string, write?: boolean): Promise<any>}} sshExec
Expand Down Expand Up @@ -343,7 +344,7 @@ export async function runConfigSubdomain(config, domaindata, subdomain, sshExec,
}
};

if (config.source || config.commands) {
if (config.source || config.commands || config.services) {
await sshExec(`shopt -s dotglob`, false);
await sshExec(`export DOMAIN='${subdomain}'`, false);
// enable managing systemd for linger user
Expand All @@ -366,6 +367,16 @@ export async function runConfigSubdomain(config, domaindata, subdomain, sshExec,
delete config.root;
}

if (config.services) {
await writeLog("$> Applying docker compose services");
await dockerExec.executeServices(config.services, subdomaindata['Home directory'] + '/public_html', subdomain);
if (typeof config.services == 'string') {
await sshExec(`docker compose up --build -f ` + config.services);
} else {
await sshExec(`docker compose up --build`);
}
}

if (Array.isArray(config.features)) {
await writeLog("$> Applying features");
for (const feature of config.features) {
Expand Down
10 changes: 10 additions & 0 deletions sudoutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const env = Object.assign({}, {
NGINX_TMP: path.join(__dirname, '.tmp/nginx'),
UNIT_SOCKET: '/var/run/unit/control.sock',
UNIT_TMP: path.join(__dirname, '.tmp/unit'),
COMPOSE_TMP: path.join(__dirname, '.tmp/compose'),
IPTABLES_PATH: '/etc/sysconfig/iptables',
IPTABLES_OUT: '/etc/sysconfig/iptables',
IPTABLES_SAVE: 'iptables-save',
Expand Down Expand Up @@ -112,6 +113,15 @@ switch (cli.args.shift()) {
rm(DEST + '.bak');
exec(`${env.NGINX_BIN} -s reload`);
exit(0);
case 'COMPOSE_GET':
arg = cli.args.shift();
cat(arg).to(env.COMPOSE_TMP);
fixOwner(env.COMPOSE_TMP);
exit(0);
case 'COMPOSE_SET':
arg = cli.args.shift();
cat(env.COMPOSE_TMP).to(arg);
exit(0);
case 'UNIT_GET':
arg = cli.args.shift();
var unit = spawn('curl', ['--unix-socket', env.UNIT_SOCKET, 'http://localhost' + arg], {
Expand Down

0 comments on commit cba0022

Please sign in to comment.