carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "crypto/ed25519"
6 "encoding/json"
7 "errors"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/domains"
12 "github.com/carlosframework/platform/internal/hostops"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16var claimTestOwner = hostops.RegistrationOwner{Console: "console", Enrollment: "enrollment", Controller: "controller"}
17
18func claimTestWriter(t *testing.T, raw release.Store) *hostops.ControlWriter {
19 t.Helper()
20 pub, _, err := ed25519.GenerateKey(nil)
21 if err != nil {
22 t.Fatal(err)
23 }
24 w, err := hostops.NewControlWriter(raw, pub, time.Now)
25 if err != nil {
26 t.Fatal(err)
27 }
28 return w
29}
30
31func seedClaimAdmission(t *testing.T, raw release.Store, account, app string, a hostops.AppAdmission) {
32 t.Helper()
33 key := "apps/" + account + "/" + app + "/admission.json"
34 body, _ := json.Marshal(a)
35 _, tag, _ := raw.Get(context.Background(), key)
36 if _, err := raw.Put(context.Background(), key, body, tag); err != nil {
37 t.Fatal(err)
38 }
39}
40
41func TestClaimAppMutationGate(t *testing.T) {
42 base := func() hostops.AppAdmission {
43 return hostops.AppAdmission{V: 1, AppID: "app", Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}}
44 }
45 for _, door := range []string{"restore", "protection", "store-grant", "store-provision", "store-rotate", "store-rotate-finish", "store-destroy", "scan-regrant", "store-declare"} {
46 t.Run(door, func(t *testing.T) {
47 ctx := context.Background()
48 begin := func(a hostops.AppAdmission, id string) (*hostops.OperationHandle, error, *hostops.ControlWriter) {
49 raw := release.NewDirStore(t.TempDir())
50 w := claimTestWriter(t, raw)
51 seedClaimAdmission(t, raw, "bab", "demo", a)
52 h, err := w.BeginExclusive(ctx, "bab", "demo", "app", hostops.AppOperation{ID: id, Kind: hostops.OpAppMutation, AppID: "app", Intent: map[string]string{"action": door}}, claimTestOwner)
53 return h, err, w
54 }
55 a := base()
56 a.State, a.Op = "closed", &hostops.AppOperation{ID: "purge", Kind: hostops.OpPurging, AppID: "app"}
57 if _, err, _ := begin(a, "mutation"); !errors.Is(err, hostops.ErrPending) {
58 t.Fatalf("purge gate: %v", err)
59 }
60 a = base()
61 a.Effects["old"] = hostops.Effect{State: "uncertain"}
62 if _, err, _ := begin(a, "mutation"); !errors.Is(err, hostops.ErrPending) {
63 t.Fatalf("uncertain gate: %v", err)
64 }
65 a = base()
66 a.Registrations["old"] = hostops.Registration{}
67 if _, err, _ := begin(a, "mutation"); !errors.Is(err, hostops.ErrPending) {
68 t.Fatalf("registration gate: %v", err)
69 }
70 h, err, w := begin(base(), "mutation")
71 if err != nil {
72 t.Fatal(err)
73 }
74 if h == nil {
75 t.Fatal("no durable exclusion")
76 }
77 if _, err = w.BeginExclusive(ctx, "bab", "demo", "app", hostops.AppOperation{ID: "second", Kind: hostops.OpAppMutation, AppID: "app", Intent: map[string]string{"action": door}}, claimTestOwner); !errors.Is(err, hostops.ErrPending) {
78 t.Fatal("concurrent mutation admitted")
79 }
80 })
81 }
82}
83
84func TestClaimAppMutationSerializesPurgeAndRecovers(t *testing.T) {
85 ctx := t.Context()
86 bucket := release.NewDirStore(t.TempDir())
87 st := openTestStore(t)
88 id, sqid, err := domains.MintAccount(ctx, bucket, "Account", "owner@keymail.dev", time.Now())
89 if err != nil {
90 t.Fatal(err)
91 }
92 if _, err = MaterializeAccounts(ctx, bucket, st); err != nil {
93 t.Fatal(err)
94 }
95 if _, err = st.CreateAppIn(id, "demo", "owner@keymail.dev", time.Now()); err != nil {
96 t.Fatal(err)
97 }
98 if err = st.migrateClaimAppIdentity(); err != nil {
99 t.Fatal(err)
100 }
101 if err = st.BindClaimAppIdentity(id, "demo", "app-id"); err != nil {
102 t.Fatal(err)
103 }
104 acct, err := st.AccountBySqid(sqid)
105 if err != nil {
106 t.Fatal(err)
107 }
108 w := claimTestWriter(t, bucket)
109 seedClaimAdmission(t, bucket, sqid, "demo", hostops.AppAdmission{V: 1, AppID: "app-id", Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}})
110 s := &Server{store: st, claimOwner: claimTestOwner, cfg: Config{HostClaim: true, Releases: claimTestBoundary(t, bucket, w), ClaimAuthority: &hostops.ProductionAuthority{ConsoleID: "console", Enrollment: "enrollment", Control: w}}}
111 calls := 0
112 err = s.claimAppMutation(ctx, acct, "demo", "store-destroy", func(ctx context.Context) error {
113 calls++
114 // A purge cannot enter while the mutation's operation holds the app.
115 if _, e := w.BeginExclusive(ctx, sqid, "demo", "app-id", hostops.AppOperation{ID: "purge", Kind: hostops.OpDeleting, AppID: "app-id"}, claimTestOwner); !errors.Is(e, hostops.ErrPending) {
116 t.Fatalf("exclusive op entered running backend: %v", e)
117 }
118 return nil
119 })
120 if err != nil {
121 t.Fatal(err)
122 }
123 current, err := hostops.ReadAdmission(ctx, bucket, sqid, "demo", "app-id")
124 if err != nil {
125 t.Fatal(err)
126 }
127 var completed string
128 for key, e := range current.Value.Effects {
129 if e.State == "complete" {
130 completed = key
131 }
132 }
133 if completed == "" {
134 t.Fatal("missing completed durable effect")
135 }
136 // Recreate exactly the persisted state at a crash before gate release.
137 crashed := current.Value
138 crashed.State, crashed.Op = "closed", &hostops.AppOperation{ID: completed, AppID: "app-id", Kind: hostops.OpAppMutation}
139 seedClaimAdmission(t, bucket, sqid, "demo", crashed)
140 if err = w.RecoverMutation(ctx, sqid, "demo", "app-id", completed); err != nil {
141 t.Fatal(err)
142 }
143 if calls != 1 {
144 t.Fatal("recovery repeated backend")
145 }
146 // A purge-owned app refuses a later mutation.
147 h, err := w.BeginExclusive(ctx, sqid, "demo", "app-id", hostops.AppOperation{ID: "deleting", Kind: hostops.OpDeleting, AppID: "app-id"}, claimTestOwner)
148 if err != nil {
149 t.Fatal(err)
150 }
151 _ = h
152 err = s.claimAppMutation(ctx, acct, "demo", "protection", func(context.Context) error { calls++; return nil })
153 if !errors.Is(err, hostops.ErrPending) || calls != 1 {
154 t.Fatalf("mutation bypassed an exclusive operation: calls=%d err=%v", calls, err)
155 }
156}
157