133 lines
5.7 KiB
Text
133 lines
5.7 KiB
Text
#!/usr/bin/env nu
|
|
# Hcloud adapter for lian-build runner dispatch.
|
|
# Protocol: nu spawn-runner.nu <spawn|destroy|list> — JSON on stdin, JSON on stdout.
|
|
# Contract: lian-build/schemas/adapter.ncl
|
|
|
|
use std log
|
|
|
|
const GOLDEN_IMAGE_PREFIX = "buildkit-runner-golden"
|
|
const DEFAULT_SSH_PORT = 22
|
|
|
|
# ── Subcommands ───────────────────────────────────────────────────────────────
|
|
|
|
# Spawn an ephemeral hcloud VM. Reads SpawnInput JSON from stdin.
|
|
# Returns SpawnOutput JSON: {handle, ssh_host, ssh_port, expires_at}
|
|
def "main spawn" [] {
|
|
let input = (^cat | from json)
|
|
|
|
let image_id = _resolve_image $input.image_ref
|
|
let server_name = $"buildkit-runner-(random uuid | str substring 0..7)"
|
|
let expires_at = (date now) + ($input.time_budget_min * 60sec) | format date "%Y-%m-%dT%H:%M:%SZ"
|
|
let server_type = _server_type $input.cpu $input.memory_gb
|
|
|
|
log info $"Spawning ($server_name) type=($server_type) location=($input.location)…"
|
|
|
|
let base_args = [
|
|
"server", "create",
|
|
"--name", $server_name,
|
|
"--image", ($image_id | into string),
|
|
"--type", $server_type,
|
|
"--location", $input.location,
|
|
"--label", "app=buildkit-runner",
|
|
"--label", $"expires_at=($expires_at)",
|
|
]
|
|
let with_key = if ($input.ssh_key_ref | is-not-empty) { $base_args | append ["--ssh-key", $input.ssh_key_ref] } else { $base_args }
|
|
let with_net = if (($input.network_ref? | default "") | is-not-empty) { $with_key | append ["--network", ($input.network_ref?)] } else { $with_key }
|
|
let create_args = if (($input.firewall_ref? | default "") | is-not-empty) { $with_net | append ["--firewall", ($input.firewall_ref?)] } else { $with_net }
|
|
|
|
let create_out = do { ^hcloud ...$create_args } | complete
|
|
if $create_out.exit_code != 0 {
|
|
error make { msg: $"hcloud server create failed:\n($create_out.stderr)" }
|
|
}
|
|
|
|
let describe_out = do { ^hcloud server describe $server_name --output json } | complete
|
|
if $describe_out.exit_code != 0 {
|
|
error make { msg: $"hcloud server describe ($server_name) failed:\n($describe_out.stderr)" }
|
|
}
|
|
let server = $describe_out.stdout | from json
|
|
let server_ip = $server.public_net.ipv4.ip
|
|
|
|
log info $"Server id=($server.id) ip=($server_ip) — waiting for SSH…"
|
|
_wait_for_ssh $server_ip 180
|
|
|
|
let dns = $input.internal_dns? | default ""
|
|
if ($dns | is-not-empty) {
|
|
_inject_resolv_conf $server_ip $dns
|
|
}
|
|
|
|
{ handle: $server_name, ssh_host: $server_ip, ssh_port: $DEFAULT_SSH_PORT, expires_at: $expires_at } | to json
|
|
}
|
|
|
|
# Destroy a runner by handle. Reads DestroyInput JSON {handle} from stdin.
|
|
def "main destroy" [] {
|
|
let input = (^cat | from json)
|
|
let handle = $input.handle
|
|
log info $"Destroying runner ($handle)…"
|
|
let out = do { ^hcloud server delete $handle } | complete
|
|
if $out.exit_code != 0 {
|
|
error make { msg: $"hcloud server delete ($handle) failed:\n($out.stderr)" }
|
|
}
|
|
}
|
|
|
|
# List all buildkit-runner instances managed by this adapter.
|
|
def "main list" [] {
|
|
let out = do { ^hcloud server list --selector app=buildkit-runner --output json } | complete
|
|
if $out.exit_code != 0 {
|
|
error make { msg: $"hcloud server list failed:\n($out.stderr)" }
|
|
}
|
|
$out.stdout | from json | each {|s|
|
|
{
|
|
handle: $s.name,
|
|
ssh_host: $s.public_net.ipv4.ip,
|
|
ssh_port: $DEFAULT_SSH_PORT,
|
|
status: (if $s.status == "running" { "running" } else if $s.status == "initializing" { "starting" } else { "unknown" }),
|
|
created_at: $s.created,
|
|
}
|
|
} | to json
|
|
}
|
|
|
|
def main [] { print "Usage: spawn-runner.nu <spawn|destroy|list>" }
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
def _resolve_image [image_ref: string] {
|
|
let by_name = do { ^hcloud image list --type snapshot --name $image_ref --output json } | complete
|
|
if $by_name.exit_code == 0 {
|
|
let imgs = $by_name.stdout | from json
|
|
if ($imgs | length) > 0 { return ($imgs | first).id }
|
|
}
|
|
let by_label = do { ^hcloud image list --type snapshot --selector $"app=($GOLDEN_IMAGE_PREFIX)" --output json } | complete
|
|
if $by_label.exit_code != 0 {
|
|
error make { msg: $"hcloud image list failed:\n($by_label.stderr)" }
|
|
}
|
|
let imgs = $by_label.stdout | from json
|
|
if ($imgs | length) == 0 {
|
|
error make { msg: $"No snapshot found — image_ref=($image_ref), label app=($GOLDEN_IMAGE_PREFIX). Run build-golden-image.nu first." }
|
|
}
|
|
($imgs | sort-by created | last).id
|
|
}
|
|
|
|
def _server_type [cpu: int, memory_gb: int] {
|
|
if $cpu <= 2 { "cax11" } else if $cpu <= 4 { "cax21" } else if $cpu <= 8 { "cax31" } else { "cax41" }
|
|
}
|
|
|
|
def _wait_for_ssh [host: string, timeout_sec: int] {
|
|
let deadline = (date now) + ($timeout_sec * 1sec)
|
|
loop {
|
|
let r = do { ^ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes $"root@($host)" true } | complete
|
|
if $r.exit_code == 0 { return }
|
|
if (date now) > $deadline {
|
|
error make { msg: $"Timeout waiting for SSH on ($host)" }
|
|
}
|
|
sleep 5sec
|
|
}
|
|
}
|
|
|
|
def _inject_resolv_conf [host: string, dns_ip: string] {
|
|
let r = do {
|
|
^ssh -o StrictHostKeyChecking=no -o BatchMode=yes $"root@($host)" $"printf 'nameserver ($dns_ip)\\n' > /etc/resolv.conf"
|
|
} | complete
|
|
if $r.exit_code != 0 {
|
|
log warning $"resolv.conf inject failed on ($host): ($r.stderr)"
|
|
}
|
|
}
|