define-alpine-the-sequel/alpine.ts

180 lines
4.6 KiB
TypeScript
Raw Permalink Normal View History

2023-02-19 20:58:18 +00:00
import {
chmod,
chown,
copyFile,
mkdir,
opendir,
symlink,
writeFile,
} from "node:fs/promises";
2023-01-16 15:14:20 +00:00
import path from "node:path";
import { cwd } from "node:process";
2023-02-08 14:46:33 +00:00
import { Fstab } from "./fstab.js";
2023-02-19 20:58:18 +00:00
import { execFile, exists } from "./helpers/better-api.js";
import { PasswdEntry, readPasswd } from "./helpers/passwd.js";
2023-02-10 00:38:00 +00:00
import { logDebug } from "./helpers/logger.js";
2023-02-19 20:58:18 +00:00
import assert from "node:assert";
2023-02-22 23:08:03 +00:00
import { Persist } from "./persist.js";
2023-02-23 01:15:24 +00:00
import { writePasswd } from "./passwd.js";
2023-01-16 15:14:20 +00:00
export class Alpine {
dir: string;
private constructor({ dir }: { dir: string }) {
this.dir = dir;
}
2023-02-08 14:46:33 +00:00
fstab: Fstab = new Fstab(this);
2023-02-22 23:08:03 +00:00
persist: Persist = new Persist(this);
packages: string[] = [];
2023-01-16 15:14:20 +00:00
async mkdirP(dir: string): Promise<void> {
2023-02-19 20:58:18 +00:00
await mkdir(this.path(dir), { recursive: true });
2023-01-16 15:14:20 +00:00
}
async writeFile(
filePath: string,
content: string,
permissions?: { uid: number; gid: number }
): Promise<void> {
const p = path.join(this.dir, filePath);
await this.mkdirP(path.dirname(filePath));
2023-02-19 20:58:18 +00:00
await writeFile(p, content);
if (permissions) {
2023-02-19 20:58:18 +00:00
await chown(p, permissions.uid, permissions.gid);
await chmod(p, 0o600);
}
2023-01-16 15:14:20 +00:00
}
async writeExecutable(filePath: string, content: string): Promise<void> {
await this.writeFile(filePath, content);
2023-02-19 20:58:18 +00:00
await chmod(this.path(filePath), 0o700);
}
async assertExists(path: string) {
assert(await exists(this.path(path)));
2023-01-16 15:14:20 +00:00
}
2023-02-08 14:46:33 +00:00
path(p: string): string {
return path.join(this.dir, p);
}
2023-01-16 15:14:20 +00:00
private getRelativeSymlink(
target: string,
filePath: string
): { target: string; filePath: string } {
const realFilePath = path.join(this.dir, filePath);
return {
target: path.relative(
path.dirname(realFilePath),
path.join(this.dir, target)
),
filePath: realFilePath,
};
}
async symlink(_target: string, _filePath: string): Promise<void> {
const { target, filePath } = this.getRelativeSymlink(_target, _filePath);
2023-02-19 20:58:18 +00:00
await symlink(target, filePath);
2023-01-16 15:14:20 +00:00
}
readPasswd(): Promise<PasswdEntry[]> {
2023-02-19 20:58:18 +00:00
return readPasswd(this.path("/etc/passwd"));
}
async userAdd(
user: string,
{
system,
homeDir,
createHome,
}: { system: boolean; homeDir?: string; createHome: boolean } = {
system: false,
homeDir: "",
createHome: true,
}
): Promise<PasswdEntry> {
await execFile("useradd", [
"--user-group",
...(system ? ["--system"] : []),
...(homeDir ? ["--home-dir", homeDir] : []),
...(createHome ? ["--create-home"] : []),
"--root",
this.dir,
user,
]);
const passwd = await this.readPasswd();
2023-02-08 14:46:33 +00:00
const entry = passwd.find((e) => e.name === user);
if (!entry) {
throw new Error("fatal(userAdd): no encontré el usuario " + user);
}
return entry;
}
async actuallyInstallPackages(): Promise<void> {
await this.installPackages(this.packages);
}
async installPackages(packages: string[]): Promise<void> {
2023-02-10 00:38:00 +00:00
logDebug(
"installPackages",
2023-02-19 20:58:18 +00:00
await execFile("apk", [
2023-02-10 00:38:00 +00:00
"add",
"--clean-protected",
"--root",
this.dir,
...packages,
])
);
2023-01-16 15:14:20 +00:00
}
async addPackages(packages: string[]): Promise<void> {
this.packages = this.packages.concat(packages);
}
2023-01-16 15:14:20 +00:00
static async makeWorld({
dir,
packages,
}: {
dir: string;
packages?: string[];
}): Promise<Alpine> {
const apkDir = path.join(dir, "/etc/apk");
2023-02-19 20:58:18 +00:00
await mkdir(apkDir, { recursive: true });
2023-01-16 15:14:20 +00:00
// hack
{
const cacheDir = path.join(cwd(), "cache");
await mkdir("cache", { recursive: true });
2023-02-19 20:58:18 +00:00
await symlink(cacheDir, path.join(apkDir, "cache"));
2023-01-16 15:14:20 +00:00
}
{
const apkKeysDir = path.join(apkDir, "keys");
const keysSrcDir = "alpine/keys";
2023-02-19 20:58:18 +00:00
await mkdir(apkKeysDir, { recursive: true });
2023-01-16 15:14:20 +00:00
for await (const { name } of await opendir(keysSrcDir))
2023-02-19 20:58:18 +00:00
await copyFile(
2023-01-16 15:14:20 +00:00
path.join(keysSrcDir, name),
path.join(apkKeysDir, name)
);
}
2023-02-19 20:58:18 +00:00
await writeFile(
2023-01-16 15:14:20 +00:00
path.join(apkDir, "repositories"),
[
"https://dl-cdn.alpinelinux.org/alpine/v3.17/main",
"https://dl-cdn.alpinelinux.org/alpine/v3.17/community",
].join("\n")
);
2023-02-10 00:43:54 +00:00
logDebug(
"makeWorld",
2023-02-19 20:58:18 +00:00
await execFile("apk", [
2023-02-10 00:43:54 +00:00
"add",
"--initdb",
"--clean-protected",
"--root",
dir,
...["alpine-baselayout", "busybox", "libc-utils", "alpine-keys"],
...(packages || []),
])
);
2023-01-16 15:14:20 +00:00
2023-02-08 14:46:33 +00:00
const alpine = new Alpine({ dir });
await alpine.fstab.write();
2023-02-22 23:08:03 +00:00
await alpine.persist.write();
2023-02-23 01:15:24 +00:00
await writePasswd(alpine);
2023-02-08 14:46:33 +00:00
return alpine;
2023-01-16 15:14:20 +00:00
}
}