carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
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.
27func 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
49type simRead struct {
50 seq int
51 body []byte
52 etag string
53 absent bool
54}
55
56type simHook struct {
57 seq int
58 pipelined bool
59}
60
61func 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