package console import ( "context" "sort" "strings" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // layoutDowngradeBlocked is design ยง8.2's rollback rule at the console: a // channel pointer must not move below release.LayoutFloor while any box // could still hold a nested row. The box-side guard (host/systemapply.go) // refuses the install too, but only on the box that runs it; this one // stops the pointer moving at all, so no box has to refuse. // // It judges OBSERVED state, never the intent: a revert refused on a box // leaves a nested row under a flat intent. A box that has not reported in // time, or whose snapshot carries no layout at all, is unknown and blocks, // because the cost of guessing is a database hidden under a mount. The // empty string means the move is allowed. func (s *Server) layoutDowngradeBlocked(ctx context.Context, target string) string { if !release.PreLayout(target) { return "" } // Every fleet's boxes, the union carlos system status reads. Strict: // a fleet whose heartbeats cannot be read is a fleet whose rows are // unknown, and unknown blocks. boxes, _, err := s.systemBoxes(ctx, true) if err != nil { return target + " predates the instance layout and the fleet's layout is unknown: could not read the boxes' snapshots: " + err.Error() } now := time.Now() var reasons []string for _, b := range boxes { switch { case status.BoxStale(b, now, status.SnapshotPeriod): reasons = append(reasons, b.Label+" is stale") case b.Metrics == nil || b.Metrics.Layout == nil: // A box whose running release parses and is below the floor // cannot have run the layout pass (the pass and its report // arrived together, in the floor release), so its silence is // flat: today's fleet, on v0.21.0 to v0.24.2, keeps its // rollback. Silence at or above the floor, or from a box whose // release is a sha or "dev" (which could be a build with the // pass), is unknown and blocks. A box at or above the floor // with no activator publishes a report of its own (Mode // "unmanaged", host/layoutleg.go), so it is not silent here. if b.Metrics != nil && release.BelowFloor(b.Metrics.Release, release.LayoutFloor) { continue } reasons = append(reasons, b.Label+" reports no layout field") default: for host, row := range b.Metrics.Layout.Rows { switch row.Layout { case "nested", "fenced", "conflict": reasons = append(reasons, b.Label+": "+host+" is "+row.Layout) } } } } if len(reasons) == 0 { return "" } sort.Strings(reasons) return target + " predates the instance layout and the fleet is not flat: " + strings.Join(reasons, "; ") + ". Run `carlos system layout revert`, wait for `carlos status --layout` to show every row flat, then retry" } // layoutMigrateBlocked refuses `layout migrate` while any box in scope // (every box for "", else the one labelled box) runs a release below // release.LayoutFloor, runs one this console cannot place and reports no // layout, or is stale: a nested row minted or moved on such a box would never // wake, so nothing may migrate before the floor release is cut and rolled. // A named box that no snapshot knows is refused too: an intent for a box // nobody can see is one nobody can check. func (s *Server) layoutMigrateBlocked(ctx context.Context, box string) string { boxes, _, err := s.systemBoxes(ctx, true) if err != nil { return "layout migrate refused: could not read the boxes' snapshots: " + err.Error() } now := time.Now() var reasons []string seen := false for _, b := range boxes { if box != "" && b.Label != box { continue } seen = true switch { case status.BoxStale(b, now, status.SnapshotPeriod): reasons = append(reasons, b.Label+" is stale") case b.Metrics == nil || b.Metrics.Release == "": reasons = append(reasons, b.Label+" reports no release") case release.BelowFloor(b.Metrics.Release, release.LayoutFloor): reasons = append(reasons, b.Label+" runs "+b.Metrics.Release) case release.PreLayout(b.Metrics.Release) && b.Metrics.Layout == nil: // A sha or "dev" (a dev box tracking main) cannot be placed // against the floor, but a layout report proves the build // carries the pass, which is what the floor stands for; with // no report there is no such proof. reasons = append(reasons, b.Label+" runs "+b.Metrics.Release+", which cannot be placed against v"+release.LayoutFloor.String()+", and reports no layout") } } if box != "" && !seen { reasons = append(reasons, "box "+box+" has not reported") } if len(reasons) == 0 { return "" } sort.Strings(reasons) return "layout migrate refused: every box in scope must run v" + release.LayoutFloor.String() + " or later and be reporting: " + strings.Join(reasons, "; ") } // layoutRevertBlocked is #132's interlock on the layout door: a revert is // refused for any box in scope ("" is every box) that has a maintenance // worker head whose status is active or revoking. A worker's bearer // credential and its private input live on the box as the agent's uid, // and a flat tenant shares that uid and the agent's /proc view, so running // a tenant flat beside a worker hands it both. A detached head holds no // credential and does not block. // // Heads are read through cfg.WorkerHeads. On main the signed WorkerHead // shape exists (release/workercontrol.go) but no object key, store reader // or lifecycle writer does yet: #132's codec and store slice landed with // "no runtime caller". TODO(#132): wire WorkerHeads to the head store once // its key and reader land, and have the box-side revert pass refuse while // a durable worker token exists, as #132 also asks. Until then no worker // is enrolled anywhere (#132, #126), and a nil seam reads as "no heads". func (s *Server) layoutRevertBlocked(ctx context.Context, box string) string { if s.cfg.WorkerHeads == nil { return "" } heads, err := s.cfg.WorkerHeads(ctx) if err != nil { return "could not read the maintenance worker heads, so the revert is refused: " + err.Error() } var reasons []string for _, h := range heads { if box != "" && h.Box != box { continue } if h.Status == "active" || h.Status == "revoking" { reasons = append(reasons, h.Box+" has a maintenance worker ("+h.Status+")") } } if len(reasons) == 0 { return "" } sort.Strings(reasons) return "layout revert refused: " + strings.Join(reasons, "; ") + ". A flat tenant shares the worker's user and can read its credential; detach the worker first" }