package release import ( "bytes" "context" "crypto/ed25519" "errors" "fmt" "math/rand" "os" "strconv" "strings" "testing" ) // TestAuthorizeInterleavings runs random schedules of one deploy request // against an owner who installs and removes pipelines, promotes and rolls // back, at random points inside the request. It checks G1 and G2 and // nothing else (spec §7.2, §12): // // G1: the last hook call before the write came after the pointer read the // write was conditioned on, and the pipeline it was given permits it. // G2: the bytes the write replaced equal the bytes that read returned. // // Seeds: DEPLOY_SIM_SEEDS (default 400). A failure prints the seed and the // event log; DEPLOY_SIM_SEED= replays exactly that seed. func TestAuthorizeInterleavings(t *testing.T) { first, last := 0, 400 if v := os.Getenv("DEPLOY_SIM_SEEDS"); v != "" { n, err := strconv.Atoi(v) if err != nil { t.Fatal(err) } last = n } if v := os.Getenv("DEPLOY_SIM_SEED"); v != "" { n, err := strconv.Atoi(v) if err != nil { t.Fatal(err) } first, last = n, n+1 } priv, _ := testKeys(t) for seed := first; seed < last; seed++ { simulateOnce(t, seed, priv) } } type simRead struct { seq int body []byte etag string absent bool } type simHook struct { seq int pipelined bool } func simulateOnce(t *testing.T, seed int, priv ed25519.PrivateKey) { t.Helper() ctx := context.Background() rng := rand.New(rand.NewSource(int64(seed))) inner := NewDirStore(t.TempDir()) for _, v := range []string{"v1", "v2", "v3", "v4"} { shipRelease(t, inner, "bab", "myapp", v, "linux-arm64", []byte("bin "+v)) } for _, v := range []string{"v1", "v2"} { if _, err := PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}); err != nil { t.Fatalf("seed %d setup: %v", seed, err) } } var ( events []string reads []simRead hooks []simHook put *storeCall replaced []byte replacedAbsent bool ) owner := func() { var err error var what string switch rng.Intn(4) { case 0: what = "install" _, err = SetPipeline(ctx, inner, priv, "bab", "myapp", []PipelineChannel{{Name: "edge"}, {Name: "stable"}}, "owner") case 1: what = "remove pipeline" err = inner.Delete(ctx, PipelineKey("bab", "myapp")) case 2: v := fmt.Sprintf("v%d", 1+rng.Intn(4)) what = "promote " + v _, err = PromoteOnLadder(ctx, inner, priv, "bab", "myapp", "edge", v, PromoteOptions{Hotfix: true}) case 3: what = "rollback" err = Rollback(ctx, inner, priv, "bab", "myapp", "edge") } events = append(events, fmt.Sprintf(" owner %s: %v", what, err)) } h := &hookStore{inner: inner} h.before = func(c storeCall) { if rng.Float64() < 0.3 { for i := 0; i <= rng.Intn(2); i++ { owner() } } events = append(events, fmt.Sprintf("%d %s %s %s", c.seq, c.op, c.key, c.ifMatch)) if c.op == "put" && c.key == edgeKey { cc := c put = &cc body, _, err := inner.Get(ctx, edgeKey) replaced, replacedAbsent = body, errors.Is(err, os.ErrNotExist) } } h.after = func(c storeCall, body []byte, etag string, err error) { if c.key == edgeKey { reads = append(reads, simRead{c.seq, body, etag, errors.Is(err, os.ErrNotExist)}) } } hook := func(p *Pipeline) error { hooks = append(hooks, simHook{h.seq, p != nil}) events = append(events, fmt.Sprintf("%d hook pipelined=%v", h.seq, p != nil)) return noPipelineOnly(p) } var err error noop := false switch rng.Intn(3) { case 0: var res PromoteResult res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v3", PromoteOptions{Authorize: hook}) noop = res.NoOp case 1: var res PromoteResult res, err = RootSigner(priv).PromoteOnLadder(ctx, h, "bab", "myapp", "edge", "v2", PromoteOptions{Authorize: hook}) noop = res.NoOp case 2: err = RootSigner(priv).RollbackAuthorized(ctx, h, "bab", "myapp", "edge", hook) } fail := func(msg string) { t.Fatalf("seed %d: %s\nreplay with DEPLOY_SIM_SEED=%d\n%s", seed, msg, seed, strings.Join(events, "\n")) } if err != nil { return // refusals and conflicts write nothing } if noop { if len(hooks) == 0 || hooks[len(hooks)-1].pipelined { fail("a no-op succeeded without a clean hook call") } return } if put == nil { fail("succeeded without writing") } var r *simRead for i := len(reads) - 1; i >= 0; i-- { rd := reads[i] if rd.seq >= put.seq { continue } if (put.ifMatch == "" && rd.absent) || (put.ifMatch != "" && !rd.absent && rd.etag == put.ifMatch) { r = &rd break } } if r == nil { fail("no pointer read matches the write's condition") } var a *simHook for i := len(hooks) - 1; i >= 0; i-- { if hooks[i].seq < put.seq { a = &hooks[i] break } } if a == nil { fail("a write landed with no hook call") } if a.seq <= r.seq { fail(fmt.Sprintf("G1: the hook at %d is not after the pointer read at %d", a.seq, r.seq)) } if a.pipelined { fail("G1: the hook saw a pipeline and the write landed") } if replacedAbsent != r.absent || !bytes.Equal(replaced, r.body) { fail("G2: the write replaced bytes other than the ones it read") } }