diff --git a/SHA256SUMS b/SHA256SUMS index b70a004..cf22705 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1 +1 @@ -1355da96421a15de6ec0d7ac0678f463762b8cad31133fb437243c6bda224b04 orca-v0.12.18-linux-amd64.tar.gz +15ee8f02ef938496ce9baae35e2971a5fcfb2d55b2c6e35e49c11285f67aeb53 orca-v0.12.18-linux-amd64.tar.gz diff --git a/docs/uat.md b/docs/uat.md index 520cf32..e621b21 100644 --- a/docs/uat.md +++ b/docs/uat.md @@ -44,15 +44,11 @@ orca init - Traefik data-plane ingress (binary + systemd unit + config) - Localhost node registered -**Pre-staging remote nodes**: `orca init` prints the orca public key path. -Deploy it to each remote host before joining: -```sh -ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@pve01 -ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@worker01 -``` - -The TOFU host-key capture is automatic — no manual fingerprint pinning needed. -The first SSH connection captures and stores the remote host key. +**Pre-staging remote nodes**: `orca init` interactively prompts for remote +host addresses and runs `ssh-copy-id` automatically (password prompt passes +through). Enter each host (pve01, worker01) when prompted, or press Enter to +skip. The orca public key is deployed to each host; TOFU host-key capture is +automatic on the first `orca node join` — no manual fingerprint pinning needed. ### Step 2: Onboard the Proxmox host diff --git a/internal/cli/init.go b/internal/cli/init.go index 90f7901..5d29ed8 100644 --- a/internal/cli/init.go +++ b/internal/cli/init.go @@ -5,15 +5,15 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "bufio" "os" + "strings" + "os/exec" "path/filepath" "time" - "github.com/google/uuid" - "golang.org/x/crypto/ssh" "github.com/spf13/cobra" - "git.cloudinit.dev/coreci/orca/internal/acl" "git.cloudinit.dev/coreci/orca/internal/certpaths" "git.cloudinit.dev/coreci/orca/internal/identity" @@ -63,8 +63,7 @@ func runInit(out interface{ Write([]byte) (int, error) }) error { Database string `json:"database"` CAFingerprint string `json:"ca_fingerprint,omitempty"` CertFingerprint string `json:"cert_fingerprint,omitempty"` - OS string `json:"os" - "path/filepath"` + OS string `json:"os"` NodeID string `json:"node_id"` NodeName string `json:"node_name"` Steps []stepResult `json:"steps"` @@ -158,8 +157,50 @@ func runInit(out interface{ Write([]byte) (int, error) }) error { summary.Steps = append(summary.Steps, stepResult{Label: "ssh-key", Status: "ok", Detail: sshKeyFp[:min(16, len(sshKeyFp))] + "..."}) if !jsonOutput { fmt.Fprintf(out, "\xe2\x9c\x93 SSH keypair provisioned: fp=%s\n", sshKeyFp[:min(16, len(sshKeyFp))]+"...") - fmt.Fprintf(out, "\n To onboard remote nodes, deploy the orca public key first:\n") - fmt.Fprintf(out, " ssh-copy-id -i %s root@\n\n", certpaths.SSHPubPath()) + } + + // Step 4a-2: Interactively pre-stage the orca public key on remote + // hosts (ssh-copy-id). Skipped in --json mode (non-interactive). + // The operator enters host addresses (one per line, empty line to + // finish). For each host, ssh-copy-id is run; if SSH key auth is + // not yet established, ssh-copy-id prompts for the password + // interactively. This makes orca init the single entry point — + // no manual pre-staging required. + if !jsonOutput { + pubPath := certpaths.SSHPubPath() + fmt.Fprintf(out, "\n Pre-stage the orca public key on remote nodes.\n") + fmt.Fprintf(out, " Enter host addresses (one per line, empty line to skip):\n") + stagedHosts := []string{} + reader := bufio.NewReader(os.Stdin) + for { + fmt.Fprintf(out, " host> ") + line, err := reader.ReadString('\n') + if err != nil { + break + } + host := strings.TrimSpace(line) + if host == "" { + break + } + // Run ssh-copy-id interactively (password prompt passes through). + fmt.Fprintf(out, " Deploying orca key to %s...\n", host) + cmd := exec.Command("ssh-copy-id", "-i", pubPath, "-o", "StrictHostKeyChecking=accept-new", "root@"+host) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(out, " \xe2\x9a\xa0 Failed to deploy key to %s: %v\n", host, err) + continue + } + fmt.Fprintf(out, " \xe2\x9c\x93 Key deployed to %s\n", host) + stagedHosts = append(stagedHosts, host) + } + if len(stagedHosts) > 0 { + summary.Steps = append(summary.Steps, stepResult{Label: "pre-stage", Status: "ok", Detail: strings.Join(stagedHosts, ", ")}) + } else { + summary.Steps = append(summary.Steps, stepResult{Label: "pre-stage", Status: "skipped", Detail: "no hosts entered"}) + } + fmt.Fprintf(out, "\n") } // Step 4b: known_hosts file (empty, 0600). Without this, the TOFU