| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "github.com/carlosframework/platform/internal/hostclaim" |
| 7 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 8 | "github.com/carlosframework/platform/internal/park" |
| 9 | "github.com/carlosframework/platform/internal/release" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | type plan12Renewable struct { |
| 16 | lost chan struct{} |
| 17 | err error |
| 18 | releases int |
| 19 | } |
| 20 | |
| 21 | func (r *plan12Renewable) Err() error { return r.err } |
| 22 | func (r *plan12Renewable) Lost() <-chan struct{} { return r.lost } |
| 23 | func (r *plan12Renewable) Release(context.Context) error { r.releases++; return nil } |
| 24 | func TestPlan12LandingResidueParkUnderItsOwnLease(t *testing.T) { |
| 25 | for _, mode := range []string{"ok", "wrong-host", "wrong-location", "wrong-bucket", "lost", "authorization"} { |
| 26 | t.Run(mode, func(t *testing.T) { |
| 27 | ctx := t.Context() |
| 28 | host := "app.example" |
| 29 | inc := strings.Repeat("a", 64) |
| 30 | loc := "instances/" + host + "/" + inc |
| 31 | now := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC) |
| 32 | claims := release.NewDirStore(t.TempDir()) |
| 33 | _, err := hostclaim.Update(ctx, claims, hostclaim.Observed{Host: host, Absent: true}, func(c *hostclaim.Claim) error { |
| 34 | c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "op", Intent: map[string]string{"park_sweep": "v1"}} |
| 35 | c.Obligations = []hostclaim.Obligation{{ID: "chore", Op: "op", Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: "ret", Park: &hostclaim.ParkScope{Location: loc, Incarnation: inc, EvidenceAt: now.Add(-time.Hour), RequestLimit: RequestLimit}}} |
| 36 | return nil |
| 37 | }) |
| 38 | if err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | data := leasetest.NewVersionedBucket() |
| 42 | data.AddBucket("data") |
| 43 | data.Put("data", loc+"/db", []byte("residue")) |
| 44 | r := &plan12Renewable{lost: make(chan struct{})} |
| 45 | acquires := 0 |
| 46 | leases := RenewingLeases{Region: "eu-west-1", Open: func(context.Context, string, string, string) (Renewable, error) { |
| 47 | acquires++ |
| 48 | if acquires > 1 { |
| 49 | return nil, errors.New("already held") |
| 50 | } |
| 51 | return r, nil |
| 52 | }} |
| 53 | bucket, scope := "data", loc |
| 54 | switch mode { |
| 55 | case "wrong-host": |
| 56 | scope = "instances/other.example/" + inc |
| 57 | case "wrong-location": |
| 58 | scope = "instances/" + host |
| 59 | case "wrong-bucket": |
| 60 | bucket = "other" |
| 61 | } |
| 62 | own, err := leases.Acquire(ctx, bucket, scope) |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | defer own.Close() |
| 67 | if mode == "lost" { |
| 68 | r.err = errors.New("lost") |
| 69 | } |
| 70 | authorized := 0 |
| 71 | engine := park.Engine{Claims: claims, Store: data, Bucket: "data", Region: "ie", Leases: AggregateLeases{Leases: leases, Resolve: ReplicaParkLeaseSet}, Now: func() time.Time { return now }, NewID: func() string { return "next" }, View: func(_ context.Context, o hostclaim.Observed) (park.View, error) { |
| 72 | return park.View{ClaimETag: o.ETag, Region: "ie"}, nil |
| 73 | }, Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { |
| 74 | authorized++ |
| 75 | if mode == "authorization" { |
| 76 | return park.ErrUnsafe |
| 77 | } |
| 78 | return nil |
| 79 | }} |
| 80 | err = engine.RunHeld(ctx, park.Work{Host: host, ID: "chore", Op: "op"}, own) |
| 81 | if mode == "ok" { |
| 82 | if err != nil && !errors.Is(err, park.ErrPending) { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | if authorized == 0 { |
| 86 | t.Fatal("authorizer bypassed") |
| 87 | } |
| 88 | versions, e := data.ListVersions(ctx, "data", loc+"/") |
| 89 | if e != nil || len(versions) != 0 { |
| 90 | t.Fatal("residue not parked", e, versions) |
| 91 | } |
| 92 | } else if err == nil { |
| 93 | t.Fatal("accepted", mode) |
| 94 | } |
| 95 | if acquires != 1 || r.releases != 0 { |
| 96 | t.Fatalf("acquires=%d releases=%d", acquires, r.releases) |
| 97 | } |
| 98 | if mode != "lost" && own.Check(ctx) != nil { |
| 99 | t.Fatal("caller lease lost") |
| 100 | } |
| 101 | }) |
| 102 | } |
| 103 | } |
| 104 | func TestPlan12LeaseSetSemantics(t *testing.T) { |
| 105 | for _, scope := range []string{"moves/app.example/op", "moves/app.example", "instances/app.example", "instances/app.example/" + strings.Repeat("a", 64)} { |
| 106 | got, err := ReplicaParkLeaseSet(t.Context(), "data", scope) |
| 107 | if err != nil || len(got) != 1 || got[0] != scope { |
| 108 | t.Fatalf("%s: %v %v", scope, got, err) |
| 109 | } |
| 110 | } |
| 111 | for _, scope := range []string{"control/staging-leases/ie/app.example", "s3://deployment/moves/app.example/op", "instances/app.example/not-an-incarnation", "moves/other/../op"} { |
| 112 | if _, err := ReplicaParkLeaseSet(t.Context(), "data", scope); err == nil { |
| 113 | t.Fatal(scope) |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | |