Files
orca/internal/cli/peer_setup.go
T
Jon Chery 03f3585f16 feat(P10b): drift detection (R-018/R-019/R-020, REQ-103..113)
internal/drift/drift.go: Detector (Watch via iter.Seq2, Aggregate,
Remediate with cooldown-on-success, Acknowledge), Config with tiered
cadence (critical 5s + Path units, standard 30s, default 60s).
internal/cli/drift.go: orca drift {show,watch,acknowledge,remediate,
config}. internal/emitter/drift_path.go: systemd Path+service unit
emitter (User=orca, ProtectSystem=strict). scripts/orca-drift-notify.sh
(sha256 event JSON), orca-remediate.sh (cooldown-on-success, transient
retry). Pre-flight gate (R-020, --force + per-ns scoping). orca
system user (REQ-111), NFS detection (D-233), orca job restart for
EnvironmentFile drift (D-235).

---ci---
project: orca
phase: 10b
milestone: v0.11
status: execute
---/ci---
2026-08-07 07:17:41 +00:00

137 lines
4.9 KiB
Go

// Package cli: peer_setup.go implements the orca system-user setup and
// NFS detection on peers (P10b-T8/T9, v0.11, REQ-111, REQ-112/D-233).
//
// `orca node join` now also creates the `orca` system user on the peer
// (so the systemd Path-unit services, which run as User=orca, have a
// uid to run as). It also detects whether /etc/orca is on an NFS mount
// and, when it is, skips emitting Path units for paths under /etc/orca
// (falling back to polling for those paths — systemd Path units on NFS
// are unreliable because inotify does not fire reliably over NFS).
//
// The setup is idempotent: re-running on an already-configured peer is
// a no-op. A --no-orca-user flag skips user creation (for environments
// with existing service accounts).
package cli
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
)
var peerSetupNoOrcaUser bool
// peerSetupTransport is the SSH surface the peer-setup code needs. It
// mirrors driftTransport; tests substitute a mock.
type peerSetupTransport interface {
Exec(ctx context.Context, peer string, cmd string) ([]byte, error)
}
// peerSetupTransportOverride is the test seam.
var peerSetupTransportOverride peerSetupTransport
func peerSetupTransportFromCtx() (peerSetupTransport, error) {
if peerSetupTransportOverride != nil {
return peerSetupTransportOverride, nil
}
t, err := driftTransportFromCtx()
if err != nil {
return nil, err
}
return t, nil
}
// PeerSetupResult records what the peer setup did.
type PeerSetupResult struct {
UserCreated bool `json:"user_created"`
EventsDir string `json:"events_dir"`
NFSOnOrca bool `json:"nfs_on_orca"`
NFSMsg string `json:"nfs_msg,omitempty"`
}
// setupOrcaUser runs the idempotent `useradd -r orca` and creates the
// drift-events directory owned by orca:orca on the peer. Returns the
// result; a transient SSH failure returns the error (no partial state).
func setupOrcaUser(ctx context.Context, transport peerSetupTransport, peer string) (*PeerSetupResult, error) {
if peer == "" {
return nil, fmt.Errorf("peer setup: peer is empty")
}
res := &PeerSetupResult{EventsDir: "/etc/orca/state/drift-events"}
if !peerSetupNoOrcaUser {
useraddCmd := "useradd -r orca -s /usr/sbin/nologin 2>/dev/null || true"
if _, err := transport.Exec(ctx, peer, useraddCmd); err != nil {
return nil, fmt.Errorf("peer setup: useradd: %w", err)
}
res.UserCreated = true
}
mkdirCmd := fmt.Sprintf("mkdir -p %s && %s", res.EventsDir, chownDriftEvents(res.EventsDir))
if _, err := transport.Exec(ctx, peer, mkdirCmd); err != nil {
return nil, fmt.Errorf("peer setup: mkdir drift-events: %w", err)
}
nfs, msg := detectNFS(ctx, transport, peer, "/etc/orca")
res.NFSOnOrca = nfs
res.NFSMsg = msg
return res, nil
}
// chownDriftEvents returns the chown command for the drift-events dir.
// When --no-orca-user is set the orca user may not exist; chown only
// when the user was created.
func chownDriftEvents(dir string) string {
if peerSetupNoOrcaUser {
return "true"
}
return fmt.Sprintf("chown orca:orca %s 2>/dev/null || true", dir)
}
// detectNFS checks whether the given path is on an NFS mount by running
// `stat -f -c %T <path>` on the peer. When the fs type contains "nfs"
// it returns (true, msg). Best-effort: a stat failure returns
// (false, "stat unavailable").
func detectNFS(ctx context.Context, transport peerSetupTransport, peer, path string) (bool, string) {
out, err := transport.Exec(ctx, peer, fmt.Sprintf("stat -f -c %%T %s 2>/dev/null || echo unknown", shellQuoteDrift(path)))
if err != nil {
return false, "stat unavailable: " + err.Error()
}
fsType := strings.TrimSpace(string(out))
if strings.Contains(fsType, "nfs") {
return true, fmt.Sprintf("%s is on NFS (%s); skipping Path units for /etc/orca paths", path, fsType)
}
return false, fsType
}
var peerSetupCmd = &cobra.Command{
Use: "peer-setup <peer>",
Short: "Create the orca system user + drift-events dir on a peer (REQ-111)",
Long: `SSH to <peer> and idempotently create the orca system user
(useradd -r orca -s /usr/sbin/nologin) and /etc/orca/state/drift-events/
owned by orca:orca. Also detects NFS on /etc/orca (REQ-112/D-233) and
logs a warning when /etc/orca is on NFS (Path units are skipped for
those paths in that case). Use --no-orca-user to skip user creation
(for environments with an existing service account).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
peer := args[0]
transport, err := peerSetupTransportFromCtx()
if err != nil {
return fmt.Errorf("ssh transport: %w", err)
}
res, err := setupOrcaUser(cmd.Context(), transport, peer)
if err != nil {
return err
}
printResult(fmt.Sprintf("✓ Peer %s set up (user=%t, nfs=%t)", peer, res.UserCreated, res.NFSOnOrca), res)
return nil
},
}
func init() {
peerSetupCmd.Flags().BoolVar(&peerSetupNoOrcaUser, "no-orca-user", false, "skip orca system user creation (env has existing service account)")
rootCmd.AddCommand(peerSetupCmd)
}