Untangle the shared foundation that the cluster subsystem depends on, moving each helper to the layer its dependency topology dictates, de-cli-ifying on the way (cli-err → error make). One-directional imports only; verified no cycles. comp-ncl-export → tools/nickel/process.nu ws-resolution (8 fns + root finders) → domain/workspace/resolve.nu (new) comp-ssh-run, comp-server-ssh-info → platform/clients/ssh.nu (new) comp-kubectl-access, comp-kubectl-ssh → domain/cluster/access.nu (new) Consumers (components.nu, components_cluster.nu, component.nu) repointed to the new locations. components.nu 2664 → 2408. Verified: NU_LIB_DIRS module-load of all 8 touched modules (no cycle, no missing command, no export error); ADR-026 hygiene (no upward imports in the new platform/domain/tools modules); `provisioning component list` runs; component/cluster --help exit 0. Prereq for slice 2b (cluster helpers → domain/cluster/introspect.nu).
52 lines
2.5 KiB
Text
52 lines
2.5 KiB
Text
# SSH client primitives — extracted from cli/components.nu (ADR-026 slice 2a-iii).
|
||
|
||
# Get SSH connection info for a server (user, key path, host) from servers.ncl.
|
||
export def comp-server-ssh-info [ws_root: string, infra: string, server: string]: nothing -> record {
|
||
let servers_path = ($ws_root | path join "infra" $infra "servers.ncl")
|
||
if not ($servers_path | path exists) {
|
||
return { user: "root", key: "", host: $server }
|
||
}
|
||
let raw = (ncl-eval-soft $servers_path (default-ncl-paths $ws_root) null)
|
||
if ($raw == null) {
|
||
return { user: "root", key: "", host: $server }
|
||
}
|
||
let srv = ($raw | get -o servers | default [] | where {|s| ($s.hostname? | default "") == $server } | get 0?)
|
||
let user = if ($srv | is-not-empty) { ($srv | get -o installer_user | default "root") } else { "root" }
|
||
let key_raw = if ($srv | is-not-empty) { ($srv | get -o ssh_key_path | default "") } else { "" }
|
||
let key = if ($key_raw | is-not-empty) { $key_raw | path expand } else { "" }
|
||
let state_path = ($ws_root | path join "infra" $infra ".servers-state.json")
|
||
let srv_state = if ($state_path | path exists) {
|
||
$state_path | open | get -o $server | default {}
|
||
} else { {} }
|
||
let host = if ($srv | is-not-empty) {
|
||
let np = ($srv | get -o networking.public_ip | default "")
|
||
if ($np | is-not-empty) {
|
||
$np
|
||
} else {
|
||
# floating_ip is a Hetzner API assignment name, not an SSH target.
|
||
# Always use public_ip from state for SSH — the server's primary reachable address.
|
||
let state_ip = ($srv_state | get -o public_ip | default "")
|
||
if ($state_ip | is-not-empty) { $state_ip } else { $server }
|
||
}
|
||
} else { $server }
|
||
{ user: $user, key: $key, host: $host }
|
||
}
|
||
|
||
# Run a command on a remote host via SSH (with or without explicit identity file).
|
||
# ConnectTimeout bounds the connect phase; ServerAlive (5s × 3) tears down a
|
||
# session that goes silent after connect so a stalled remote command can never
|
||
# hang the caller indefinitely. Callers that run kubectl wrap it in `timeout`.
|
||
export def comp-ssh-run [user: string, key: string, host: string, cmd: string]: nothing -> record {
|
||
let opts = [
|
||
-o ConnectTimeout=10
|
||
-o ServerAliveInterval=5
|
||
-o ServerAliveCountMax=3
|
||
-o BatchMode=yes
|
||
]
|
||
if ($key | is-not-empty) {
|
||
do { ^ssh -i $key ...$opts $"($user)@($host)" $cmd } | complete
|
||
} else {
|
||
do { ^ssh ...$opts $"($user)@($host)" $cmd } | complete
|
||
}
|
||
}
|
||
|