package edgesfile import ( "context" "log" "slices" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // EdgeIPs is the deployment's live edge address set: every EIP published by a // box whose STATUS SNAPSHOT is still fresh, unioned with env (the operator's // PLATFORM_EDGE_IPS). // // The union is the whole failure story. Nothing here deletes anything, so the // fold can only WIDEN what counts as "pointing at us" — which means a short // fold, an unreadable store, or a deployment with no snapshots at all // degrades to exactly the pre-fold behaviour (env only) and never below it. // Contrast internal/steering, which fails CLOSED on a short fold because a // short fold THERE deletes DNS records. // // Liveness is the status snapshot, never the edges file's own Generation: // Generation moves only when the file is WRITTEN, and the writer's change // gate exists precisely so an unchanged file is never republished — so a // healthy box's Generation is arbitrarily old and would fail any age bound. // statusTick's WriteBox is unconditional every tick, so BoxStale is a real // liveness signal. Without one, a retired box's lingering edges file would // keep a DEAD customer domain reading "pointing" forever. // // LIMITATION, and the reason for the log line below: that join reads both // halves out of the ONE store it is handed, but the two halves are written // through different ones. Edges files go through openDeployStore // (CARLOS_DEPLOYMENT_BUCKET, unprefixed); box snapshots go through // openStatusStore, which honours CARLOS_STATUS_BUCKET and rebases everything // under CARLOS_STATUS_PREFIX. They are the same store by DEFAULT, not by // guarantee — README.md's fleet section suggests a separate status bucket // outright — and on a deployment that splits them nothing here can see the // other half, so every edges file is dropped as unproven and the answer is // the env list. That degradation is SAFE (env-only is exactly the // pre-fold behaviour) but it is INVISIBLE, which is the failure class this // whole rule exists to avoid. So when files were found and none of them // joined, say so — configuration is the likely cause and no test can catch // it. func EdgeIPs(ctx context.Context, s release.Store, env []string, now time.Time) []string { out := append([]string(nil), env...) add := func(ip string) { if ip != "" && !slices.Contains(out, ip) { out = append(out, ip) } } if s == nil { return out } live := map[string]bool{} boxes, err := status.LoadBoxes(ctx, s) if err != nil { log.Printf("edgesfile: could not load box snapshots (%v) — edge set falls back to the configured list", err) return out } for _, b := range boxes { if !status.BoxStale(b, now, status.SnapshotPeriod) { live[b.Label] = true } } keys, err := s.List(ctx, Prefix) if err != nil { log.Printf("edgesfile: could not list %s (%v) — edge set falls back to the configured list", Prefix, err) return out } joined := 0 for _, key := range keys { body, _, err := s.Get(ctx, key) if err != nil { log.Printf("edgesfile: %s: %v", key, err) continue } f, err := Parse(body) if err != nil { log.Printf("edgesfile: %s: %v", key, err) continue } if !live[f.Box] { continue // no fresh snapshot: not evidence of a live edge } joined++ add(f.EIP) } // Files exist and not one of them proved live. A single retired box is // the ordinary reading of a partial miss, but a TOTAL miss usually is // not: either every box in the deployment went dark at once, or the two // halves of the join are looking at different buckets (see the doc // comment), or the box names on the two sides have drifted apart. All // three are silent otherwise — the answer is simply the env list, with // no error to return and nothing red anywhere. This line is the // tripwire. Nothing is logged when there are no files at all: a // deployment that has not published any is an ordinary state, not a // fault. if len(keys) > 0 && joined == 0 { log.Printf("edgesfile: %d edges file(s) under %s but no fresh box snapshot matched any of them — edge set is the configured list only (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(keys), Prefix) } return out } // DeploymentRoutes is every hostname a LIVE box publishes as a real route — // a home entry WITHOUT the alias mark. It is the evidence a hostname claim is // checked against before it may materialize as an alias. // // Alias entries are excluded deliberately and the exclusion is the whole // point: an alias IS a materialized claim, so counting one would make every // claim reserve its own hostname against itself and no re-attach or // re-derivation would ever succeed again. Only a NON-alias home entry — an // operator-minted or reconciler-minted route row — is evidence that some // other tenant's traffic already belongs to that name. // // Live is the same box-snapshot rule EdgeIPs uses, and for the same reason: a // retired box's lingering file must not go on reserving hostnames forever. // // Best-effort by contract: a store that cannot be read returns nil, and a nil // answer means "nothing known", which callers must treat as permissive. The // per-box byHost check and the fold's route preference are the other two // layers; this one is the only one that can act across boxes, but it must not // be able to break ordinary attach when the bucket is unavailable. func DeploymentRoutes(ctx context.Context, s release.Store, now time.Time) map[string]bool { if s == nil { return nil } live := map[string]bool{} boxes, err := status.LoadBoxes(ctx, s) if err != nil { log.Printf("edgesfile: could not load box snapshots (%v) — deployment route set unknown this pass", err) return nil } for _, b := range boxes { if !status.BoxStale(b, now, status.SnapshotPeriod) { live[b.Label] = true } } keys, err := s.List(ctx, Prefix) if err != nil { log.Printf("edgesfile: could not list %s (%v) — deployment route set unknown this pass", Prefix, err) return nil } out := map[string]bool{} joined := 0 for _, key := range keys { body, _, err := s.Get(ctx, key) if err != nil { log.Printf("edgesfile: %s: %v", key, err) continue } f, err := Parse(body) if err != nil { log.Printf("edgesfile: %s: %v", key, err) continue } if !live[f.Box] { continue // no fresh snapshot: not evidence of a live route } joined++ for host, h := range f.Hosts { if h.Role == RoleHome && !h.Alias { out[host] = true } } } // EdgeIPs' tripwire, and it matters MORE here. There a total miss // degrades to the operator's configured edge list, which is merely the // pre-fold behaviour; here it degrades to "no hostname is anyone's", // which silently disarms a SECURITY guard — the answer is an empty map, // no error, nothing red, and every claim materializes exactly as it did // before this rule existed. The likely causes are the same three // (deployment vs status bucket split, every box dark at once, box-name // drift), and they are all invisible otherwise. Silent on zero files: a // deployment that has published none is an ordinary state, not a fault. if len(keys) > 0 && joined == 0 { log.Printf("edgesfile: %d edges file(s) under %s but no fresh box snapshot matched any of them — the deployment route set is EMPTY this pass, so hostname claims are unchecked against other boxes' routes (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(keys), Prefix) } return out }