diff --git a/alpine.ts b/alpine.ts index e483eb2..a054dd6 100644 --- a/alpine.ts +++ b/alpine.ts @@ -1,19 +1,19 @@ -import { mkdir, opendir } from "node:fs/promises"; +import { + chmod, + chown, + copyFile, + mkdir, + opendir, + symlink, + writeFile, +} from "node:fs/promises"; import path from "node:path"; import { cwd } from "node:process"; import { Fstab } from "./fstab.js"; -import { execFile } from "./helpers/better-api.js"; -import { PasswdEntry, sudoReadPasswd } from "./helpers/passwd.js"; -import { - sudoChmod, - sudoChown, - sudoCopy, - sudoMkdirP, - sudoSymlink, - sudoWriteExecutable, - sudoWriteFile, -} from "./helpers/sudo.js"; +import { execFile, exists } from "./helpers/better-api.js"; +import { PasswdEntry, readPasswd } from "./helpers/passwd.js"; import { logDebug } from "./helpers/logger.js"; +import assert from "node:assert"; export class Alpine { dir: string; @@ -23,7 +23,7 @@ export class Alpine { fstab: Fstab = new Fstab(this); async mkdirP(dir: string): Promise { - await sudoMkdirP(path.join(this.dir, dir)); + await mkdir(this.path(dir), { recursive: true }); } async writeFile( filePath: string, @@ -32,22 +32,22 @@ export class Alpine { ): Promise { const p = path.join(this.dir, filePath); await this.mkdirP(path.dirname(filePath)); - await sudoWriteFile(p, content); + await writeFile(p, content); if (permissions) { - await sudoChown(p, `${permissions.uid}:${permissions.gid}`); - await sudoChmod(p, "600"); + await chown(p, permissions.uid, permissions.gid); + await chmod(p, 0o600); } } async writeExecutable(filePath: string, content: string): Promise { await this.writeFile(filePath, content); - await sudoChmod(path.join(this.dir, filePath), "700"); + await chmod(this.path(filePath), 0o700); + } + async assertExists(path: string) { + assert(await exists(this.path(path))); } path(p: string): string { return path.join(this.dir, p); } - sudoWriteExecutable(filePath: string, content: string): Promise { - return sudoWriteExecutable(this.path(filePath), content); - } private getRelativeSymlink( target: string, filePath: string @@ -63,20 +63,14 @@ export class Alpine { } async symlink(_target: string, _filePath: string): Promise { const { target, filePath } = this.getRelativeSymlink(_target, _filePath); - await sudoSymlink(target, filePath); + await symlink(target, filePath); } readPasswd(): Promise { - return sudoReadPasswd(this.path("/etc/passwd")); + return readPasswd(this.path("/etc/passwd")); } async userAdd(user: string): Promise { - await execFile("sudo", [ - "useradd", - "--user-group", - "--root", - this.dir, - user, - ]); + await execFile("useradd", ["--user-group", "--root", this.dir, user]); const passwd = await this.readPasswd(); const entry = passwd.find((e) => e.name === user); if (!entry) { @@ -88,8 +82,7 @@ export class Alpine { async addPackages(packages: string[]): Promise { logDebug( "addPackages", - await execFile("sudo", [ - "apk", + await execFile("apk", [ "add", "--clean-protected", "--root", @@ -107,27 +100,27 @@ export class Alpine { packages?: string[]; }): Promise { const apkDir = path.join(dir, "/etc/apk"); - await sudoMkdirP(apkDir); + await mkdir(apkDir, { recursive: true }); // hack { const cacheDir = path.join(cwd(), "cache"); await mkdir("cache", { recursive: true }); - await sudoSymlink(cacheDir, path.join(apkDir, "cache")); + await symlink(cacheDir, path.join(apkDir, "cache")); } { const apkKeysDir = path.join(apkDir, "keys"); const keysSrcDir = "alpine/keys"; - await sudoMkdirP(apkKeysDir); + await mkdir(apkKeysDir, { recursive: true }); for await (const { name } of await opendir(keysSrcDir)) - await sudoCopy( + await copyFile( path.join(keysSrcDir, name), path.join(apkKeysDir, name) ); } - await sudoWriteFile( + await writeFile( path.join(apkDir, "repositories"), [ "https://dl-cdn.alpinelinux.org/alpine/v3.17/main", @@ -136,8 +129,7 @@ export class Alpine { ); logDebug( "makeWorld", - await execFile("sudo", [ - "apk", + await execFile("apk", [ "add", "--initdb", "--clean-protected", diff --git a/fstab.ts b/fstab.ts index 9eb2531..d883d8e 100644 --- a/fstab.ts +++ b/fstab.ts @@ -1,5 +1,4 @@ import { Alpine } from "./alpine.js"; -import { sudoChmod, sudoMkdirP, sudoWriteFile } from "./helpers/sudo.js"; export class Fstab { private alpine: Alpine; @@ -17,20 +16,19 @@ export class Fstab { .map(([key, val]) => `,${key}=${val}`) .join(""); await this.addMount(`tmpfs ${path} tmpfs defaults,noexec,nosuid${add} 0 0`); - await sudoMkdirP(this.alpine.path(path)); + await this.alpine.mkdirP(path); } // Writes fstab to disk. // Intended for internal use only, use addMount or addTmpfs instead async write() { - const path = this.alpine.path("/etc/fstab"); - await sudoWriteFile( - path, + await this.alpine.writeFile( + "/etc/fstab", this.mounts.join("\n") + // Busybox mount no entiende la última línea sino - "\n" + "\n", + { uid: 0, gid: 0 } ); - await sudoChmod(path, "600"); } } interface TmpfsOptions { diff --git a/helpers/better-api.ts b/helpers/better-api.ts index cf8e656..6661a93 100644 --- a/helpers/better-api.ts +++ b/helpers/better-api.ts @@ -3,7 +3,7 @@ import { execFile as execFileCallback, spawn as spawnCallback, } from "node:child_process"; -import { access } from "node:fs/promises"; +import { access, stat } from "node:fs/promises"; export const execFile = promisify(execFileCallback); export const spawn = promisify(spawnCallback); @@ -16,3 +16,12 @@ export async function canAccess(path: string): Promise { return false; } } + +export async function exists(path: string): Promise { + try { + await stat(path); + return true; + } catch { + return false; + } +} diff --git a/helpers/passwd.ts b/helpers/passwd.ts index c4e927f..f4ad1d0 100644 --- a/helpers/passwd.ts +++ b/helpers/passwd.ts @@ -1,4 +1,4 @@ -import { sudoReadFile } from "./sudo.js"; +import { readFile } from "node:fs/promises"; export interface PasswdEntry { name: string; @@ -31,7 +31,7 @@ export function parsePasswd(content: string): PasswdEntry[] { return entry; }); } -export async function sudoReadPasswd(path: string): Promise { - const content = await sudoReadFile(path); +export async function readPasswd(path: string): Promise { + const content = await readFile(path, "utf-8"); return parsePasswd(content); } diff --git a/index.ts b/index.ts index 3e165f8..d83ab1e 100644 --- a/index.ts +++ b/index.ts @@ -6,7 +6,6 @@ import { Alpine } from "./alpine.js"; import { generateForgejoSecretsFile } from "./services/forgejo/secrets.js"; import { generateGrafanaSecretsFile } from "./services/grafana/secrets.js"; import { execFile } from "./helpers/better-api.js"; -import { sudoChown, sudoChownToRunningUser } from "./helpers/sudo.js"; import { setupKernel } from "./kernel.js"; import { runQemu } from "./qemu.js"; import { Runit } from "./runit/index.js"; @@ -23,6 +22,13 @@ if (process.argv[2] === "generate-secrets") { exit(0); } +async function timed(fn: () => Promise): Promise { + console.time(fn.toString()); + const r = await fn(); + console.timeEnd(fn.toString()); + return r; +} + { console.time("Building"); @@ -32,11 +38,12 @@ if (process.argv[2] === "generate-secrets") { await mkdir(kernelDir, { recursive: true }); const rootfsDir = await mkdtemp(path.join(tmpdir(), "define-alpine-")); - await sudoChown(rootfsDir, "root:root"); console.debug(rootfsDir); - const alpine = await Alpine.makeWorld({ dir: rootfsDir }); + const alpine = await timed(() => Alpine.makeWorld({ dir: rootfsDir })); - await alpine.addPackages(["helix", "htop", "iproute2-ss", "socat"]); + await timed(() => + alpine.addPackages(["helix", "htop", "iproute2-ss", "socat"]) + ); await alpine.writeFile( "/root/.ash_history", ` @@ -44,18 +51,17 @@ socat tcp-listen:80,reuseaddr,fork tcp:localhost:3050 & `, { uid: 0, gid: 0 } ); - await installFluentBit(alpine); - const runit = await Runit.setup(alpine); - await setupDhcpcd(alpine, runit); - await setupNtpsec(alpine, runit); - await setupForgejo(alpine, runit); - await setupLoki(alpine, runit); - await setupGrafana(alpine, runit); - const kernel = await setupKernel(alpine, kernelDir); + await timed(() => installFluentBit(alpine)); + const runit = await timed(() => Runit.setup(alpine)); + await timed(() => setupDhcpcd(alpine, runit)); + await timed(() => setupNtpsec(alpine, runit)); + await timed(() => setupForgejo(alpine, runit)); + await timed(() => setupLoki(alpine, runit)); + await timed(() => setupGrafana(alpine, runit)); + const kernel = await timed(() => setupKernel(alpine, kernelDir)); const squashfs = path.join(artifactsDir, "image.squashfs"); - await execFile("sudo", [ - "mksquashfs", + await execFile("mksquashfs", [ alpine.dir, squashfs, "-root-mode", @@ -67,9 +73,8 @@ socat tcp-listen:80,reuseaddr,fork tcp:localhost:3050 & "-noappend", "-quiet", ]); - await sudoChownToRunningUser(squashfs); console.timeEnd("Building"); - runQemu(squashfs, kernel, { graphic: true }); + // runQemu(squashfs, kernel, { graphic: true }); } diff --git a/kernel.ts b/kernel.ts index 41f8fbe..edfd479 100644 --- a/kernel.ts +++ b/kernel.ts @@ -1,9 +1,6 @@ -import { constants } from "node:fs"; -import { copyFile } from "node:fs/promises"; +import { copyFile, rm } from "node:fs/promises"; import path from "node:path"; import { Alpine } from "./alpine.js"; -import { canAccess } from "./helpers/better-api.js"; -import { sudoChownToRunningUser, sudoCopy, sudoRm } from "./helpers/sudo.js"; export type Kind = "lts" | "virt"; export type Kernel = { @@ -77,11 +74,9 @@ default=lts vmlinuz: path.join(output, "vmlinuz"), }; - await sudoCopy(initramfs, kernel.initramfs); - await sudoCopy(vmlinuz, kernel.vmlinuz); - await sudoChownToRunningUser(kernel.initramfs); - await sudoChownToRunningUser(kernel.vmlinuz); - await sudoRm(initramfs); - await sudoRm(vmlinuz); + await copyFile(initramfs, kernel.initramfs); + await copyFile(vmlinuz, kernel.vmlinuz); + await rm(initramfs); + await rm(vmlinuz); return kernel; } diff --git a/package.json b/package.json index 81bbeeb..8538538 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "main": "index.js", "scripts": { "build": "esbuild --log-level=warning --target=node18 --platform=node --sourcemap --outdir=build-javascript --format=esm --bundle index.ts", - "run": "pnpm build && node --enable-source-maps build-javascript/index.js", + "build:qemu": "esbuild --log-level=warning --target=node18 --platform=node --sourcemap --outdir=build-javascript --format=esm --bundle qemu.ts", + "build-image": "pnpm build && doas node --enable-source-maps build-javascript/index.js", + "run": "pnpm build-image && pnpm build:qemu && node --enable-source-maps build-javascript/qemu.js", "//test": "pnpm build && node --enable-source-maps build-javascript/**/*.test.js", "tsc:check": "tsc --noEmit" }, diff --git a/qemu.ts b/qemu.ts index 857def3..de1f268 100644 --- a/qemu.ts +++ b/qemu.ts @@ -2,11 +2,10 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { execFile } from "./helpers/better-api.js"; -import { Kernel } from "./kernel.js"; export async function runQemu( squashfs: string, - kernel: Kernel, + kernel: { initramfs: string; vmlinuz: string }, { graphic, noShutdown }: { graphic?: boolean; noShutdown?: boolean } = { graphic: true, noShutdown: false, @@ -59,3 +58,12 @@ export async function runQemu( await rm(tmp, { recursive: true, force: true }); } } + +await runQemu( + "artifacts/image.squashfs", + { + initramfs: "artifacts/kernel/initramfs", + vmlinuz: "artifacts/kernel/vmlinuz", + }, + { graphic: true } +); diff --git a/runit/index.ts b/runit/index.ts index 8c5f1cc..f7fad96 100644 --- a/runit/index.ts +++ b/runit/index.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { readFile, rmdir } from "node:fs/promises"; import path from "node:path"; import { Alpine } from "../alpine.js"; @@ -60,12 +60,12 @@ export class Runit { ); // https://wiki.gentoo.org/wiki/Runit#Reboot_and_shutdown - await alpine.sudoWriteExecutable( + await alpine.writeExecutable( "/usr/local/sbin/rpoweroff", `#!/bin/sh runit-init 0` ); - await alpine.sudoWriteExecutable( + await alpine.writeExecutable( "/usr/local/sbin/rreboot", `#!/bin/sh runit-init 6` @@ -100,10 +100,10 @@ exec chpst -P getty 38400 ttyS0 linux` logScript?: string ): Promise { const runScriptPath = path.join("/etc/sv/", name, "/run"); - await this.alpine.sudoWriteExecutable(runScriptPath, script); + await this.alpine.writeExecutable(runScriptPath, script); if (logScript) { const logScriptPath = path.join("/etc/sv/", name, "/log/run"); - await this.alpine.sudoWriteExecutable(logScriptPath, logScript); + await this.alpine.writeExecutable(logScriptPath, logScript); await this.alpine.symlink( `/run/runit/supervise.${name}.log`, path.join("/etc/sv/", name, "/log/supervise") diff --git a/services/forgejo/index.ts b/services/forgejo/index.ts index 85f93d0..a4f036a 100644 --- a/services/forgejo/index.ts +++ b/services/forgejo/index.ts @@ -1,14 +1,13 @@ import { buildForgejo } from "./build.js"; import { Alpine } from "../../alpine.js"; import { Runit } from "../../runit/index.js"; -import { join } from "node:path"; import { loadForgejoSecretsFile } from "./secrets.js"; -import { sudoCopy } from "../../helpers/sudo.js"; import { FluentBitParser, runitLokiLogger } from "../../software/fluentbit.js"; +import { copyFile } from "node:fs/promises"; export async function setupForgejo(alpine: Alpine, runit: Runit) { const bin = await buildForgejo(); - await sudoCopy(bin, join(alpine.dir, "/usr/local/bin/forgejo")); + await copyFile(bin, alpine.path("/usr/local/bin/forgejo")); await alpine.addPackages(["tzdata", "git"]); const entry = await alpine.userAdd("_forgejo"); diff --git a/services/grafana/index.ts b/services/grafana/index.ts index 9da57ea..bd934b7 100644 --- a/services/grafana/index.ts +++ b/services/grafana/index.ts @@ -49,7 +49,7 @@ datasources: user ); - await alpine.sudoWriteExecutable( + await alpine.writeExecutable( "/usr/local/sbin/nulo-grafana-cli", `#!/bin/sh cd / diff --git a/services/loki/index.ts b/services/loki/index.ts index e827d69..638fc95 100644 --- a/services/loki/index.ts +++ b/services/loki/index.ts @@ -1,12 +1,11 @@ -import { join } from "node:path"; +import { copyFile } from "node:fs/promises"; import { Alpine } from "../../alpine.js"; -import { sudoCopy } from "../../helpers/sudo.js"; import { Runit } from "../../runit/index.js"; import { buildLoki } from "./build.js"; export async function setupLoki(alpine: Alpine, runit: Runit): Promise { const bin = await buildLoki(); - await sudoCopy(bin, join(alpine.dir, "/usr/local/bin/loki")); + await copyFile(bin, alpine.path("/usr/local/bin/loki")); const user = await alpine.userAdd("loki"); diff --git a/services/ntpsec.ts b/services/ntpsec.ts index fd3313b..78c053b 100644 --- a/services/ntpsec.ts +++ b/services/ntpsec.ts @@ -1,5 +1,4 @@ import { Alpine } from "../alpine.js"; -import { sudoWriteFile } from "../helpers/sudo.js"; import { Runit } from "../runit/index.js"; import { FluentBitParser, runitLokiLogger } from "../software/fluentbit.js"; @@ -10,8 +9,8 @@ export async function setupNtpsec(alpine: Alpine, runit: Runit) { // file:///usr/share/doc/ntpsec/quick.html // file:///usr/share/doc/ntpsec/NTS-QuickStart.html // XXX: revisar driftfile, creo que tiene que poder escribir pero está readonly - await sudoWriteFile( - alpine.path("/etc/ntp.conf"), + await alpine.writeFile( + "/etc/ntp.conf", ` driftfile /var/lib/ntp/ntp.drift diff --git a/software/fluentbit.ts b/software/fluentbit.ts index fb48cb9..880a81c 100644 --- a/software/fluentbit.ts +++ b/software/fluentbit.ts @@ -1,7 +1,7 @@ +import { constants, copyFile } from "node:fs/promises"; import { join } from "node:path"; import { Alpine } from "../alpine.js"; import { buildRepro, reproRun } from "../helpers/repro-run.js"; -import { sudoCopy } from "../helpers/sudo.js"; const parsersPath = "/etc/fluent-bit/parsers.conf"; @@ -9,7 +9,11 @@ export async function installFluentBit(alpine: Alpine): Promise { const bin = await buildFluentBit(); await saveParsers(alpine); await alpine.addPackages(["musl-fts", "yaml"]); - await sudoCopy(bin, join(alpine.dir, "/usr/local/bin/fluent-bit")); + await copyFile( + bin, + alpine.path("/usr/local/bin/fluent-bit"), + constants.COPYFILE_FICLONE + ); } // ## Script generators