carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// An app's platform apex is <app>.<sqid>.<AppsDomain>, and it is that app's.
4// Two things can put somebody else there. The instances create door is one:
5// a sibling registering an instance at another app's apex, which that door
6// refuses (live or trashed — apiinstances.go). The other is the name arriving
7// AFTER the instance: an app created, renamed or moved to a name whose apex a
8// sibling already registered while the name was free. Nothing mints a route
9// over a host that already has one (the domains pass defers to byHost for
10// ever), so the new app would never have its own URL — and `carlos email
11// events push` would post its recipients' addresses to the sibling. This file
12// is the check those doors share, and a startup report for anything that got
13// there before the check existed.
14
15import (
16 "context"
17 "errors"
18 "log"
19 "os"
20 "strings"
21
22 "github.com/carlosframework/platform/internal/domains"
23 "github.com/carlosframework/platform/internal/provision"
24)
25
26// apexHost is the platform apex app would have in the account with this
27// sqid, or "" on a deployment with no apps domain (there is no apex space to
28// collide in).
29func (s *Server) apexHost(sqid, app string) string {
30 domain := strings.ToLower(strings.TrimSpace(s.cfg.AppsDomain))
31 if domain == "" {
32 return ""
33 }
34 return app + "." + sqid + "." + domain
35}
36
37// apexHold is who already holds an apex: the app a route or an instance
38// record belongs to — "" for a route that names no app (a status page, a bare
39// proxy, anything hand-added) — whether that app is in the trash, and the
40// account a route says it serves when that is not the apex's own.
41type apexHold struct {
42 Holder string
43 Trashed bool
44 OtherAccount string
45}
46
47// apexErr says which store an apexHolder failure came from, so a door can
48// tell a member the truth about what could not be checked.
49type apexErr struct {
50 bucket bool
51 err error
52}
53
54func (e *apexErr) Error() string { return e.err.Error() }
55func (e *apexErr) Unwrap() error { return e.err }
56
57// apexBucketFailure reports whether err came from the release store rather
58// than from this box's registry or database.
59func apexBucketFailure(err error) bool {
60 var ae *apexErr
61 return errors.As(err, &ae) && ae.bucket
62}
63
64// apexHolder answers whether the platform apex name would have in acct is
65// already held by something that is not name's own — nil when it is free.
66// It looks where a holder could be: a route at the host, whatever put it
67// there, and an instance record at it under any other app of the account,
68// live or trashed, which the provisioning reconciler would turn into a route.
69//
70// A route is name's own only when it says so on both counts: its app is name
71// and its account, if it names one, is acct. A route with no app at all holds
72// the host exactly as firmly as one with an app — the domains pass defers to
73// it all the same.
74//
75// The route half reads THIS console box's registry, as appHasRoutes and
76// routeAccountFor do; a route hand-added on another box is invisible to it,
77// while a reconciler-minted one is covered by its record. It fails CLOSED:
78// an error is an error, never "free".
79func (s *Server) apexHolder(ctx context.Context, acct *Account, name string) (*apexHold, error) {
80 host := s.apexHost(acct.Sqid, name)
81 if host == "" {
82 return nil, nil
83 }
84 return s.hostHolder(ctx, acct, host, name)
85}
86
87// hostHolder answers whether host is already held by an app of acct other
88// than own — nil when it is free, or when the only holder is own itself.
89// apexHolder is this question asked of an app's platform apex; the instance
90// create door asks it of whatever host a record is about to claim, because
91// instance records are keyed per app (provision.RecordKey) and nothing in
92// the key stops two apps of one account from both writing the same host. The
93// provisioning pass would then keep whichever record it listed first and
94// skip the other with a log line (provision/reconcile.go, "duplicate host"),
95// so the loser looks created and serves nothing, or serves the other app's
96// process under its own name.
97//
98// A flat canary host (<canary>-<app>.<domain>, discussion 207) is how that
99// became easy to reach: canary "a" of app "b-c" and canary "a-b" of app "c"
100// are the same label.
101//
102// Where a holder could be, and the fail-closed rule, are apexHolder's: a
103// route at the host, whatever put it there, and an instance record at it
104// under any other app of the account, live or trashed.
105func (s *Server) hostHolder(ctx context.Context, acct *Account, host, own string) (*apexHold, error) {
106 name := own
107 if s.cfg.Registry != "" {
108 reg, err := openRegistry(s.cfg.Registry)
109 if err != nil {
110 return nil, &apexErr{err: err}
111 }
112 rt, err := reg.Get(host)
113 if err != nil {
114 return nil, &apexErr{err: err}
115 }
116 if rt != nil && (rt.App != name || (rt.Account != "" && rt.Account != acct.Sqid)) {
117 hold := &apexHold{Holder: rt.App}
118 if rt.Account != "" && rt.Account != acct.Sqid {
119 hold.OtherAccount = rt.Account
120 }
121 return hold, nil
122 }
123 }
124 if s.cfg.Releases == nil {
125 return nil, nil
126 }
127 live, err := s.store.AppsIn(acct.ID)
128 if err != nil {
129 return nil, &apexErr{err: err}
130 }
131 trashed, err := s.store.TrashedAppsIn(acct.ID)
132 if err != nil {
133 return nil, &apexErr{err: err}
134 }
135 type candidate struct {
136 app string
137 trashed bool
138 }
139 var apps []candidate
140 for _, a := range live {
141 apps = append(apps, candidate{app: a})
142 }
143 for _, t := range trashed {
144 apps = append(apps, candidate{app: t.App, trashed: true})
145 }
146 for _, c := range apps {
147 if c.app == name {
148 continue
149 }
150 rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, c.app, host)
151 if errors.Is(err, os.ErrNotExist) {
152 continue
153 }
154 if err != nil {
155 return nil, &apexErr{bucket: true, err: err}
156 }
157 if rec != nil {
158 return &apexHold{Holder: c.app, Trashed: c.trashed}, nil
159 }
160 }
161 return nil, nil
162}
163
164// hostClaimHolder is the other half of hostHolder: the global hostname claim
165// (domains.ClaimKey, one object per host whoever asks) that an attached alias
166// is derived from. An alias has no instance record and, until the domains
167// pass materializes it, no route either, so hostHolder cannot see it; the
168// claim is the authoritative record and one GET. A route minted at a host
169// somebody else has claimed would shadow their alias at the edge.
170//
171// It returns who the claim belongs to when that is not own in acct, nil when
172// there is no claim or it is own's. A claim of another account comes back
173// with only OtherAccount set and no Holder: domains.ErrHostUnavailable is
174// deliberately neutral about who owns a hostname, and this door must not be a
175// way round that. Anything but "no such claim" is an error: it fails closed.
176func (s *Server) hostClaimHolder(ctx context.Context, acct *Account, host, own string) (*apexHold, error) {
177 if s.cfg.Releases == nil {
178 return nil, nil
179 }
180 c, err := domains.GetClaim(ctx, s.cfg.Releases, host)
181 if errors.Is(err, os.ErrNotExist) {
182 return nil, nil
183 }
184 if err != nil {
185 return nil, &apexErr{bucket: true, err: err}
186 }
187 switch {
188 case c.Account != acct.Sqid:
189 return &apexHold{OtherAccount: c.Account}, nil
190 case c.App != own:
191 return &apexHold{Holder: c.App}, nil
192 }
193 return nil, nil
194}
195
196// claimHeldSentence is hostHeldSentence for a hostname claim. A claim of
197// another account is refused without saying whose or that it is a claim.
198func claimHeldSentence(host string, hold *apexHold) string {
199 if hold.OtherAccount != "" {
200 return hostHeldSentence(host, nil)
201 }
202 return hostHeldSentence(host, hold)
203}
204
205// apexHeldSentence is the refusal, in one voice for every door that gives
206// it, with the advice that fits who holds the host.
207func apexHeldSentence(host string, hold *apexHold) string {
208 switch {
209 case hold == nil:
210 return host + " — this app's platform address — is already held"
211 case hold.OtherAccount != "" && hold.Holder != "":
212 return host + " — this app's platform address — is already routed to " + hold.Holder + " in account " + hold.OtherAccount + "; that is an operator repair"
213 case hold.OtherAccount != "":
214 return host + " — this app's platform address — is already routed for account " + hold.OtherAccount + "; that is an operator repair"
215 case hold.Holder == "":
216 return host + " — this app's platform address — already has a route with no app; an operator has to remove it first"
217 case hold.Trashed:
218 return host + " — this app's platform address — is already an instance of " + hold.Holder +
219 ", which is in the trash; restore " + hold.Holder + " and remove that instance, or purge it, first"
220 }
221 return host + " — this app's platform address — is already an instance of " + hold.Holder +
222 "; remove that instance first, or choose another name"
223}
224
225// hostHeldSentence is the instance create door's refusal for a host another
226// app of the account already holds (hostHolder), with the advice that fits
227// who holds it. It does not claim the host is anyone's apex: that is
228// apexHeldSentence's case, and the door's own apex check answers it first.
229func hostHeldSentence(host string, hold *apexHold) string {
230 switch {
231 case hold == nil:
232 return host + " is already held"
233 case hold.OtherAccount != "" && hold.Holder != "":
234 return host + " is already routed to " + hold.Holder + " in account " + hold.OtherAccount + "; that is an operator repair"
235 case hold.OtherAccount != "":
236 return host + " is already routed for account " + hold.OtherAccount + "; that is an operator repair"
237 case hold.Holder == "":
238 return host + " already has a route with no app; an operator has to remove it first"
239 case hold.Trashed:
240 return host + " is already held by " + hold.Holder + ", which is in the trash; choose another address"
241 }
242 return host + " is already held by " + hold.Holder + "; choose another address"
243}
244
245// ReportApexSquats logs, once, every route that sits at an app's platform apex
246// but belongs to a different app — the state the checks above now prevent,
247// left over from before they existed. It reports and repairs nothing: which
248// app is in the wrong place is a person's call, and moving a live route is
249// not something to do quietly at boot. Started from cmd/console, never from
250// NewServer.
251func (s *Server) ReportApexSquats(ctx context.Context) {
252 for _, line := range s.apexSquats(ctx) {
253 log.Printf("console: apex squat: %s", line)
254 }
255}
256
257// apexSquats is ReportApexSquats' finding, one line per squat.
258func (s *Server) apexSquats(ctx context.Context) []string {
259 domain := strings.ToLower(strings.TrimSpace(s.cfg.AppsDomain))
260 if domain == "" || s.cfg.Registry == "" {
261 return nil
262 }
263 reg, err := openRegistry(s.cfg.Registry)
264 if err != nil {
265 log.Printf("console: apex squat check: open registry: %v", err)
266 return nil
267 }
268 routes, err := reg.List()
269 if err != nil {
270 log.Printf("console: apex squat check: list routes: %v", err)
271 return nil
272 }
273 var out []string
274 for _, rt := range routes {
275 rest, ok := strings.CutSuffix(rt.Host, "."+domain)
276 if !ok {
277 continue
278 }
279 label, sqid, ok := strings.Cut(rest, ".")
280 if !ok || strings.Contains(sqid, ".") {
281 continue
282 }
283 // The same test apexHolder applies: a route is the label's own only
284 // when its app is the label and its account, if it names one, is the
285 // apex's.
286 if rt.App == label && (rt.Account == "" || rt.Account == sqid) {
287 continue
288 }
289 acct, err := s.store.AccountBySqid(sqid)
290 if err != nil || acct == nil {
291 continue
292 }
293 live, trashed, err := s.store.AppNameState(acct.ID, label)
294 if err != nil || (!live && !trashed) {
295 continue
296 }
297 state := "live"
298 if trashed {
299 state = "in the trash"
300 }
301 owner := rt.App
302 if owner == "" {
303 owner = "no app"
304 }
305 inAcct := rt.Account
306 if inAcct == "" {
307 inAcct = "no account"
308 }
309 out = append(out, rt.Host+" is app "+label+"'s platform apex ("+label+" is "+state+"), but the route belongs to "+owner+" ("+inAcct+")")
310 }
311 return out
312}
313