carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostbox"
15 "github.com/carlosframework/platform/internal/hostclaim"
16 "github.com/carlosframework/platform/internal/hostguard"
17 "github.com/carlosframework/platform/internal/hostops"
18 "github.com/carlosframework/platform/internal/registry"
19 "github.com/carlosframework/platform/internal/rootstate"
20)
21
22type migrationTimerFault struct {
23 rootstate.Store
24 fail bool
25}
26
27func (s *migrationTimerFault) Write(n string, b []byte) error {
28 if err := s.Store.Write(n, b); err != nil {
29 return err
30 }
31 if s.fail {
32 s.fail = false
33 return os.ErrInvalid
34 }
35 return nil
36}
37func TestPlan14RootMigrationTimerAdoption(t *testing.T) {
38 testRootMigrationTimerAdoption(t, func(t *testing.T) rootstate.Store { return &rollbackDisk{dir: t.TempDir()} }, nil)
39}
40func testRootMigrationTimerAdoption(t *testing.T, open func(*testing.T) rootstate.Store, prepareRegistry func(*testing.T, *ClaimRuntimeResources)) {
41 for _, attack := range []string{"none", "intent-sync", "after-row", "after-inventory", "after-root", "registration", "contents", "foreign-owner", "erased-row", "extra-row", "missing-receipt", "missing-deny"} {
42 t.Run(attack, func(t *testing.T) {
43 store, r, owners, _ := fix4JournalFixture(t)
44 if prepareRegistry != nil {
45 prepareRegistry(t, &r)
46 }
47 actor, err := owners.Identity.Verify(t.Context())
48 if err != nil {
49 t.Fatal(err)
50 }
51 target, receipt := migrationHistoricFixture(t, store, r, owners.ClaimOwnerBindings, actor, &hostops.BackfillPlan{Hosts: map[string]hostops.BackfillHost{}})
52 j, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "root.db"))
53 if err != nil {
54 t.Fatal(err)
55 }
56 defer j.Close()
57 svc, err := hostbox.NewJournalService(j)
58 if err != nil {
59 t.Fatal(err)
60 }
61 if err := svc.MigrateLegacy(t.Context(), filepath.Join(r.DataDir, ".hostclaims"), actor.Box, actor.Enrollment, func(context.Context) (func(), error) { return func() {}, nil }, legacyJournalProof(store, owners.RetirementAuthority.Directory)); err != nil {
62 t.Fatal(err)
63 }
64 fstate := &migrationTimerFault{Store: open(t)}
65 defer fstate.Close()
66 root := hostguard.NewRootAuthority(fstate)
67 guard := hostguard.Guard{Root: root, LockDir: t.TempDir(), DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}
68 item := rootMigrationItem{Installation: hostguard.RootInstallation{Host: target.Host, Incarnation: target.Incarnation, Stamp: target.Stamp, Account: "bab", App: "app", Record: "apps/bab/app/instances/" + target.Host + ".json", Box: target.Box, Enrollment: target.Enrollment, Migration: 1}, Session: target.Session, ClaimETag: target.ClaimETag}
69 if attack != "missing-deny" {
70 if err := root.RetainMigrationDeny(t.Context(), item.Installation, item.Session); err != nil {
71 t.Fatal(err)
72 }
73 }
74 if attack == "missing-deny" {
75 if err := guard.EstablishRoot(t.Context(), item.Installation, func(*hostclaim.Claim) error { return nil }); err != nil {
76 t.Fatal(err)
77 }
78 }
79 if attack != "missing-receipt" {
80 if _, err := guard.BeginFence(t.Context(), target); err != nil {
81 t.Fatal(err)
82 }
83 if err := guard.CompleteFence(t.Context(), target, receipt, time.Now(), func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil {
84 t.Fatal(err)
85 }
86 }
87 state := &migrationTimerFault{Store: open(t), fail: attack == "intent-sync"}
88 defer state.Close()
89 m := rootMigrationTimers{state: state, registry: r.Registry, inventory: svc.Inventory, guard: guard}
90 original, err := r.Registry.GetTimer(target.Host, "refresh")
91 if err != nil {
92 t.Fatal(err)
93 }
94 canonical := registry.Route{Host: target.Host, Account: "bab", App: "app"}
95 if attack == "missing-receipt" || attack == "missing-deny" {
96 if m.adopt(t.Context(), item, target, canonical) == nil {
97 t.Fatal("adoption without stopped root authority")
98 }
99 got, err := r.Registry.GetTimer(target.Host, "refresh")
100 if err != nil || *got != *original {
101 t.Fatal("row changed before root authority", got, err)
102 }
103 if _, exists, err := m.load(item); err != nil || exists {
104 t.Fatal("intent written before root authority", exists, err)
105 }
106 return
107 }
108 if attack != "none" {
109 state.fail = true
110 if m.adopt(t.Context(), item, target, canonical) == nil {
111 t.Fatal("intent sync acknowledged")
112 }
113 got, _ := r.Registry.GetTimer(target.Host, "refresh")
114 if *got != *original {
115 t.Fatal("row changed before durable intent")
116 }
117 rec, ok, err := m.load(item)
118 if err != nil || !ok {
119 t.Fatal(ok, err)
120 }
121 owned := rec.Rows[0]
122 owned.ClaimOwnership = m.owner(item, owned.Registration)
123 switch attack {
124 case "after-row", "after-inventory", "after-root":
125 if err := r.Registry.AdoptClaimTimer(t.Context(), rec.Rows[0], owned.ClaimOwnership); err != nil {
126 t.Fatal(err)
127 }
128 if attack != "after-row" {
129 if err := svc.Inventory.WriteStopInventory(t.Context(), rec.Next); err != nil {
130 t.Fatal(err)
131 }
132 }
133 if attack == "after-root" {
134 fstate.fail = true
135 if err := root.CompleteMigrationInventory(t.Context(), target, rec.Revision+1, rec.Artifacts, rec.Revision); err == nil {
136 t.Fatal("root sync acknowledged")
137 }
138 }
139 case "foreign-owner":
140 if err := r.Registry.AdoptClaimTimer(t.Context(), *original, registry.ClaimOwnership{ClaimIncarnation: "foreign", ClaimStamp: 2, ClaimOwner: "foreign", ClaimRowID: original.Registration}); err != nil {
141 t.Fatal(err)
142 }
143 case "registration", "contents":
144 changed := *original
145 if attack == "registration" {
146 changed.Registration = "replacement"
147 }
148 if attack == "contents" {
149 changed.Path = "/changed"
150 }
151 if err := r.Registry.ReplaceTimers(target.Host, []registry.Timer{changed}); err != nil {
152 t.Fatal(err)
153 }
154 case "erased-row":
155 if err := r.Registry.DeleteTimer(target.Host, "refresh"); err != nil {
156 t.Fatal(err)
157 }
158 case "extra-row":
159 if err := r.Registry.UpsertTimer(registry.Timer{Host: target.Host, Name: "extra", Kind: "cron", Spec: "0 0 * * *", Path: "/extra"}); err != nil {
160 t.Fatal(err)
161 }
162 }
163 }
164 err = m.adopt(t.Context(), item, target, canonical)
165 refused := attack == "registration" || attack == "contents" || attack == "foreign-owner" || attack == "erased-row" || attack == "extra-row"
166 if (err != nil) != refused {
167 t.Fatalf("refused=%v: %v", refused, err)
168 }
169 if refused {
170 return
171 }
172 if err = m.adopt(t.Context(), item, target, canonical); err != nil {
173 t.Fatal("replay", err)
174 }
175 got, err := r.Registry.GetTimer(target.Host, "refresh")
176 want := *original
177 want.ClaimOwnership = m.owner(item, original.Registration)
178 if err != nil || *got != want {
179 t.Fatal("identity or bookkeeping changed", got, err)
180 }
181 in, rev, err := svc.Inventory.Snapshot(t.Context(), target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp})
182 if err != nil || rev != 2 {
183 t.Fatal("inventory revision", rev, err)
184 }
185 e := in.Entries[1]
186 if e.ManagedRoot != "registry" || e.RowIdentity != original.Registration || e.Ownership != want.ClaimOwner {
187 t.Fatal("timer not inventoried", e)
188 }
189 retained, err := root.Installation(t.Context(), target.Host, hostguard.Captured{Incarnation: target.Incarnation, Stamp: target.Stamp})
190 if err != nil || retained.InventoryRevision != rev {
191 t.Fatal("root revision", retained, err)
192 }
193 var decoded StopReceipt
194 if json.Unmarshal(receipt, &decoded) != nil {
195 t.Fatal("receipt")
196 }
197 saved, err := guard.FenceReceiptFor(t.Context(), target)
198 if err != nil || string(saved.Bytes) != string(receipt) {
199 t.Fatal("receipt changed", err)
200 }
201 if _, err := state.Read("absent"); !errors.Is(err, os.ErrNotExist) {
202 t.Fatal(err)
203 }
204 })
205 }
206}
207