stabilize, add daemon state, add base build script

This commit is contained in:
JOLIMAITRE Matthieu 2023-12-10 22:04:40 +01:00
parent a1963cf491
commit 241d50e42a
12 changed files with 512 additions and 46 deletions

14
instance/src/bin/proxy.ts Normal file → Executable file
View file

@ -1,11 +1,15 @@
#!/bin/env -S deno run -A --unstable
await main();
if (import.meta.main) await main();
async function main() {
const [from_port, to_ip, to_port] = Deno.args;
if (to_port === undefined) Deno.exit(2);
if (to_port === undefined) {
console.error("[proxy] usage: ./proxy.ts <from_port> <to_ip> <to_port>");
Deno.exit(2);
}
const server = Deno.listen({ transport: "tcp", port: parseInt(from_port) });
console.log("[proxy] listening on port", from_port, "redirecting to", to_ip, "port", to_port);
for await (const connection of server) {
try {
@ -16,3 +20,9 @@ async function main() {
} catch (_) { /* isok */ }
}
}
export function proxy_command(from_port: number, to_ip: string, to_port: number) {
const path = new URL("", import.meta.url).pathname;
const args = [from_port, to_ip, to_port].map((v) => v.toString());
return new Deno.Command(path, { args });
}

View file

@ -1,6 +1,13 @@
export type { Base, BaseContext } from "./lib/create.ts";
import { toText } from "https://deno.land/std@0.208.0/streams/to_text.ts";
import { lines } from "./lib/utils.ts";
import { lines, log_from, loop_process, LoopProcess, run, sleep } from "./lib/utils.ts";
import { ContainerConfig } from "./lib/config.ts";
import { container_paths } from "./lib/paths.ts";
import { container_command, get_container_addresses } from "./lib/nspawn.ts";
import { proxy_command } from "./bin/proxy.ts";
const log = log_from("lib");
export type CmdStatus = ReturnType<typeof new_cmd_status>;
export function new_cmd_status() {
@ -17,15 +24,22 @@ export function new_cmd_disable(name: string) {
return { kind: "disable" as const, name };
}
export type CmdReload = ReturnType<typeof new_cmd_reload>;
export function new_cmd_reload(name: string) {
return { kind: "reload" as const, name };
}
export type CmdStop = ReturnType<typeof new_cmd_stop>;
export function new_cmd_stop() {
return { kind: "stop" as const };
}
export type Cmd = CmdStatus | CmdEnable | CmdDisable | CmdStop;
export type Cmd = CmdStatus | CmdEnable | CmdDisable | CmdReload | CmdStop;
export function daemon_listen(sock_path: string) {
export async function daemon_listen(sock_path: string) {
const server = Deno.listen({ transport: "unix", path: sock_path });
await Deno.chmod(sock_path, 0o775);
await run("chgrp", "wheel", sock_path);
const generator = async function* () {
for await (const request of server) {
const respond = async (message: string) => {
@ -53,12 +67,52 @@ export async function daemon_send(sock_path: string, command: Cmd) {
return await toText(request.readable);
}
export type Runner = ReturnType<typeof start_runner>;
export type Runner = Awaited<ReturnType<typeof start_runner>>;
export function start_runner(config: ContainerConfig) {
const { name } = config;
const paths = container_paths(name);
const command = container_command(name, paths.root, {
boot: true,
veth: true,
cmd_opts: {
stdin: "null",
stdout: "null",
},
});
const proxies = [] as LoopProcess[];
const container_loop = loop_process(command, {
on_start: () => {
log("container", name, "started");
(async () => {
await sleep(1_000);
const [address] = await get_container_addresses(name);
proxies.push(...start_proxies(address, config.redirects));
})();
},
on_stop: async () => {
log("container", name, "stopped");
for (const p of proxies) await p.kill();
await sleep(500);
proxies.splice(0, proxies.length);
},
});
return {
name: config.name,
name,
config,
container_loop,
stop: async () => {
// TODO
for (const p of proxies) await p.kill();
await container_loop.kill();
},
};
}
function start_proxies(address: string, redirects: [number, number][]) {
const redirections = redirects
.map(([from_port, to_port]) => {
return loop_process(proxy_command(from_port, address, to_port), {
on_start: () => console.log("starting proxy", from_port, "to", address, "port", to_port),
});
});
return redirections;
}

View file

@ -1,5 +1,7 @@
import { container_paths } from "./paths.ts";
import { exists } from "./utils.ts";
import { container_paths, containers_path } from "./paths.ts";
import { exists, log_from } from "./utils.ts";
const log = log_from("config");
export type ContainerConfig = ReturnType<typeof new_container_config>;
export function new_container_config(name: string) {
@ -11,11 +13,27 @@ export function new_container_config(name: string) {
}
export async function load_container_config(name: string) {
const config_path = container_paths(name).config;
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 = JSON.parse(content);
const default_ = new_container_config(name);
if (read.version < default_.version) throw new Error("read conf version is outdated");
return { ...default_, ...read };
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_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[];
}

View file

@ -0,0 +1,69 @@
// import { blob } from "https://deno.land/std@0.208.0/streams/mod.ts"
import { ContainerConfig } from "./config.ts";
import { base_paths } from "./paths.ts";
import { lines, log_from } from "./utils.ts";
export type Base = (context: BaseContext) => unknown;
const log = log_from("BUILDING");
export type BaseContext = ReturnType<typeof new_container_context>;
export function new_container_context(root_dir: string, config: ContainerConfig, known_containers: ContainerConfig[]) {
return {
root_dir,
config,
sh: async (script: string) => {
log(`running '${script.replace("\n", " ").slice(0, 20)}...'`);
const process = new Deno.Command("sudo", {
args: ["sh", "-c", "set -e;" + script],
stdin: "null",
stdout: "piped",
}).spawn();
log_lines_prefixed(" | ", process.stdout);
const res = await process.status;
if (!res.success) throw new Error("process failed");
},
sh_in: async (script: string) => {
log(`inside '${root_dir}' running '${script.replace("\n", " ").slice(0, 20)}...'`);
const process = new Deno.Command("sudo", {
args: ["systemd-nspawn", `--directory=${root_dir}`, "-P"],
stdin: "piped",
stdout: "piped",
}).spawn();
write_all(process.stdin, "set -e;" + script + "\nexit 0\n");
log_lines_prefixed(" | ", process.stdout);
const res = await process.status;
if (!res.success) throw new Error("process failed");
},
redirect: (from: number, to: number) => {
log("redirects", from, "to", to);
config.redirects.push([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 = [...used_by_known_containers, ...used_by_self];
let result = target;
while (used.includes(result)) result += 1;
return result;
},
};
}
async function log_lines_prefixed(prefix: string, stream: ReadableStream<Uint8Array>) {
for await (const line of lines(stream)) {
console.log(prefix + line);
}
}
async function write_all(stream: WritableStream<Uint8Array>, text: string) {
await stream.getWriter().write(new TextEncoder().encode(text));
}
export async function load_base(name: string) {
const paths = base_paths(name);
const module = await import(paths.module);
const build = module["build"] as (context: BaseContext) => unknown;
if (build === undefined) throw new Error("module does not contain a build procedure");
return { build };
}

View file

@ -1 +1,37 @@
// wrapper
import { log_from, sleep } from "./utils.ts";
const log = log_from("nspawn");
export function container_command(name: string, directory: string, opts?: {
veth?: boolean;
boot?: boolean;
cmd_opts?: Deno.CommandOptions;
}) {
const args = [
`--machine=${name}`,
`--directory=${directory}`,
];
if (opts?.veth ?? false) args.push("--network-veth");
if (opts?.boot ?? false) args.push("--boot");
const command = new Deno.Command("systemd-nspawn", { ...opts?.cmd_opts, args });
return command;
}
export async function get_container_addresses(name: string) {
const command = new Deno.Command("machinectl", { args: ["status", name], stdout: "piped" });
while (true) {
await sleep(500);
log("getting address of", name);
const { code, stdout } = await command.output();
if (code != 0) continue;
const output = new TextDecoder().decode(stdout);
const [_, rest] = output.split(" Address: ");
if (rest === undefined) continue;
const [raw] = rest.split(" OS: ");
const addresses = raw.trim().split("\n").map((l) => l.trim());
if (addresses.length === 0) continue;
return addresses;
}
}

View file

@ -16,7 +16,21 @@ export function containers_path() {
export function container_paths(name: string) {
const base = containers_path() + "/" + name;
const conf = base + "/config.json";
const configuration = base + "/config.json";
const root = base + "/root";
return { base, configuration: conf, root };
return { base, configuration, root };
}
export function state_path() {
return instance_root_path() + "/local/state.json";
}
export function bases_path() {
return instance_root_path() + "/local/bases";
}
export function base_paths(name: string) {
const base = bases_path() + "/" + name;
const module = base + "/module.ts";
return { base, module };
}

View file

@ -1,5 +1,6 @@
import { TextLineStream } from "https://deno.land/std@0.208.0/streams/mod.ts";
export type Channel<T> = ReturnType<typeof channel<T>>;
export function channel<T>() {
const inner = {
queue: [] as T[],
@ -41,3 +42,54 @@ export function lines(readable: ReadableStream<Uint8Array>) {
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream());
}
const async_noop = async () => {};
export type LoopProcess = ReturnType<typeof loop_process>;
export function loop_process(
command: Deno.Command,
opts?: { delay?: number; on_start?: () => unknown; on_stop?: () => unknown },
) {
const events = {
on_start: opts?.on_start ?? async_noop,
on_stop: opts?.on_stop ?? async_noop,
};
const kill_sig = channel<"kill">();
async function launch() {
while (true) {
await events.on_start();
const child_process = command.spawn();
const result = await Promise.any([kill_sig.receive(), child_process.output()]);
if (result === "kill") {
await events.on_stop();
child_process.kill();
break;
}
await events.on_stop();
await sleep(opts?.delay ?? 500);
}
}
const looping = launch();
return {
kill: async () => {
kill_sig.send("kill"), await looping;
},
events,
looping,
};
}
export function sleep(ms: number) {
return new Promise((resolver) => setTimeout(resolver, ms));
}
export async function run(command: string, ...args: string[]) {
const { code } = await new Deno.Command(command, { args }).output();
if (code != 0) throw new Error(`command ${command} failed`);
}
export function log_from(...prefixes: string[]) {
const prefix = prefixes.map((p) => `[${p}]`).join("").padStart(10);
return function (...args: unknown[]) {
console.log(prefix, ...args);
};
}