| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "crypto/ed25519" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "math/rand" |
| 10 | "os" |
| 11 | "strconv" |
| 12 | "strings" |
| 13 | "testing" |
| 14 | ) |
| 15 | |
| 16 | // TestAuthorizeInterleavings runs random schedules of one deploy request |
| 17 | // against an owner who installs and removes pipelines, promotes and rolls |
| 18 | // back, at random points inside the request. It checks G1 and G2 and |
| 19 | // nothing else (spec §7.2, §12): |
| 20 | // |
| 21 | // G1: the last hook call before the write came after the pointer read the |
| 22 | // write was conditioned on, and the pipeline it was given permits it. |
| 23 | // G2: the bytes the write replaced equal the bytes that read returned. |
| 24 | // |
| 25 | // Seeds: DEPLOY_SIM_SEEDS (default 400). A failure prints the seed and the |
| 26 | // event log; DEPLOY_SIM_SEED=<n> replays exactly that seed. |
| 27 | func TestAuthorizeInterleavings(t *testing.T) { |
| 28 | first, last := 0, 400 |
| 29 | if v := os.Getenv("DEPLOY_SIM_SEEDS"); v != "" { |
| 30 | n, err := strconv.Atoi(v) |
| 31 | if err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | last = n |
| 35 | } |
| 36 | if v := os.Getenv("DEPLOY_SIM_SEED"); v != "" { |
| 37 | n, err := strconv.Atoi(v) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | first, last = n, n+1 |
| 42 | } |
| 43 | priv, _ := testKeys(t) |
| 44 | for seed := first; seed < last; seed++ { |
| 45 | simulateOnce(t, seed, priv) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | type simRead struct { |
| 50 | seq int |
| 51 | body []byte |
| 52 | etag string |
| 53 | absent bool |
| 54 | } |
| 55 | |
| 56 | type simHook struct { |
| 57 | seq int |
| 58 | pipelined bool |
| 59 | } |
| 60 | |
| 61 | func simulateOnce(t *testing.T, seed int, priv ed25519.PrivateKey) { |
| 62 | t.Helper() |
| 63 | ctx := context.Background() |
| 64 | rng := rand.New(rand.NewSource(int64(seed))) |
| 65 | inner := NewDirStore(t.TempDir()) |
| 66 | for _, v := range []string{"v1", "v2", "v3", "v4"} { |
| 67 | shipRelease(t, inner, "bab", "myapp", v, "linux-arm64", []byte("bin "+v)) |
| 68 | } |
| 69 | for _, v := range []string{"v1", "v2"} { |
| 70 | if _, err := PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}); err != nil { |
| 71 | t.Fatalf("seed %d setup: %v", seed, err) |
| 72 | } |
| 73 | } |
| 74 | var ( |
| 75 | events []string |
| 76 | reads []simRead |
| 77 | hooks []simHook |
| 78 | put *storeCall |
| 79 | replaced []byte |
| 80 | replacedAbsent bool |
| 81 | ) |
| 82 | owner := func() { |
| 83 | var err error |
| 84 | var what string |
| 85 | switch rng.Intn(4) { |
| 86 | case 0: |
| 87 | what = "install" |
| 88 | _, err = SetPipeline(ctx, inner, priv, "bab", "myapp", []PipelineChannel{{Name: "edge"}, {Name: "stable"}}, "owner") |
| 89 | case 1: |
| 90 | what = "remove pipeline" |
| 91 | err = inner.Delete(ctx, PipelineKey("bab", "myapp")) |
| 92 | case 2: |
| 93 | v := fmt.Sprintf("v%d", 1+rng.Intn(4)) |
| 94 | what = "promote " + v |
| 95 | _, err = PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}) |
| 96 | case 3: |
| 97 | what = "rollback" |
| 98 | err = Rollback(ctx, inner, priv, "bab", "myapp", "edge") |
| 99 | } |
| 100 | events = append(events, fmt.Sprintf(" owner %s: %v", what, err)) |
| 101 | } |
| 102 | h := &hookStore{inner: inner} |
| 103 | h.before = func(c storeCall) { |
| 104 | if rng.Float64() < 0.3 { |
| 105 | for i := 0; i <= rng.Intn(2); i++ { |
| 106 | owner() |
| 107 | } |
| 108 | } |
| 109 | events = append(events, fmt.Sprintf("%d %s %s %s", c.seq, c.op, c.key, c.ifMatch)) |
| 110 | if c.op == "put" && c.key == edgeKey { |
| 111 | cc := c |
| 112 | put = &cc |
| 113 | body, _, err := inner.Get(ctx, edgeKey) |
| 114 | replaced, replacedAbsent = body, errors.Is(err, os.ErrNotExist) |
| 115 | } |
| 116 | } |
| 117 | h.after = func(c storeCall, body []byte, etag string, err error) { |
| 118 | if c.key == edgeKey { |
| 119 | reads = append(reads, simRead{c.seq, body, etag, errors.Is(err, os.ErrNotExist)}) |
| 120 | } |
| 121 | } |
| 122 | hook := func(p *Pipeline) error { |
| 123 | hooks = append(hooks, simHook{h.seq, p != nil}) |
| 124 | events = append(events, fmt.Sprintf("%d hook pipelined=%v", h.seq, p != nil)) |
| 125 | return noPipelineOnly(p) |
| 126 | } |
| 127 | var err error |
| 128 | noop := false |
| 129 | switch rng.Intn(3) { |
| 130 | case 0: |
| 131 | var res PromoteResult |
| 132 | res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v3", PromoteOptions{Authorize: hook}) |
| 133 | noop = res.NoOp |
| 134 | case 1: |
| 135 | var res PromoteResult |
| 136 | res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v2", PromoteOptions{Authorize: hook}) |
| 137 | noop = res.NoOp |
| 138 | case 2: |
| 139 | err = RootSigner(priv).RollbackAuthorized(ctx, h, "bab", "myapp", "edge", hook) |
| 140 | } |
| 141 | fail := func(msg string) { |
| 142 | t.Fatalf("seed %d: %s\nreplay with DEPLOY_SIM_SEED=%d\n%s", seed, msg, seed, strings.Join(events, "\n")) |
| 143 | } |
| 144 | if err != nil { |
| 145 | return // refusals and conflicts write nothing |
| 146 | } |
| 147 | if noop { |
| 148 | if len(hooks) == 0 || hooks[len(hooks)-1].pipelined { |
| 149 | fail("a no-op succeeded without a clean hook call") |
| 150 | } |
| 151 | return |
| 152 | } |
| 153 | if put == nil { |
| 154 | fail("succeeded without writing") |
| 155 | } |
| 156 | var r *simRead |
| 157 | for i := len(reads) - 1; i >= 0; i-- { |
| 158 | rd := reads[i] |
| 159 | if rd.seq >= put.seq { |
| 160 | continue |
| 161 | } |
| 162 | if (put.ifMatch == "" && rd.absent) || (put.ifMatch != "" && !rd.absent && rd.etag == put.ifMatch) { |
| 163 | r = &rd |
| 164 | break |
| 165 | } |
| 166 | } |
| 167 | if r == nil { |
| 168 | fail("no pointer read matches the write's condition") |
| 169 | } |
| 170 | var a *simHook |
| 171 | for i := len(hooks) - 1; i >= 0; i-- { |
| 172 | if hooks[i].seq < put.seq { |
| 173 | a = &hooks[i] |
| 174 | break |
| 175 | } |
| 176 | } |
| 177 | if a == nil { |
| 178 | fail("a write landed with no hook call") |
| 179 | } |
| 180 | if a.seq <= r.seq { |
| 181 | fail(fmt.Sprintf("G1: the hook at %d is not after the pointer read at %d", a.seq, r.seq)) |
| 182 | } |
| 183 | if a.pipelined { |
| 184 | fail("G1: the hook saw a pipeline and the write landed") |
| 185 | } |
| 186 | if replacedAbsent != r.absent || !bytes.Equal(replaced, r.body) { |
| 187 | fail("G2: the write replaced bytes other than the ones it read") |
| 188 | } |
| 189 | } |
| 190 | |