add nginx wrapper and refactor

This commit is contained in:
JOLIMAITRE Matthieu 2024-02-03 20:15:35 +01:00
parent 9216063aa0
commit 6f92727bb3
9 changed files with 530 additions and 92 deletions

View file

@ -1,61 +1,9 @@
import { container_paths, containers_path, state_config_path } from "./paths.ts";
import { exists, log_from } from "./utils.ts";
import { log_from } from "./utils.ts";
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
const log = log_from("config");
export function new_container_config(name: string): ContainerConfig {
return {
name,
version: 0,
redirects: [] as [number, number][],
};
}
export type ContainerConfig = ReturnType<typeof parse_container_config>;
export function parse_container_config(json: string) {
return z.object({
name: z.string(),
version: z.number(),
redirects: z.array(
z.tuple([
z.number(),
z.number(),
]).or(z.tuple([
z.number(),
z.number(),
z.literal("tcp").or(z.literal("udp")),
])),
),
}).parse(JSON.parse(json));
}
export async function load_container_config(name: string) {
const config_path = container_paths(name).configuration;
log("loading config for", name);
if (!exists(config_path)) return null;
const content = await Deno.readTextFile(config_path);
const read = parse_container_config(content);
const default_ = new_container_config(name);
if (read.version < default_.version) throw new Error("read conf version is outdated");
return { ...default_, ...read } as ContainerConfig;
}
export async function save_container_config(config: ContainerConfig) {
log("saving config of", config.name);
const config_path = container_paths(config.name).configuration;
const serialized = JSON.stringify(config, null, 4);
await Deno.writeTextFile(config_path, serialized);
}
export async function load_all_container_configs() {
const names = Array.from(Deno.readDirSync(containers_path()))
.filter((e) => e.isDirectory)
.map((e) => e.name);
const results = await Promise.all(names.map((name) => load_container_config(name)));
return results.filter((item) => item != null) as ContainerConfig[];
}
export type StateConfig = ReturnType<typeof new_config>;
export function new_config(app_key: string, app_secret: string, consumer_key: string) {
return { app_key, app_secret, consumer_key };
@ -72,3 +20,87 @@ export async function load_state_config() {
return result;
}
}
const CONTAINER_CONFIG_VERSION = 1;
export class ContainerConfig {
name;
redirections;
constructor(name: string, redirections: ContainerConfigRedirection[]) {
this.name = name;
this.redirections = redirections;
}
public static new_default(name: string) {
return new ContainerConfig(name, []);
}
public static async load(name: string) {
const path = container_paths(name).configuration;
const content = await Deno.readTextFile(path);
return ContainerConfig.deserialize(content);
}
public async save() {
const path = container_paths(this.name).configuration;
await Deno.writeTextFile(path, this.serialize());
}
public static async *load_all() {
for await (const { isDirectory, name } of Deno.readDir(containers_path())) {
if (!isDirectory) continue;
yield await ContainerConfig.load(name);
}
}
public static deserialize(raw: string) {
const unknown = JSON.parse(raw);
const parsed = parse(unknown);
return new ContainerConfig(parsed.name, parsed.redirections);
}
public serialize() {
const raw: SerializedContainerConfig = {
version: CONTAINER_CONFIG_VERSION,
name: this.name,
redirections: this.redirections,
};
return JSON.stringify(raw);
}
public *used_host_ports() {
for (const redir of this.redirections) {
if (redir.kind === "http") yield redir.port;
if (redir.kind === "port") yield redir.from;
}
}
}
function parse(input: unknown) {
const redir_port = z.object({
kind: z.literal("port"),
from: z.number(),
to: z.number(),
});
const redir_http = z.object({
kind: z.literal("http"),
tls: z.boolean(),
port: z.number(),
domain: z.string(),
});
// TODO : redir DNS SRV
const redirection = redir_http.or(redir_port);
return z.object({
name: z.string(),
version: z.number(),
redirections: z.array(redirection),
}).parse(input);
}
type SerializedContainerConfig = ReturnType<typeof parse>;
export type ContainerConfigRedirection = SerializedContainerConfig["redirections"][0];

