feat(P12): --type linux SSH-join + UAT plan + signoff script (REQ-161..163)

--type linux (REQ-161):
- internal/linux/bootstrap.go: SSH bootstrap for generic Linux workers
  (orcas pubkey, system user, drift-events dir; no PVE role/sudoers)
- internal/cli/node.go: joinLinux function + --type linux dispatch
- peer-setup kept as documented fallback

UAT plan (REQ-162):
- docs/uat.md: 3-host topology (lead Ubuntu + pve01 Proxmox + worker01
  Ubuntu), 22 step-by-step commands, 35-claim matrix, Proxmox
  prerequisite + alternative 3xUbuntu path (C-48), signoff procedure

UAT signoff script (REQ-163, C-47):
- scripts/uat-signoff.sh: 35 idempotent read-only assertions, exit 0
  iff all pass. Includes 4 critical-path assertions: job deploys to
  remote, ACL deny-by-default, seal/unseal round-trip, OIDC health
- scripts/uat-smoke.sh: 13 CI-tested pure-CLI assertions for .coreci.yml

Tests: node join --type linux test, fingerprint test updated, smoke
test all 13 pass.

---ci---
project: orca
phase: 12
milestone: v0.13
status: complete
requirements:
  covered: [161, 162, 163]
---/ci---
This commit is contained in:
Jon Chery
2026-08-10 14:33:29 +00:00
parent b6dd86fdf3
commit 52e17aefbf
7 changed files with 848 additions and 6 deletions
+73 -2
View File
@@ -15,6 +15,7 @@ import (
"github.com/spf13/cobra"
"git.cloudinit.dev/coreci/orca/internal/certpaths"
"git.cloudinit.dev/coreci/orca/internal/linux"
"git.cloudinit.dev/coreci/orca/internal/engine"
"git.cloudinit.dev/coreci/orca/internal/model"
"git.cloudinit.dev/coreci/orca/internal/proxmox"
@@ -73,16 +74,22 @@ var nodeJoinCmd = &cobra.Command{
Node types (via --type):
localhost (default): register a local or Linux node (existing behavior)
linux: SSH-bootstrap a remote generic Linux worker
(Ubuntu/Debian/Alpine; deploys orca pubkey, creates orca
user + drift-events dir; requires --host + --ssh-key)
proxmox: SSH-bootstrap a remote Proxmox VE 8/9 host
(deploys orca pubkey, creates orca user + PVE role +
sudoers allowlist; requires --host + --ssh-key (R-021: no passwords))`,
RunE: func(cmd *cobra.Command, args []string) error {
if joinHostKeyFP != "" && joinType != "proxmox" {
return fmt.Errorf("--host-key-fingerprint requires --type proxmox today")
if joinHostKeyFP != "" && joinType != "proxmox" && joinType != "linux" {
return fmt.Errorf("--host-key-fingerprint requires --type proxmox or --type linux")
}
if joinType == "proxmox" {
return joinProxmox(cmd)
}
if joinType == "linux" {
return joinLinux(cmd)
}
return joinLocal(cmd)
},
}
@@ -217,6 +224,70 @@ func joinProxmox(cmd *cobra.Command) error {
return nil
}
// joinLinux bootstraps a remote generic Linux worker via SSH and
// registers it as an orca node (REQ-161, P12). Uses SSH key auth
// (R-021: no passwords).
func joinLinux(cmd *cobra.Command) error {
if joinHost == "" {
return fmt.Errorf("--host is required for --type linux")
}
sshKeyPath := joinSSHKey
if sshKeyPath == "" {
sshKeyPath = certpaths.SSHKeyPath()
}
if sshKeyPath == "" {
return fmt.Errorf("SSH key path is required for --type linux (R-021: no passwords; use --ssh-key or pre-stage the orca key)")
}
ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
defer cancel()
result, err := linux.BootstrapLinux(ctx, linux.Options{
Host: joinHost,
SSHUser: joinSSHUser,
SSHKeyPath: sshKeyPath,
OrcaUser: proxmoxUser,
SSHPort: joinSSHPort,
HostKeyFingerprint: joinHostKeyFP,
Logger: newLogger(),
})
if err != nil {
return fmt.Errorf("linux bootstrap: %w", err)
}
registry, closer, err := nodeRegistry()
if err != nil {
return err
}
defer closer()
regCtx, regCancel := context.WithTimeout(ctx, 5*time.Second)
defer regCancel()
node := &model.Node{
ID: uuid.NewString(),
Name: result.NodeName,
Address: result.NodeAddress,
State: model.NodeStateReady,
JoinedAt: time.Now().UTC(),
LastSeen: time.Now().UTC(),
Kind: string(model.NodeKindLinux),
OS: "linux",
}
if err := registry.Join(regCtx, node); err != nil {
return fmt.Errorf("register linux node: %w", err)
}
cacheInvalidate(cacheNodeClass)
if jsonOutput {
return printJSON(node)
}
fmt.Fprintf(cmd.OutOrStdout(), "\xe2\x9c\x93 Linux worker joined: %s (%s) at %s\n", node.ID, node.Name, node.Address)
if result.HostKeyFingerprint != "" {
fmt.Fprintf(cmd.OutOrStdout(), " host key: %s\n", result.HostKeyFingerprint)
}
return nil
}
var nodeLeaveCmd = &cobra.Command{
Use: "leave [node-id]",
Short: "Remove a node from the orca registry",