155 lines
6.8 KiB
Text
155 lines
6.8 KiB
Text
# infra-catalog Runner Interface — ComputeProvider contract
|
|
#
|
|
# Defines the contract that any compute-provider component must satisfy.
|
|
# A conforming implementation is a script (typically runner.nu in the component
|
|
# root) that accepts subcommands with JSON on stdin and writes JSON to stdout.
|
|
#
|
|
# Invocation pattern (callers always go through `runner-dispatch`):
|
|
# {cpu: 4, memory_gb: 8, ...} | runner-dispatch "/path/to/provider/runner.nu" spawn
|
|
#
|
|
# Implementors source this module to validate their own I/O:
|
|
# use catalog/runner.nu [validate-spawn-input, validate-spawn-output]
|
|
#
|
|
# The interface is intentionally narrow: lian-build, Vapora, and any other tool
|
|
# that needs ephemeral compute only need these four commands. Provider-specific
|
|
# operations (snapshot management, network topology, etc.) belong in the
|
|
# provider's own extended interface, not here.
|
|
|
|
# ── Input / output shapes ─────────────────────────────────────────────────────
|
|
|
|
# Shape of the JSON record passed to `spawn`.
|
|
# All fields that carry provider-specific meaning are string references:
|
|
# the caller does not interpret them — the provider script does.
|
|
export def spawn-input-shape []: nothing -> record {
|
|
{
|
|
cpu: { type: "int", required: true, doc: "Minimum CPU cores." }
|
|
memory_gb: { type: "int", required: true, doc: "Minimum RAM in GB." }
|
|
disk_gb: { type: "int", required: true, doc: "Minimum root disk in GB." }
|
|
time_budget_min: { type: "int", required: true, doc: "Expected lifetime. Provider may enforce TTL." }
|
|
image_ref: { type: "string", required: true, doc: "Provider-specific image/snapshot reference." }
|
|
location: { type: "string", required: true, doc: "Provider-specific region or datacenter." }
|
|
ssh_key_ref: { type: "string", required: true, doc: "SSH key name registered with the provider." }
|
|
network_ref: { type: "string", required: false, doc: "Private network name or null." }
|
|
firewall_ref: { type: "string", required: false, doc: "Firewall name or null." }
|
|
tags: { type: "record", required: false, doc: "Arbitrary key-value labels." }
|
|
}
|
|
}
|
|
|
|
# Shape of the JSON record returned by `spawn`.
|
|
# `handle` is opaque to the caller — it is stored and returned verbatim
|
|
# to `destroy` and `status`. Providers encode whatever they need in it
|
|
# (server name, instance ID, etc.).
|
|
export def spawn-output-shape []: nothing -> record {
|
|
{
|
|
handle: { type: "string", required: true, doc: "Opaque provider reference. Returned to destroy/status." }
|
|
ssh_host: { type: "string", required: true, doc: "IP or hostname to reach the runner." }
|
|
ssh_port: { type: "int", required: true, doc: "SSH port." }
|
|
expires_at: { type: "string", required: false, doc: "ISO-8601 expiry, or null if provider has no TTL." }
|
|
}
|
|
}
|
|
|
|
# Shape passed to `destroy` and `status`.
|
|
export def handle-input-shape []: nothing -> record {
|
|
{
|
|
handle: { type: "string", required: true, doc: "Value returned by spawn." }
|
|
}
|
|
}
|
|
|
|
# Shape returned by `status`.
|
|
export def status-output-shape []: nothing -> record {
|
|
{
|
|
running: { type: "bool", required: true, doc: "Whether the runner is reachable." }
|
|
ssh_host: { type: "string", required: false, doc: "Current IP, if available." }
|
|
}
|
|
}
|
|
|
|
# Shape returned by `describe`.
|
|
export def describe-output-shape []: nothing -> record {
|
|
{
|
|
locations: { type: "list<string>", required: true, doc: "Available regions/datacenters." }
|
|
server_types: {
|
|
type: "list<record>",
|
|
required: true,
|
|
doc: "Available sizes. Each record: {name, cpu, memory_gb, disk_gb}."
|
|
}
|
|
image_query: { type: "string", required: false, doc: "How to discover available images, or null." }
|
|
}
|
|
}
|
|
|
|
# ── Validators ────────────────────────────────────────────────────────────────
|
|
|
|
# Validate a spawn input record against the contract.
|
|
# Errors immediately with a descriptive message on contract violation.
|
|
export def validate-spawn-input []: record -> record {
|
|
let input = $in
|
|
let required = [cpu, memory_gb, disk_gb, time_budget_min, image_ref, location, ssh_key_ref]
|
|
let missing = $required | where { |f| not ($f in ($input | columns)) }
|
|
if not ($missing | is-empty) {
|
|
error make { msg: $"runner spawn: missing required fields: ($missing | str join ', ')" }
|
|
}
|
|
if ($input.cpu | into int) < 1 {
|
|
error make { msg: "runner spawn: cpu must be >= 1" }
|
|
}
|
|
if ($input.memory_gb | into int) < 1 {
|
|
error make { msg: "runner spawn: memory_gb must be >= 1" }
|
|
}
|
|
$input
|
|
}
|
|
|
|
# Validate a spawn output record before returning it to the caller.
|
|
export def validate-spawn-output []: record -> record {
|
|
let output = $in
|
|
let required = [handle, ssh_host, ssh_port]
|
|
let missing = $required | where { |f| not ($f in ($output | columns)) }
|
|
if not ($missing | is-empty) {
|
|
error make { msg: $"runner spawn output: missing required fields: ($missing | str join ', ')" }
|
|
}
|
|
$output
|
|
}
|
|
|
|
# ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
#
|
|
# The dispatcher is the single point through which all tools invoke a provider.
|
|
# It isolates callers from the path and invocation mechanics of the provider
|
|
# script. Adding or replacing a provider never requires changes in the caller.
|
|
#
|
|
# Usage:
|
|
# {cpu: 4, memory_gb: 8, disk_gb: 20, time_budget_min: 60,
|
|
# image_ref: "buildkit-runner-latest", location: "fsn1",
|
|
# ssh_key_ref: "orchestrator-buildkit"}
|
|
# | runner-dispatch "/catalog/providers/hetzner/runner.nu" spawn
|
|
#
|
|
# {handle: "buildkit-runner-abc123"} | runner-dispatch "/catalog/providers/hetzner/runner.nu" destroy
|
|
|
|
export def runner-dispatch [
|
|
script: path, # absolute path to the provider's runner.nu
|
|
command: string, # spawn | destroy | status | describe
|
|
]: record -> any {
|
|
let input = $in
|
|
|
|
if not ($script | path exists) {
|
|
error make { msg: $"runner-dispatch: provider script not found: ($script)" }
|
|
}
|
|
|
|
let allowed = [spawn destroy status describe]
|
|
if not ($command in $allowed) {
|
|
error make { msg: $"runner-dispatch: unknown command '($command)'. Allowed: ($allowed | str join ', ')" }
|
|
}
|
|
|
|
let json_input = $input | to json
|
|
let result = $json_input | nu $script $command | complete
|
|
|
|
if $result.exit_code != 0 {
|
|
error make {
|
|
msg: $"runner ($command) failed (exit ($result.exit_code))"
|
|
label: {
|
|
text: ($result.stderr | str trim)
|
|
span: (metadata $script).span
|
|
}
|
|
}
|
|
}
|
|
|
|
if $command in [spawn status describe] {
|
|
$result.stdout | str trim | from json
|
|
}
|
|
}
|