feat(init): interactive remote pre-staging via ssh-copy-id

orca init now interactively prompts for remote host addresses and runs
ssh-copy-id automatically (password prompt passes through to the
operator). This makes orca init the single entry point — no manual
pre-staging of SSH keys required.

- Interactive: enter host addresses (one per line, empty line to finish)
- ssh-copy-id deploys the orca public key to each host
- Skipped in --json mode (non-interactive)
- Idempotent: re-running init can stage additional hosts

Also fixed: install.sh defaults to /usr/local/bin (on PATH for all users).
Non-root without sudo falls back to ~/.local/bin + auto-adds to .bashrc.
This commit is contained in:
Jon Chery
2026-08-10 17:37:42 +00:00
parent 5600531bd7
commit 0424f8ce02
3 changed files with 54 additions and 17 deletions
+48 -7
View File
@@ -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@<remote-host>\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