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 "encoding/json"
6 "errors"
7 "fmt"
8 "strings"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/hostbox"
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/hostguard"
14)
15
16type noProcessKernelFake struct {
17 stopKernelFunc
18 calls int
19 target hostguard.FenceTarget
20 err error
21 during func()
22}
23
24func (k *noProcessKernelFake) VerifyNoProcess(_ context.Context, t hostguard.FenceTarget, _ StopInventory) (StopKernelEvidence, error) {
25 k.calls++
26 k.target = t
27 if k.during != nil {
28 k.during()
29 }
30 if k.err != nil {
31 return StopKernelEvidence{}, k.err
32 }
33 return StopKernelEvidence{InventorySHA256: strings.Repeat("b", 64), LocalJournalSHA256: strings.Repeat("c", 64), CgroupIdentities: []string{"absent:carlos-exec@shop.example"}, UnitInvocationIDs: []string{}}, nil
34}
35
36// changingClaims returns obs on the first read and next afterwards.
37type changingClaims struct {
38 obs, next hostclaim.Observed
39 reads int
40 change bool
41}
42
43func (c *changingClaims) Read(context.Context, string) (hostclaim.Observed, error) {
44 c.reads++
45 if c.change && c.reads > 1 {
46 return c.next, nil
47 }
48 return c.obs, nil
49}
50
51// noProcessFixture is a created incarnation that never admitted a writer,
52// executed on box-1 (serving, session 0).
53func noProcessFixture(t *testing.T) (ClaimStopCoordinator, StopRequest, *noProcessKernelFake, *changingClaims) {
54 t.Helper()
55 c, req, _ := claimStopFixture(t)
56 observed, err := c.Claims.Read(t.Context(), req.Host)
57 if err != nil {
58 t.Fatal(err)
59 }
60 claim := *observed.Claim
61 claim.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: req.Box}
62 claim.Holder = &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11}
63 observed.Claim = &claim
64 claims := &changingClaims{obs: observed}
65 c.Claims = claims
66 kernel := &noProcessKernelFake{}
67 c.Kernel = kernel
68 c.Drainer = stopDrainFunc(func(context.Context, StopRequest, string) (DrainAcknowledgement, error) {
69 t.Fatal("a session-zero stop asked for a producer drain")
70 return DrainAcknowledgement{}, nil
71 })
72 req.Region = "eu-west-1"
73 req.WriterSession = 0
74 req.OperationID, req.Phase, req.ObligationID = "", "", ""
75 return c, req, kernel, claims
76}
77
78func TestNoProcessWitnessPublishesASessionZeroReceipt(t *testing.T) {
79 c, req, kernel, _ := noProcessFixture(t)
80 result, err := c.FenceAndStop(t.Context(), req)
81 if err != nil {
82 t.Fatal(err)
83 }
84 if kernel.calls != 1 || kernel.target.Session != 0 || kernel.target.FenceID != "" {
85 t.Fatalf("kernel = %d %+v", kernel.calls, kernel.target)
86 }
87 var receipt StopReceipt
88 if err := json.Unmarshal(result.Bytes, &receipt); err != nil {
89 t.Fatal(err)
90 }
91 if receipt.Kind != StopReceiptNoProcess || receipt.WriterSession != 0 || receipt.FenceID != "" || receipt.ProducerDrainDigest != "" || receipt.ClaimETag != req.ExpectedClaimETag || receipt.ClaimGen != 19 {
92 t.Fatalf("receipt = %+v", receipt)
93 }
94 if !strings.Contains(result.Key, "/serving-evidence/box-1/") {
95 t.Fatalf("key = %s", result.Key)
96 }
97 stored, _, err := c.Evidence.Get(t.Context(), result.Key)
98 if err != nil || string(stored) == "" {
99 t.Fatalf("not published: %v", err)
100 }
101 if _, err := validateTransportStop(req, result); err != nil {
102 t.Fatalf("the agent's transport check refuses it: %v", err)
103 }
104 // No fence is installed: the claim's ETag, which any admission changes,
105 // is what binds the witness.
106 called := false
107 if err := c.Guard.With(t.Context(), req.Host, hostguard.Captured{Incarnation: req.Incarnation, Stamp: req.InstallationStamp}, func(*hostclaim.Claim) error { return nil }, func() error { called = true; return nil }); err != nil || !called {
108 t.Fatalf("a fence was installed: %v", err)
109 }
110}
111
112func TestNoProcessWitnessRefuses(t *testing.T) {
113 for name, tc := range map[string]struct {
114 change func(*ClaimStopCoordinator, *StopRequest, *noProcessKernelFake, *changingClaims)
115 want error
116 }{
117 "a writer was admitted": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) {
118 cl.obs.Claim.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: "box-1", Session: 1}}
119 }, ErrClaimStopIdentity},
120 "a legacy holder without retention": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, cl *changingClaims) {
121 cl.obs.Claim.Holder.WriterBox = r.Box
122 }, ErrClaimStopIdentity},
123 "a foreign writer box is named": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) {
124 cl.obs.Claim.Holder.WriterBox = "box-2"
125 }, ErrClaimStopIdentity},
126 "another executor": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) {
127 cl.obs.Claim.Op.Exec = "box-2"
128 }, ErrClaimStopIdentity},
129 "a stale claim": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) {
130 r.ExpectedClaimETag = "etag-0"
131 }, ErrClaimStopIdentity},
132 "an admission during the observation": {func(_ *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, cl *changingClaims) {
133 next := cl.obs
134 claim := *next.Claim
135 claim.Holder = &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11, Session: 1, WriterBox: "box-1"}
136 next.Claim, next.ETag = &claim, "etag-2"
137 cl.next, cl.change = next, true
138 }, ErrClaimStopIdentity},
139 "recovery": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) {
140 r.Recover = true
141 }, ErrClaimStopUnavailable},
142 "cleanup": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) {
143 r.Cleanup = true
144 }, ErrClaimStopUnavailable},
145 "another controller": {func(_ *ClaimStopCoordinator, r *StopRequest, _ *noProcessKernelFake, _ *changingClaims) {
146 r.ControllerID = "controller-2"
147 }, ErrClaimStopIdentity},
148 "the kernel sees a process": {func(_ *ClaimStopCoordinator, _ *StopRequest, k *noProcessKernelFake, _ *changingClaims) {
149 k.err = ErrClaimStopInventory
150 }, ErrClaimStopInventory},
151 "a kernel without the witness": {func(c *ClaimStopCoordinator, _ *StopRequest, _ *noProcessKernelFake, _ *changingClaims) {
152 c.Kernel = stopKernelFunc(nil)
153 }, ErrClaimStopUnavailable},
154 } {
155 t.Run(name, func(t *testing.T) {
156 c, req, kernel, claims := noProcessFixture(t)
157 tc.change(&c, &req, kernel, claims)
158 if _, err := c.FenceAndStop(t.Context(), req); !errors.Is(err, tc.want) {
159 t.Fatalf("got %v want %v", err, tc.want)
160 }
161 keys, _ := c.Evidence.List(t.Context(), "control/")
162 if len(keys) != 0 {
163 t.Fatalf("a refused witness published %v", keys)
164 }
165 })
166 }
167}
168
169// The agent asks root for the witness with its own tuple and offers no
170// drain: the session never launched anything.
171func TestStopCapturedSessionZeroAsksRootForTheWitness(t *testing.T) {
172 f := newRecoveryFixture(t)
173 observed := f.read()
174 if observed.Claim.Holder.Session != 0 {
175 t.Fatalf("fixture session = %d", observed.Claim.Holder.Session)
176 }
177 var got StopRequest
178 var drain StopDrainer
179 a := ClaimAssembly{Claims: f.claims, Wake: &ClaimWakeAssembly{}}
180 a.stopLocal = func(context.Context) (claimStopLocal, error) {
181 return claimStopLocal{Actor: hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Region: "ie", Revision: 7, Binding: "7"}, BootID: "boot-1", ControllerID: "controller-1", PID: 42, ProcessStart: "start-42"}, nil
182 }
183 a.collectStop = func(_ context.Context, r StopRequest, d StopDrainer) (StopResult, error) {
184 got, drain = r, d
185 return StopResult{}, errors.New("stop here")
186 }
187 if err := a.stopCapturedSession(t.Context(), observed); err == nil || err.Error() != "stop here" {
188 t.Fatalf("got %v", err)
189 }
190 if got.WriterSession != 0 || got.BootID != "boot-1" || got.ControllerID != "controller-1" || got.Recover || got.ExpectedClaimETag != observed.ETag || got.Region != "ie" {
191 t.Fatalf("request = %+v", got)
192 }
193 if _, ok := drain.(recoveryStopDrain); !ok {
194 t.Fatalf("drain = %T", drain)
195 }
196}
197
198// A refused admission at session zero launched nothing: its cleanup needs
199// no witness, and publishes no receipt.
200func TestFailedSessionZeroAdmissionStopsNothing(t *testing.T) {
201 stops := 0
202 w := &ClaimWakeAssembly{Stop: func(context.Context, hostclaim.Observed) error { stops++; return nil }}
203 o := hostclaim.Observed{Host: "shop.example", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"}}}
204 if err := w.stopAttempt(t.Context(), &claimWakeAttempt{}, o); err != nil || stops != 0 {
205 t.Fatalf("stopAttempt = %v, stops %d", err, stops)
206 }
207 o.Claim.Holder.Session = 1
208 if err := w.stopAttempt(t.Context(), &claimWakeAttempt{}, o); err != nil || stops != 1 {
209 t.Fatalf("an admitted session was not stopped: %v, stops %d", err, stops)
210 }
211}
212
213func TestNoProcessWitnessRetainsBeforePublication(t *testing.T) {
214 for _, reject := range []bool{false, true} {
215 t.Run(fmt.Sprint(reject), func(t *testing.T) {
216 c, req, _, claims := noProcessFixture(t)
217 claims.obs.Claim.Holder.WriterBox = req.Box
218 calls := 0
219 c.RetainNoProcess = func(ctx context.Context, o hostclaim.Observed, result StopResult) error {
220 calls++
221 if o.ETag != req.ExpectedClaimETag || o.Claim.Holder.Session != 0 || result.Target.FenceID != "" {
222 t.Fatal("wrong retained observation")
223 }
224 keys, err := c.Evidence.List(ctx, "control/")
225 if err != nil || len(keys) != 0 {
226 t.Fatal("published before retention")
227 }
228 if reject {
229 return hostbox.ErrChanged
230 }
231 return nil
232 }
233 _, err := c.FenceAndStop(t.Context(), req)
234 if calls != 1 || (err != nil) != reject {
235 t.Fatalf("retention %d %v", calls, err)
236 }
237 if reject {
238 keys, _ := c.Evidence.List(t.Context(), "control/")
239 if len(keys) != 0 {
240 t.Fatal("published despite failed retention")
241 }
242 }
243 })
244 }
245}
246