package console import ( "context" "sort" "time" "github.com/carlosframework/platform/internal/status" ) // updatesEnforced is ยง4.4's roll-order gate: pin, manual and major may be // turned on only while every box in the deployment reports a binary that // applies them and protects the builds they name from retention. The housekeeper // duty moves between boxes, so one old box anywhere could prune a pinned // build. Expected boxes are the edges records and active fleet members, as for // the GeoIP rollout verdict (geoipInventory); a box that is expected and has // no fresh heartbeat blocks, and so does an incomplete read. Unlike the GeoIP // verdict, an incomplete heartbeat read blocks here too, and so do an // inventory that found no box at all and a fleet registry that was never // read (unset, absent, or failing): a gate that has not seen every box // cannot vouch for every box. // // The blocking lines name platform box labels and fleet names. They are // operator detail, logged by the doors and never shown to app members. func (s *Server) updatesEnforced(ctx context.Context) (ok bool, blocking []string, err error) { inv, err := s.geoipInventory(ctx) if err != nil { return false, nil, err } now := time.Now() keys := map[geoipBoxKey]bool{} for k := range inv.expected { keys[k] = true } for k, b := range inv.boxes { if !status.BoxStale(b, now, status.SnapshotPeriod) { keys[k] = true } } for k := range keys { b, seen := inv.boxes[k] switch { case !seen: blocking = append(blocking, k.String()+": no heartbeat") case status.BoxStale(b, now, status.SnapshotPeriod): blocking = append(blocking, k.String()+": no heartbeat since "+b.GeneratedAt.UTC().Format(time.RFC3339)) case b.Metrics == nil || b.Metrics.UpdateSettings < status.UpdateSettingsProtocol: blocking = append(blocking, k.String()+": runs a binary that does not apply update settings") } } if len(keys) == 0 { blocking = append(blocking, "no box was found to report a binary") } // An unset or absent registry is a fleet list nobody read, not an empty // one (Astra r1 #1): the GeoIP verdict may read it as platform-only, but // this gate cannot vouch for fleet boxes it never listed. A registry that // failed to open or list also leaves expectedComplete false; this one // line, with the reason, covers both. if !inv.fleetsListed { blocking = append(blocking, "the fleet registry could not be read ("+inv.fleetsUnread+"), so no fleet box was checked") } if (inv.fleetsListed && !inv.expectedComplete) || !inv.heartbeatsComplete { blocking = append(blocking, "the list of boxes could not be read in full") } sort.Strings(blocking) return len(blocking) == 0, blocking, nil }