carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// The infrastructure inventory pass: once an hour, ask the deployment's
2// cloud provider what it is carrying and write the answer to
3// infraview.InventoryKey in the deployment bucket, where
4// /fleet/architecture reads it.
5//
6// The failure posture is cepull.go's, verbatim and for the same reasons.
7// Construction-class failures — no AWS config, no usable credential, an
8// AccessDenied-shaped answer to the first calls — latch the pass off for
9// the process after a single log line. There is no "the IAM grant might
10// appear later" retry worth making, and a deployment without the grant must
11// show "not granted" rather than filling its log with an hourly error.
12// Everything else — throttling, a 5xx, a network blip — logs and the next
13// tick tries again.
14package console
15
16import (
17 "context"
18 "encoding/json"
19 "errors"
20 "fmt"
21 "log"
22 "os"
23 "time"
24
25 "github.com/carlosframework/platform/internal/awsinv"
26 "github.com/carlosframework/platform/internal/infraview"
27)
28
29// infraPassInterval matches economicsPassInterval: an estate changes on the
30// timescale of a deploy, not a request, and the refresh button covers the
31// moment an operator actually needs it now.
32const infraPassInterval = time.Hour
33
34// infraPassTimeout bounds one whole inventory — every region, every
35// service. A pass that hangs must not hold the goroutine until the process
36// restarts.
37const infraPassTimeout = 2 * time.Minute
38
39// errInfraDisabled is infraPass's sentinel for "this process already
40// latched off" — StartInfraPass matches it with errors.Is to stop ticking
41// rather than log it hourly forever: the latch is permanent for the
42// process, so there is nothing left for the goroutine to do. A direct
43// caller (the refresh door, a later task) still gets the error back from
44// infraPass itself; only the ticker's own logging loop is silenced.
45var errInfraDisabled = errors.New("console: infra inventory disabled for this process")
46
47// StartInfraPass runs infraPass immediately and then hourly until ctx is
48// done — StartEconomicsPass's shape (economicspass.go:38).
49func (s *Server) StartInfraPass(ctx context.Context) {
50 if s.cfg.Releases == nil {
51 return // no bucket, nowhere to write, nothing to do
52 }
53 t := time.NewTicker(infraPassInterval)
54 defer t.Stop()
55 for {
56 if err := s.infraPass(ctx); err != nil {
57 if errors.Is(err, errInfraDisabled) {
58 return // permanent for the process — nothing left to tick for
59 }
60 log.Printf("console: infra pass: %v", err)
61 }
62 // The pass that FLIPS the latch returns its own wrapped error, not
63 // the sentinel — it is only the NEXT call that hits the top-of-
64 // function guard. Checking the latch itself here means the goroutine
65 // leaves as soon as there is provably nothing left to do, instead of
66 // parking on the ticker for an hour first. The log line above has
67 // already been printed exactly once, which is the whole contract.
68 if s.infraDisabled.Load() {
69 return
70 }
71 select {
72 case <-ctx.Done():
73 return
74 case <-t.C:
75 }
76 }
77}
78
79// resolveInventory is the seam: the test fake when one is set, otherwise a
80// lazily constructed awsinv.Provider reused for the life of the process.
81func (s *Server) resolveInventory(ctx context.Context) (infraview.Inventory, error) {
82 if s.inventory != nil {
83 return s.inventory(ctx)
84 }
85 if s.infraDisabled.Load() {
86 return infraview.Inventory{}, errInfraDisabled
87 }
88 s.infraOnce.Do(func() {
89 p, err := awsinv.New(ctx)
90 if err != nil {
91 // Construction failed. Auth-shaped means no grant: latch.
92 // Anything else means this process cannot build a provider at
93 // all, which is equally permanent for this process.
94 log.Printf("console: infra inventory unavailable: %v", err)
95 s.infraDisabled.Store(true)
96 return
97 }
98 s.infraProvider = p
99 })
100 if s.infraProvider == nil {
101 s.infraDisabled.Store(true)
102 return infraview.Inventory{}, errInfraDisabled
103 }
104 return s.infraProvider.Inventory(ctx)
105}
106
107// infraPass takes one inventory and stores it. An auth-shaped failure
108// latches; everything else is returned for the caller to log and retry.
109func (s *Server) infraPass(ctx context.Context) error {
110 if s.infraDisabled.Load() {
111 return errInfraDisabled
112 }
113 ctx, cancel := context.WithTimeout(ctx, infraPassTimeout)
114 defer cancel()
115
116 inv, err := s.resolveInventory(ctx)
117 if err != nil {
118 // awsinv.IsAuthShaped alone covers this seam: it matches both a
119 // denied smithy code AND awsinv.ErrNoGrant (the "every read was
120 // denied" assembly-time sentinel, which carries no smithy.APIError
121 // to classify by construction), so isAuthShapedError's narrower,
122 // smithy-only check can never turn a false into a true here.
123 if awsinv.IsAuthShaped(err) {
124 s.infraDisabled.Store(true)
125 return fmt.Errorf("console: infra inventory not granted (disabled for this process): %w", err)
126 }
127 return fmt.Errorf("console: infra inventory: %w", err)
128 }
129 return s.putInventory(ctx, inv)
130}
131
132// putInventory CAS-writes the snapshot — upsertBill's read-etag-then-put
133// (economicspass.go:942), with os.ErrNotExist meaning "first ever write".
134func (s *Server) putInventory(ctx context.Context, inv infraview.Inventory) error {
135 if s.cfg.Releases == nil {
136 return nil
137 }
138 _, etag, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey)
139 switch {
140 case err == nil:
141 case errors.Is(err, os.ErrNotExist):
142 etag = ""
143 default:
144 return fmt.Errorf("console: inventory: read: %w", err)
145 }
146 body, err := json.Marshal(inv)
147 if err != nil {
148 return fmt.Errorf("console: inventory: marshal: %w", err)
149 }
150 if _, err := s.cfg.Releases.Put(ctx, infraview.InventoryKey, body, etag); err != nil {
151 return fmt.Errorf("console: inventory: write: %w", err)
152 }
153 return nil
154}
155
156// loadInventory reads the stored snapshot for the page. The second return
157// is why there is nothing to show, and it distinguishes the three states
158// that must never be rendered as one: "never" (no pass has ever succeeded),
159// "ungranted" (this process latched off), and a read error. "no resources"
160// is a claim; none of these three earn it.
161func (s *Server) loadInventory(ctx context.Context) (*infraview.Inventory, string) {
162 if s.cfg.Releases == nil {
163 return nil, "never"
164 }
165 body, _, err := s.cfg.Releases.Get(ctx, infraview.InventoryKey)
166 switch {
167 case errors.Is(err, os.ErrNotExist):
168 if s.infraDisabled.Load() {
169 return nil, "ungranted"
170 }
171 return nil, "never"
172 case err != nil:
173 log.Printf("console: inventory read: %v", err)
174 return nil, "unreadable"
175 }
176 var inv infraview.Inventory
177 if err := json.Unmarshal(body, &inv); err != nil {
178 log.Printf("console: inventory parse: %v", err)
179 return nil, "unreadable"
180 }
181 return &inv, ""
182}
183