9991e3d561
P10 — step-ca cluster CA (D-101) + lead eligibility (R-003). step-ca (internal/stepca/stepca.go, REQ-076): - Client wraps step CLI via SSH on the lead (no Go step-ca client lib). - Init: step ca init --name --dns --address --provisioner orca-admin. Root mirrored to paths.CACertPath() (cluster/ca.crt, v0.9 location). - IssueServerCert: 90-day (2160h) server cert with SANs. IssueSVID: 24h SVID with SPIFFE ID as URI SAN, provisioner orca-admin. RenewServerCert. Fingerprint. 96.6% coverage. Lead rules (internal/cluster/lead.go, R-003): - IsLeadEligible: linux=true, proxmox=false, unknown=false. - ValidateLeadRotation: refuses proxmox nodes with R-003 message, refuses unregistered nodes. 100% coverage. 26 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent. ---ci--- project: orca phase: P10 milestone: v0.9 status: execute ---/ci---
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
// Package cluster holds cluster-wide invariants that are not owned
|
|
// by a single subsystem. The first inhabitant is the lead-eligibility
|
|
// rule R-003: the cluster lead is always a bare Linux node; Proxmox
|
|
// nodes are permanently ineligible because their kernel is shared
|
|
// with guest VMs/containers and a lead failure there takes down the
|
|
// hypervisor too.
|
|
//
|
|
// The package is deliberately decoupled from the scheduler: it owns
|
|
// its own minimal NodeInfo (Hostname + Kind) so it can be unit-tested
|
|
// without pulling in the scheduler's capacity model. The scheduler's
|
|
// scheduler.NodeInfo has a `Kind string` field with the same values
|
|
// ("linux", "proxmox"); callers convert at the boundary.
|
|
package cluster
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// NodeKind classifies a node for lead-eligibility purposes (R-003).
|
|
// The string values match scheduler.NodeInfo.Kind and model.NodeKind
|
|
// so callers can pass either representation through without mapping.
|
|
type NodeKind string
|
|
|
|
const (
|
|
// NodeKindLinux is a bare Linux node — lead-eligible (R-003).
|
|
NodeKindLinux NodeKind = "linux"
|
|
// NodeKindProxmox is a Proxmox VE host — permanently lead-
|
|
// ineligible (R-003): the hypervisor kernel is shared with
|
|
// guests, so a lead process there is a blast-radius hazard.
|
|
NodeKindProxmox NodeKind = "proxmox"
|
|
)
|
|
|
|
// ErrProxmoxNotLead is returned when a Proxmox node is proposed as
|
|
// the new cluster lead (R-003).
|
|
var ErrProxmoxNotLead = errors.New("Proxmox nodes cannot hold the cluster lead role (R-003)")
|
|
|
|
// ErrNodeNotRegistered is returned when the proposed lead is not in
|
|
// the supplied node list at all.
|
|
var ErrNodeNotRegistered = errors.New("cluster: proposed lead is not a registered node")
|
|
|
|
// NodeInfo is the minimal node projection the lead rules need. It is
|
|
// intentionally smaller than scheduler.NodeInfo so this package has
|
|
// no upstream dependency on the scheduler.
|
|
type NodeInfo struct {
|
|
Hostname string
|
|
Kind NodeKind
|
|
}
|
|
|
|
// IsLeadEligible reports whether a node of the given kind may hold
|
|
// the cluster lead role (R-003). Linux nodes are eligible; Proxmox
|
|
// nodes are permanently ineligible; any other kind (including the
|
|
// empty string) is treated as ineligible.
|
|
func IsLeadEligible(kind NodeKind) bool {
|
|
return kind == NodeKindLinux
|
|
}
|
|
|
|
// ValidateLeadRotation checks that newLead is a registered Linux node
|
|
// and refuses Proxmox nodes with ErrProxmoxNotLead (R-003). It returns
|
|
// ErrNodeNotRegistered when newLead is not in nodes at all. The check
|
|
// is case-sensitive on hostname; node registries in Orca are
|
|
// case-normalized at the store layer so this matches reality.
|
|
func ValidateLeadRotation(newLead string, nodes []NodeInfo) error {
|
|
for _, n := range nodes {
|
|
if n.Hostname != newLead {
|
|
continue
|
|
}
|
|
if n.Kind == NodeKindProxmox {
|
|
return ErrProxmoxNotLead
|
|
}
|
|
if n.Kind == NodeKindLinux {
|
|
return nil
|
|
}
|
|
// Registered but neither linux nor proxmox (e.g. "localhost"
|
|
// auto-registered node, or a future kind). Treat unknown kinds
|
|
// as ineligible rather than guessing.
|
|
return fmt.Errorf("cluster: node %q has ineligible kind %q: %w", newLead, n.Kind, ErrProxmoxNotLead)
|
|
}
|
|
// Not found in the registry at all.
|
|
return fmt.Errorf("cluster: node %q not found: %w", newLead, ErrNodeNotRegistered)
|
|
}
|
|
|
|
// String renders a NodeKind for logs. It lowercases to match the
|
|
// on-disk representation regardless of how the caller constructed it.
|
|
func (k NodeKind) String() string { return strings.ToLower(string(k)) }
|