carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Task 6: the self-hoster's "what would I publish?" check. DryRun runs the
2// REAL writer/merger/publisher path — CollectBox, WriteBox, BuildFleet,
3// PublishFleet — over a fixture registry deliberately stuffed with needles
4// (hostnames, a machine id, mid-band fractions), then reads every object it
5// just published back out of the store and fails loudly if any needle
6// survived. It exists so an operator can run `carlos status -dry-run <dir>`
7// against a scratch directory BEFORE ever pointing this binary at a real
8// bucket, and see for themselves that the privacy contract holds end to end
9// rather than trusting the package's own unit tests never regress.
10package status
11
12import (
13 "context"
14 "encoding/json"
15 "fmt"
16 "io"
17 "strings"
18 "time"
19
20 "github.com/carlosframework/platform/internal/registry"
21 "github.com/carlosframework/platform/internal/release"
22)
23
24// dryRunOptInApp is the ONE app in DryRun's fixture registry that opts into
25// the status page via StatusNamed. It exists to prove the leak-scan doesn't
26// overreach: an opted-in app name is a deliberate publication, not a leak,
27// so DryRun asserts this name DOES appear in fleet.apps — the flip side of
28// every other needle below, which must NOT appear anywhere.
29const dryRunOptInApp = "demoapp"
30
31// dryRunNeedles are every fixture secret DryRun seeds into its throwaway
32// registry and probes. None of these substrings may appear in ANY object
33// DryRun publishes. "prod-secret" is listed both whole and as a bare prefix
34// so a leak that only carries part of the hostname (e.g. truncated in some
35// future summary field) still trips the scan. The fixture fractions are
36// checked as JSON numbers separately: timestamps can legitimately contain
37// the same digits in their fractional seconds.
38var dryRunNeedles = []string{
39 "prod-secret.example.com", // fixture hostname
40 "prod-secret", // hostname's leak-prone substring on its own
41 "example.com", // the fixture domain generally
42 "ip-10-0-0-7", // fixture machine id
43}
44
45const (
46 dryRunMemFrac = 0.73
47 dryRunDiskFrac = 0.95
48)
49
50var dryRunFractionNeedles = []float64{dryRunMemFrac, dryRunDiskFrac}
51
52// dryRunMetricsNeedles are the distinctive raw values Task 5's BoxMetrics
53// carries — real numbers and strings that Box.Metrics is the ONE deliberate
54// exception privileged to publish (see snapshot.go's package doc), and which
55// therefore MUST appear in the published box object. Unlike dryRunNeedles
56// above (banned everywhere, box object included), these are allowed inside
57// status/boxes/*.json and banned only from everything else DryRun publishes
58// — see the scope split in DryRun's scan loop below. Each value is
59// deliberately distinctive (the repeated 42s, a value with no legitimate
60// reason to appear in a template or a fleet-wide aggregate) so a false
61// negative from an incidental digit collision is effectively impossible.
62var dryRunMetricsNeedles = []string{
63 "4242424242", // fixture MemTotalBytes, verbatim
64 "4242424241", // fixture MemAvailBytes, verbatim
65 "4242424240", // fixture DiskFreeBytes, verbatim
66 "42.42", // fixture Load1, verbatim
67 "dryrun4242sha", // fixture Release, verbatim
68 "reconnecting", // fixture Tunnel, verbatim (a real, valid Tunnel value)
69}
70
71// dryRunRouteVersionHost and dryRunRouteVersionVersion are Task 8's own
72// needle pair (#78): a THIRD un-opted route, distinct from leakyapp and
73// svc/internal above, whose only purpose is carrying a Version — the one
74// thing that makes CollectBox populate Box.Metrics.RouteVersions, this
75// package's one hostname-keyed field. Neither string reuses any existing
76// needle (not dryRunNeedles, not dryRunMetricsNeedles, not demoapp's own
77// "abc1234"): route_versions gets its own dedicated positive/negative check,
78// scope-split exactly like dryRunMetricsNeedles below, rather than riding
79// along on an assertion built for something else.
80const (
81 dryRunRouteVersionHost = "staged-secret.internal"
82 dryRunRouteVersionVersion = "stagedver9012"
83)
84
85// dryRunRouteVersionNeedles is the scope-split pair for the fixture above:
86// REQUIRED inside status/boxes/*.json (proof that RouteVersions actually
87// attached — an unexercised field is a field this self-check can't vouch
88// for) and BANNED everywhere else DryRun publishes (proof that a
89// route_versions hostname, unlike a band word or an aggregate, never
90// reaches fleet.json or the rendered page). Same reasoning and the same
91// scan-loop shape as dryRunMetricsNeedles, because RouteVersions lives in
92// the identical private home: Box.Metrics.
93var dryRunRouteVersionNeedles = []string{dryRunRouteVersionHost, dryRunRouteVersionVersion}
94
95// dryRunRouteServingHost and dryRunRouteServingServing are Task 13's needle
96// pair (plan 5's ServingVersion): a THIRD un-opted route, distinct from
97// all others, whose only purpose is carrying a ServingVersion — the one
98// thing that makes CollectBox populate Box.Metrics.RouteServing. Neither
99// string reuses any existing needle: route_serving gets its own
100// dedicated positive/negative check, scope-split exactly like
101// dryRunRouteVersionNeedles, rather than riding along on an assertion
102// built for something else.
103const (
104 dryRunRouteServingHost = "serving-secret.internal"
105 dryRunRouteServingServing = "servingver5678"
106)
107
108// dryRunRouteServingNeedles is the scope-split pair for the fixture above:
109// REQUIRED inside status/boxes/*.json (proof that RouteServing actually
110// attached — an unexercised field is a field this self-check can't vouch
111// for) and BANNED everywhere else DryRun publishes (proof that
112// route_serving hostnames never reach fleet.json or the rendered page).
113// Same reasoning and the same scan-loop shape as dryRunRouteVersionNeedles,
114// because RouteServing lives in the identical private home: Box.Metrics.
115var dryRunRouteServingNeedles = []string{dryRunRouteServingHost, dryRunRouteServingServing}
116
117// dryRunUpstreamHost and dryRunUpstreamAddr are the multi-upstream plan's own
118// needle pair (Task 6) for Box.Metrics.Upstreams — the probe table, which is
119// the most identifying thing this block carries: a route hostname as the KEY
120// and a real backend address inside the value. Distinct strings again from
121// every other needle set here (an address shape no other fixture uses, so a
122// digit collision with dryRunMetricsNeedles' 4242s is impossible), and scoped
123// the same way for the same reason: REQUIRED in status/boxes/*.json, so the
124// self-check can't pass by never populating the field, and BANNED everywhere
125// else DryRun publishes.
126const (
127 dryRunUpstreamHost = "upstream-secret.internal"
128 dryRunUpstreamAddr = "10.77.77.7:7717"
129)
130
131var dryRunUpstreamNeedles = []string{dryRunUpstreamHost, dryRunUpstreamAddr}
132
133// dryRunConfigRefusalHost is the config guard's needle (amadan #213):
134// Box.Metrics.ConfigRefusals names the hosts a keyless box refuses, so the
135// same scope split applies. REQUIRED in status/boxes/*.json and BANNED in
136// every other published object.
137const dryRunConfigRefusalHost = "refused-secret.internal"
138
139var dryRunConfigRefusalNeedles = []string{dryRunConfigRefusalHost}
140
141// dryRunReleaseRefusalHost is the release refusal needle (D1):
142// Box.Metrics.ReleaseRefusals names the hosts whose build this box rolled
143// back, so the same scope split applies. REQUIRED in status/boxes/*.json and
144// BANNED in every other published object.
145const dryRunReleaseRefusalHost = "refused-build.internal"
146
147var dryRunReleaseRefusalNeedles = []string{dryRunReleaseRefusalHost}
148
149// dryRunProofWaitHost is the proof wait needle (D3): Box.Metrics.ProofWaits
150// names the hosts this box is holding for a dev proof, so the same scope
151// split applies. REQUIRED in status/boxes/*.json and BANNED in every other
152// published object.
153const dryRunProofWaitHost = "proof-wait.internal"
154
155var dryRunProofWaitNeedles = []string{dryRunProofWaitHost}
156
157// dryRunRolloutHost is the rollout report needle: Box.Metrics.Rollout names
158// the instances the rollout scheduler manages (hosts, failure records), so
159// the same scope split applies. REQUIRED in status/boxes/*.json and BANNED in
160// every other published object.
161const dryRunRolloutHost = "rollout-host.internal"
162
163var dryRunRolloutNeedles = []string{dryRunRolloutHost}
164
165// dryRunBuildStateHost and dryRunBuildStateBuild are the build-state needles
166// (settings spec rev 11 §4.4): Box.Metrics.BuildStates names the hosts this
167// box runs and the builds their build states name, so the same scope split
168// applies. REQUIRED in status/boxes/*.json and BANNED in every other
169// published object.
170const (
171 dryRunBuildStateHost = "build-state-host.internal"
172 dryRunBuildStateBuild = "v-dryrun-build-state"
173)
174
175var dryRunBuildStateNeedles = []string{dryRunBuildStateHost, dryRunBuildStateBuild}
176
177// dryRunUpdatesHost and dryRunUpdatesBuild are the update-decision needles
178// (settings spec §4.3): Box.Metrics.Updates names the hosts this box runs and
179// the builds root chose for them, so the same scope split applies. REQUIRED in
180// status/boxes/*.json and BANNED in every other published object.
181const (
182 dryRunUpdatesHost = "updates-host.internal"
183 dryRunUpdatesBuild = "v-dryrun-updates"
184)
185
186var dryRunUpdatesNeedles = []string{dryRunUpdatesHost, dryRunUpdatesBuild}
187
188// ScanForLeaks reports which of banned's strings appear anywhere in j. It is
189// the single implementation both DryRun's self-check and
190// contract_test.go's scanForLeaks test helper run — the test package can't
191// call an unexported function here, and duplicating the logic in both places
192// would let them silently drift into checking different things, which is
193// exactly the kind of gap a needle could slip through.
194func ScanForLeaks(j []byte, banned []string) []string {
195 s := string(j)
196 var found []string
197 for _, b := range banned {
198 if strings.Contains(s, b) {
199 found = append(found, b)
200 }
201 }
202 return found
203}
204
205// dryRunFixtures builds a registry and probe set stuffed with needles: three
206// un-opted hosts (one instance, one service, and one instance whose only job
207// is carrying a Version — see dryRunRouteVersionNeedles) that must vanish
208// from every public surface, one opted-in app that must survive by name, and
209// mid-band fractions — 0.73 and 0.95 sit inside the "busy"/"at-capacity"
210// bands per snapshot.go's 0.70/0.90 cuts, so banding actually gets exercised
211// rather than trivially cleared at an edge.
212func dryRunFixtures() ([]registry.Route, Probes) {
213 routes := []registry.Route{
214 {
215 Host: "prod-secret.example.com", Kind: "instance", App: "leakyapp",
216 Status: "ready", Socket: "/run/carlos/leakyapp.sock",
217 },
218 {
219 Host: "demoapp.internal", Kind: "instance", App: dryRunOptInApp,
220 Status: "ready", StatusNamed: true, Channel: "stable", Version: "abc1234",
221 },
222 {
223 Host: "svc-secret.example.com", Kind: "service", App: "svc/internal",
224 Status: "ready",
225 },
226 {
227 // Un-opted, so its host must not surface via the apps feed either
228 // — the ONLY reason this route exists is to give CollectBox a
229 // Version to fold into Box.Metrics.RouteVersions (see the needle
230 // pair's own doc comment above).
231 Host: dryRunRouteVersionHost, Kind: "instance", App: "stagedapp",
232 Status: "ready", Version: dryRunRouteVersionVersion,
233 },
234 {
235 // Un-opted, so its host must not surface via the apps feed either
236 // — the ONLY reason this route exists is to give CollectBox a
237 // ServingVersion to fold into Box.Metrics.RouteServing (see the
238 // needle pair's own doc comment above).
239 Host: dryRunRouteServingHost, Kind: "sidecar", App: "servingapp",
240 Status: "ready", ServingVersion: dryRunRouteServingServing,
241 },
242 }
243
244 memFrac, diskFrac := dryRunMemFrac, dryRunDiskFrac
245 lag := int64(120)
246 adopting := true // a known reading, so adoption_running publishes non-null and gets scanned
247 configPath := true // ditto for config_path_running (issue #44)
248 restartPath := true // ditto for restart_path_running (issue #113)
249 expiredCanaries := 2 // ditto for expired_canaries — a known count, not the unexercised nil path
250
251 // Task 5's raw Metrics fixtures: distinctive values (see
252 // dryRunMetricsNeedles) that MUST land in the published box object and
253 // MUST NOT land anywhere else DryRun publishes — the positive half of
254 // proving Box.Metrics's private exception actually stays private.
255 memTotal, memAvail, diskFree := int64(4242424242), int64(4242424241), int64(4242424240)
256 load1 := 42.42
257
258 probes := Probes{
259 MachineID: "ip-10-0-0-7",
260 Region: "us-east-1",
261 MemFrac: &memFrac,
262 DiskFrac: &diskFrac,
263 LagSecs: &lag,
264 AdoptRunning: &adopting,
265 ConfigPathRunning: &configPath,
266 RestartPathRunning: &restartPath,
267 ExpiredCanaries: &expiredCanaries,
268 // Every hibernating route reports "listening" so Awake/Asleep publish
269 // non-null — an unexercised null field is a field DryRun can't scan.
270 Listening: func(string) *bool { v := true; return &v },
271 MemTotalBytes: &memTotal,
272 MemAvailBytes: &memAvail,
273 Load1: &load1,
274 DiskFreeBytes: &diskFree,
275 Release: "dryrun4242sha",
276 Tunnel: "reconnecting",
277 // One refused host (amadan #213), so the scope check below has
278 // something to require in the private block and ban elsewhere.
279 ConfigRefusals: []ConfigRefusal{
280 {Host: dryRunConfigRefusalHost, Reason: "no_key", Since: time.Now().Add(-time.Hour), Last: time.Now(), Count: 2},
281 },
282 ConfigRefusalsTotal: 1,
283 // One rolled-back build (D1), for the same scope check.
284 ReleaseRefusals: []ReleaseRefusal{
285 {Host: dryRunReleaseRefusalHost, Version: "v-dryrun", Reason: "exited: status=2", RefusedAt: time.Now(), Kind: "wrong-program"},
286 },
287 // One host held for a dev proof (D3), for the same scope check.
288 ProofWaits: []ProofWait{
289 {Host: dryRunProofWaitHost, Version: "v-dryrun", SHA256: "dryrunproofsha", Prover: "dev", Reason: "no proof from dev", Since: time.Now()},
290 },
291 // One held instance and its failure record, for the rollout scope check.
292 Rollout: &RolloutReport{
293 Hosts: map[string]HostRollout{
294 dryRunRolloutHost: {Account: "dryrun", App: "app", Installed: "v-dryrun", Serving: "v-old", Good: "v-old", Awake: true,
295 Attempt: AttemptFailed, AttemptBuild: "v-dryrun", HeldBy: "v-dryrun", Reason: ReasonHealth},
296 },
297 Apps: map[string]AppRollout{"dryrun/app": {Queued: 1}},
298 Failures: []RolloutFailure{
299 {Account: "dryrun", App: "app", Build: "v-dryrun", Host: dryRunRolloutHost, Mode: "overlap", Reason: ReasonHealth, PromotedAt: time.Now(), At: time.Now()},
300 },
301 },
302 // One host's build-state builds, for the build_states scope check.
303 BuildStates: map[string]BuildStatePins{
304 dryRunBuildStateHost: {Account: "dryrun", App: "app", Builds: []string{dryRunBuildStateBuild}, Complete: true},
305 },
306 // One host's update decision, for the updates scope check.
307 UpdateSettings: UpdateSettingsProtocol,
308 Updates: map[string]HostUpdates{
309 dryRunUpdatesHost: {Desired: dryRunUpdatesBuild, Reason: "manual", Completed: 1},
310 },
311 // The probe table, keyed by a route host and carrying a backend
312 // address — see dryRunUpstreamNeedles. One up and one down upstream,
313 // so the down one's reason string is exercised too.
314 Upstreams: map[string][]UpstreamState{
315 dryRunUpstreamHost: {
316 {Addr: dryRunUpstreamAddr, Up: true, Since: time.Now().Add(-time.Hour)},
317 {Addr: "10.77.77.8:7717", Up: false, Since: time.Now().Add(-time.Minute), Fails: 3, LastError: "dial: connection refused"},
318 },
319 },
320 }
321 return routes, probes
322}
323
324// appPublished reports whether name appears in the fleet's opted-in apps —
325// used to assert dryRunOptInApp made it through, the positive half of the
326// privacy contract that a pure leak-scan (which only ever proves absence)
327// can't check on its own.
328func appPublished(f *Fleet, name string) bool {
329 for _, a := range f.Apps {
330 if a.Name == name {
331 return true
332 }
333 }
334 return false
335}
336
337// bandFieldKeys are the JSON key names that ever carry a band word (see
338// snapshot.go's loadWords/capacityWords): Box's "load" and "capacity" fields
339// (the latter a bare string), each boxes[] element's "load", and Fleet's
340// nested "capacity.level". Keyed by field name rather than a fixed JSON
341// path so the same check works unmodified against Box's shape, Fleet's
342// shape, and any future struct that reuses these field names.
343var bandFieldKeys = map[string]bool{"load": true, "capacity": true, "level": true}
344
345// scanBandFieldsForDigits walks decoded JSON looking for band-word fields
346// and flags any whose value carries a digit. Per the plan's privacy rule, a
347// band field is a WORD (comfortable/busy/critical, plenty/getting-full/at-
348// capacity), never a number — a digit surviving into one of these fields
349// means a raw fraction escaped banding somewhere between CollectBox and the
350// published bytes, independent of whether it happens to match one of the
351// exact fixture fractions in dryRunNeedles.
352func scanBandFieldsForDigits(v interface{}) []string {
353 var bad []string
354 var walk func(interface{})
355 walk = func(v interface{}) {
356 switch t := v.(type) {
357 case map[string]interface{}:
358 for k, val := range t {
359 if bandFieldKeys[k] {
360 if s, ok := val.(string); ok && strings.ContainsAny(s, "0123456789") {
361 bad = append(bad, fmt.Sprintf("%s=%q", k, s))
362 }
363 }
364 walk(val)
365 }
366 case []interface{}:
367 for _, elem := range t {
368 walk(elem)
369 }
370 }
371 }
372 walk(v)
373 return bad
374}
375
376func scanForNumericLeaks(v interface{}, banned []float64) []string {
377 seen := map[float64]bool{}
378 var walk func(interface{})
379 walk = func(v interface{}) {
380 switch t := v.(type) {
381 case map[string]interface{}:
382 for _, value := range t {
383 walk(value)
384 }
385 case []interface{}:
386 for _, value := range t {
387 walk(value)
388 }
389 case float64:
390 for _, needle := range banned {
391 if t == needle {
392 seen[needle] = true
393 }
394 }
395 }
396 }
397 walk(v)
398 var found []string
399 for _, needle := range banned {
400 if seen[needle] {
401 found = append(found, fmt.Sprintf("%g", needle))
402 }
403 }
404 return found
405}
406
407// DryRun runs the real publish pipeline into a DirStore rooted at dir, then
408// reads back everything it just published and fails on any leaked needle —
409// see the package doc comment above for why. Progress (each published key)
410// is written to out as it happens, ending with the literal "dry-run: no
411// leaks" attestation the plan's Step-1 test greps for; dir is expected to be
412// a scratch directory (t.TempDir() in tests, an operator-chosen throwaway
413// path on the command line) since DryRun writes real objects into it.
414func DryRun(dir string, out io.Writer) error {
415 ctx := context.Background()
416 s := release.NewDirStore(dir)
417
418 routes, probes := dryRunFixtures()
419 now := time.Now()
420
421 box := CollectBox(routes, probes, now)
422 if err := WriteBox(ctx, s, box); err != nil {
423 return fmt.Errorf("dry run: write box: %w", err)
424 }
425
426 fleet, err := BuildFleet(ctx, s, routes, now, 5*time.Minute)
427 if err != nil {
428 return fmt.Errorf("dry run: build fleet: %w", err)
429 }
430 if err := PublishFleet(ctx, s, fleet); err != nil {
431 return fmt.Errorf("dry run: publish fleet: %w", err)
432 }
433
434 // The positive check: opt-in is the one exception a leak-scan can't
435 // prove by itself (absence of a leak says nothing about whether the
436 // thing that SHOULD be there actually is).
437 if !appPublished(fleet, dryRunOptInApp) {
438 return fmt.Errorf("dry run: opted-in app %q must appear in fleet.apps — opt-in path is broken", dryRunOptInApp)
439 }
440
441 keys, err := s.List(ctx, "status/")
442 if err != nil {
443 return fmt.Errorf("dry run: list published objects: %w", err)
444 }
445 if len(keys) == 0 {
446 return fmt.Errorf("dry run: nothing published under status/ — publish path is broken")
447 }
448
449 // boxKeySeen tracks whether the scan actually reached a status/boxes/
450 // object carrying the Metrics fixtures — the positive half of the
451 // Metrics scope check below, the same reason appPublished exists for
452 // the opt-in app: a leak-scan that finds nothing proves nothing about
453 // whether the thing that SHOULD be there actually is.
454 boxKeySeen := false
455 // routeVersionKeySeen is boxKeySeen's sibling for Task 8's own scoped
456 // pair (dryRunRouteVersionNeedles): whether the scan actually reached a
457 // status/boxes/ object carrying the route_versions fixture, so this
458 // self-check can't pass by simply never exercising RouteVersions.
459 routeVersionKeySeen := false
460 // routeServingKeySeen is the same positive half for the serving version
461 // map (dryRunRouteServingNeedles) — Box.Metrics.RouteServing, plan 5
462 // Task 2.
463 routeServingKeySeen := false
464 // upstreamKeySeen is the same positive half for the probe table
465 // (dryRunUpstreamNeedles) — Box.Metrics.Upstreams, Task 6.
466 upstreamKeySeen := false
467 // configRefusalKeySeen is the same positive half for the config guard's
468 // refusal list (dryRunConfigRefusalNeedles), amadan #213.
469 configRefusalKeySeen := false
470 // releaseRefusalKeySeen is the same positive half for D1's release
471 // refusal list (dryRunReleaseRefusalNeedles).
472 releaseRefusalKeySeen := false
473 // proofWaitKeySeen is the same positive half for D3's proof wait list
474 // (dryRunProofWaitNeedles).
475 proofWaitKeySeen := false
476 // rolloutKeySeen is the same positive half for the rollout report
477 // (dryRunRolloutNeedles).
478 rolloutKeySeen := false
479 // buildStateKeySeen is the same positive half for the build-state report
480 // (dryRunBuildStateNeedles).
481 buildStateKeySeen := false
482 // updatesKeySeen is the same positive half for the update decisions
483 // (dryRunUpdatesNeedles).
484 updatesKeySeen := false
485
486 for _, key := range keys {
487 body, _, err := s.Get(ctx, key)
488 if err != nil {
489 return fmt.Errorf("dry run: read back %s: %w", key, err)
490 }
491 fmt.Fprintf(out, "published %s (%d bytes)\n", key, len(body))
492
493 if leaks := ScanForLeaks(body, dryRunNeedles); len(leaks) > 0 {
494 return fmt.Errorf("dry run: %s leaked %v", key, leaks)
495 }
496
497 // dryRunMetricsNeedles are scoped: status/boxes/*.json is Box.Metrics's
498 // one privileged, private home (see snapshot.go's package doc), so
499 // the raw fixture values are EXPECTED there and must NOT be scanned
500 // out — that isn't a leak, it's the field doing its job. Every other
501 // key DryRun publishes (fleet.json, index.html, status.css) is
502 // exactly what BuildFleet/PublishFleet expose beyond the bucket, so
503 // those raw values must never appear there.
504 if strings.HasPrefix(key, "status/boxes/") {
505 if leaks := ScanForLeaks(body, dryRunMetricsNeedles); len(leaks) == 0 {
506 // None of the fixtures made it in at all — Metrics isn't
507 // being attached, which means this check isn't exercising
508 // anything (a false "no leak").
509 return fmt.Errorf("dry run: %s: expected raw Metrics fixtures %v, found none — Box.Metrics not attached?", key, dryRunMetricsNeedles)
510 }
511 boxKeySeen = true
512 } else if leaks := ScanForLeaks(body, dryRunMetricsNeedles); len(leaks) > 0 {
513 return fmt.Errorf("dry run: %s leaked raw Metrics fixture %v — Box.Metrics must never reach the public feed", key, leaks)
514 }
515
516 // dryRunRouteVersionNeedles gets the identical scope split as
517 // dryRunMetricsNeedles just above, for the identical reason:
518 // RouteVersions lives in Box.Metrics too, so status/boxes/*.json is
519 // its one privileged home and every other published key must never
520 // carry it.
521 if strings.HasPrefix(key, "status/boxes/") {
522 if leaks := ScanForLeaks(body, dryRunRouteVersionNeedles); len(leaks) == 0 {
523 return fmt.Errorf("dry run: %s: expected route_versions fixtures %v, found none — RouteVersions not attached?", key, dryRunRouteVersionNeedles)
524 }
525 routeVersionKeySeen = true
526 } else if leaks := ScanForLeaks(body, dryRunRouteVersionNeedles); len(leaks) > 0 {
527 return fmt.Errorf("dry run: %s leaked a route_versions fixture %v — RouteVersions must never reach the public feed", key, leaks)
528 }
529
530 // dryRunRouteServingNeedles: same scope split as RouteVersions.
531 // RouteServing (plan 5's ServingVersion) is also a Box.Metrics field
532 // with hostnames, so status/boxes/*.json is its one privileged home
533 // and every other published key must never carry it.
534 if strings.HasPrefix(key, "status/boxes/") {
535 if leaks := ScanForLeaks(body, dryRunRouteServingNeedles); len(leaks) == 0 {
536 return fmt.Errorf("dry run: %s: expected route_serving fixtures %v, found none — RouteServing not attached?", key, dryRunRouteServingNeedles)
537 }
538 routeServingKeySeen = true
539 } else if leaks := ScanForLeaks(body, dryRunRouteServingNeedles); len(leaks) > 0 {
540 return fmt.Errorf("dry run: %s leaked a route_serving fixture %v — RouteServing must never reach the public feed", key, leaks)
541 }
542
543 // dryRunUpstreamNeedles: the third scope split, on the same pattern.
544 // Upstreams is Box.Metrics's other hostname-keyed field and the only
545 // one carrying backend ADDRESSES, so status/boxes/*.json is its one
546 // privileged home and fleet.json/index.html must never show either
547 // half of the pair.
548 if strings.HasPrefix(key, "status/boxes/") {
549 if leaks := ScanForLeaks(body, dryRunUpstreamNeedles); len(leaks) == 0 {
550 return fmt.Errorf("dry run: %s: expected upstreams fixtures %v, found none — Upstreams not attached?", key, dryRunUpstreamNeedles)
551 }
552 upstreamKeySeen = true
553 } else if leaks := ScanForLeaks(body, dryRunUpstreamNeedles); len(leaks) > 0 {
554 return fmt.Errorf("dry run: %s leaked an upstreams fixture %v — Upstreams must never reach the public feed", key, leaks)
555 }
556
557 // dryRunConfigRefusalNeedles: the fourth scope split. The refusal list
558 // names real hosts, so status/boxes/*.json is its one private home.
559 if strings.HasPrefix(key, "status/boxes/") {
560 if leaks := ScanForLeaks(body, dryRunConfigRefusalNeedles); len(leaks) == 0 {
561 return fmt.Errorf("dry run: %s: expected config_refusals fixtures %v, found none — ConfigRefusals not attached?", key, dryRunConfigRefusalNeedles)
562 }
563 configRefusalKeySeen = true
564 } else if leaks := ScanForLeaks(body, dryRunConfigRefusalNeedles); len(leaks) > 0 {
565 return fmt.Errorf("dry run: %s leaked a config_refusals fixture %v — ConfigRefusals must never reach the public feed", key, leaks)
566 }
567
568 // dryRunReleaseRefusalNeedles: the fifth scope split, on the same
569 // pattern. The rolled-back builds name real hosts.
570 if strings.HasPrefix(key, "status/boxes/") {
571 if leaks := ScanForLeaks(body, dryRunReleaseRefusalNeedles); len(leaks) == 0 {
572 return fmt.Errorf("dry run: %s: expected release_refusals fixtures %v, found none — ReleaseRefusals not attached?", key, dryRunReleaseRefusalNeedles)
573 }
574 releaseRefusalKeySeen = true
575 } else if leaks := ScanForLeaks(body, dryRunReleaseRefusalNeedles); len(leaks) > 0 {
576 return fmt.Errorf("dry run: %s leaked a release_refusals fixture %v — ReleaseRefusals must never reach the public feed", key, leaks)
577 }
578
579 // dryRunProofWaitNeedles: the sixth scope split, on the same
580 // pattern. The hosts held for a dev proof are real route names.
581 if strings.HasPrefix(key, "status/boxes/") {
582 if leaks := ScanForLeaks(body, dryRunProofWaitNeedles); len(leaks) == 0 {
583 return fmt.Errorf("dry run: %s: expected proof_waits fixtures %v, found none — ProofWaits not attached?", key, dryRunProofWaitNeedles)
584 }
585 proofWaitKeySeen = true
586 } else if leaks := ScanForLeaks(body, dryRunProofWaitNeedles); len(leaks) > 0 {
587 return fmt.Errorf("dry run: %s leaked a proof_waits fixture %v — ProofWaits must never reach the public feed", key, leaks)
588 }
589
590 // dryRunRolloutNeedles: the seventh scope split, on the same
591 // pattern. The rollout report names the managed instances.
592 if strings.HasPrefix(key, "status/boxes/") {
593 if leaks := ScanForLeaks(body, dryRunRolloutNeedles); len(leaks) == 0 {
594 return fmt.Errorf("dry run: %s: expected rollout fixtures %v, found none — Rollout not attached?", key, dryRunRolloutNeedles)
595 }
596 rolloutKeySeen = true
597 } else if leaks := ScanForLeaks(body, dryRunRolloutNeedles); len(leaks) > 0 {
598 return fmt.Errorf("dry run: %s leaked a rollout fixture %v — Rollout must never reach the public feed", key, leaks)
599 }
600
601 // dryRunBuildStateNeedles: the eighth scope split, on the same
602 // pattern. The build-state report names hosts and builds.
603 if strings.HasPrefix(key, "status/boxes/") {
604 if leaks := ScanForLeaks(body, dryRunBuildStateNeedles); len(leaks) == 0 {
605 return fmt.Errorf("dry run: %s: expected build_states fixtures %v, found none. BuildStates not attached?", key, dryRunBuildStateNeedles)
606 }
607 buildStateKeySeen = true
608 } else if leaks := ScanForLeaks(body, dryRunBuildStateNeedles); len(leaks) > 0 {
609 return fmt.Errorf("dry run: %s leaked a build_states fixture %v. BuildStates must never reach the public feed", key, leaks)
610 }
611
612 // dryRunUpdatesNeedles: the ninth scope split, on the same pattern.
613 // The update decisions name hosts and builds.
614 if strings.HasPrefix(key, "status/boxes/") {
615 if leaks := ScanForLeaks(body, dryRunUpdatesNeedles); len(leaks) == 0 {
616 return fmt.Errorf("dry run: %s: expected updates fixtures %v, found none. Updates not attached?", key, dryRunUpdatesNeedles)
617 }
618 updatesKeySeen = true
619 } else if leaks := ScanForLeaks(body, dryRunUpdatesNeedles); len(leaks) > 0 {
620 return fmt.Errorf("dry run: %s leaked an updates fixture %v. Updates must never reach the public feed", key, leaks)
621 }
622
623 // Page assets (index.html, status.css) aren't JSON and simply fail
624 // to decode here — silently skipped, since the band-digit check
625 // only means anything for objects that actually carry Load/Capacity
626 // fields.
627 var decoded interface{}
628 if err := json.Unmarshal(body, &decoded); err == nil {
629 if leaks := scanForNumericLeaks(decoded, dryRunFractionNeedles); len(leaks) > 0 {
630 return fmt.Errorf("dry run: %s leaked %v", key, leaks)
631 }
632 if bad := scanBandFieldsForDigits(decoded); len(bad) > 0 {
633 return fmt.Errorf("dry run: %s: band field carries a digit: %v", key, bad)
634 }
635 }
636 }
637
638 if !boxKeySeen {
639 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — Metrics scope check never ran")
640 }
641 if !routeVersionKeySeen {
642 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — route_versions scope check never ran")
643 }
644 if !routeServingKeySeen {
645 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — route_serving scope check never ran")
646 }
647 if !upstreamKeySeen {
648 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — upstreams scope check never ran")
649 }
650 if !configRefusalKeySeen {
651 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — config_refusals scope check never ran")
652 }
653 if !releaseRefusalKeySeen {
654 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — release_refusals scope check never ran")
655 }
656 if !proofWaitKeySeen {
657 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — proof_waits scope check never ran")
658 }
659 if !rolloutKeySeen {
660 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — rollout scope check never ran")
661 }
662 if !buildStateKeySeen {
663 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned. The build_states scope check never ran")
664 }
665
666 if !updatesKeySeen {
667 return fmt.Errorf("dry run: no status/boxes/*.json object was scanned. The updates scope check never ran")
668 }
669
670 fmt.Fprintln(out, "dry-run: no leaks")
671 return nil
672}
673