| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strconv" |
| 9 | "sync" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/activator" |
| 13 | "github.com/carlosframework/platform/internal/cgroup" |
| 14 | "github.com/carlosframework/platform/internal/hostbox" |
| 15 | "github.com/carlosframework/platform/internal/hostclaim" |
| 16 | execlease "github.com/carlosframework/platform/internal/hostexec" |
| 17 | "github.com/carlosframework/platform/internal/hostguard" |
| 18 | "github.com/carlosframework/platform/internal/instancectl" |
| 19 | ) |
| 20 | |
| 21 | type launchClaims struct { |
| 22 | mu sync.Mutex |
| 23 | obs hostclaim.Observed |
| 24 | } |
| 25 | |
| 26 | func (c *launchClaims) Read(_ context.Context, host string) (hostclaim.Observed, error) { |
| 27 | c.mu.Lock() |
| 28 | defer c.mu.Unlock() |
| 29 | o := c.obs |
| 30 | if o.Claim != nil { |
| 31 | cl := *o.Claim |
| 32 | h := *cl.Holder |
| 33 | cl.Holder = &h |
| 34 | o.Claim = &cl |
| 35 | } |
| 36 | return o, nil |
| 37 | } |
| 38 | |
| 39 | func (c *launchClaims) set(fn func(*hostclaim.Claim)) { |
| 40 | c.mu.Lock() |
| 41 | defer c.mu.Unlock() |
| 42 | cl := *c.obs.Claim |
| 43 | h := *cl.Holder |
| 44 | cl.Holder = &h |
| 45 | fn(&cl) |
| 46 | c.obs.Claim = &cl |
| 47 | } |
| 48 | |
| 49 | type launchUnits struct { |
| 50 | mu sync.Mutex |
| 51 | starts []instancectl.ClaimUnit |
| 52 | stops []string |
| 53 | startErr error |
| 54 | beforeCheck func() |
| 55 | sameErr error |
| 56 | goneErr error |
| 57 | // goneAfterStartErr answers Gone only once a start was attempted. |
| 58 | goneAfterStartErr error |
| 59 | stopErr error |
| 60 | started instancectl.ClaimUnitStarted |
| 61 | } |
| 62 | |
| 63 | func (u *launchUnits) Start(_ context.Context, in instancectl.ClaimUnit) (instancectl.ClaimUnitStarted, error) { |
| 64 | u.mu.Lock() |
| 65 | defer u.mu.Unlock() |
| 66 | u.starts = append(u.starts, in) |
| 67 | if u.startErr != nil { |
| 68 | return instancectl.ClaimUnitStarted{}, u.startErr |
| 69 | } |
| 70 | return u.started, nil |
| 71 | } |
| 72 | |
| 73 | func (u *launchUnits) Stop(_ context.Context, host string) error { |
| 74 | u.mu.Lock() |
| 75 | defer u.mu.Unlock() |
| 76 | u.stops = append(u.stops, host) |
| 77 | return u.stopErr |
| 78 | } |
| 79 | |
| 80 | func (u *launchUnits) Gone(context.Context, string, string) error { |
| 81 | u.mu.Lock() |
| 82 | defer u.mu.Unlock() |
| 83 | if u.goneAfterStartErr != nil && len(u.starts) > 0 { |
| 84 | return u.goneAfterStartErr |
| 85 | } |
| 86 | return u.goneErr |
| 87 | } |
| 88 | |
| 89 | func (u *launchUnits) counts() (int, int) { |
| 90 | u.mu.Lock() |
| 91 | defer u.mu.Unlock() |
| 92 | return len(u.starts), len(u.stops) |
| 93 | } |
| 94 | |
| 95 | type launchKernel struct { |
| 96 | path string |
| 97 | want hostbox.LaunchFile |
| 98 | verifyErr error |
| 99 | observed ClaimLaunchObserved |
| 100 | observeErr error |
| 101 | observe func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) |
| 102 | digest string |
| 103 | } |
| 104 | |
| 105 | func (k *launchKernel) Verify(path string, want hostbox.LaunchFile) error { |
| 106 | if k.verifyErr != nil { |
| 107 | return k.verifyErr |
| 108 | } |
| 109 | if path != k.path || want != k.want { |
| 110 | return hostbox.ErrChanged |
| 111 | } |
| 112 | return nil |
| 113 | } |
| 114 | |
| 115 | func (k *launchKernel) Observe(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) { |
| 116 | if k.observe != nil { |
| 117 | return k.observe(instancectl.ClaimUnitStarted{}) |
| 118 | } |
| 119 | return k.observed, k.observeErr |
| 120 | } |
| 121 | |
| 122 | type launchFixture struct { |
| 123 | root *ClaimLaunchRoot |
| 124 | claims *launchClaims |
| 125 | units *launchUnits |
| 126 | kernel *launchKernel |
| 127 | peer StopPeer |
| 128 | req hostbox.RoleLaunchRequest |
| 129 | journal *hostbox.LaunchJournal |
| 130 | target hostguard.FenceTarget |
| 131 | actor *hostbox.Actor |
| 132 | } |
| 133 | |
| 134 | func newLaunchFixture(t *testing.T, op hostclaim.Op) *launchFixture { |
| 135 | return newLaunchFixtureHost(t, op, "shop.example") |
| 136 | } |
| 137 | |
| 138 | func newLaunchFixtureHost(t *testing.T, op hostclaim.Op, host string) *launchFixture { |
| 139 | t.Helper() |
| 140 | boot, err := execlease.BootID() |
| 141 | if err != nil { |
| 142 | t.Skip("root launch requires Linux") |
| 143 | } |
| 144 | start, err := claimConstructionProcessStart() |
| 145 | if err != nil { |
| 146 | t.Fatal(err) |
| 147 | } |
| 148 | data := t.TempDir() |
| 149 | if err := os.Mkdir(filepath.Join(data, "hostguard"), 0o770); err != nil { |
| 150 | t.Fatal(err) |
| 151 | } |
| 152 | claim := &hostclaim.Claim{V: 2, Host: host, Gen: 3, Holder: &hostclaim.Holder{Incarnation: "inc-1", Stamp: 11, Session: 13, WriterBox: "box-1"}, Op: op} |
| 153 | claims := &launchClaims{obs: hostclaim.Observed{Host: host, Claim: claim, ETag: "etag-1"}} |
| 154 | guard := hostguard.Guard{DataDir: data, LockDir: t.TempDir(), Claims: claims} |
| 155 | if err := guard.Activate(t.Context(), host, "box-1", nil); err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) |
| 159 | if err != nil { |
| 160 | t.Fatal(err) |
| 161 | } |
| 162 | t.Cleanup(func() { journal.Close() }) |
| 163 | previousAgentRoot := claimContainmentAgentRoot |
| 164 | claimContainmentAgentRoot = func(context.Context) (string, error) { return "/sys/fs/cgroup/system.slice/carlos-edge.service", nil } |
| 165 | t.Cleanup(func() { claimContainmentAgentRoot = previousAgentRoot }) |
| 166 | target := hostguard.FenceTarget{Region: "eu", Host: host, Incarnation: "inc-1", Stamp: 11, Session: 13, Op: op.ID, Phase: op.Phase, Box: "box-1", Enrollment: "enrollment-1", BootID: boot, ControllerID: "controller-1", ClaimETag: "etag-1", DirectoryRevision: "7", RosterRevision: 7} |
| 167 | if err = journal.Prelaunch(t.Context(), hostbox.LaunchInventory{ControllerPID: os.Getpid(), ProcessStart: start, Target: target, Groups: []hostbox.LaunchGroup{{RelativePath: filepath.Join("system.slice/carlos-edge.service", cgroup.ExecName(claimContainmentName(target))), Device: 1, Inode: 9}}, NoState: true}); err != nil { |
| 168 | t.Fatal(err) |
| 169 | } |
| 170 | actor := &hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Binding: "7", Region: "eu", Eligible: true} |
| 171 | exe := hostbox.LaunchFile{Device: 5, Inode: 6} |
| 172 | units := &launchUnits{started: instancectl.ClaimUnitStarted{Unit: instancectl.UnitName(instancectl.RoleRun, host), InvocationID: "unit-inv-1", ControlGroup: instancectl.ClaimUnitControlGroup(host), MainPID: 4242}} |
| 173 | kernel := &launchKernel{path: "/opt/carlos/versions/v1/app", want: exe, observed: ClaimLaunchObserved{ProcessStart: "ps-4242", TenantPID: 4243, TenantStart: "ps-4243", Group: hostbox.LaunchFile{Device: 3, Inode: 4}, Executable: exe}} |
| 174 | peer := StopPeer{UID: 1001, PID: os.Getpid(), ProcessStart: start, BootID: boot, ControllerID: "controller-1"} |
| 175 | root := &ClaimLaunchRoot{ValidateExecutable: func(string, string, string) error { return nil }, Guard: guard, Journal: journal, Units: units, Kernel: kernel, Actor: func(context.Context) (hostbox.Actor, error) { return *actor, nil }} |
| 176 | reservation := hostbox.LaunchReservation{ID: "attempt-1:main", Role: "main", InvocationID: "inv-1", Inputs: "inputs-1", Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(host)}, Executable: exe} |
| 177 | req := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStart, Reservation: &reservation, Executable: kernel.path, MemoryMax: cgroup.TierDefault.MemoryMax} |
| 178 | f := &launchFixture{root: root, claims: claims, units: units, kernel: kernel, peer: peer, req: req, journal: journal, target: target, actor: actor} |
| 179 | configureRev2Launch(t, f) |
| 180 | return f |
| 181 | } |
| 182 | |
| 183 | func servingOp() hostclaim.Op { return hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"} } |
| 184 | |
| 185 | func (f *launchFixture) noReservation(t *testing.T, name string) { |
| 186 | t.Helper() |
| 187 | if starts, _ := f.units.counts(); starts != 0 { |
| 188 | t.Fatalf("%s reached the unit start", name) |
| 189 | } |
| 190 | if _, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID); err == nil { |
| 191 | t.Fatalf("%s journaled a reservation", name) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | func TestClaimLaunchRootStartsExactlyTheReservedRole(t *testing.T) { |
| 196 | f := newLaunchFixture(t, servingOp()) |
| 197 | resp, err := f.root.Serve(t.Context(), f.peer, f.req) |
| 198 | if err != nil { |
| 199 | t.Fatal(err) |
| 200 | } |
| 201 | group := hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(f.target.Host), Device: 3, Inode: 4, InvocationID: "unit-inv-1"} |
| 202 | if resp.V != hostbox.RoleLaunchV || resp.Protocol != hostbox.RoleLaunchProtocol || resp.Grant == nil || resp.Grant.Check(*f.req.Reservation) != nil || resp.Grant.Group != group || resp.Grant.PID != 4242 || resp.Grant.ProcessStart != "ps-4242" { |
| 203 | t.Fatalf("grant=%+v", resp) |
| 204 | } |
| 205 | if len(f.units.starts) != 1 || f.units.starts[0] != (instancectl.ClaimUnit{Host: f.target.Host, Executable: f.kernel.path, MemoryMax: cgroup.TierDefault.MemoryMax}) { |
| 206 | t.Fatalf("unit starts=%+v", f.units.starts) |
| 207 | } |
| 208 | got, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID) |
| 209 | if err != nil || got.State != hostbox.UnitStarted || got.Group != group || got.PID != 4242 || got.ProcessStart != "ps-4242" || got.Executable != f.req.Reservation.Executable { |
| 210 | t.Fatalf("journal=%+v %v", got, err) |
| 211 | } |
| 212 | // A replay of the same request, after success, is refused without effect. |
| 213 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); !errors.Is(err, hostbox.ErrChanged) { |
| 214 | t.Fatalf("replay: %v", err) |
| 215 | } |
| 216 | if starts, _ := f.units.counts(); starts != 1 { |
| 217 | t.Fatal("replay started a second unit") |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | func TestClaimLaunchRootSerializesConcurrentDuplicates(t *testing.T) { |
| 222 | f := newLaunchFixture(t, servingOp()) |
| 223 | var wg sync.WaitGroup |
| 224 | errs := make(chan error, 4) |
| 225 | for range 4 { |
| 226 | wg.Add(1) |
| 227 | go func() { defer wg.Done(); _, err := f.root.Serve(t.Context(), f.peer, f.req); errs <- err }() |
| 228 | } |
| 229 | wg.Wait() |
| 230 | close(errs) |
| 231 | ok := 0 |
| 232 | for err := range errs { |
| 233 | if err == nil { |
| 234 | ok++ |
| 235 | } |
| 236 | } |
| 237 | if starts, _ := f.units.counts(); ok != 1 || starts != 1 { |
| 238 | t.Fatalf("successes=%d starts=%d", ok, starts) |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | func TestClaimLaunchRootTrustsOnlyItsOwnProviders(t *testing.T) { |
| 243 | for name, mutate := range map[string]func(*launchFixture){ |
| 244 | "other controller": func(f *launchFixture) { f.peer.ControllerID = "controller-2" }, |
| 245 | "other boot": func(f *launchFixture) { f.peer.BootID = "boot-2" }, |
| 246 | "root uid": func(f *launchFixture) { f.peer.UID = 0 }, |
| 247 | "not the record's pid": func(f *launchFixture) { f.peer.PID = 43 }, |
| 248 | "restarted controller": func(f *launchFixture) { f.peer.ProcessStart = "start-43" }, |
| 249 | "roster box": func(f *launchFixture) { f.actor.Box = "box-2" }, |
| 250 | "roster enrollment": func(f *launchFixture) { f.actor.Enrollment = "enrollment-2" }, |
| 251 | "box binding": func(f *launchFixture) { f.actor.Binding = "8" }, |
| 252 | "roster region": func(f *launchFixture) { f.actor.Region = "us" }, |
| 253 | "quarantined box": func(f *launchFixture) { f.actor.Quarantined = true }, |
| 254 | "holder session": func(f *launchFixture) { f.claims.set(func(c *hostclaim.Claim) { c.Holder.Session = 14 }) }, |
| 255 | "holder stamp": func(f *launchFixture) { f.claims.set(func(c *hostclaim.Claim) { c.Holder.Stamp = 12 }) }, |
| 256 | "executor elsewhere": func(f *launchFixture) { f.claims.set(func(c *hostclaim.Claim) { c.Op.Exec = "box-2" }) }, |
| 257 | "writer elsewhere": func(f *launchFixture) { f.claims.set(func(c *hostclaim.Claim) { c.Holder.WriterBox = "box-2" }) }, |
| 258 | "box declared lost": func(f *launchFixture) { f.claims.set(func(c *hostclaim.Claim) { c.Lost = []string{"box-1"} }) }, |
| 259 | "operation began": func(f *launchFixture) { |
| 260 | f.claims.set(func(c *hostclaim.Claim) { c.Op.ID, c.Op.Kind = "op-9", hostclaim.KindParking }) |
| 261 | }, |
| 262 | "build not verified": func(f *launchFixture) { f.kernel.verifyErr = hostbox.ErrChanged }, |
| 263 | "build path differs": func(f *launchFixture) { f.req.Executable = "/opt/carlos/versions/v2/app" }, |
| 264 | "tier not a platform": func(f *launchFixture) { f.req.MemoryMax = 12345 }, |
| 265 | "reservation target op": func(f *launchFixture) { r := *f.req.Reservation; r.Target.Op = "op-1"; f.req.Reservation = &r }, |
| 266 | } { |
| 267 | f := newLaunchFixture(t, servingOp()) |
| 268 | mutate(f) |
| 269 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { |
| 270 | t.Fatalf("%s launched", name) |
| 271 | } |
| 272 | f.noReservation(t, name) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | func TestClaimLaunchRootRefusesWithoutThePrelaunchRecord(t *testing.T) { |
| 277 | f := newLaunchFixture(t, servingOp()) |
| 278 | r := *f.req.Reservation |
| 279 | r.Target.Session = 14 |
| 280 | f.claims.set(func(c *hostclaim.Claim) { c.Holder.Session = 14 }) |
| 281 | f.req.Reservation = &r |
| 282 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { |
| 283 | t.Fatal("launched a session with no prelaunch record") |
| 284 | } |
| 285 | if starts, _ := f.units.counts(); starts != 0 { |
| 286 | t.Fatal("unit started") |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | func TestClaimLaunchRootProbeIsBoundToTheOperationAndInstallStamp(t *testing.T) { |
| 291 | probing := hostclaim.Op{Kind: hostclaim.KindLanding, ID: "op-1", Phase: hostclaim.PhaseLandingProbing, Exec: "box-1", Journal: map[string]string{"install_stamp": "11"}} |
| 292 | f := newLaunchFixture(t, probing) |
| 293 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 294 | t.Fatalf("probe: %v", err) |
| 295 | } |
| 296 | for name, mutate := range map[string]func(*hostclaim.Claim){ |
| 297 | "install stamp": func(c *hostclaim.Claim) { c.Op.Journal = map[string]string{"install_stamp": "12"} }, |
| 298 | "probe passed": func(c *hostclaim.Claim) { c.Op.Phase = hostclaim.PhaseLandingProbed }, |
| 299 | "other operation": func(c *hostclaim.Claim) { c.Op.ID = "op-2" }, |
| 300 | "relocation no pin": func(c *hostclaim.Claim) { c.Op.Kind, c.Op.Journal = hostclaim.KindRelocating, nil }, |
| 301 | } { |
| 302 | f := newLaunchFixture(t, probing) |
| 303 | f.claims.set(mutate) |
| 304 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { |
| 305 | t.Fatalf("%s launched", name) |
| 306 | } |
| 307 | f.noReservation(t, name) |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | func TestClaimLaunchRootRefusesAFencedSession(t *testing.T) { |
| 312 | probing := hostclaim.Op{Kind: hostclaim.KindLanding, ID: "op-1", Phase: hostclaim.PhaseLandingProbing, Exec: "box-1"} |
| 313 | f := newLaunchFixture(t, probing) |
| 314 | fence := f.target |
| 315 | fence.FenceID, fence.ObligationID = "fence-1", "obligation-1" |
| 316 | if _, err := f.root.Guard.BeginFence(t.Context(), fence); err != nil { |
| 317 | t.Fatal(err) |
| 318 | } |
| 319 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); !errors.Is(err, hostguard.ErrFenced) { |
| 320 | t.Fatalf("fenced session launched: %v", err) |
| 321 | } |
| 322 | f.noReservation(t, "fenced session") |
| 323 | } |
| 324 | |
| 325 | func TestClaimLaunchRootCleansUpAStartItCannotVouchFor(t *testing.T) { |
| 326 | f := newLaunchFixture(t, servingOp()) |
| 327 | f.kernel.observed.Executable = hostbox.LaunchFile{Device: 5, Inode: 7} |
| 328 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { |
| 329 | t.Fatal("swapped build accepted") |
| 330 | } |
| 331 | if _, stops := f.units.counts(); stops != 1 { |
| 332 | t.Fatal("swapped build left running") |
| 333 | } |
| 334 | got, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID) |
| 335 | if err != nil || got.State != hostbox.UnitSettled { |
| 336 | t.Fatalf("journal=%+v %v", got, err) |
| 337 | } |
| 338 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); !errors.Is(err, hostbox.ErrChanged) { |
| 339 | t.Fatalf("spent reservation retried: %v", err) |
| 340 | } |
| 341 | |
| 342 | // A failed start whose unit root cannot prove gone stays pending, so the |
| 343 | // stop journal keeps refusing a receipt. |
| 344 | f = newLaunchFixture(t, servingOp()) |
| 345 | f.units.startErr = errors.New("start failed") |
| 346 | f.units.goneAfterStartErr = errors.New("still active") |
| 347 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil { |
| 348 | t.Fatal("failed start accepted") |
| 349 | } |
| 350 | got, err = f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID) |
| 351 | if err != nil || got.State != hostbox.UnitIntended { |
| 352 | t.Fatalf("unproven unit settled: %+v %v", got, err) |
| 353 | } |
| 354 | sj, err := f.journal.ReadStopKernelJournal(t.Context(), f.target) |
| 355 | if err != nil || sj.Settled { |
| 356 | t.Fatalf("stop journal settled=%v %v", sj.Settled, err) |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | func TestClaimLaunchRootStopSettlesAndBurnsReservations(t *testing.T) { |
| 361 | f := newLaunchFixture(t, servingOp()) |
| 362 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 363 | t.Fatal(err) |
| 364 | } |
| 365 | stop := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStop, Reservation: f.req.Reservation} |
| 366 | other := f.peer |
| 367 | other.ControllerID = "controller-2" |
| 368 | if _, err := f.root.Serve(t.Context(), other, stop); err == nil { |
| 369 | t.Fatal("another controller stopped the role") |
| 370 | } |
| 371 | f.units.goneErr = errors.New("still active") |
| 372 | if _, err := f.root.Serve(t.Context(), f.peer, stop); err == nil { |
| 373 | t.Fatal("stop reported success while the unit stayed") |
| 374 | } |
| 375 | f.units.goneErr = nil |
| 376 | if _, err := f.root.Serve(t.Context(), f.peer, stop); err != nil { |
| 377 | t.Fatal(err) |
| 378 | } |
| 379 | got, err := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID) |
| 380 | if err != nil || got.State != hostbox.UnitSettled { |
| 381 | t.Fatalf("journal=%+v %v", got, err) |
| 382 | } |
| 383 | _, before := f.units.counts() |
| 384 | if _, err := f.root.Serve(t.Context(), f.peer, stop); err != nil { |
| 385 | t.Fatalf("idempotent stop: %v", err) |
| 386 | } |
| 387 | if _, after := f.units.counts(); after != before { |
| 388 | t.Fatal("settled stop touched the unit again") |
| 389 | } |
| 390 | |
| 391 | // A stop for a reservation root never saw burns it. |
| 392 | g := newLaunchFixture(t, servingOp()) |
| 393 | stop.Reservation = g.req.Reservation |
| 394 | if _, err := g.root.Serve(t.Context(), g.peer, stop); err != nil { |
| 395 | t.Fatal(err) |
| 396 | } |
| 397 | if _, err := g.root.Serve(t.Context(), g.peer, g.req); !errors.Is(err, hostbox.ErrChanged) { |
| 398 | t.Fatalf("start after stop: %v", err) |
| 399 | } |
| 400 | if starts, _ := g.units.counts(); starts != 0 { |
| 401 | t.Fatal("burnt reservation started") |
| 402 | } |
| 403 | } |
| 404 | |
| 405 | func TestClaimLaunchRootCapabilityNeedsEveryProvider(t *testing.T) { |
| 406 | f := newLaunchFixture(t, servingOp()) |
| 407 | q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchCapability} |
| 408 | resp, err := f.root.Serve(t.Context(), f.peer, q) |
| 409 | if err != nil || resp.Protocol != hostbox.RoleLaunchProtocol || resp.Grant != nil { |
| 410 | t.Fatalf("capability=%+v %v", resp, err) |
| 411 | } |
| 412 | for name, strip := range map[string]func(*ClaimLaunchRoot){ |
| 413 | "local": func(r *ClaimLaunchRoot) { r.Local = nil }, |
| 414 | "clock": func(r *ClaimLaunchRoot) { r.Clock = nil }, |
| 415 | "data root": func(r *ClaimLaunchRoot) { r.DataRoot = "" }, |
| 416 | "units": func(r *ClaimLaunchRoot) { r.Units = nil }, |
| 417 | "kernel": func(r *ClaimLaunchRoot) { r.Kernel = nil }, |
| 418 | "journal": func(r *ClaimLaunchRoot) { r.Journal = nil }, |
| 419 | "actor": func(r *ClaimLaunchRoot) { r.Actor = nil }, |
| 420 | "guard": func(r *ClaimLaunchRoot) { r.Guard.Claims = nil }, |
| 421 | } { |
| 422 | g := newLaunchFixture(t, servingOp()) |
| 423 | strip(g.root) |
| 424 | if _, err := g.root.Serve(t.Context(), g.peer, q); !errors.Is(err, activator.ErrClaimIsolationUnavailable) { |
| 425 | t.Fatalf("%s missing: %v", name, err) |
| 426 | } |
| 427 | if _, err := g.root.Serve(t.Context(), g.peer, g.req); !errors.Is(err, activator.ErrClaimIsolationUnavailable) { |
| 428 | t.Fatalf("%s missing start: %v", name, err) |
| 429 | } |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | func TestClaimUnitStopKernelRequiresTheUnitGone(t *testing.T) { |
| 434 | f := newLaunchFixture(t, servingOp()) |
| 435 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 436 | t.Fatal(err) |
| 437 | } |
| 438 | inner := 0 |
| 439 | k := claimUnitStopKernel{Journal: f.journal, Units: f.units, Inner: stopKernelFunc(func(_ context.Context, target hostguard.FenceTarget, _ StopInventory, _ DrainAcknowledgement) (StopKernelEvidence, error) { |
| 440 | inner++ |
| 441 | j, err := f.journal.ReadStopKernelJournal(t.Context(), target) |
| 442 | if err != nil || !j.Settled { |
| 443 | t.Fatalf("inner verifier saw an unsettled unit: %+v %v", j, err) |
| 444 | } |
| 445 | return StopKernelEvidence{CgroupIdentities: []string{"cg"}}, nil |
| 446 | })} |
| 447 | f.units.goneErr = errors.New("still active") |
| 448 | if _, err := k.VerifyStopped(t.Context(), f.target, StopInventory{}, DrainAcknowledgement{}); err == nil || inner != 0 { |
| 449 | t.Fatalf("running unit verified: %v inner=%d", err, inner) |
| 450 | } |
| 451 | f.units.goneErr = nil |
| 452 | ev, err := k.VerifyStopped(t.Context(), f.target, StopInventory{}, DrainAcknowledgement{}) |
| 453 | if err != nil || inner != 1 || len(ev.UnitInvocationIDs) != 1 || ev.UnitInvocationIDs[0] != "unit-inv-1" { |
| 454 | t.Fatalf("evidence=%+v %v inner=%d", ev, err, inner) |
| 455 | } |
| 456 | } |
| 457 | |
| 458 | func TestClaimUnitStopKernelGuardsRecovery(t *testing.T) { |
| 459 | f := newLaunchFixture(t, servingOp()) |
| 460 | if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| 461 | t.Fatal(err) |
| 462 | } |
| 463 | called := 0 |
| 464 | k := claimUnitStopKernel{Journal: f.journal, Units: f.units, Inner: recoveryKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 465 | called++ |
| 466 | return StopKernelEvidence{}, DrainAcknowledgement{}, nil |
| 467 | })} |
| 468 | f.units.goneErr = errors.New("still active") |
| 469 | if _, _, err := k.RecoverStopped(t.Context(), f.target, StopInventory{}); err == nil || called != 0 { |
| 470 | t.Fatalf("recovery settled a running unit: %v called=%d", err, called) |
| 471 | } |
| 472 | f.units.goneErr = nil |
| 473 | ev, _, err := k.RecoverStopped(t.Context(), f.target, StopInventory{}) |
| 474 | if err != nil || called != 1 || len(ev.UnitInvocationIDs) != 1 { |
| 475 | t.Fatalf("recovery=%+v %v called=%d", ev, err, called) |
| 476 | } |
| 477 | if _, ok := StopKernelVerifier(claimUnitStopKernel{Inner: stopKernelFunc(nil)}).(stopRecoveryKernel); !ok { |
| 478 | t.Fatal("wrapper hides recovery") |
| 479 | } |
| 480 | if _, _, err := (claimUnitStopKernel{Journal: f.journal, Units: f.units, Inner: stopKernelFunc(nil)}).RecoverStopped(t.Context(), f.target, StopInventory{}); !errors.Is(err, ErrClaimStopUnavailable) { |
| 481 | t.Fatalf("recovery without an inner recovery kernel: %v", err) |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | type recoveryKernelFunc func(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) |
| 486 | |
| 487 | func (f recoveryKernelFunc) VerifyStopped(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) { |
| 488 | return StopKernelEvidence{}, ErrClaimStopUnavailable |
| 489 | } |
| 490 | |
| 491 | func (f recoveryKernelFunc) RecoverStopped(ctx context.Context, t hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) { |
| 492 | return f(ctx, t, in) |
| 493 | } |
| 494 | |
| 495 | var _ = strconv.Itoa |
| 496 | |
| 497 | func (u *launchUnits) StartChecked(ctx context.Context, in instancectl.ClaimUnit, _ instancectl.Launch, check func() error) (instancectl.ClaimUnitStarted, error) { |
| 498 | if u.beforeCheck != nil { |
| 499 | u.beforeCheck() |
| 500 | } |
| 501 | if err := check(); err != nil { |
| 502 | return instancectl.ClaimUnitStarted{}, err |
| 503 | } |
| 504 | return u.Start(ctx, in) |
| 505 | } |
| 506 | func (u *launchUnits) Same(context.Context, instancectl.ClaimUnitStarted) error { return u.sameErr } |
| 507 | func (k *launchKernel) VerifyDigest(_ context.Context, path string, want hostbox.LaunchFile, digest string) error { |
| 508 | if digest != k.digest { |
| 509 | return hostbox.ErrChanged |
| 510 | } |
| 511 | return k.Verify(path, want) |
| 512 | } |
| 513 | |
| 514 | func (u *launchUnits) StartReserved(ctx context.Context, in instancectl.ClaimUnit, l instancectl.Launch, p instancectl.ClaimExecPermit, check func() error) (instancectl.ClaimUnitStarted, error) { |
| 515 | if !p.Valid() { |
| 516 | return instancectl.ClaimUnitStarted{}, instancectl.ErrClaimUnit |
| 517 | } |
| 518 | return u.StartChecked(ctx, in, l, check) |
| 519 | } |
| 520 | |