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
34
README.md
Normal file
34
README.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# BDMGR.D TS
|
||||||
|
|
||||||
|
Implémentation de BDMGR daemon en typescript.
|
||||||
|
|
||||||
|
## Dépendances
|
||||||
|
|
||||||
|
Deno :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://deno.land/x/install/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Démon :
|
||||||
|
```sh
|
||||||
|
sudo ./daemon.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Controleur :
|
||||||
|
```sh
|
||||||
|
control.ts - controls BDMGRDTS daemon.
|
||||||
|
Usage:
|
||||||
|
./control.ts Command [ARG...]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
help Prints a help message.
|
||||||
|
stop Stops the daemon.
|
||||||
|
status Get a list of enabled containers.
|
||||||
|
create NAME BASE Create a new container from a specific base.
|
||||||
|
enable NAME Enables a container.
|
||||||
|
disable NAME Disables a container.
|
||||||
|
reload NAME Reloads a container, equivalent to enable then disable.
|
||||||
|
```
|
|
@ -1,31 +1,93 @@
|
||||||
#!/bin/env -S deno run -A --unstable
|
#!/bin/env -S deno run -A --unstable
|
||||||
|
|
||||||
import { daemon_send, new_cmd_disable, new_cmd_enable, new_cmd_status } from "./src/lib.ts";
|
import { daemon_send, new_cmd_disable, new_cmd_enable, new_cmd_status, new_cmd_stop } from "./src/lib.ts";
|
||||||
import { socket_path } from "./src/lib/paths.ts";
|
import { load_all_configs, new_container_config, save_container_config } from "./src/lib/config.ts";
|
||||||
|
import { load_base, new_container_context } from "./src/lib/create.ts";
|
||||||
|
import { container_paths, socket_path } from "./src/lib/paths.ts";
|
||||||
|
import { log_from, run } from "./src/lib/utils.ts";
|
||||||
|
|
||||||
await main();
|
const log = log_from("control");
|
||||||
|
|
||||||
|
if (import.meta.main) await main();
|
||||||
async function main() {
|
async function main() {
|
||||||
const [kind, ...rest] = Deno.args;
|
const [command, ...rest] = Deno.args;
|
||||||
|
|
||||||
if (kind === "status") {
|
if (["-h", "--help", "help"].includes(command)) {
|
||||||
const res = await daemon_send(socket_path(), new_cmd_status());
|
return console.log(`control.ts - controls BDMGRDTS daemon.
|
||||||
console.log({ res });
|
Usage:
|
||||||
|
./control.ts Command [ARG...]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
help Prints a help message.
|
||||||
|
stop Stops the daemon.
|
||||||
|
status Get a list of enabled containers.
|
||||||
|
create NAME BASE Create a new container from a specific base.
|
||||||
|
enable NAME Enables a container.
|
||||||
|
disable NAME Disables a container.
|
||||||
|
reload NAME Reloads a container, equivalent to enable then disable.
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === "enable") {
|
if (command === "create") {
|
||||||
|
const [name, base] = rest;
|
||||||
|
if (name === undefined) log("expects NAME argument");
|
||||||
|
if (base === undefined) log("expects BASE argument");
|
||||||
|
await create(name, base);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "status") {
|
||||||
|
const res = await daemon_send(socket_path(), new_cmd_status());
|
||||||
|
log(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "enable") {
|
||||||
const [name] = rest;
|
const [name] = rest;
|
||||||
|
if (name === undefined) log("expects NAME argument");
|
||||||
const res = await daemon_send(socket_path(), new_cmd_enable(name));
|
const res = await daemon_send(socket_path(), new_cmd_enable(name));
|
||||||
console.log({ res });
|
log(res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === "disable") {
|
if (command === "disable") {
|
||||||
const [name] = rest;
|
const [name] = rest;
|
||||||
|
if (name === undefined) log("expects NAME argument");
|
||||||
const res = await daemon_send(socket_path(), new_cmd_disable(name));
|
const res = await daemon_send(socket_path(), new_cmd_disable(name));
|
||||||
console.log({ res });
|
log(res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === "stop") {
|
if (command === "reload") {
|
||||||
const res = await daemon_send(socket_path(), new_cmd_status());
|
const [name] = rest;
|
||||||
console.log({ res });
|
if (name === undefined) log("expects NAME argument");
|
||||||
|
const res = await daemon_send(socket_path(), new_cmd_enable(name));
|
||||||
|
log(res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (command === "stop") {
|
||||||
|
const res = await daemon_send(socket_path(), new_cmd_stop());
|
||||||
|
log(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log("unknown command", command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(name: string, base_name: string) {
|
||||||
|
log("loading base", base_name);
|
||||||
|
const base = await load_base(base_name);
|
||||||
|
const known_containers = await load_all_configs();
|
||||||
|
|
||||||
|
const paths = container_paths(name);
|
||||||
|
await Deno.mkdir(paths.base);
|
||||||
|
const configuration = new_container_config(name);
|
||||||
|
|
||||||
|
log("creating the container", name, "at", paths.base);
|
||||||
|
const context = new_container_context(paths.root, configuration, known_containers);
|
||||||
|
await Deno.mkdir(paths.root);
|
||||||
|
await run("sudo", "chown", "root:root", paths.root);
|
||||||
|
await base.build(context);
|
||||||
|
await save_container_config(configuration);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,127 @@
|
||||||
#!/bin/env -S deno run -A --unstable
|
#!/bin/env -S deno run -A --unstable
|
||||||
|
|
||||||
import { daemon_listen, Runner, start_runner } from "./src/lib.ts";
|
import { daemon_listen, Runner, start_runner } from "./src/lib.ts";
|
||||||
import { new_container_config } from "./src/lib/config.ts";
|
import { load_container_config } from "./src/lib/config.ts";
|
||||||
import { socket_path } from "./src/lib/paths.ts";
|
import { socket_path, state_path } from "./src/lib/paths.ts";
|
||||||
|
import { log_from, sleep } from "./src/lib/utils.ts";
|
||||||
|
|
||||||
await main();
|
const log = log_from("daemon");
|
||||||
|
|
||||||
|
if (import.meta.main) await main();
|
||||||
async function main() {
|
async function main() {
|
||||||
const server = daemon_listen(socket_path());
|
const state = await create_state();
|
||||||
const enabled = new Map<string, Runner>();
|
const server = await daemon_listen(socket_path());
|
||||||
|
log("listening to", socket_path());
|
||||||
|
|
||||||
async function finish() {
|
async function finish() {
|
||||||
|
log("stopping");
|
||||||
server.server.close();
|
server.server.close();
|
||||||
|
for (const runner of state.enabled.values()) {
|
||||||
|
try {
|
||||||
|
await runner.stop();
|
||||||
|
} catch (_) { /* on s'en fou */ }
|
||||||
|
}
|
||||||
await Deno.remove(socket_path());
|
await Deno.remove(socket_path());
|
||||||
for (const runner of enabled.values()) await runner.stop();
|
|
||||||
Deno.exit(0);
|
Deno.exit(0);
|
||||||
}
|
}
|
||||||
Deno.addSignalListener("SIGINT", finish);
|
Deno.addSignalListener("SIGINT", finish);
|
||||||
console.log("listening to", socket_path());
|
|
||||||
|
|
||||||
for await (const { cmd, respond } of server) {
|
for await (const { cmd, respond } of server) {
|
||||||
console.log("received", { cmd });
|
log("received", cmd);
|
||||||
|
|
||||||
if (cmd.kind === "status") {
|
if (cmd.kind === "status") {
|
||||||
await respond(JSON.stringify(Array.from(enabled.keys())));
|
await respond(JSON.stringify(state.list()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.kind === "enable") {
|
if (cmd.kind === "enable") {
|
||||||
const config = new_container_config(cmd.name); // TODO
|
(async () => {
|
||||||
const runner = start_runner(config);
|
try {
|
||||||
enabled.set(cmd.name, runner);
|
await state.enable(cmd.name);
|
||||||
await respond("enabled");
|
await state.save();
|
||||||
|
await respond("enabled");
|
||||||
|
} catch (error) {
|
||||||
|
log("experienced failure", error);
|
||||||
|
await respond(`${error}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.kind === "disable") {
|
if (cmd.kind === "disable") {
|
||||||
await enabled.get(cmd.name)?.stop();
|
(async () => {
|
||||||
enabled.delete(cmd.name);
|
try {
|
||||||
await respond("disabled");
|
await state.disable(cmd.name);
|
||||||
|
await state.save();
|
||||||
|
await respond("disabled");
|
||||||
|
} catch (error) {
|
||||||
|
log("experienced failure", error);
|
||||||
|
await respond(`${error}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd.kind === "reload") {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
await state.disable(cmd.name);
|
||||||
|
await sleep(500);
|
||||||
|
await state.enable(cmd.name);
|
||||||
|
await respond("reloaded");
|
||||||
|
} catch (error) {
|
||||||
|
log("experienced failure", error);
|
||||||
|
await respond(`${error}`);
|
||||||
|
}
|
||||||
|
})();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.kind === "stop") {
|
if (cmd.kind === "stop") {
|
||||||
await respond(JSON.stringify("stopping, ++"));
|
await state.save();
|
||||||
|
await respond("adios");
|
||||||
await finish();
|
await finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
await respond("unknown");
|
await respond("unknown command");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function create_state() {
|
||||||
|
const self = {
|
||||||
|
enabled: new Map<string, Runner>(),
|
||||||
|
async enable(name: string) {
|
||||||
|
if (self.enabled.has(name)) throw new Error("Container already enabled");
|
||||||
|
log("loading container", name);
|
||||||
|
const config = await load_container_config(name); // TODO
|
||||||
|
if (config === null) throw new Error("can't read config");
|
||||||
|
log("starting container", name);
|
||||||
|
self.enabled.set(name, await start_runner(config));
|
||||||
|
log("container", name, "started");
|
||||||
|
},
|
||||||
|
async disable(name: string) {
|
||||||
|
const container = self.enabled.get(name);
|
||||||
|
if (container === undefined) throw new Error("Container not found");
|
||||||
|
await container.stop();
|
||||||
|
self.enabled.delete(name);
|
||||||
|
},
|
||||||
|
list() {
|
||||||
|
return Array.from(self.enabled.keys());
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
const content = JSON.stringify({ enabled: self.list() });
|
||||||
|
await Deno.writeTextFile(state_path(), content);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
log("trying to recover state from", state_path());
|
||||||
|
const loaded = JSON.parse(await Deno.readTextFile(state_path()));
|
||||||
|
for (const name of loaded.enabled ?? []) {
|
||||||
|
await self.enable(name);
|
||||||
|
}
|
||||||
|
log("successfully loaded from", state_path());
|
||||||
|
} catch (error) {
|
||||||
|
log("experienced failure", error);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
26
instance/local/bases/arch/module.ts
Normal file
26
instance/local/bases/arch/module.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { Base, BaseContext } from "../../../src/lib.ts";
|
||||||
|
|
||||||
|
export { build };
|
||||||
|
|
||||||
|
const build: Base = async (c) => {
|
||||||
|
// installs base
|
||||||
|
await c.sh(`pacstrap -K -c '${c.root_dir}' base`);
|
||||||
|
await c.sh(`rm -fr '${c.root_dir}/etc/securetty' '${c.root_dir}/usr/share/factory/etc/securetty'`);
|
||||||
|
|
||||||
|
await c.sh_in(`printf 'root:noussommesdescrabes' | chpasswd --crypt-method DES`);
|
||||||
|
await pacman(c, "nano", "git", "base-devel");
|
||||||
|
await c.sh_in(`systemctl enable systemd-networkd.service`);
|
||||||
|
|
||||||
|
// ssh
|
||||||
|
await pacman(c, "openssh");
|
||||||
|
await c.sh_in(`
|
||||||
|
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
|
||||||
|
systemctl enable sshd.service
|
||||||
|
`);
|
||||||
|
const port = c.available_port(2250);
|
||||||
|
c.redirect(port, 22);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function pacman(c: BaseContext, ...packages: string[]) {
|
||||||
|
await c.sh_in(`pacman -Syu --noconfirm '${packages.join("' '")}'`);
|
||||||
|
}
|
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
|
#!/bin/env -S deno run -A --unstable
|
||||||
|
|
||||||
await main();
|
if (import.meta.main) await main();
|
||||||
async function main() {
|
async function main() {
|
||||||
const [from_port, to_ip, to_port] = Deno.args;
|
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) });
|
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) {
|
for await (const connection of server) {
|
||||||
try {
|
try {
|
||||||
|
@ -16,3 +20,9 @@ async function main() {
|
||||||
} catch (_) { /* isok */ }
|
} 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 { 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 { 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 type CmdStatus = ReturnType<typeof new_cmd_status>;
|
||||||
export function 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 };
|
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 type CmdStop = ReturnType<typeof new_cmd_stop>;
|
||||||
export function new_cmd_stop() {
|
export function new_cmd_stop() {
|
||||||
return { kind: "stop" as const };
|
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 });
|
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* () {
|
const generator = async function* () {
|
||||||
for await (const request of server) {
|
for await (const request of server) {
|
||||||
const respond = async (message: string) => {
|
const respond = async (message: string) => {
|
||||||
|
@ -53,12 +67,52 @@ export async function daemon_send(sock_path: string, command: Cmd) {
|
||||||
return await toText(request.readable);
|
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) {
|
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 {
|
return {
|
||||||
name: config.name,
|
name,
|
||||||
|
config,
|
||||||
|
container_loop,
|
||||||
stop: async () => {
|
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 { container_paths, containers_path } from "./paths.ts";
|
||||||
import { exists } from "./utils.ts";
|
import { exists, log_from } from "./utils.ts";
|
||||||
|
|
||||||
|
const log = log_from("config");
|
||||||
|
|
||||||
export type ContainerConfig = ReturnType<typeof new_container_config>;
|
export type ContainerConfig = ReturnType<typeof new_container_config>;
|
||||||
export function new_container_config(name: string) {
|
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) {
|
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;
|
if (!exists(config_path)) return null;
|
||||||
const content = await Deno.readTextFile(config_path);
|
const content = await Deno.readTextFile(config_path);
|
||||||
const read = JSON.parse(content);
|
const read = JSON.parse(content);
|
||||||
const default_ = new_container_config(name);
|
const default_ = new_container_config(name);
|
||||||
if (read.version < default_.version) throw new Error("read conf version is outdated");
|
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
|
// 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) {
|
export function container_paths(name: string) {
|
||||||
const base = containers_path() + "/" + name;
|
const base = containers_path() + "/" + name;
|
||||||
const conf = base + "/config.json";
|
const configuration = base + "/config.json";
|
||||||
const root = base + "/root";
|
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";
|
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>() {
|
export function channel<T>() {
|
||||||
const inner = {
|
const inner = {
|
||||||
queue: [] as T[],
|
queue: [] as T[],
|
||||||
|
@ -41,3 +42,54 @@ export function lines(readable: ReadableStream<Uint8Array>) {
|
||||||
.pipeThrough(new TextDecoderStream())
|
.pipeThrough(new TextDecoderStream())
|
||||||
.pipeThrough(new TextLineStream());
|
.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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
14
sync.sh
Executable file
14
sync.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$(realpath "$0")")"
|
||||||
|
|
||||||
|
|
||||||
|
if [ $# -lt 1 ]
|
||||||
|
then exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
DEST="$1"
|
||||||
|
|
||||||
|
|
||||||
|
rsync -avzhP ./instance "$DEST"
|
Loading…
Add table
Add a link
Reference in a new issue