Compare commits

..

3 commits

Author SHA1 Message Date
eb0a9d937d make it work 2023-04-05 17:25:45 -03:00
c6cd179efd vm.xml 2023-04-04 16:54:37 -03:00
5c5be0420f ignorar imagenes de disco 2023-04-04 16:54:36 -03:00
6 changed files with 245 additions and 347 deletions

7
.gitignore vendored
View file

@ -1,4 +1,7 @@
artifacts artifacts
node_modules node_modules
index.js *.ext4
index.js.map *.qcow2
.env
initramfs-virt
vmlinuz-virt

199
index.js Normal file
View file

@ -0,0 +1,199 @@
// @ts-check
import { nanoid } from "nanoid";
import { execFile as execFileCallback } from "node:child_process";
import {
constants,
copyFile,
mkdir,
opendir,
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 * as dotenv from "dotenv";
dotenv.config();
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", "local", "default"]);
await writeFile(
"interfaces",
`auto lo
iface lo inet loopback
auto eth0
iface eth0
use dhcp`,
{ mode: 0o600 }
);
await execFile("doas", [
"mv",
"interfaces",
join(currRootPath, "/etc/network/interfaces"),
]);
await execFile("doas", [
"chown",
"0:0",
join(currRootPath, "/etc/network/interfaces"),
]);
const woodpeckerSecret = getConfig("WOODPECKER_SECRET");
const woodpeckerServer = getConfig("WOODPECKER_SERVER");
await writeFile(
"woodpecker.start",
`#!/bin/sh
docker pull docker.io/woodpeckerci/woodpecker-agent:latest || exit $?
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_MAX_PROCS=8 \
docker.io/woodpeckerci/woodpecker-agent:latest \
agent`,
{ mode: 0o700 }
);
await execFile("doas", [
"mv",
"woodpecker.start",
join(currRootPath, "/etc/local.d/woodpecker.start"),
]);
await execFile("doas", [
"chown",
"0:0",
join(currRootPath, "/etc/local.d/woodpecker.start"),
]);
await chroot(currRootPath, ["rc-update", "add", "localmount", "boot"]);
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("doas", ["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", [
"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("doas", [
"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 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")
);
}
/**
* @param {string} rootfs
*/
async function writeFstab(rootfs) {
const virtualDiskName = "/dev/vda";
await writeFile(
"fstab",
`${virtualDiskName} / ext4 rw,relatime,discard 0 1
`
);
await execFile("doas", ["mv", "fstab", join(rootfs, "/etc/fstab")]);
}
/**
* Corre comando en chroot
* @param {string} rootfs
* @param {string[]} cmd
*/
async function chroot(rootfs, cmd) {
await execFile("doas", ["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}`);
}

View file

@ -1,96 +0,0 @@
import { nanoid } from "nanoid";
import { execFile as execFileCallback } from "node:child_process";
import {
copyFile,
mkdir,
opendir,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { promisify } from "node:util";
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", "docker", "default"]);
await chroot(currRootPath, ["rc-update", "add", "localmount", "boot"]);
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("doas", ["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("qemu-img", ["convert", "-O", "qcow2", ext4Path, "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;
}
async function setupBasicApkEnvironment(rootfs: string) {
{
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",
].join("\n")
);
}
async function writeFstab(rootfs: string) {
const virtualDiskName = "/dev/vda";
await writeFile(
"fstab",
`${virtualDiskName} / ext4 rw,relatime,discard 0 1
`
);
await execFile("doas", ["mv", "fstab", join(rootfs, "/etc/fstab")]);
}
async function chroot(rootfs: string, cmd: string[]) {
await execFile("doas", ["chroot", rootfs, ...cmd]);
}

View file

@ -1,14 +1,13 @@
{ {
"type": "module", "type": "module",
"scripts": { "scripts": {
"run": "esbuild --log-level=warning --target=node18 --platform=node --sourcemap --outdir=. --format=esm --bundle index.ts && node --enable-source-maps index.js" "run": "node index.js"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.15.5", "@types/node": "^18.15.5"
"esbuild": "^0.17.12",
"typescript": "^5.0.2"
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.0.3",
"nanoid": "^4.0.1" "nanoid": "^4.0.1"
} }
} }

View file

@ -1,261 +1,31 @@
lockfileVersion: 5.4 lockfileVersion: '6.0'
specifiers:
'@types/node': ^18.15.5
esbuild: ^0.17.12
nanoid: ^4.0.1
typescript: ^5.0.2
dependencies: dependencies:
nanoid: 4.0.1 dotenv:
specifier: ^16.0.3
version: 16.0.3
nanoid:
specifier: ^4.0.1
version: 4.0.1
devDependencies: devDependencies:
'@types/node': 18.15.5 '@types/node':
esbuild: 0.17.12 specifier: ^18.15.5
typescript: 5.0.2 version: 18.15.5
packages: packages:
/@esbuild/android-arm/0.17.12: /@types/node@18.15.5:
resolution: {integrity: sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm64/0.17.12:
resolution: {integrity: sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-x64/0.17.12:
resolution: {integrity: sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-arm64/0.17.12:
resolution: {integrity: sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-x64/0.17.12:
resolution: {integrity: sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-arm64/0.17.12:
resolution: {integrity: sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-x64/0.17.12:
resolution: {integrity: sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm/0.17.12:
resolution: {integrity: sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm64/0.17.12:
resolution: {integrity: sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ia32/0.17.12:
resolution: {integrity: sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64/0.17.12:
resolution: {integrity: sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-mips64el/0.17.12:
resolution: {integrity: sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ppc64/0.17.12:
resolution: {integrity: sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-riscv64/0.17.12:
resolution: {integrity: sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-s390x/0.17.12:
resolution: {integrity: sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-x64/0.17.12:
resolution: {integrity: sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-x64/0.17.12:
resolution: {integrity: sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-x64/0.17.12:
resolution: {integrity: sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/sunos-x64/0.17.12:
resolution: {integrity: sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-arm64/0.17.12:
resolution: {integrity: sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-ia32/0.17.12:
resolution: {integrity: sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-x64/0.17.12:
resolution: {integrity: sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@types/node/18.15.5:
resolution: {integrity: sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==} resolution: {integrity: sha512-Ark2WDjjZO7GmvsyFFf81MXuGTA/d6oP38anyxWOL6EREyBKAxKoFHwBhaZxCfLRLpO8JgVXwqOwSwa7jRcjew==}
dev: true dev: true
/esbuild/0.17.12: /dotenv@16.0.3:
resolution: {integrity: sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==} resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true dev: false
requiresBuild: true
optionalDependencies:
'@esbuild/android-arm': 0.17.12
'@esbuild/android-arm64': 0.17.12
'@esbuild/android-x64': 0.17.12
'@esbuild/darwin-arm64': 0.17.12
'@esbuild/darwin-x64': 0.17.12
'@esbuild/freebsd-arm64': 0.17.12
'@esbuild/freebsd-x64': 0.17.12
'@esbuild/linux-arm': 0.17.12
'@esbuild/linux-arm64': 0.17.12
'@esbuild/linux-ia32': 0.17.12
'@esbuild/linux-loong64': 0.17.12
'@esbuild/linux-mips64el': 0.17.12
'@esbuild/linux-ppc64': 0.17.12
'@esbuild/linux-riscv64': 0.17.12
'@esbuild/linux-s390x': 0.17.12
'@esbuild/linux-x64': 0.17.12
'@esbuild/netbsd-x64': 0.17.12
'@esbuild/openbsd-x64': 0.17.12
'@esbuild/sunos-x64': 0.17.12
'@esbuild/win32-arm64': 0.17.12
'@esbuild/win32-ia32': 0.17.12
'@esbuild/win32-x64': 0.17.12
dev: true
/nanoid/4.0.1: /nanoid@4.0.1:
resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==} resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==}
engines: {node: ^14 || ^16 || >=18} engines: {node: ^14 || ^16 || >=18}
hasBin: true hasBin: true
dev: false dev: false
/typescript/5.0.2:
resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==}
engines: {node: '>=12.20'}
hasBin: true
dev: true

View file

@ -4,3 +4,26 @@
1. lo configuré para que borre en el disco real si se borra algo dentro de la vm 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. 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 1. no tengo que exponer mi docker real al agent porque está en una vm con el suyo
## compilar imágen
```sh
node index.js
```
copiar `initramfs-virt vmlinuz-virt vm.qcow2` al servidor a `/var/lib/libvirt/images`
## definir vm
```sh
virsh shutdown woodpecker-in-a-vm
virsh undefine woodpecker-in-a-vm
virt-install --name woodpecker-in-a-vm \
--osinfo alpinelinux3.16 \
--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" \
--noautoconsole
```