correr el builder como root

This commit is contained in:
Cat /dev/Nulo 2023-02-19 17:58:18 -03:00
parent baae0a2803
commit 26e9feff98
14 changed files with 105 additions and 95 deletions

View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
const { target, filePath } = this.getRelativeSymlink(_target, _filePath);
await sudoSymlink(target, filePath);
await symlink(target, filePath);
}
readPasswd(): Promise<PasswdEntry[]> {
return sudoReadPasswd(this.path("/etc/passwd"));
return readPasswd(this.path("/etc/passwd"));
}
async userAdd(user: string): Promise<PasswdEntry> {
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<void> {
logDebug(
"addPackages",
await execFile("sudo", [
"apk",
await execFile("apk", [
"add",
"--clean-protected",
"--root",
@ -107,27 +100,27 @@ export class Alpine {
packages?: string[];
}): Promise<Alpine> {
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",

View file

@ -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 {

View file

@ -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<boolean> {
return false;
}
}
export async function exists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}

View file

@ -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<PasswdEntry[]> {
const content = await sudoReadFile(path);
export async function readPasswd(path: string): Promise<PasswdEntry[]> {
const content = await readFile(path, "utf-8");
return parsePasswd(content);
}

View file

@ -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<T>(fn: () => Promise<T>): Promise<T> {
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 });
}

View file

@ -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;
}

View file

@ -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"
},

12
qemu.ts
View file

@ -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 }
);

View file

@ -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<void> {
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")

View file

@ -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");

View file

@ -49,7 +49,7 @@ datasources:
user
);
await alpine.sudoWriteExecutable(
await alpine.writeExecutable(
"/usr/local/sbin/nulo-grafana-cli",
`#!/bin/sh
cd /

View file

@ -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<void> {
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");

View file

@ -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

View file

@ -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<void> {
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