stabilize, add daemon state, add base build script
This commit is contained in:
parent
a1963cf491
commit
241d50e42a
12 changed files with 512 additions and 46 deletions
14
instance/src/bin/proxy.ts
Normal file → Executable file
14
instance/src/bin/proxy.ts
Normal file → Executable 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 });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
69
instance/src/lib/create.ts
Normal file
69
instance/src/lib/create.ts
Normal 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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue