195 lines
4.6 KiB
JavaScript
195 lines
4.6 KiB
JavaScript
import { nanoid } from "nanoid";
|
|
import { execFile as execFileCallback } from "node:child_process";
|
|
import {
|
|
constants,
|
|
copyFile,
|
|
mkdir,
|
|
opendir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
symlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { init } from "@nulo/apkit";
|
|
import * as dotenv from "dotenv";
|
|
dotenv.config({ override: true });
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
// TODO: configurar runsc
|
|
|
|
const currRootPath = await setupRootfsDir();
|
|
const alpine = await init(currRootPath);
|
|
await alpine.install(["alpine-base", "fish", "docker", "linux-virt"]);
|
|
await alpine.rcUpdate("sysinit", "networking");
|
|
await alpine.rcUpdate("boot", "docker");
|
|
await alpine.rcUpdate("boot", "mdev");
|
|
await alpine.rcUpdate("default", "ntpd");
|
|
await alpine.rcUpdate("default", "local");
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @param {((file: string) => string) | ((file: string) => Promise<string>)} patchFunc
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function patchFile(path, patchFunc) {
|
|
let file = await readFile(path, "utf-8");
|
|
file = await patchFunc(file);
|
|
await writeFile(path, file);
|
|
}
|
|
|
|
await patchFile(
|
|
join(currRootPath, "/etc/modprobe.d/blacklist.conf"),
|
|
(blacklist) => blacklist.replace("tiny_power_button", "button")
|
|
);
|
|
await patchFile(
|
|
join(currRootPath, "/etc/rc.conf"),
|
|
(rc) => rc + '\nrc_cgroup_mode="unified"\n'
|
|
);
|
|
|
|
await writeFile(
|
|
"interfaces",
|
|
`auto lo
|
|
iface lo inet loopback
|
|
|
|
auto eth0
|
|
iface eth0
|
|
use dhcp`,
|
|
{ mode: 0o600 }
|
|
);
|
|
await execFile("sudo", [
|
|
"mv",
|
|
"interfaces",
|
|
join(currRootPath, "/etc/network/interfaces"),
|
|
]);
|
|
await execFile("sudo", [
|
|
"chown",
|
|
"0:0",
|
|
join(currRootPath, "/etc/network/interfaces"),
|
|
]);
|
|
|
|
const woodpeckerServer = getConfig("WOODPECKER_SERVER");
|
|
const woodpeckerAgentSecret = getConfig("WOODPECKER_AGENT_SECRET");
|
|
|
|
await writeFile(
|
|
"woodpecker.start",
|
|
`#!/bin/sh
|
|
|
|
modprobe tiny-power-button
|
|
|
|
while ! docker pull docker.io/woodpeckerci/woodpecker-agent:latest; do
|
|
sleep 1
|
|
done
|
|
exec docker run --detach --rm \
|
|
--name=woodpecker-agent \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-e WOODPECKER_SERVER='${woodpeckerServer}' \
|
|
-e WOODPECKER_AGENT_SECRET='${woodpeckerAgentSecret}' \
|
|
-e WOODPECKER_MAX_PROCS=8 \
|
|
docker.io/woodpeckerci/woodpecker-agent:latest \
|
|
agent`,
|
|
{ mode: 0o700 }
|
|
);
|
|
await execFile("sudo", [
|
|
"mv",
|
|
"woodpecker.start",
|
|
join(currRootPath, "/etc/local.d/woodpecker.start"),
|
|
]);
|
|
await execFile("sudo", [
|
|
"chown",
|
|
"0:0",
|
|
join(currRootPath, "/etc/local.d/woodpecker.start"),
|
|
]);
|
|
|
|
await alpine.rcUpdate("boot", "localmount");
|
|
await writeFstab(currRootPath);
|
|
|
|
// make VM disk image
|
|
const ext4Path = "raw.ext4";
|
|
await execFile("qemu-img", ["create", "-f", "raw", ext4Path, "50G"]);
|
|
await execFile("mkfs.ext4", [ext4Path]);
|
|
|
|
const mntdir = join(tmpdir(), "woodpecker-in-a-vm-" + nanoid());
|
|
await mkdir(mntdir);
|
|
await execFile("sudo", ["mount", ext4Path, mntdir]);
|
|
for (const path of await readdir(currRootPath))
|
|
await execFile("sudo", ["cp", "-r", join(currRootPath, path), mntdir]);
|
|
await execFile("sudo", ["umount", mntdir]);
|
|
await execFile("sudo", [
|
|
"qemu-img",
|
|
"convert",
|
|
"-O",
|
|
"qcow2",
|
|
ext4Path,
|
|
"vm.qcow2",
|
|
]);
|
|
|
|
await copyFile(
|
|
join(currRootPath, "boot/vmlinuz-virt"),
|
|
"./vmlinuz-virt",
|
|
constants.COPYFILE_FICLONE
|
|
);
|
|
await copyFile(
|
|
join(currRootPath, "boot/initramfs-virt"),
|
|
"./initramfs-virt",
|
|
constants.COPYFILE_FICLONE
|
|
);
|
|
await execFile("sudo", [
|
|
"chown",
|
|
"1000:1000",
|
|
"./vmlinuz-virt",
|
|
"./initramfs-virt",
|
|
"./vm.qcow2",
|
|
]);
|
|
|
|
async function setupRootfsDir() {
|
|
const artifactsPath = "./artifacts";
|
|
const currRootPath = join(artifactsPath, "rootfs" + nanoid());
|
|
await mkdir(currRootPath, { recursive: true });
|
|
|
|
{
|
|
const currentPath = join(artifactsPath, "current");
|
|
try {
|
|
await rm(currentPath);
|
|
} catch {}
|
|
await symlink(basename(currRootPath), currentPath);
|
|
}
|
|
return "./" + currRootPath;
|
|
}
|
|
|
|
/**
|
|
* @param {string} rootfs
|
|
*/
|
|
async function writeFstab(rootfs) {
|
|
const rootfsName = "/dev/vda";
|
|
const virtualDiskName = "/dev/vda";
|
|
await writeFile(
|
|
"fstab",
|
|
`${rootfsName} / squashfs rw,relatime,discard 0 1
|
|
`
|
|
);
|
|
await execFile("sudo", ["mv", "fstab", join(rootfs, "/etc/fstab")]);
|
|
}
|
|
|
|
/**
|
|
* Corre comando en chroot
|
|
* @param {string} rootfs
|
|
* @param {string[]} cmd
|
|
*/
|
|
async function chroot(rootfs, cmd) {
|
|
await execFile("sudo", ["chroot", rootfs, ...cmd]);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @returns {string} la variable conseguida
|
|
*/
|
|
function getConfig(name) {
|
|
if (name in process.env) return process.env[name] || "";
|
|
else throw new Error(`Falta la variable ${name}`);
|
|
}
|