provisioning-catalog/components/buildkit_runner/scripts/spawn-runner.nu

134 lines
5.7 KiB
Text
Raw Permalink Normal View History

#!/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)"
}
}