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>
510 lines
14 KiB
Text
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"
|
|
}
|
|
}
|