rigging/cli/rigging.nu
Christopher Mühl 892161eae2
feat: initial rigging — multi-repo NixOS + Nomad infrastructure management
Flake-parts module that repos import to declare hosts, jobs, and secrets.
Nushell CLI (rigging) aggregates multiple repos and provides unified
management: host deploy/build, job run/plan/stop, secret list/rekey.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:09:32 +01:00

510 lines
14 KiB
Text

# rigging — multi-repo infrastructure management CLI
# Aggregates bosun-enabled repos and provides unified host/job/secret management.
def config-path []: nothing -> string {
$env.HOME | path join ".config" "bosun" "config.toml"
}
def load-config []: nothing -> record {
let p = (config-path)
if ($p | path exists) {
open $p
} else {
{ repos: {} }
}
}
def save-config [cfg: record]: nothing -> nothing {
let p = (config-path)
let dir = ($p | path dirname)
if not ($dir | path exists) { mkdir $dir }
$cfg | to toml | save -f $p
}
# Build manifest derivation and read the JSON
def read-manifest [repo_path: string]: nothing -> record {
let out = (^nix build $"($repo_path)#bosun-manifest" --no-link --print-out-paths | str trim)
open $out
}
# Load all repos with their manifests
def load-all []: nothing -> list<record> {
let cfg = (load-config)
if ($cfg.repos | is-empty) {
print $"(ansi yellow)No repos registered. Use `rigging repo add <path>` first.(ansi reset)"
return []
}
$cfg.repos | transpose name meta | each { |r|
let manifest = (read-manifest $r.meta.path)
{ name: $r.name, path: $r.meta.path, manifest: $manifest }
}
}
# Find which repo owns a given job
def find-job-repo [name: string]: nothing -> record {
let repos = (load-all)
let matches = ($repos | where { |r| $name in ($r.manifest.jobs | columns) })
if ($matches | is-empty) {
print $"(ansi red)Error: job '($name)' not found in any registered repo(ansi reset)"
exit 1
}
$matches | first
}
# Find which repo owns a given host
def find-host-repo [name: string]: nothing -> record {
let repos = (load-all)
let matches = ($repos | where { |r| $name in ($r.manifest.hosts | columns) })
if ($matches | is-empty) {
print $"(ansi red)Error: host '($name)' not found in any registered repo(ansi reset)"
exit 1
}
$matches | first
}
# --- Top-level ---
# Rigging — multi-repo infrastructure management
def main []: nothing -> nothing {
print "rigging — multi-repo infrastructure management"
print ""
print "Usage: rigging <command>"
print ""
print "Commands:"
print " repo Manage registered repos"
print " status Aggregated overview of all repos"
print " host Host management (list, deploy, build)"
print " job Nomad job management (list, run, plan, stop, ...)"
print " secret Secret management (list, rekey)"
print ""
print $"Config: (config-path)"
}
# --- Repo management ---
# Manage registered repos
def "main repo" []: nothing -> nothing {
main repo list
}
# Register a new repo
def "main repo add" [
path: string # Path to the repo (must contain a flake with bosun-manifest)
]: nothing -> nothing {
let abs_path = ($path | path expand)
if not ($"($abs_path)/flake.nix" | path exists) {
print $"(ansi red)Error: no flake.nix found at ($abs_path)(ansi reset)"
exit 1
}
print $"(ansi blue)→(ansi reset) Building manifest for ($abs_path)..."
let manifest = (read-manifest $abs_path)
let name = $manifest.name
let cfg = (load-config)
let repos = ($cfg.repos | upsert $name { path: $abs_path })
save-config { repos: $repos }
let n_hosts = ($manifest.hosts | columns | length)
let n_jobs = ($manifest.jobs | columns | length)
print $"(ansi green)✓(ansi reset) Registered '($name)' — ($n_hosts) hosts, ($n_jobs) jobs"
}
# Remove a registered repo
def "main repo remove" [
name: string # Name of the repo to remove
]: nothing -> nothing {
let cfg = (load-config)
if $name not-in ($cfg.repos | columns) {
print $"(ansi red)Error: repo '($name)' not registered(ansi reset)"
exit 1
}
let repos = ($cfg.repos | reject $name)
save-config { repos: $repos }
print $"(ansi green)✓(ansi reset) Removed '($name)'"
}
# List registered repos
def "main repo list" []: nothing -> nothing {
let cfg = (load-config)
if ($cfg.repos | is-empty) {
print "No repos registered. Use `rigging repo add <path>` to add one."
return
}
$cfg.repos | transpose name meta | each { |r|
let manifest = (try { read-manifest $r.meta.path } catch { null })
if $manifest != null {
let n_hosts = ($manifest.hosts | columns | length)
let n_jobs = ($manifest.jobs | columns | length)
{ name: $r.name, path: $r.meta.path, hosts: $n_hosts, jobs: $n_jobs }
} else {
{ name: $r.name, path: $r.meta.path, hosts: "?", jobs: "?" }
}
} | table
| print
}
# --- Status ---
# Aggregated overview of all repos
def "main status" []: nothing -> nothing {
let repos = (load-all)
if ($repos | is-empty) { return }
print $"(ansi blue_bold)Repos(ansi reset)"
$repos | each { |r|
let n_hosts = ($r.manifest.hosts | columns | length)
let n_jobs = ($r.manifest.jobs | columns | length)
{ repo: $r.name, path: $r.path, hosts: $n_hosts, jobs: $n_jobs }
} | table | print
print ""
print $"(ansi blue_bold)Hosts(ansi reset)"
let hosts = ($repos | each { |r|
$r.manifest.hosts | transpose name cfg | each { |h|
{
host: $h.name
repo: $r.name
system: $h.cfg.system
class: $h.cfg.class
target: ($h.cfg.targetHost? | default "local")
tags: ($h.cfg.tags | str join ", ")
}
}
} | flatten)
if ($hosts | is-empty) {
print " (none)"
} else {
$hosts | table | print
}
print ""
print $"(ansi blue_bold)Jobs(ansi reset)"
let jobs = ($repos | each { |r|
$r.manifest.jobs | transpose name meta | each { |j|
{
job: $j.name
repo: $r.name
type: $j.meta.type
datacenters: ($j.meta.datacenters | str join ", ")
parameterized: $j.meta.parameterized
}
}
} | flatten)
if ($jobs | is-empty) {
print " (none)"
} else {
$jobs | table | print
}
}
# --- Host management ---
# Host management
def "main host" []: nothing -> nothing {
main host list
}
# List all hosts across repos
def "main host list" [
--tag: string # Filter by tag
]: nothing -> nothing {
let repos = (load-all)
if ($repos | is-empty) { return }
let hosts = ($repos | each { |r|
$r.manifest.hosts | transpose name cfg | each { |h|
{
host: $h.name
repo: $r.name
system: $h.cfg.system
class: $h.cfg.class
target: ($h.cfg.targetHost? | default "local")
tags: ($h.cfg.tags | default [])
}
}
} | flatten)
let filtered = if $tag != null {
$hosts | where { |h| $tag in $h.tags }
} else {
$hosts
}
$filtered | update tags { |r| $r.tags | str join ", " } | table | print
}
# Deploy a host (nixos-rebuild or darwin-rebuild)
def "main host deploy" [
name: string # Host name
--dry-run # Print command without executing
]: nothing -> nothing {
let repo = (find-host-repo $name)
let host_cfg = ($repo.manifest.hosts | get $name)
let cmd = (build-deploy-cmd $repo.path $name $host_cfg false)
if $dry_run {
print $"(ansi yellow)DRY RUN(ansi reset) — would execute:"
print $" ($cmd | str join ' ')"
return
}
print $"(ansi blue)→(ansi reset) Deploying ($name) from ($repo.name)..."
^...$cmd
}
# Build a host (without activating)
def "main host build" [
name: string # Host name
--dry-run # Print command without executing
]: nothing -> nothing {
let repo = (find-host-repo $name)
let host_cfg = ($repo.manifest.hosts | get $name)
let cmd = (build-deploy-cmd $repo.path $name $host_cfg true)
if $dry_run {
print $"(ansi yellow)DRY RUN(ansi reset) — would execute:"
print $" ($cmd | str join ' ')"
return
}
print $"(ansi blue)→(ansi reset) Building ($name) from ($repo.name)..."
^...$cmd
}
# Construct the rebuild command for a host
def build-deploy-cmd [
repo_path: string
name: string
host_cfg: record
build_only: bool
]: nothing -> list<string> {
let rebuilder = if $host_cfg.class == "darwin" { "darwin-rebuild" } else { "nixos-rebuild" }
let action = if $build_only { "build" } else { "switch" }
mut cmd = [$rebuilder $action "--flake" $"($repo_path)#($name)"]
if $host_cfg.targetHost? != null {
$cmd = ($cmd | append ["--target-host" $host_cfg.targetHost "--use-remote-sudo"])
}
if $host_cfg.buildHost? != null {
$cmd = ($cmd | append ["--build-host" $host_cfg.buildHost])
}
$cmd
}
# --- Job management ---
# Nomad job management
def "main job" []: nothing -> nothing {
main job list
}
# List all Nomad jobs across repos
def "main job list" []: nothing -> nothing {
let repos = (load-all)
if ($repos | is-empty) { return }
let jobs = ($repos | each { |r|
$r.manifest.jobs | transpose name meta | each { |j|
{
job: $j.name
repo: $r.name
type: $j.meta.type
datacenters: ($j.meta.datacenters | str join ", ")
parameterized: $j.meta.parameterized
}
}
} | flatten)
if ($jobs | is-empty) {
print "No jobs found."
} else {
$jobs | table | print
}
}
# Compile and deploy a job to Nomad
def "main job run" [
name: string # Job name
--dry-run # Print command without executing
...rest: string # Extra args passed to the repo-local bosun CLI (e.g. -v KEY=VALUE)
]: nothing -> nothing {
let repo = (find-job-repo $name)
let args = if $dry_run {
["run" $name "--dry-run" ...$rest]
} else {
["run" $name ...$rest]
}
print $"(ansi blue)→(ansi reset) Running job ($name) from ($repo.name)..."
^nix run $"($repo.path)#bosun" -- ...$args
}
# Plan a job deployment (dry-run)
def "main job plan" [
name: string # Job name
...rest: string # Extra args
]: nothing -> nothing {
let repo = (find-job-repo $name)
print $"(ansi blue)→(ansi reset) Planning job ($name) from ($repo.name)..."
^nix run $"($repo.path)#bosun" -- "plan" $name ...$rest
}
# Stop a running job
def "main job stop" [
name: string # Job name
--dry-run # Print command without executing
]: nothing -> nothing {
let repo = (find-job-repo $name)
let args = if $dry_run {
["stop" $name "--dry-run"]
} else {
["stop" $name]
}
print $"(ansi blue)→(ansi reset) Stopping job ($name)..."
^nix run $"($repo.path)#bosun" -- ...$args
}
# Show job status
def "main job status" [
name?: string # Job name (omit for all)
]: nothing -> nothing {
if $name == null {
# Find any repo with nomad enabled and query status
let repos = (load-all)
let nomad_repos = ($repos | where { |r| not ($r.manifest.nomad | is-empty) })
if ($nomad_repos | is-empty) {
print "No repos with Nomad enabled."
return
}
let repo = ($nomad_repos | first)
^nix run $"($repo.path)#bosun" -- "status"
} else {
let repo = (find-job-repo $name)
^nix run $"($repo.path)#bosun" -- "status" $name
}
}
# Show job logs
def "main job logs" [
name: string # Job name
task?: string # Task name (optional)
]: nothing -> nothing {
let repo = (find-job-repo $name)
if $task != null {
^nix run $"($repo.path)#bosun" -- "logs" $name $task
} else {
^nix run $"($repo.path)#bosun" -- "logs" $name
}
}
# Pretty-print compiled job JSON
def "main job inspect" [
name: string # Job name
...rest: string # Extra args (e.g. -v KEY=VALUE)
]: nothing -> nothing {
let repo = (find-job-repo $name)
^nix run $"($repo.path)#bosun" -- "inspect" $name ...$rest
}
# Generate all job JSON files
def "main job generate" [
dir?: string # Output directory (default: ./generated)
...rest: string # Extra args
]: nothing -> nothing {
let repos = (load-all)
let nomad_repos = ($repos | where { |r| not ($r.manifest.jobs | is-empty) })
for repo in $nomad_repos {
print $"(ansi blue)→(ansi reset) Generating jobs for ($repo.name)..."
let args = if $dir != null {
["generate" $dir ...$rest]
} else {
["generate" ...$rest]
}
^nix run $"($repo.path)#bosun" -- ...$args
}
}
# Dispatch a parameterized job
def "main job dispatch" [
name?: string # Job name (omit to list parameterized jobs)
...rest: string # Extra args (e.g. -m KEY=VALUE)
]: nothing -> nothing {
if $name == null {
# List parameterized jobs
let repos = (load-all)
let param_jobs = ($repos | each { |r|
$r.manifest.jobs | transpose jname meta
| where { |j| $j.meta.parameterized }
| each { |j| { job: $j.jname, repo: $r.name } }
} | flatten)
if ($param_jobs | is-empty) {
print "No parameterized jobs found."
} else {
print "Parameterized jobs:"
$param_jobs | table | print
}
return
}
let repo = (find-job-repo $name)
^nix run $"($repo.path)#bosun" -- "dispatch" $name ...$rest
}
# --- Secret management ---
# Secret management
def "main secret" []: nothing -> nothing {
main secret list
}
# List secrets across repos
def "main secret list" []: nothing -> nothing {
let cfg = (load-config)
if ($cfg.repos | is-empty) {
print "No repos registered."
return
}
$cfg.repos | transpose name meta | each { |r|
let secrets_dir = $"($r.meta.path)/secrets"
if ($secrets_dir | path exists) {
let files = (glob $"($secrets_dir)/**/*.age")
$files | each { |f|
let rel = ($f | str replace $"($r.meta.path)/" "")
{ repo: $r.name, secret: $rel }
}
} else {
[]
}
} | flatten | table | print
}
# Rekey secrets for a repo
def "main secret rekey" [
--repo: string # Repo name (omit to rekey all)
]: nothing -> nothing {
let cfg = (load-config)
let targets = if $repo != null {
if $repo not-in ($cfg.repos | columns) {
print $"(ansi red)Error: repo '($repo)' not registered(ansi reset)"
exit 1
}
[{ name: $repo, path: ($cfg.repos | get $repo | get path) }]
} else {
$cfg.repos | transpose name meta | each { |r| { name: $r.name, path: $r.meta.path } }
}
for target in $targets {
print $"(ansi blue)→(ansi reset) Rekeying secrets for ($target.name)..."
^nix run $"($target.path)#agenix" -- rekey -a
print $"(ansi green)✓(ansi reset) ($target.name) rekeyed"
}
}