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);
|
2023-02-20 18:20:37 +00:00
|
|
|
packages: string[] = [];
|
2023-01-16 15:14:20 +00:00
|
|
|
|
2023-02-10 02:36:01 +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
|
|
|
}
|
2023-02-10 02:36:01 +00:00
|
|
|
async writeFile(
|
|
|
|
filePath: string,
|
|
|
|
content: string,
|
|
|
|
permissions?: { uid: number; gid: number }
|
|
|
|
): Promise<void> {
|
2023-02-17 00:26:34 +00:00
|
|
|
const p = path.join(this.dir, filePath);
|
2023-02-10 02:36:01 +00:00
|
|
|
await this.mkdirP(path.dirname(filePath));
|
2023-02-19 20:58:18 +00:00
|
|
|
await writeFile(p, content);
|
2023-02-17 00:26:34 +00:00
|
|
|
if (permissions) {
|
2023-02-19 20:58:18 +00:00
|
|
|
await chown(p, permissions.uid, permissions.gid);
|
|
|
|
await chmod(p, 0o600);
|
2023-02-17 00:26:34 +00:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2023-02-10 02:36:01 +00:00
|
|
|
readPasswd(): Promise<PasswdEntry[]> {
|
2023-02-19 20:58:18 +00:00
|
|
|
return readPasswd(this.path("/etc/passwd"));
|
2023-02-10 02:36:01 +00:00
|
|
|
}
|
2023-02-20 18:20:37 +00:00
|
|
|
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,
|
|
|
|
]);
|
2023-02-10 02:36:01 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-02-20 18:20:37 +00:00
|
|
|
async actuallyInstallPackages(): Promise<void> {
|
|
|
|
await this.installPackages(this.packages);
|
|
|
|
}
|
|
|
|
|
|
|
|
async installPackages(packages: string[]): Promise<void> {
|
2023-02-10 00:38:00 +00:00
|
|
|
logDebug(
|
2023-02-20 18:20:37 +00:00
|
|
|
"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
|
|
|
}
|
2023-02-20 18:20:37 +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
|
|
|
}
|
|
|
|
}
|