carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "github.com/carlosframework/platform/internal/activator"
7 "github.com/carlosframework/platform/internal/genselect"
8 "github.com/carlosframework/platform/internal/hostbox"
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/hostguard"
11 "github.com/carlosframework/platform/internal/hostops"
12 "github.com/carlosframework/platform/internal/hostwrite"
13 "github.com/carlosframework/platform/internal/lease"
14 "github.com/carlosframework/platform/internal/park"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17 "path/filepath"
18 "strings"
19 "testing"
20 "time"
21)
22
23type parkRegistrationEvidence struct{}
24
25func (parkRegistrationEvidence) VerifyRetirement(context.Context, hostclaim.Observed, []lease.ObjectVersion) (string, time.Time, error) {
26 return "retired", time.Unix(10, 0), nil
27}
28
29type parkRegistrationWake struct {
30 parkRegistrationEvidence
31 opens int
32 failure error
33}
34
35func (p *parkRegistrationWake) Open(context.Context, release.Store, lease.VersionedStore, hostbox.Actor, ClaimMoveReadAccess) (ClaimMovesOwners, ClaimFilesManifest, error) {
36 p.opens++
37 return ClaimMovesOwners{}, nil, p.failure
38}
39func (*parkRegistrationWake) View(context.Context, hostclaim.Observed) (park.View, error) {
40 return park.View{}, nil
41}
42func (*parkRegistrationWake) LeaseSet(context.Context, string, string) ([]string, error) {
43 return []string{"lease"}, nil
44}
45func (*parkRegistrationWake) Close(context.Context) error { return nil }
46
47func TestMoveParksRegistrationIndependentAndAtomic(t *testing.T) {
48 claimMoveSources.Lock()
49 oldSource, oldParks := claimMoveSources.Source, claimMoveSources.Parks
50 claimMoveSources.Source = nil
51 claimMoveSources.Parks = nil
52 claimMoveSources.Unlock()
53 t.Cleanup(func() {
54 claimMoveSources.Lock()
55 claimMoveSources.Source = oldSource
56 claimMoveSources.Parks = oldParks
57 claimMoveSources.Unlock()
58 })
59 err := RegisterProductionClaimMoveParks(nil, nil)
60 for _, name := range []string{"view", "regional evidence"} {
61 if err == nil || !strings.Contains(err.Error(), name) {
62 t.Fatalf("missing %s: %v", name, err)
63 }
64 }
65 if claimMoveSources.Parks != nil {
66 t.Fatal("partial registration")
67 }
68 source := &parkRegistrationWake{}
69 if err := RegisterProductionClaimMoveParks(source.View, source); err != nil {
70 t.Fatal(err)
71 }
72 if source.opens != 0 || claimMoveSources.Source != nil {
73 t.Fatal("parks opened or registered wake")
74 }
75 if err := RegisterProductionClaimMoveParks(source.View, source); !errors.Is(err, hostwrite.ErrUnavailable) {
76 t.Fatalf("replacement: %v", err)
77 }
78 parks := claimMoveSources.Parks
79 if err := RegisterProductionClaimMoveSources(source); err != nil {
80 t.Fatal(err)
81 }
82 if claimMoveSources.Parks != parks || claimMoveSources.Source != source {
83 t.Fatal("wake registration replaced parks")
84 }
85 if err := RegisterProductionClaimMoveSources(source); !errors.Is(err, hostwrite.ErrUnavailable) {
86 t.Fatal("duplicate wake registration accepted")
87 }
88}
89
90func TestMoveParksWakeFailureRemainsWakeRefusal(t *testing.T) {
91 boom := errors.New("wake failed")
92 source := &parkRegistrationWake{failure: boom}
93 out := ClaimOwnerBindings{}
94 openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, source, &out, func(context.Context) error { return nil })
95 if source.opens != 1 || !errors.Is(out.wakeUnavailable, boom) {
96 t.Fatalf("wake failure lost: %v", out.wakeUnavailable)
97 }
98 w := refusingClaimMoveWake("box", nil, out.wakeUnavailable)
99 if err := w.Prerequisites(t.Context(), registry.Route{}, hostclaim.Observed{}, activator.StartPolicy{}); !errors.Is(err, boom) {
100 t.Fatalf("wake opened: %v", err)
101 }
102 openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, nil, &out, func(context.Context) error { return nil })
103 if out.wakeUnavailable == nil || !strings.Contains(out.wakeUnavailable.Error(), "C8") {
104 t.Fatal("missing wake not named")
105 }
106}
107
108func TestMoveParksVerifiedIdentityAndRealEngine(t *testing.T) {
109 t.Parallel()
110 identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", ControlBucket: "control", Revision: 1, Binding: "1"})
111 actor, err := identity.Verify(t.Context())
112 if err != nil {
113 t.Fatal(err)
114 }
115 check := claimBoundIdentity(identity.Verify, actor)
116 sentinel := errors.New("real view")
117 calls := 0
118 engine := park.Engine{Bucket: actor.Bucket, Region: actor.Region, View: func(context.Context, hostclaim.Observed) (park.View, error) { calls++; return park.View{}, sentinel }}
119 providers := ClaimProviders{moveParks: &claimMoveParks{local: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, engine: engine, check: check, leases: func(context.Context, string, string) ([]string, error) { return []string{"real lease"}, nil }}}
120 got, leases, _, err := providers.MoveParks(t.Context())
121 if err != nil {
122 t.Fatal(err)
123 }
124 if _, err := got.View(t.Context(), hostclaim.Observed{}); !errors.Is(err, sentinel) || calls != 1 {
125 t.Fatalf("engine replaced: %v", err)
126 }
127 if names, err := leases(t.Context(), actor.Bucket, "scope"); err != nil || len(names) != 1 || names[0] != "real lease" {
128 t.Fatal("leases replaced")
129 }
130 mutations := map[string]func(*hostbox.Actor){"enrollment": func(a *hostbox.Actor) { a.Enrollment = "new" }, "instance": func(a *hostbox.Actor) { a.Cloud.InstanceID = "new" }, "control bucket": func(a *hostbox.Actor) { a.ControlBucket = "new" }, "data bucket": func(a *hostbox.Actor) { a.Bucket = "new" }, "namespace": func(a *hostbox.Actor) { a.Namespace = "new" }, "binding": func(a *hostbox.Actor) { a.Binding += "-changed" }, "account": func(a *hostbox.Actor) { a.Cloud.Account = "new" }}
131 for name, mutate := range mutations {
132 t.Run(name, func(t *testing.T) {
133 changed := actor
134 mutate(&changed)
135 providers.moveParks.check = claimBoundIdentity(func(context.Context) (hostbox.Actor, error) { return changed, nil }, actor)
136 if _, _, _, err := providers.MoveParks(t.Context()); !errors.Is(err, hostbox.ErrChanged) {
137 t.Fatalf("identity change accepted: %v", err)
138 }
139 })
140 }
141 // The roster write counter is not part of the bound identity: holds and
142 // other boxes' rows move it without changing this box's binding.
143 t.Run("revision", func(t *testing.T) {
144 changed := actor
145 changed.Revision++
146 providers.moveParks.check = claimBoundIdentity(func(context.Context) (hostbox.Actor, error) { return changed, nil }, actor)
147 if _, _, _, err := providers.MoveParks(t.Context()); err != nil {
148 t.Fatalf("roster revision bump refused: %v", err)
149 }
150 })
151 if _, _, _, err := (ClaimProviders{}).MoveParks(t.Context()); !errors.Is(err, hostwrite.ErrUnavailable) {
152 t.Fatal("unbuilt accessor accepted")
153 }
154}
155
156type moveParkNoData struct{ lease.VersionedStore }
157type moveParkNoLocal struct{ hostbox.LocalAuthority }
158
159func TestMoveParksBuildWithFailedWakeUsesRegionalEngine(t *testing.T) {
160 ctx := t.Context()
161 base := t.TempDir()
162 reg, err := registry.Open(filepath.Join(base, "registry.db"))
163 if err != nil {
164 t.Fatal(err)
165 }
166 defer reg.Close()
167 inventory, err := openLegacyInventory(t, filepath.Join(base, "inventory.db"))
168 if err != nil {
169 t.Fatal(err)
170 }
171 defer inventory.Close()
172 store := release.NewDirStore(filepath.Join(base, "store"))
173 identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", ControlBucket: "control", Revision: 1, Binding: "1"})
174 actor, err := identity.Verify(ctx)
175 if err != nil {
176 t.Fatal(err)
177 }
178 boom := errors.New("configured wake failed")
179 wakeSource := &parkRegistrationWake{failure: boom}
180 owners := ClaimOwnerBindings{}
181 openProductionClaimMoveWake(ctx, store, actor, wakeSource, &owners, claimBoundIdentity(identity.Verify, actor))
182 viewCalls := 0
183 a := ClaimAssembly{
184 wakeUnavailable: owners.wakeUnavailable, Claims: store, Guard: hostguard.Guard{DataDir: base}, Identity: identity,
185 RetirementAuthority: &hostops.ProductionAuthority{Store: store, Now: time.Now},
186 Local: moveParkNoLocal{}, Data: moveParkNoData{}, Wake: &ClaimWakeAssembly{}, Stop: &ClaimStopCoordinator{},
187 Controllers: productionStopControllers{}, Launches: &hostbox.LaunchJournal{},
188 Reconcile: &hostbox.ReconcileAdapter{Registry: reg, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: filepath.Join(base, "retirement")}},
189 ManagedRoots: map[string]string{"data": base, "opt": filepath.Join(base, "opt"), "units": filepath.Join(base, "units")},
190 Evidence: parkRegistrationEvidence{},
191 LeaseSet: func(context.Context, string, string) ([]string, error) { return []string{"actual-inventory"}, nil },
192 View: func(context.Context, hostclaim.Observed) (park.View, error) { viewCalls++; return park.View{}, nil },
193 Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil },
194 AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil },
195 PublicationEvidence: func(context.Context, hostclaim.Observed) error { return nil }, NewID: func() string { return "id" }, Close: func(context.Context) error { return nil },
196 }
197 providers, err := a.Build(ctx)
198 if err != nil {
199 t.Fatal(err)
200 }
201 engine, leases, local, err := providers.MoveParks(ctx)
202 if local == nil {
203 t.Fatal("missing local authorizer")
204 }
205 if err := local(ctx, &hostclaim.Claim{Host: "app.example"}, hostclaim.Obligation{Kind: "park-local"}); err != nil {
206 t.Fatal("local authorizer went through VerifyBound", err)
207 }
208 if err != nil {
209 t.Fatal(err)
210 }
211 if err := providers.Wake.Prerequisites(ctx, registry.Route{}, hostclaim.Observed{}, activator.StartPolicy{}); !errors.Is(err, boom) {
212 t.Fatalf("wake error lost: %v", err)
213 }
214 if _, err := engine.View(ctx, hostclaim.Observed{}); err != nil || viewCalls != 1 {
215 t.Fatalf("wrong view: %v", err)
216 }
217 names, err := leases(ctx, actor.Bucket, "scope")
218 if err != nil || len(names) != 1 || names[0] != "actual-inventory" {
219 t.Fatalf("wrong inventory: %v %v", names, err)
220 }
221 // NewClaimRegionalWorker strengthens Authorize. The accessor must return
222 // that final engine, not its earlier permissive construction value.
223 if err := engine.Authorize(ctx, hostclaim.Observed{}, hostclaim.Obligation{}); err == nil {
224 t.Fatal("accessor bypassed retirement evidence guard")
225 }
226 regional := providers.Regional.(hostbox.RegionalWorker)
227 if engine.Store.(hostbox.RequestStore).Store != regional.Regional.Engine.Store.(hostbox.RequestStore).Store || engine.Claims != regional.Regional.Engine.Claims {
228 t.Fatal("accessor reconstructed another engine")
229 }
230 if err := providers.Close(ctx); err != nil {
231 t.Fatal(err)
232 }
233}
234