// 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 ` 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 ", Short: "Create the orca system user + drift-events dir on a peer (REQ-111)", Long: `SSH to 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) }