| 1 | package caps |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "go/parser" |
| 6 | "go/token" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | ) |
| 12 | |
| 13 | // liveChannels mirrors internal/release/laddercheck_test.go's list — the |
| 14 | // channels production is running on RIGHT NOW. Copied rather than imported: |
| 15 | // internal/caps must not import internal/release (and importing this test |
| 16 | // package from there would prove the wrong direction), so four literal |
| 17 | // strings are cheaper than restructuring either package to share them. If |
| 18 | // laddercheck_test.go's list ever changes, this one should be re-copied by |
| 19 | // hand — a shared constant would be one more thing this package would need |
| 20 | // to import from outside itself. |
| 21 | var liveChannels = []string{"canary/platform-adoption", "canary/keymail", "canary/console", "canary/rehearsal"} |
| 22 | |
| 23 | // TestCapabilitiesNeverConsultAChannelName is this package's half of the |
| 24 | // design's channel-neutrality claim (spec: "Nothing in this change may |
| 25 | // consult, constrain, or be constrained by a channel name" — the same |
| 26 | // promise internal/release/laddercheck_test.go pins for the ladder itself). |
| 27 | // canary/* in, canary/* out — the fleet runs on those channels and a |
| 28 | // capability change that refused, favoured, or even NOTICED one would stop |
| 29 | // deploys production depends on. |
| 30 | // |
| 31 | // Two halves, because neither alone is the whole claim: |
| 32 | // |
| 33 | // - structural: no identifier or literal in this package's own PRODUCTION |
| 34 | // source (not its tests) says "channel" or "canary" outside a comment. |
| 35 | // Parsed with go/parser rather than grepped so that a doc comment |
| 36 | // EXPLAINING channel-neutrality (as this very comment does) can never |
| 37 | // trip the test that states it. |
| 38 | // - behavioural: this package's inputs — Plan, Set, Grants, Ops — carry no |
| 39 | // channel field to begin with, and RenderService/RenderSocket take |
| 40 | // (app, *Set, Grants) only, never a Host and never a channel. So a route |
| 41 | // "on" any of production's four live channels converges byte-identical |
| 42 | // unit text for the same declared and granted capabilities. Converging |
| 43 | // the same Plan under a Host named for each channel and diffing the |
| 44 | // rendered bytes (never the path, which DOES vary by host) is what makes |
| 45 | // that a test rather than an assertion about code nobody re-reads. |
| 46 | func TestCapabilitiesNeverConsultAChannelName(t *testing.T) { |
| 47 | t.Run("structural", func(t *testing.T) { |
| 48 | assertNoChannelWord(t, ".") |
| 49 | }) |
| 50 | t.Run("behavioural", func(t *testing.T) { |
| 51 | s := &Set{ |
| 52 | V: Version, Command: "relay", Daemon: true, Children: true, RuntimeDir: "keymail", |
| 53 | Ports: []Port{{Port: 25, Name: "mx"}}, |
| 54 | Data: &Data{Replicate: DataReplicateApp}, |
| 55 | HostRole: true, |
| 56 | } |
| 57 | g := Grants{Ports: map[int]bool{25: true}, HostRole: true} |
| 58 | |
| 59 | var first []byte |
| 60 | var firstChannel string |
| 61 | for _, ch := range liveChannels { |
| 62 | // The channel name is sanitized into a legal hostname ONLY so this |
| 63 | // loop has something host-shaped to converge under. Host is not a |
| 64 | // render input — RenderService and RenderSocket never take one — |
| 65 | // so this is not smuggling the channel in through the back door; |
| 66 | // it is the strongest form of "cannot reach it" available: even a |
| 67 | // value DERIVED from the channel name, sitting in the one plan |
| 68 | // field a channel might plausibly have leaked into, changes |
| 69 | // nothing about what gets written. |
| 70 | host := "relay-" + strings.NewReplacer("canary/", "", "/", "-").Replace(ch) + ".example.com" |
| 71 | etc := t.TempDir() |
| 72 | o := Ops{ |
| 73 | EtcDir: etc, |
| 74 | StateRoot: t.TempDir(), |
| 75 | Systemctl: func(...string) error { return nil }, |
| 76 | Query: func(...string) bool { return false }, |
| 77 | EnsureUser: func(string) error { return nil }, |
| 78 | } |
| 79 | p := Plan{Host: host, App: "keymail", Set: s, Grants: g} |
| 80 | if _, err := Converge(o, p); err != nil { |
| 81 | t.Fatalf("converge for channel %q: %v", ch, err) |
| 82 | } |
| 83 | svcPath, _ := DropInPaths(etc, host) |
| 84 | body, err := os.ReadFile(svcPath) |
| 85 | if err != nil { |
| 86 | t.Fatalf("read rendered drop-in for channel %q: %v", ch, err) |
| 87 | } |
| 88 | if first == nil { |
| 89 | first, firstChannel = body, ch |
| 90 | continue |
| 91 | } |
| 92 | if !bytes.Equal(first, body) { |
| 93 | t.Errorf("materialized capabilities differ between channel %q and %q for an identical declared+granted set:\n%s\n---\n%s", |
| 94 | firstChannel, ch, first, body) |
| 95 | } |
| 96 | } |
| 97 | }) |
| 98 | } |
| 99 | |
| 100 | // assertNoChannelWord fails if any non-test .go file directly in dir mentions |
| 101 | // "channel" or "canary" outside a comment. Comments are found with |
| 102 | // go/parser's ParseComments and blanked out of the source before the search, |
| 103 | // so a doc comment stating the neutrality rule (this file's own included) |
| 104 | // can never be mistaken for a violation of it. |
| 105 | func assertNoChannelWord(t *testing.T, dir string) { |
| 106 | t.Helper() |
| 107 | entries, err := os.ReadDir(dir) |
| 108 | if err != nil { |
| 109 | t.Fatal(err) |
| 110 | } |
| 111 | fset := token.NewFileSet() |
| 112 | for _, e := range entries { |
| 113 | name := e.Name() |
| 114 | if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { |
| 115 | continue |
| 116 | } |
| 117 | path := filepath.Join(dir, name) |
| 118 | src, err := os.ReadFile(path) |
| 119 | if err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | file, err := parser.ParseFile(fset, path, src, parser.ParseComments) |
| 123 | if err != nil { |
| 124 | t.Fatalf("parse %s: %v", path, err) |
| 125 | } |
| 126 | sanitized := append([]byte(nil), src...) |
| 127 | for _, cg := range file.Comments { |
| 128 | start := fset.Position(cg.Pos()).Offset |
| 129 | end := fset.Position(cg.End()).Offset |
| 130 | for i := start; i < end && i < len(sanitized); i++ { |
| 131 | sanitized[i] = ' ' |
| 132 | } |
| 133 | } |
| 134 | lower := strings.ToLower(string(sanitized)) |
| 135 | for _, word := range []string{"channel", "canary"} { |
| 136 | if strings.Contains(lower, word) { |
| 137 | t.Errorf("%s mentions %q outside a comment — internal/caps must never consult or constrain a channel name", name, word) |
| 138 | } |
| 139 | } |
| 140 | } |
| 141 | } |
| 142 | |