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:
+1
-1
@@ -1 +1 @@
|
|||||||
1355da96421a15de6ec0d7ac0678f463762b8cad31133fb437243c6bda224b04 orca-v0.12.18-linux-amd64.tar.gz
|
15ee8f02ef938496ce9baae35e2971a5fcfb2d55b2c6e35e49c11285f67aeb53 orca-v0.12.18-linux-amd64.tar.gz
|
||||||
|
|||||||
+5
-9
@@ -44,15 +44,11 @@ orca init
|
|||||||
- Traefik data-plane ingress (binary + systemd unit + config)
|
- Traefik data-plane ingress (binary + systemd unit + config)
|
||||||
- Localhost node registered
|
- Localhost node registered
|
||||||
|
|
||||||
**Pre-staging remote nodes**: `orca init` prints the orca public key path.
|
**Pre-staging remote nodes**: `orca init` interactively prompts for remote
|
||||||
Deploy it to each remote host before joining:
|
host addresses and runs `ssh-copy-id` automatically (password prompt passes
|
||||||
```sh
|
through). Enter each host (pve01, worker01) when prompted, or press Enter to
|
||||||
ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@pve01
|
skip. The orca public key is deployed to each host; TOFU host-key capture is
|
||||||
ssh-copy-id -i $ORCA_HOME/orca_ssh_key.pub root@worker01
|
automatic on the first `orca node join` — no manual fingerprint pinning needed.
|
||||||
```
|
|
||||||
|
|
||||||
The TOFU host-key capture is automatic — no manual fingerprint pinning needed.
|
|
||||||
The first SSH connection captures and stores the remote host key.
|
|
||||||
|
|
||||||
### Step 2: Onboard the Proxmox host
|
### Step 2: Onboard the Proxmox host
|
||||||
|
|
||||||
|
|||||||
+48
-7
@@ -5,15 +5,15 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"bufio"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"git.cloudinit.dev/coreci/orca/internal/acl"
|
"git.cloudinit.dev/coreci/orca/internal/acl"
|
||||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||||
"git.cloudinit.dev/coreci/orca/internal/identity"
|
"git.cloudinit.dev/coreci/orca/internal/identity"
|
||||||
@@ -63,8 +63,7 @@ func runInit(out interface{ Write([]byte) (int, error) }) error {
|
|||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
CAFingerprint string `json:"ca_fingerprint,omitempty"`
|
CAFingerprint string `json:"ca_fingerprint,omitempty"`
|
||||||
CertFingerprint string `json:"cert_fingerprint,omitempty"`
|
CertFingerprint string `json:"cert_fingerprint,omitempty"`
|
||||||
OS string `json:"os"
|
OS string `json:"os"`
|
||||||
"path/filepath"`
|
|
||||||
NodeID string `json:"node_id"`
|
NodeID string `json:"node_id"`
|
||||||
NodeName string `json:"node_name"`
|
NodeName string `json:"node_name"`
|
||||||
Steps []stepResult `json:"steps"`
|
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))] + "..."})
|
summary.Steps = append(summary.Steps, stepResult{Label: "ssh-key", Status: "ok", Detail: sshKeyFp[:min(16, len(sshKeyFp))] + "..."})
|
||||||
if !jsonOutput {
|
if !jsonOutput {
|
||||||
fmt.Fprintf(out, "\xe2\x9c\x93 SSH keypair provisioned: fp=%s\n", sshKeyFp[:min(16, len(sshKeyFp))]+"...")
|
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
|
// Step 4b: known_hosts file (empty, 0600). Without this, the TOFU
|
||||||
|
|||||||
Reference in New Issue
Block a user