View file

@ -37,11 +37,11 @@ export function new_container_context(root_dir: string, config: ContainerConfig,
},
redirect: (from: number, to: number) => {
log("redirects", from, "to", to);
config.redirects.push([from, to]);
config.redirections.push({ kind: "port", from, to });
},
available_port: (target: number) => {
const used_by_known_containers = known_containers.map((c) => c.redirects[0]).flat();
const used_by_self = config.redirects.map((r) => r[0]);
const used_by_known_containers = known_containers.map((c) => [...c.used_host_ports()]).flat();
const used_by_self = config.used_host_ports();
const used = [...used_by_known_containers, ...used_by_self];
let result = target;
while (used.includes(result)) result += 1;

42
instance/src/lib/nginx.ts Normal file
View file

@ -0,0 +1,42 @@
import { run } from "./utils.ts";
import * as path from "https://deno.land/std@0.214.0/path/mod.ts";
class NginxController {
proxy_target_url;
enabled_conf_dir;
constructor(proxy_target_url: string, enabled_conf_dir: string) {
this.proxy_target_url = proxy_target_url;
this.enabled_conf_dir = enabled_conf_dir;
}
public async add_proxy(url: string, port: number, conf_dir: string) {
const conf_file_content = `
server {
listen 80;
listen [::]:80;
server_name ${url};
location / {
proxy_pass http://${this.proxy_target_url}:${port};
}
}
`;
const conf_file_path = path.join(conf_dir, url + ".conf");
const enabled_conf_file_path = path.join(this.enabled_conf_dir, url + ".conf");
await Deno.writeTextFile(conf_file_path, conf_file_content);
await run("ln", "-s", await Deno.realPath(conf_file_path), enabled_conf_file_path);
await this.reload();
}
public async remove_proxy(url: string, conf_dir: string) {
const conf_file_path = path.join(conf_dir, url + ".conf");
const enabled_conf_file_path = path.join(this.enabled_conf_dir, url + ".conf");
await Deno.remove(enabled_conf_file_path);
await Deno.remove(conf_file_path);
await this.reload();
}
private async reload() {
await run("systemctl", "restart", "nginx");
}
}

View file

@ -1,13 +1,14 @@
// wrapper
import { log_from, sleep } from "./utils.ts";
import { ContainerConfigRedirection } from "./config.ts";
const log = log_from("nspawn");
export function container_command(name: string, directory: string, opts?: {
veth?: boolean;
boot?: boolean;
ports?: ([number, number] | [number, number, "tcp" | "udp"])[];
redirections?: ContainerConfigRedirection[];
cmd_opts?: Deno.CommandOptions;
syscall_filter?: string[];
}) {
@ -17,8 +18,12 @@ export function container_command(name: string, directory: string, opts?: {
];
if (opts?.veth ?? false) args.push("--network-veth");
if (opts?.boot ?? false) args.push("--boot");
for (const [from, to, proto] of opts?.ports ?? []) {
args.push(proto === undefined ? `--port=${from}:${to}` : `--port=${proto}:${from}:${to}`);
for (const redir of opts?.redirections ?? []) {
if (redir.kind === "http") args.push(`--port=${redir.port}`);
if (redir.kind === "port") {
args.push(`--port=tcp:${redir.from}:${redir.to}`);
args.push(`--port=udp:${redir.from}:${redir.to}`);
}
}
for (const call of opts?.syscall_filter ?? []) args.push(`--system-call-filter=${call}`);
const command = new Deno.Command("systemd-nspawn", { ...opts?.cmd_opts, args });

View file

@ -93,3 +93,9 @@ export function log_from(...prefixes: string[]) {
console.log(prefix, ...args);
};
}
export async function async_collect<T>(gen: AsyncIterable<T>) {
const result = [] as T[];
for await (const item of gen) result.push(item);
return result;
}