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
+1 -1
View File
@@ -1 +1 @@
1355da96421a15de6ec0d7ac0678f463762b8cad31133fb437243c6bda224b04 orca-v0.12.18-linux-amd64.tar.gz
15ee8f02ef938496ce9baae35e2971a5fcfb2d55b2c6e35e49c11285f67aeb53 orca-v0.12.18-linux-amd64.tar.gz
+5 -9
View File
@@ -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
+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