5c3b218822
gates / gates (push) Successful in 47s
The self-serve decommission already dropped the drive from the guest-bind record; the operator-signed one did
not, so a forgotten drive left a record the 20-second re-assert warned about forever ("skipping non-enrolled
drive"). DecommissionExecutor.SetGuestBinds; red-proved. Code only; the CHANGELOG follows the release (0.159.1).
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
107 lines
5.0 KiB
Go
107 lines
5.0 KiB
Go
package signedjobs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
)
|
|
|
|
// opDecommission is the op class this executor serves (mirrors reconcile.ClassDecommission; the
|
|
// literal avoids importing reconcile here just for the string).
|
|
const opDecommission = "decommission"
|
|
|
|
// IntentDecommissioner records a PERMANENT drive decommission keyed by the drive's storage
|
|
// durable-id. Satisfied by *storage.IntentStore (SetDecommissioned). Decommission is the operator's
|
|
// "permanent removal" intent: once recorded, the self-heal watchdog never auto-mounts the drive
|
|
// again (it SURVIVES absent/present), cleared only by an explicit re-commission.
|
|
type IntentDecommissioner interface {
|
|
SetDecommissioned(durableID string) error
|
|
}
|
|
|
|
// decommissionParams is the verified params of a decommission op. The drive is named by its STORAGE
|
|
// durable-id — the SAME key the storage observer / watchdog use (e.g. "uuid:<fs-uuid>"), NOT the
|
|
// device-level byid:/byuuid: scheme the storage_wipe op uses. That is deliberate: decommission
|
|
// manipulates the intent MAP the watchdog reads, so the signed id must be the watchdog's key or the
|
|
// recorded intent would never gate a remount.
|
|
type decommissionParams struct {
|
|
DurableID string `json:"durable_id"`
|
|
}
|
|
|
|
// DecommissionExecutor is the operator "permanent removal" consumer (slice 10 P3 completion). It
|
|
// makes the previously-unreachable IntentDecommissioned state reachable — but ONLY via a gate-
|
|
// VERIFIED operator signature (decommission is classified destructive in reconcile.Classify, so the
|
|
// signed-jobs gate refuses it pending_signature without a valid operator signature). It is distinct
|
|
// from a customer-confirmable safe eject: permanent removal is never customer-authorizable.
|
|
//
|
|
// The binding IS the durable-id: the signed op authorizes decommissioning exactly that drive on
|
|
// exactly the signed host (host_scope is checked by the gate). No /dev resolution is needed — and
|
|
// none is wanted, because a drive can legitimately be decommissioned while physically ABSENT.
|
|
type DecommissionExecutor struct {
|
|
intent IntentDecommissioner
|
|
binds GuestBindPruner // optional (v0.159.1): drop the drive from every guest's bind record, as the self-serve path does
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// GuestBindPruner is the per-guest enrolled-bind record (*localapi.GuestBindStore). The self-serve decommission prunes
|
|
// the drive from it; the signed one did not, so a forgotten drive (R-1124) left a record the 20-second guest-bind
|
|
// re-assert warned about forever ("skipping non-enrolled drive"), measured on demo-hp 2026-10-17.
|
|
type GuestBindPruner interface {
|
|
Guests() map[int][]string
|
|
Remove(vmid int, durableID string) error
|
|
}
|
|
|
|
// SetGuestBinds wires the bind record (nil-safe: no record, nothing pruned).
|
|
func (d *DecommissionExecutor) SetGuestBinds(b GuestBindPruner) { d.binds = b }
|
|
|
|
// NewDecommissionExecutor wires the executor to the agent's intent store.
|
|
func NewDecommissionExecutor(intent IntentDecommissioner, logger *slog.Logger) *DecommissionExecutor {
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
return &DecommissionExecutor{intent: intent, logger: logger}
|
|
}
|
|
|
|
// Execute implements signedjobs.Executor for the decommission op class.
|
|
func (d *DecommissionExecutor) Execute(_ context.Context, op string, params json.RawMessage) error {
|
|
if op != opDecommission {
|
|
return ErrNoExecutor // not ours — the runner leaves it queued for the owning executor
|
|
}
|
|
if d.intent == nil {
|
|
// No intent store on this host (open failure) → the op can't be honored. Don't clear it as
|
|
// "done"; surface a hard error so the runner logs it and the operator can retry once fixed.
|
|
return fmt.Errorf("decommission: no intent store wired on this host — cannot record decommission")
|
|
}
|
|
var p decommissionParams
|
|
if err := json.Unmarshal(params, &p); err != nil {
|
|
return fmt.Errorf("decommission: bad params: %w", err)
|
|
}
|
|
if p.DurableID == "" {
|
|
// The durable-id IS the resource binding; a decommission with no id is unbound — refuse.
|
|
return fmt.Errorf("decommission: op has no durable_id — refusing an unbound decommission")
|
|
}
|
|
|
|
// The gate has already verified the operator signature + burned the nonce durably BEFORE this
|
|
// point. This log line is the audit trail of an operator-authorized permanent decommission.
|
|
d.logger.Warn("decommission: executing operator-signed PERMANENT decommission", "durable_id", p.DurableID)
|
|
if err := d.intent.SetDecommissioned(p.DurableID); err != nil {
|
|
return fmt.Errorf("decommission: record intent for %q: %w", p.DurableID, err)
|
|
}
|
|
if d.binds != nil {
|
|
for vmid, ids := range d.binds.Guests() {
|
|
for _, id := range ids {
|
|
if id != p.DurableID {
|
|
continue
|
|
}
|
|
if err := d.binds.Remove(vmid, id); err != nil {
|
|
d.logger.Warn("decommission: guest-bind record not pruned", "vmid", vmid, "durable_id", id, "err", err)
|
|
} else {
|
|
d.logger.Info("decommission: drive removed from the guest's bind record", "vmid", vmid, "durable_id", id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
d.logger.Warn("decommission: operator-signed decommission complete (drive will never auto-mount)", "durable_id", p.DurableID)
|
|
return nil
|
|
}
|