hopefully computers don't exist in the real world

This commit is contained in:
Cat /dev/Nulo 2023-08-09 18:42:57 -03:00
parent 48008e3110
commit 46d08afd90
6 changed files with 75 additions and 65 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
@nulo:registry=https://gitea.nulo.in/api/packages/nulo/npm/

112
index.js
View file

@ -1,4 +1,3 @@
// @ts-check
import { nanoid } from "nanoid";
import { execFile as execFileCallback } from "node:child_process";
import {
@ -6,6 +5,7 @@ import {
copyFile,
mkdir,
opendir,
readFile,
readdir,
rm,
symlink,
@ -14,31 +14,42 @@ import {
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();
dotenv.config({ override: true });
const execFile = promisify(execFileCallback);
// TODO: configurar runsc
// TODO: cache de apk (robar de define-alpine-the-sequel)
const currRootPath = await setupRootfsDir();
await setupBasicApkEnvironment(currRootPath);
const packages = ["alpine-base", "fish", "docker", "linux-virt"];
await execFile("doas", [
"apk",
"--initdb",
"--clean-protected",
"--root",
currRootPath,
"add",
...packages,
]);
await chroot(currRootPath, ["rc-update", "add", "networking", "sysinit"]);
await chroot(currRootPath, ["rc-update", "add", "docker", "boot"]);
await chroot(currRootPath, ["rc-update", "add", "acpid", "default"]);
await chroot(currRootPath, ["rc-update", "add", "ntpd", "default"]);
await chroot(currRootPath, ["rc-update", "add", "local", "default"]);
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",
@ -50,46 +61,51 @@ iface eth0
use dhcp`,
{ mode: 0o600 }
);
await execFile("doas", [
await execFile("sudo", [
"mv",
"interfaces",
join(currRootPath, "/etc/network/interfaces"),
]);
await execFile("doas", [
await execFile("sudo", [
"chown",
"0:0",
join(currRootPath, "/etc/network/interfaces"),
]);
const woodpeckerSecret = getConfig("WOODPECKER_SECRET");
const woodpeckerServer = getConfig("WOODPECKER_SERVER");
const woodpeckerAgentSecret = getConfig("WOODPECKER_AGENT_SECRET");
await writeFile(
"woodpecker.start",
`#!/bin/sh
docker pull docker.io/woodpeckerci/woodpecker-agent:latest || exit $?
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_SECRET='${woodpeckerSecret}' \
-e WOODPECKER_AGENT_SECRET='${woodpeckerAgentSecret}' \
-e WOODPECKER_MAX_PROCS=8 \
docker.io/woodpeckerci/woodpecker-agent:latest \
agent`,
{ mode: 0o700 }
);
await execFile("doas", [
await execFile("sudo", [
"mv",
"woodpecker.start",
join(currRootPath, "/etc/local.d/woodpecker.start"),
]);
await execFile("doas", [
await execFile("sudo", [
"chown",
"0:0",
join(currRootPath, "/etc/local.d/woodpecker.start"),
]);
await chroot(currRootPath, ["rc-update", "add", "localmount", "boot"]);
await alpine.rcUpdate("boot", "localmount");
await writeFstab(currRootPath);
// make VM disk image
@ -99,11 +115,11 @@ await execFile("mkfs.ext4", [ext4Path]);
const mntdir = join(tmpdir(), "woodpecker-in-a-vm-" + nanoid());
await mkdir(mntdir);
await execFile("doas", ["mount", ext4Path, mntdir]);
await execFile("sudo", ["mount", ext4Path, mntdir]);
for (const path of await readdir(currRootPath))
await execFile("doas", ["cp", "-r", join(currRootPath, path), mntdir]);
await execFile("doas", ["umount", mntdir]);
await execFile("doas", [
await execFile("sudo", ["cp", "-r", join(currRootPath, path), mntdir]);
await execFile("sudo", ["umount", mntdir]);
await execFile("sudo", [
"qemu-img",
"convert",
"-O",
@ -122,7 +138,7 @@ await copyFile(
"./initramfs-virt",
constants.COPYFILE_FICLONE
);
await execFile("doas", [
await execFile("sudo", [
"chown",
"1000:1000",
"./vmlinuz-virt",
@ -142,43 +158,21 @@ async function setupRootfsDir() {
} catch {}
await symlink(basename(currRootPath), currentPath);
}
return currRootPath;
}
/**
* @param {string} rootfs
*/
async function setupBasicApkEnvironment(rootfs) {
{
const apkKeysDir = join(rootfs, "/etc/apk/keys");
const keysSrcDir = "alpine/keys";
await mkdir(apkKeysDir, { recursive: true });
for await (const { name } of await opendir(keysSrcDir))
await copyFile(join(keysSrcDir, name), join(apkKeysDir, name));
}
await writeFile(
join(rootfs, "/etc/apk/repositories"),
[
// "https://dl-cdn.alpinelinux.org/alpine/v3.17/main",
// "https://dl-cdn.alpinelinux.org/alpine/v3.17/community",
"http://alpinelinux.c3sl.ufpr.br/v3.17/main",
"http://alpinelinux.c3sl.ufpr.br/v3.17/community",
].join("\n")
);
return "./" + currRootPath;
}
/**
* @param {string} rootfs
*/
async function writeFstab(rootfs) {
const rootfsName = "/dev/vda";
const virtualDiskName = "/dev/vda";
await writeFile(
"fstab",
`${virtualDiskName} / ext4 rw,relatime,discard 0 1
`${rootfsName} / squashfs rw,relatime,discard 0 1
`
);
await execFile("doas", ["mv", "fstab", join(rootfs, "/etc/fstab")]);
await execFile("sudo", ["mv", "fstab", join(rootfs, "/etc/fstab")]);
}
/**
@ -187,7 +181,7 @@ async function writeFstab(rootfs) {
* @param {string[]} cmd
*/
async function chroot(rootfs, cmd) {
await execFile("doas", ["chroot", rootfs, ...cmd]);
await execFile("sudo", ["chroot", rootfs, ...cmd]);
}
/**

View file

@ -8,6 +8,7 @@
},
"dependencies": {
"dotenv": "^16.0.3",
"nanoid": "^4.0.1"
"nanoid": "^4.0.1",
"@nulo/apkit": "https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz"
}
}

View file

@ -1,6 +1,13 @@
lockfileVersion: '6.0'
lockfileVersion: '6.1'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@nulo/apkit':
specifier: https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz
version: '@gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz'
dotenv:
specifier: ^16.0.3
version: 16.0.3
@ -29,3 +36,9 @@ packages:
engines: {node: ^14 || ^16 || >=18}
hasBin: true
dev: false
'@gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz':
resolution: {tarball: https://gitea.nulo.in/Nulo/apkit/archive/hackyshit.tar.gz}
name: '@nulo/apkit'
version: 0.0.1
dev: false

View file

@ -1,11 +1,10 @@
# woodpecker-in-a-vm
1. script que genera una vm con woodpecker agent adentro
1. lo configuré para que borre en el disco real si se borra algo dentro de la vm
1. como woodpecker-agent no tiene state, puedo regenerar la vm con el script las veces que quiera
1. lo configuré para que borre en el disco real si se borra algo dentro de la vm
1. como woodpecker-agent no tiene state, puedo regenerar la vm con el script las veces que quiera
1. no tengo que exponer mi docker real al agent porque está en una vm con el suyo
## compilar imágen
```sh
@ -24,6 +23,6 @@ virt-install --name woodpecker-in-a-vm \
--memory 4096 --vcpus 4 \
--import \
--disk path=/var/lib/libvirt/images/vm.qcow2,format=qcow2 \
--boot kernel=/var/lib/libvirt/images/vmlinuz-virt,initrd=/var/lib/libvirt/images/initramfs-virt,kernel_args="console=/dev/ttyS0 quiet root=/dev/vda rw modules=ext4" \
--boot kernel=/var/lib/libvirt/images/vmlinuz-virt,initrd=/var/lib/libvirt/images/initramfs-virt,kernel_args="console=/dev/ttyS0 quiet root=/dev/vda rw modules=ext4 tiny_power_button.power_signal=12" \
--noautoconsole
```

View file

@ -4,6 +4,8 @@
"module": "es2022",
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true,
"checkJs": true,
"strict": true
}
}