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 "net"
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostbox"
17 "github.com/carlosframework/platform/internal/hostclaim"
18 execlease "github.com/carlosframework/platform/internal/hostexec"
19 "github.com/carlosframework/platform/internal/hostwrite"
20 "github.com/carlosframework/platform/internal/instancectl"
21 "github.com/carlosframework/platform/internal/provision"
22)
23
24type rev2Local struct{ err error }
25
26func (l *rev2Local) Check(context.Context, hostbox.Actor, *hostclaim.Claim, hostbox.Selection) error {
27 return l.err
28}
29
30type rev2Clock struct {
31 wall time.Time
32 boot int64
33}
34
35func (c *rev2Clock) Now() (time.Time, int64, error) { return c.wall, c.boot, nil }
36func (c *rev2Clock) After(d time.Duration) <-chan time.Time { return time.After(d) }
37
38type rev2Attempts struct {
39 attempt hostclaim.WakeRetryAttempt
40 op hostclaim.Op
41 stamp, session uint64
42}
43
44func (a *rev2Attempts) ReadAttempt(_ context.Context, o hostclaim.Observed) (hostclaim.WakeRetryAttempt, error) {
45 if o.Claim.Op.ID != a.op.ID || o.Claim.Op.Kind != a.op.Kind || o.Claim.Op.Phase != a.op.Phase || o.Claim.Holder.Stamp != a.stamp || o.Claim.Holder.Session != a.session {
46 return hostclaim.WakeRetryAttempt{}, hostbox.ErrChanged
47 }
48 return a.attempt, nil
49}
50
51func configureRev2Launch(t *testing.T, f *launchFixture) {
52 t.Helper()
53 f.root.DataRoot = f.root.Guard.DataDir
54 f.root.Local = &rev2Local{}
55 f.root.Clock = &rev2Clock{wall: time.Now(), boot: 100}
56 f.root.ReadyBudget, f.root.ReadyPoll = 40*time.Millisecond, time.Millisecond
57 f.root.ExecLockDir = t.TempDir()
58 f.kernel.digest = strings.Repeat("a", 64)
59 f.kernel.observed.Running = f.kernel.observed.Executable
60 f.req.Spec = &hostbox.LaunchSpec{Identity: hostwrite.Identity{Config: "config", Release: "release"}, Executable: f.kernel.path, Digest: f.kernel.digest, Args: []string{"--socket", filepath.Join(provision.InstanceRunDir(f.target.Host), "app.sock"), "--db", provision.NestedDBPath(f.root.DataRoot, f.target.Host)}, Env: []string{"CARLOS_ADMIN_TOKEN=test"}}
61 digest, err := f.req.Spec.Hash()
62 if err != nil {
63 t.Fatal(err)
64 }
65 f.req.Reservation.Inputs = digest
66 a := hostclaim.WakeRetryAttempt{ID: "attempt-1", InvocationID: "inv-1", LaunchInputs: digest, Box: f.target.Box, Enrollment: f.target.Enrollment, BootID: f.peer.BootID, ControllerID: f.peer.ControllerID, Session: f.target.Session}
67 if f.claims.obs.Claim.Op.Kind == hostclaim.KindServing {
68 f.claims.set(func(c *hostclaim.Claim) {
69 c.Holder.WakeRetry = &hostclaim.WakeRetry{EpisodeID: "episode", State: "attempting", AttemptsReserved: 1, Attempt: &a}
70 })
71 } else {
72 f.root.Attempts = map[string]claimLaunchAttemptReader{f.claims.obs.Claim.Op.Kind: &rev2Attempts{attempt: a, op: f.claims.obs.Claim.Op, stamp: f.target.Stamp, session: f.target.Session}}
73 }
74 configureLaunchParticipation(t, f)
75}
76func writeRev2Local(t *testing.T, f *launchFixture, status string, deadline int64) {
77 t.Helper()
78 q := execlease.ExecutionRequest{Schema: 1, ID: "begin", Operation: "executionBegin", Host: f.target.Host, Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Session: f.target.Session, Op: f.target.Op}
79 r := execlease.ExecutionRecord{Schema: 1, Request: q, Peer: execlease.ControllerProcess{KernelBootID: f.peer.BootID, PID: f.peer.PID, Start: f.peer.ProcessStart}, Authority: execlease.ExecutionAuthority{Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, Installation: "root-installation"}, Broker: "root", Token: "root-token", Status: status, SafeUntil: deadline, LeaseETag: "root-etag", Expires: time.Now().Add(time.Minute), Replay: map[string]execlease.ExecutionReplay{}}
80 raw, _ := json.Marshal(r)
81 if err := f.root.executionState.Write(f.target.Host+".json", raw); err != nil {
82 t.Fatal(err)
83 }
84}
85
86func rev2Operation() hostclaim.Op {
87 return hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "operation", Phase: "probing", Exec: "box-1", Journal: map[string]string{"install_stamp": "11"}}
88}
89
90func TestPlan11Rev2OperationVerifierAndLocalState(t *testing.T) {
91 for _, name := range []string{"valid", "no verifier", "wrong attempt", "wrong input", "expired", "pending", "lost", "admission closed", "released live lock", "missing state"} {
92 t.Run(name, func(t *testing.T) {
93 f := newLaunchFixture(t, rev2Operation())
94 switch name {
95 case "no verifier":
96 f.root.Attempts = nil
97 case "wrong attempt":
98 f.root.Attempts[hostclaim.KindRelocating].(*rev2Attempts).attempt.InvocationID = "other"
99 case "wrong input":
100 f.req.Spec.Env = []string{"CARLOS_ADMIN_TOKEN=other"}
101 case "expired":
102 writeRev2Local(t, f, "active", 100)
103 case "pending":
104 writeRev2Local(t, f, "pending", 1000)
105 case "lost":
106 writeRev2Local(t, f, "lost", 1000)
107 case "admission closed":
108 f.root.Local.(*rev2Local).err = execlease.ErrLost
109 case "released live lock":
110 if err := os.Remove(filepath.Join(f.root.executionState.(*launchParticipationDisk).dir, f.target.Host+".live")); err != nil {
111 t.Fatal(err)
112 }
113 case "missing state":
114 if err := os.Remove(filepath.Join(f.root.executionState.(*launchParticipationDisk).dir, f.target.Host+".json")); err != nil {
115 t.Fatal(err)
116 }
117 }
118 _, err := f.root.Serve(t.Context(), f.peer, f.req)
119 if name == "valid" {
120 if err != nil {
121 t.Fatal(err)
122 }
123 return
124 }
125 if err == nil {
126 t.Fatal("unsafe operation launch")
127 }
128 f.noReservation(t, name)
129 })
130 }
131}
132
133func TestPlan11Rev2RechecksAfterUnitPreparation(t *testing.T) {
134 for _, name := range []string{"deadline", "quiet", "closed", "reservation", "roster"} {
135 t.Run(name, func(t *testing.T) {
136 f := newLaunchFixture(t, rev2Operation())
137 f.units.beforeCheck = func() {
138 switch name {
139 case "deadline":
140 f.root.Clock.(*rev2Clock).boot = 1000
141 case "quiet":
142 f.claims.set(func(c *hostclaim.Claim) { c.QuietAfter = map[string]time.Time{"eu": time.Now().Add(time.Hour)} })
143 case "closed":
144 f.root.Local.(*rev2Local).err = execlease.ErrLost
145 case "reservation":
146 f.root.Attempts[hostclaim.KindRelocating].(*rev2Attempts).attempt.ID = "replaced"
147 case "roster":
148 f.actor.Eligible = false
149 }
150 }
151 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil {
152 t.Fatal("stale final check")
153 }
154 if starts, _ := f.units.counts(); starts != 0 {
155 t.Fatal("tenant code started")
156 }
157 got, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID)
158 if err != nil || got.State != hostbox.UnitSettled {
159 t.Fatalf("unstarted intention not settled: %+v %v", got, err)
160 }
161 })
162 }
163}
164
165func TestPlan11Rev2WaitsForSameInvocation(t *testing.T) {
166 for _, name := range []string{"slow valid", "timeout", "replacement invocation", "reused pid", "wrong executable", "cancel"} {
167 t.Run(name, func(t *testing.T) {
168 f := newLaunchFixture(t, servingOp())
169 calls := 0
170 ctx, cancel := context.WithCancel(t.Context())
171 defer cancel()
172 f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
173 calls++
174 got := f.kernel.observed
175 if name == "replacement invocation" && calls == 1 {
176 f.units.sameErr = instancectl.ErrClaimUnit
177 }
178 if name == "reused pid" && calls > 1 {
179 got.ProcessStart = "reused"
180 }
181 if name == "cancel" {
182 cancel()
183 }
184 if calls < 4 || name == "timeout" {
185 return ClaimLaunchObserved{ProcessStart: got.ProcessStart, Group: got.Group}, errClaimLaunchPending
186 }
187 if name == "wrong executable" {
188 got.Running.Inode++
189 }
190 return got, nil
191 }
192 _, err := f.root.Serve(ctx, f.peer, f.req)
193 starts, stops := f.units.counts()
194 if starts != 1 || f.claims.obs.Claim.Holder.WakeRetry.AttemptsReserved != 1 {
195 t.Fatalf("readiness consumed an attempt: %d", starts)
196 }
197 if name == "slow valid" {
198 if err != nil || stops != 0 || calls != 4 {
199 t.Fatalf("valid start killed: %v calls=%d stops=%d", err, calls, stops)
200 }
201 return
202 }
203 wantStops := 1
204 if name == "replacement invocation" {
205 wantStops = 0
206 }
207 if err == nil || stops != wantStops {
208 t.Fatalf("unverified start granted or replacement stopped: %v stops=%d", err, stops)
209 }
210 if name == "timeout" && !errors.Is(err, context.DeadlineExceeded) {
211 t.Fatal(err)
212 }
213 })
214 }
215}
216
217func TestPlan11Rev2DigestAndExecutable(t *testing.T) {
218 f := newLaunchFixture(t, servingOp())
219 s := f.req.Spec
220 want, err := ClaimLaunchDigest(ClaimLaunchInputs{Identity: s.Identity, Executable: s.Executable, ExecutableSHA256: s.Digest, Args: s.Args, Env: s.Env})
221 got, e := s.Hash()
222 if err != nil || e != nil || got != want {
223 t.Fatalf("digest contract drift: %q %q %v %v", got, want, err, e)
224 }
225 for _, name := range []string{"config", "release", "executable", "sha", "args", "env", "inode"} {
226 t.Run(name, func(t *testing.T) {
227 f := newLaunchFixture(t, servingOp())
228 s := *f.req.Spec
229 f.req.Spec = &s
230 switch name {
231 case "config":
232 s.Identity.Config = "other"
233 case "release":
234 s.Identity.Release = "other"
235 case "executable":
236 s.Executable = "/opt/other"
237 case "sha":
238 s.Digest = strings.Repeat("b", 64)
239 case "args":
240 s.Args = append([]string(nil), s.Args...)
241 s.Args[1] += "x"
242 case "env":
243 s.Env = []string{"CARLOS_ADMIN_TOKEN=other"}
244 case "inode":
245 f.req.Reservation.Executable.Inode++
246 }
247 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil {
248 t.Fatal("input substitution launched")
249 }
250 f.noReservation(t, name)
251 })
252 }
253}
254
255func TestPlan11Rev2KindPreflightRefusesUnverifiedOperation(t *testing.T) {
256 f := newLaunchFixture(t, servingOp())
257 f.root.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }
258 client := NewClaimRoleRootClient(serveClaimRoot(t, ClaimRootWithLaunch{Launch: f.root}))
259 roles := &ClaimRoleAssembly{Root: client}
260 for _, kind := range []string{hostclaim.KindLanding, hostclaim.KindRelocating, "unknown"} {
261 if err := roles.PreflightKind(t.Context(), kind); err == nil {
262 t.Fatalf("unverified %s passed prerequisites", kind)
263 }
264 }
265 f.noReservation(t, "unsupported kinds")
266 if err := roles.PreflightKind(t.Context(), hostclaim.KindServing); err != nil {
267 t.Fatal(err)
268 }
269}
270