// The infrastructure inventory pass: once an hour, ask the deployment's // cloud provider what it is carrying and write the answer to // infraview.InventoryKey in the deployment bucket, where // /fleet/architecture reads it. // // The failure posture is cepull.go's, verbatim and for the same reasons. // Construction-class failures — no AWS config, no usable credential, an // AccessDenied-shaped answer to the first calls — latch the pass off for // the process after a single log line. There is no "the IAM grant might // appear later" retry worth making, and a deployment without the grant must // show "not granted" rather than filling its log with an hourly error. // Everything else — throttling, a 5xx, a network blip — logs and the next // tick tries again. package console import ( "context" "encoding/json" "errors" "fmt" "log" "os" "time" "github.com/carlosframework/platform/internal/awsinv" "github.com/carlosframework/platform/internal/infraview" ) // infraPassInterval matches economicsPassInterval: an estate changes on the // timescale of a deploy, not a request, and the refresh button covers the // moment an operator actually needs it now. const infraPassInterval = time.Hour // infraPassTimeout bounds one whole inventory — every region, every // service. A pass that hangs must not hold the goroutine until the process // restarts. const infraPassTimeout = 2 * time.Minute // errInfraDisabled is infraPass's sentinel for "this process already // latched off" — StartInfraPass matches it with errors.Is to stop ticking // rather than log it hourly forever: the latch is permanent for the // process, so there is nothing left for the goroutine to do. A direct // caller (the refresh door, a later task) still gets the error back from // infraPass itself; only the ticker's own logging loop is silenced. var errInfraDisabled = errors.New("console: infra inventory disabled for this process") // StartInfraPass runs infraPass immediately and then hourly until ctx is // done — StartEconomicsPass's shape (economicspass.go:38). func (s *Server) StartInfraPass(ctx context.Context) { if s.cfg.Releases == nil { return // no bucket, nowhere to write, nothing to do } t := time.NewTicker(infraPassInterval) defer t.Stop() for { if err := s.infraPass(ctx); err != nil { if errors.Is(err, errInfraDisabled) { return // permanent for the process — nothing left to tick for } log.Printf("console: infra pass: %v", err) } // The pass that FLIPS the latch returns its own wrapped error, not // the sentinel — it is only the NEXT call that hits the top-of- // function guard. Checking the latch itself here means the goroutine // leaves as soon as there is provably nothing left to do, instead of // parking on the ticker for an hour first. The log line above has // already been printed exactly once, which is the whole contract. if s.infraDisabled.Load() { return } select { case <-ctx.Done(): return case <-t.C: } } } // resolveInventory is the seam: the test fake when one is set, otherwise a // lazily constructed awsinv.Provider reused for the life of the process. func (s *Server) resolveInventory(ctx context.Context) (infraview.Inventory, error) { if s.inventory != nil { return s.inventory(ctx) } if s.infraDisabled.Load() { return infraview.Inventory{}, errInfraDisabled } s.infraOnce.Do(func() { p, err := awsinv.New(ctx) if err != nil { // Construction failed. Auth-shaped means no grant: latch. // Anything else means this process cannot build a provider at // all, which is equally permanent for this process. log.Printf("console: infra inventory unavailable: %v", err) s.infraDisabled.Store(true) return } s.infraProvider = p }) if s.infraProvider == nil { s.infraDisabled.Store(true) return infraview.Inventory{}, errInfraDisabled } return s.infraProvider.Inventory(ctx) } // infraPass takes one inventory and stores it. An auth-shaped failure // latches; everything else is returned for the caller to log and retry. func (s *Server) infraPass(ctx context.Context) error { if s.infraDisabled.Load() { return errInfraDisabled } ctx, cancel := context.WithTimeout(ctx, infraPassTimeout) defer cancel() inv, err := s.resolveInventory(ctx) if err != nil { // awsinv.IsAuthShaped alone covers this seam: it matches both a // denied smithy code AND awsinv.ErrNoGrant (the "every read was // denied" assembly-time sentinel, which carries no smithy.APIError // to classify by construction), so isAuthShapedError's narrower, // smithy-only check can never turn a false into a true here. if awsinv.IsAuthShaped(err) { s.infraDisabled.Store(true) return fmt.Errorf("console: infra inventory not granted (disabled for this process): %w", err) } return fmt.Errorf("console: infra inventory: %w", err) } return s.putInventory(ctx, inv) } // putInventory CAS-writes the snapshot — upsertBill's read-etag-then-put // (economicspass.go:942), with os.ErrNotExist meaning "first ever write". func (s *Server) putInventory(ctx context.Context, inv infraview.Inventory) error { if s.cfg.Releases == nil { return nil } _, etag, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey) switch { case err == nil: case errors.Is(err, os.ErrNotExist): etag = "" default: return fmt.Errorf("console: inventory: read: %w", err) } body, err := json.Marshal(inv) if err != nil { return fmt.Errorf("console: inventory: marshal: %w", err) } if _, err := s.cfg.Releases.Put(ctx, infraview.InventoryKey, body, etag); err != nil { return fmt.Errorf("console: inventory: write: %w", err) } return nil } // loadInventory reads the stored snapshot for the page. The second return // is why there is nothing to show, and it distinguishes the three states // that must never be rendered as one: "never" (no pass has ever succeeded), // "ungranted" (this process latched off), and a read error. "no resources" // is a claim; none of these three earn it. func (s *Server) loadInventory(ctx context.Context) (*infraview.Inventory, string) { if s.cfg.Releases == nil { return nil, "never" } body, _, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey) switch { case errors.Is(err, os.ErrNotExist): if s.infraDisabled.Load() { return nil, "ungranted" } return nil, "never" case err != nil: log.Printf("console: inventory read: %v", err) return nil, "unreadable" } var inv infraview.Inventory if err := json.Unmarshal(body, &inv); err != nil { log.Printf("console: inventory parse: %v", err) return nil, "unreadable" } return &inv, "" }