// Task 6: the self-hoster's "what would I publish?" check. DryRun runs the // REAL writer/merger/publisher path — CollectBox, WriteBox, BuildFleet, // PublishFleet — over a fixture registry deliberately stuffed with needles // (hostnames, a machine id, mid-band fractions), then reads every object it // just published back out of the store and fails loudly if any needle // survived. It exists so an operator can run `carlos status -dry-run ` // against a scratch directory BEFORE ever pointing this binary at a real // bucket, and see for themselves that the privacy contract holds end to end // rather than trusting the package's own unit tests never regress. package status import ( "context" "encoding/json" "fmt" "io" "strings" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // dryRunOptInApp is the ONE app in DryRun's fixture registry that opts into // the status page via StatusNamed. It exists to prove the leak-scan doesn't // overreach: an opted-in app name is a deliberate publication, not a leak, // so DryRun asserts this name DOES appear in fleet.apps — the flip side of // every other needle below, which must NOT appear anywhere. const dryRunOptInApp = "demoapp" // dryRunNeedles are every fixture secret DryRun seeds into its throwaway // registry and probes. None of these substrings may appear in ANY object // DryRun publishes. "prod-secret" is listed both whole and as a bare prefix // so a leak that only carries part of the hostname (e.g. truncated in some // future summary field) still trips the scan. The fixture fractions are // checked as JSON numbers separately: timestamps can legitimately contain // the same digits in their fractional seconds. var dryRunNeedles = []string{ "prod-secret.example.com", // fixture hostname "prod-secret", // hostname's leak-prone substring on its own "example.com", // the fixture domain generally "ip-10-0-0-7", // fixture machine id } const ( dryRunMemFrac = 0.73 dryRunDiskFrac = 0.95 ) var dryRunFractionNeedles = []float64{dryRunMemFrac, dryRunDiskFrac} // dryRunMetricsNeedles are the distinctive raw values Task 5's BoxMetrics // carries — real numbers and strings that Box.Metrics is the ONE deliberate // exception privileged to publish (see snapshot.go's package doc), and which // therefore MUST appear in the published box object. Unlike dryRunNeedles // above (banned everywhere, box object included), these are allowed inside // status/boxes/*.json and banned only from everything else DryRun publishes // — see the scope split in DryRun's scan loop below. Each value is // deliberately distinctive (the repeated 42s, a value with no legitimate // reason to appear in a template or a fleet-wide aggregate) so a false // negative from an incidental digit collision is effectively impossible. var dryRunMetricsNeedles = []string{ "4242424242", // fixture MemTotalBytes, verbatim "4242424241", // fixture MemAvailBytes, verbatim "4242424240", // fixture DiskFreeBytes, verbatim "42.42", // fixture Load1, verbatim "dryrun4242sha", // fixture Release, verbatim "reconnecting", // fixture Tunnel, verbatim (a real, valid Tunnel value) } // dryRunRouteVersionHost and dryRunRouteVersionVersion are Task 8's own // needle pair (#78): a THIRD un-opted route, distinct from leakyapp and // svc/internal above, whose only purpose is carrying a Version — the one // thing that makes CollectBox populate Box.Metrics.RouteVersions, this // package's one hostname-keyed field. Neither string reuses any existing // needle (not dryRunNeedles, not dryRunMetricsNeedles, not demoapp's own // "abc1234"): route_versions gets its own dedicated positive/negative check, // scope-split exactly like dryRunMetricsNeedles below, rather than riding // along on an assertion built for something else. const ( dryRunRouteVersionHost = "staged-secret.internal" dryRunRouteVersionVersion = "stagedver9012" ) // dryRunRouteVersionNeedles is the scope-split pair for the fixture above: // REQUIRED inside status/boxes/*.json (proof that RouteVersions actually // attached — an unexercised field is a field this self-check can't vouch // for) and BANNED everywhere else DryRun publishes (proof that a // route_versions hostname, unlike a band word or an aggregate, never // reaches fleet.json or the rendered page). Same reasoning and the same // scan-loop shape as dryRunMetricsNeedles, because RouteVersions lives in // the identical private home: Box.Metrics. var dryRunRouteVersionNeedles = []string{dryRunRouteVersionHost, dryRunRouteVersionVersion} // dryRunRouteServingHost and dryRunRouteServingServing are Task 13's needle // pair (plan 5's ServingVersion): a THIRD un-opted route, distinct from // all others, whose only purpose is carrying a ServingVersion — the one // thing that makes CollectBox populate Box.Metrics.RouteServing. Neither // string reuses any existing needle: route_serving gets its own // dedicated positive/negative check, scope-split exactly like // dryRunRouteVersionNeedles, rather than riding along on an assertion // built for something else. const ( dryRunRouteServingHost = "serving-secret.internal" dryRunRouteServingServing = "servingver5678" ) // dryRunRouteServingNeedles is the scope-split pair for the fixture above: // REQUIRED inside status/boxes/*.json (proof that RouteServing actually // attached — an unexercised field is a field this self-check can't vouch // for) and BANNED everywhere else DryRun publishes (proof that // route_serving hostnames never reach fleet.json or the rendered page). // Same reasoning and the same scan-loop shape as dryRunRouteVersionNeedles, // because RouteServing lives in the identical private home: Box.Metrics. var dryRunRouteServingNeedles = []string{dryRunRouteServingHost, dryRunRouteServingServing} // dryRunUpstreamHost and dryRunUpstreamAddr are the multi-upstream plan's own // needle pair (Task 6) for Box.Metrics.Upstreams — the probe table, which is // the most identifying thing this block carries: a route hostname as the KEY // and a real backend address inside the value. Distinct strings again from // every other needle set here (an address shape no other fixture uses, so a // digit collision with dryRunMetricsNeedles' 4242s is impossible), and scoped // the same way for the same reason: REQUIRED in status/boxes/*.json, so the // self-check can't pass by never populating the field, and BANNED everywhere // else DryRun publishes. const ( dryRunUpstreamHost = "upstream-secret.internal" dryRunUpstreamAddr = "10.77.77.7:7717" ) var dryRunUpstreamNeedles = []string{dryRunUpstreamHost, dryRunUpstreamAddr} // dryRunConfigRefusalHost is the config guard's needle (amadan #213): // Box.Metrics.ConfigRefusals names the hosts a keyless box refuses, so the // same scope split applies. REQUIRED in status/boxes/*.json and BANNED in // every other published object. const dryRunConfigRefusalHost = "refused-secret.internal" var dryRunConfigRefusalNeedles = []string{dryRunConfigRefusalHost} // dryRunReleaseRefusalHost is the release refusal needle (D1): // Box.Metrics.ReleaseRefusals names the hosts whose build this box rolled // back, so the same scope split applies. REQUIRED in status/boxes/*.json and // BANNED in every other published object. const dryRunReleaseRefusalHost = "refused-build.internal" var dryRunReleaseRefusalNeedles = []string{dryRunReleaseRefusalHost} // dryRunProofWaitHost is the proof wait needle (D3): Box.Metrics.ProofWaits // names the hosts this box is holding for a dev proof, so the same scope // split applies. REQUIRED in status/boxes/*.json and BANNED in every other // published object. const dryRunProofWaitHost = "proof-wait.internal" var dryRunProofWaitNeedles = []string{dryRunProofWaitHost} // dryRunRolloutHost is the rollout report needle: Box.Metrics.Rollout names // the instances the rollout scheduler manages (hosts, failure records), so // the same scope split applies. REQUIRED in status/boxes/*.json and BANNED in // every other published object. const dryRunRolloutHost = "rollout-host.internal" var dryRunRolloutNeedles = []string{dryRunRolloutHost} // dryRunBuildStateHost and dryRunBuildStateBuild are the build-state needles // (settings spec rev 11 §4.4): Box.Metrics.BuildStates names the hosts this // box runs and the builds their build states name, so the same scope split // applies. REQUIRED in status/boxes/*.json and BANNED in every other // published object. const ( dryRunBuildStateHost = "build-state-host.internal" dryRunBuildStateBuild = "v-dryrun-build-state" ) var dryRunBuildStateNeedles = []string{dryRunBuildStateHost, dryRunBuildStateBuild} // dryRunUpdatesHost and dryRunUpdatesBuild are the update-decision needles // (settings spec §4.3): Box.Metrics.Updates names the hosts this box runs and // the builds root chose for them, so the same scope split applies. REQUIRED in // status/boxes/*.json and BANNED in every other published object. const ( dryRunUpdatesHost = "updates-host.internal" dryRunUpdatesBuild = "v-dryrun-updates" ) var dryRunUpdatesNeedles = []string{dryRunUpdatesHost, dryRunUpdatesBuild} // ScanForLeaks reports which of banned's strings appear anywhere in j. It is // the single implementation both DryRun's self-check and // contract_test.go's scanForLeaks test helper run — the test package can't // call an unexported function here, and duplicating the logic in both places // would let them silently drift into checking different things, which is // exactly the kind of gap a needle could slip through. func ScanForLeaks(j []byte, banned []string) []string { s := string(j) var found []string for _, b := range banned { if strings.Contains(s, b) { found = append(found, b) } } return found } // dryRunFixtures builds a registry and probe set stuffed with needles: three // un-opted hosts (one instance, one service, and one instance whose only job // is carrying a Version — see dryRunRouteVersionNeedles) that must vanish // from every public surface, one opted-in app that must survive by name, and // mid-band fractions — 0.73 and 0.95 sit inside the "busy"/"at-capacity" // bands per snapshot.go's 0.70/0.90 cuts, so banding actually gets exercised // rather than trivially cleared at an edge. func dryRunFixtures() ([]registry.Route, Probes) { routes := []registry.Route{ { Host: "prod-secret.example.com", Kind: "instance", App: "leakyapp", Status: "ready", Socket: "/run/carlos/leakyapp.sock", }, { Host: "demoapp.internal", Kind: "instance", App: dryRunOptInApp, Status: "ready", StatusNamed: true, Channel: "stable", Version: "abc1234", }, { Host: "svc-secret.example.com", Kind: "service", App: "svc/internal", Status: "ready", }, { // Un-opted, so its host must not surface via the apps feed either // — the ONLY reason this route exists is to give CollectBox a // Version to fold into Box.Metrics.RouteVersions (see the needle // pair's own doc comment above). Host: dryRunRouteVersionHost, Kind: "instance", App: "stagedapp", Status: "ready", Version: dryRunRouteVersionVersion, }, { // Un-opted, so its host must not surface via the apps feed either // — the ONLY reason this route exists is to give CollectBox a // ServingVersion to fold into Box.Metrics.RouteServing (see the // needle pair's own doc comment above). Host: dryRunRouteServingHost, Kind: "sidecar", App: "servingapp", Status: "ready", ServingVersion: dryRunRouteServingServing, }, } memFrac, diskFrac := dryRunMemFrac, dryRunDiskFrac lag := int64(120) adopting := true // a known reading, so adoption_running publishes non-null and gets scanned configPath := true // ditto for config_path_running (issue #44) restartPath := true // ditto for restart_path_running (issue #113) expiredCanaries := 2 // ditto for expired_canaries — a known count, not the unexercised nil path // Task 5's raw Metrics fixtures: distinctive values (see // dryRunMetricsNeedles) that MUST land in the published box object and // MUST NOT land anywhere else DryRun publishes — the positive half of // proving Box.Metrics's private exception actually stays private. memTotal, memAvail, diskFree := int64(4242424242), int64(4242424241), int64(4242424240) load1 := 42.42 probes := Probes{ MachineID: "ip-10-0-0-7", Region: "us-east-1", MemFrac: &memFrac, DiskFrac: &diskFrac, LagSecs: &lag, AdoptRunning: &adopting, ConfigPathRunning: &configPath, RestartPathRunning: &restartPath, ExpiredCanaries: &expiredCanaries, // Every hibernating route reports "listening" so Awake/Asleep publish // non-null — an unexercised null field is a field DryRun can't scan. Listening: func(string) *bool { v := true; return &v }, MemTotalBytes: &memTotal, MemAvailBytes: &memAvail, Load1: &load1, DiskFreeBytes: &diskFree, Release: "dryrun4242sha", Tunnel: "reconnecting", // One refused host (amadan #213), so the scope check below has // something to require in the private block and ban elsewhere. ConfigRefusals: []ConfigRefusal{ {Host: dryRunConfigRefusalHost, Reason: "no_key", Since: time.Now().Add(-time.Hour), Last: time.Now(), Count: 2}, }, ConfigRefusalsTotal: 1, // One rolled-back build (D1), for the same scope check. ReleaseRefusals: []ReleaseRefusal{ {Host: dryRunReleaseRefusalHost, Version: "v-dryrun", Reason: "exited: status=2", RefusedAt: time.Now(), Kind: "wrong-program"}, }, // One host held for a dev proof (D3), for the same scope check. ProofWaits: []ProofWait{ {Host: dryRunProofWaitHost, Version: "v-dryrun", SHA256: "dryrunproofsha", Prover: "dev", Reason: "no proof from dev", Since: time.Now()}, }, // One held instance and its failure record, for the rollout scope check. Rollout: &RolloutReport{ Hosts: map[string]HostRollout{ dryRunRolloutHost: {Account: "dryrun", App: "app", Installed: "v-dryrun", Serving: "v-old", Good: "v-old", Awake: true, Attempt: AttemptFailed, AttemptBuild: "v-dryrun", HeldBy: "v-dryrun", Reason: ReasonHealth}, }, Apps: map[string]AppRollout{"dryrun/app": {Queued: 1}}, Failures: []RolloutFailure{ {Account: "dryrun", App: "app", Build: "v-dryrun", Host: dryRunRolloutHost, Mode: "overlap", Reason: ReasonHealth, PromotedAt: time.Now(), At: time.Now()}, }, }, // One host's build-state builds, for the build_states scope check. BuildStates: map[string]BuildStatePins{ dryRunBuildStateHost: {Account: "dryrun", App: "app", Builds: []string{dryRunBuildStateBuild}, Complete: true}, }, // One host's update decision, for the updates scope check. UpdateSettings: UpdateSettingsProtocol, Updates: map[string]HostUpdates{ dryRunUpdatesHost: {Desired: dryRunUpdatesBuild, Reason: "manual", Completed: 1}, }, // The probe table, keyed by a route host and carrying a backend // address — see dryRunUpstreamNeedles. One up and one down upstream, // so the down one's reason string is exercised too. Upstreams: map[string][]UpstreamState{ dryRunUpstreamHost: { {Addr: dryRunUpstreamAddr, Up: true, Since: time.Now().Add(-time.Hour)}, {Addr: "10.77.77.8:7717", Up: false, Since: time.Now().Add(-time.Minute), Fails: 3, LastError: "dial: connection refused"}, }, }, } return routes, probes } // appPublished reports whether name appears in the fleet's opted-in apps — // used to assert dryRunOptInApp made it through, the positive half of the // privacy contract that a pure leak-scan (which only ever proves absence) // can't check on its own. func appPublished(f *Fleet, name string) bool { for _, a := range f.Apps { if a.Name == name { return true } } return false } // bandFieldKeys are the JSON key names that ever carry a band word (see // snapshot.go's loadWords/capacityWords): Box's "load" and "capacity" fields // (the latter a bare string), each boxes[] element's "load", and Fleet's // nested "capacity.level". Keyed by field name rather than a fixed JSON // path so the same check works unmodified against Box's shape, Fleet's // shape, and any future struct that reuses these field names. var bandFieldKeys = map[string]bool{"load": true, "capacity": true, "level": true} // scanBandFieldsForDigits walks decoded JSON looking for band-word fields // and flags any whose value carries a digit. Per the plan's privacy rule, a // band field is a WORD (comfortable/busy/critical, plenty/getting-full/at- // capacity), never a number — a digit surviving into one of these fields // means a raw fraction escaped banding somewhere between CollectBox and the // published bytes, independent of whether it happens to match one of the // exact fixture fractions in dryRunNeedles. func scanBandFieldsForDigits(v interface{}) []string { var bad []string var walk func(interface{}) walk = func(v interface{}) { switch t := v.(type) { case map[string]interface{}: for k, val := range t { if bandFieldKeys[k] { if s, ok := val.(string); ok && strings.ContainsAny(s, "0123456789") { bad = append(bad, fmt.Sprintf("%s=%q", k, s)) } } walk(val) } case []interface{}: for _, elem := range t { walk(elem) } } } walk(v) return bad } func scanForNumericLeaks(v interface{}, banned []float64) []string { seen := map[float64]bool{} var walk func(interface{}) walk = func(v interface{}) { switch t := v.(type) { case map[string]interface{}: for _, value := range t { walk(value) } case []interface{}: for _, value := range t { walk(value) } case float64: for _, needle := range banned { if t == needle { seen[needle] = true } } } } walk(v) var found []string for _, needle := range banned { if seen[needle] { found = append(found, fmt.Sprintf("%g", needle)) } } return found } // DryRun runs the real publish pipeline into a DirStore rooted at dir, then // reads back everything it just published and fails on any leaked needle — // see the package doc comment above for why. Progress (each published key) // is written to out as it happens, ending with the literal "dry-run: no // leaks" attestation the plan's Step-1 test greps for; dir is expected to be // a scratch directory (t.TempDir() in tests, an operator-chosen throwaway // path on the command line) since DryRun writes real objects into it. func DryRun(dir string, out io.Writer) error { ctx := context.Background() s := release.NewDirStore(dir) routes, probes := dryRunFixtures() now := time.Now() box := CollectBox(routes, probes, now) if err := WriteBox(ctx, s, box); err != nil { return fmt.Errorf("dry run: write box: %w", err) } fleet, err := BuildFleet(ctx, s, routes, now, 5*time.Minute) if err != nil { return fmt.Errorf("dry run: build fleet: %w", err) } if err := PublishFleet(ctx, s, fleet); err != nil { return fmt.Errorf("dry run: publish fleet: %w", err) } // The positive check: opt-in is the one exception a leak-scan can't // prove by itself (absence of a leak says nothing about whether the // thing that SHOULD be there actually is). if !appPublished(fleet, dryRunOptInApp) { return fmt.Errorf("dry run: opted-in app %q must appear in fleet.apps — opt-in path is broken", dryRunOptInApp) } keys, err := s.List(ctx, "status/") if err != nil { return fmt.Errorf("dry run: list published objects: %w", err) } if len(keys) == 0 { return fmt.Errorf("dry run: nothing published under status/ — publish path is broken") } // boxKeySeen tracks whether the scan actually reached a status/boxes/ // object carrying the Metrics fixtures — the positive half of the // Metrics scope check below, the same reason appPublished exists for // the opt-in app: a leak-scan that finds nothing proves nothing about // whether the thing that SHOULD be there actually is. boxKeySeen := false // routeVersionKeySeen is boxKeySeen's sibling for Task 8's own scoped // pair (dryRunRouteVersionNeedles): whether the scan actually reached a // status/boxes/ object carrying the route_versions fixture, so this // self-check can't pass by simply never exercising RouteVersions. routeVersionKeySeen := false // routeServingKeySeen is the same positive half for the serving version // map (dryRunRouteServingNeedles) — Box.Metrics.RouteServing, plan 5 // Task 2. routeServingKeySeen := false // upstreamKeySeen is the same positive half for the probe table // (dryRunUpstreamNeedles) — Box.Metrics.Upstreams, Task 6. upstreamKeySeen := false // configRefusalKeySeen is the same positive half for the config guard's // refusal list (dryRunConfigRefusalNeedles), amadan #213. configRefusalKeySeen := false // releaseRefusalKeySeen is the same positive half for D1's release // refusal list (dryRunReleaseRefusalNeedles). releaseRefusalKeySeen := false // proofWaitKeySeen is the same positive half for D3's proof wait list // (dryRunProofWaitNeedles). proofWaitKeySeen := false // rolloutKeySeen is the same positive half for the rollout report // (dryRunRolloutNeedles). rolloutKeySeen := false // buildStateKeySeen is the same positive half for the build-state report // (dryRunBuildStateNeedles). buildStateKeySeen := false // updatesKeySeen is the same positive half for the update decisions // (dryRunUpdatesNeedles). updatesKeySeen := false for _, key := range keys { body, _, err := s.Get(ctx, key) if err != nil { return fmt.Errorf("dry run: read back %s: %w", key, err) } fmt.Fprintf(out, "published %s (%d bytes)\n", key, len(body)) if leaks := ScanForLeaks(body, dryRunNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked %v", key, leaks) } // dryRunMetricsNeedles are scoped: status/boxes/*.json is Box.Metrics's // one privileged, private home (see snapshot.go's package doc), so // the raw fixture values are EXPECTED there and must NOT be scanned // out — that isn't a leak, it's the field doing its job. Every other // key DryRun publishes (fleet.json, index.html, status.css) is // exactly what BuildFleet/PublishFleet expose beyond the bucket, so // those raw values must never appear there. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunMetricsNeedles); len(leaks) == 0 { // None of the fixtures made it in at all — Metrics isn't // being attached, which means this check isn't exercising // anything (a false "no leak"). return fmt.Errorf("dry run: %s: expected raw Metrics fixtures %v, found none — Box.Metrics not attached?", key, dryRunMetricsNeedles) } boxKeySeen = true } else if leaks := ScanForLeaks(body, dryRunMetricsNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked raw Metrics fixture %v — Box.Metrics must never reach the public feed", key, leaks) } // dryRunRouteVersionNeedles gets the identical scope split as // dryRunMetricsNeedles just above, for the identical reason: // RouteVersions lives in Box.Metrics too, so status/boxes/*.json is // its one privileged home and every other published key must never // carry it. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunRouteVersionNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected route_versions fixtures %v, found none — RouteVersions not attached?", key, dryRunRouteVersionNeedles) } routeVersionKeySeen = true } else if leaks := ScanForLeaks(body, dryRunRouteVersionNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a route_versions fixture %v — RouteVersions must never reach the public feed", key, leaks) } // dryRunRouteServingNeedles: same scope split as RouteVersions. // RouteServing (plan 5's ServingVersion) is also a Box.Metrics field // with hostnames, so status/boxes/*.json is its one privileged home // and every other published key must never carry it. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunRouteServingNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected route_serving fixtures %v, found none — RouteServing not attached?", key, dryRunRouteServingNeedles) } routeServingKeySeen = true } else if leaks := ScanForLeaks(body, dryRunRouteServingNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a route_serving fixture %v — RouteServing must never reach the public feed", key, leaks) } // dryRunUpstreamNeedles: the third scope split, on the same pattern. // Upstreams is Box.Metrics's other hostname-keyed field and the only // one carrying backend ADDRESSES, so status/boxes/*.json is its one // privileged home and fleet.json/index.html must never show either // half of the pair. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunUpstreamNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected upstreams fixtures %v, found none — Upstreams not attached?", key, dryRunUpstreamNeedles) } upstreamKeySeen = true } else if leaks := ScanForLeaks(body, dryRunUpstreamNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked an upstreams fixture %v — Upstreams must never reach the public feed", key, leaks) } // dryRunConfigRefusalNeedles: the fourth scope split. The refusal list // names real hosts, so status/boxes/*.json is its one private home. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunConfigRefusalNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected config_refusals fixtures %v, found none — ConfigRefusals not attached?", key, dryRunConfigRefusalNeedles) } configRefusalKeySeen = true } else if leaks := ScanForLeaks(body, dryRunConfigRefusalNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a config_refusals fixture %v — ConfigRefusals must never reach the public feed", key, leaks) } // dryRunReleaseRefusalNeedles: the fifth scope split, on the same // pattern. The rolled-back builds name real hosts. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunReleaseRefusalNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected release_refusals fixtures %v, found none — ReleaseRefusals not attached?", key, dryRunReleaseRefusalNeedles) } releaseRefusalKeySeen = true } else if leaks := ScanForLeaks(body, dryRunReleaseRefusalNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a release_refusals fixture %v — ReleaseRefusals must never reach the public feed", key, leaks) } // dryRunProofWaitNeedles: the sixth scope split, on the same // pattern. The hosts held for a dev proof are real route names. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunProofWaitNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected proof_waits fixtures %v, found none — ProofWaits not attached?", key, dryRunProofWaitNeedles) } proofWaitKeySeen = true } else if leaks := ScanForLeaks(body, dryRunProofWaitNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a proof_waits fixture %v — ProofWaits must never reach the public feed", key, leaks) } // dryRunRolloutNeedles: the seventh scope split, on the same // pattern. The rollout report names the managed instances. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunRolloutNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected rollout fixtures %v, found none — Rollout not attached?", key, dryRunRolloutNeedles) } rolloutKeySeen = true } else if leaks := ScanForLeaks(body, dryRunRolloutNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a rollout fixture %v — Rollout must never reach the public feed", key, leaks) } // dryRunBuildStateNeedles: the eighth scope split, on the same // pattern. The build-state report names hosts and builds. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunBuildStateNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected build_states fixtures %v, found none. BuildStates not attached?", key, dryRunBuildStateNeedles) } buildStateKeySeen = true } else if leaks := ScanForLeaks(body, dryRunBuildStateNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked a build_states fixture %v. BuildStates must never reach the public feed", key, leaks) } // dryRunUpdatesNeedles: the ninth scope split, on the same pattern. // The update decisions name hosts and builds. if strings.HasPrefix(key, "status/boxes/") { if leaks := ScanForLeaks(body, dryRunUpdatesNeedles); len(leaks) == 0 { return fmt.Errorf("dry run: %s: expected updates fixtures %v, found none. Updates not attached?", key, dryRunUpdatesNeedles) } updatesKeySeen = true } else if leaks := ScanForLeaks(body, dryRunUpdatesNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked an updates fixture %v. Updates must never reach the public feed", key, leaks) } // Page assets (index.html, status.css) aren't JSON and simply fail // to decode here — silently skipped, since the band-digit check // only means anything for objects that actually carry Load/Capacity // fields. var decoded interface{} if err := json.Unmarshal(body, &decoded); err == nil { if leaks := scanForNumericLeaks(decoded, dryRunFractionNeedles); len(leaks) > 0 { return fmt.Errorf("dry run: %s leaked %v", key, leaks) } if bad := scanBandFieldsForDigits(decoded); len(bad) > 0 { return fmt.Errorf("dry run: %s: band field carries a digit: %v", key, bad) } } } if !boxKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — Metrics scope check never ran") } if !routeVersionKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — route_versions scope check never ran") } if !routeServingKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — route_serving scope check never ran") } if !upstreamKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — upstreams scope check never ran") } if !configRefusalKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — config_refusals scope check never ran") } if !releaseRefusalKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — release_refusals scope check never ran") } if !proofWaitKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — proof_waits scope check never ran") } if !rolloutKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned — rollout scope check never ran") } if !buildStateKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned. The build_states scope check never ran") } if !updatesKeySeen { return fmt.Errorf("dry run: no status/boxes/*.json object was scanned. The updates scope check never ran") } fmt.Fprintln(out, "dry-run: no leaks") return nil }