| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/cgroup" |
| 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/hostguard" |
| 20 | "github.com/carlosframework/platform/internal/hostwrite" |
| 21 | "github.com/carlosframework/platform/internal/instancectl" |
| 22 | "github.com/carlosframework/platform/internal/provision" |
| 23 | "github.com/carlosframework/platform/internal/registry" |
| 24 | ) |
| 25 | |
| 26 | // L2, launch authorize (claimlaunch_authority.go): root's launch authority |
| 27 | // reads the production identity fresh. A reservation whose target carries the |
| 28 | // box binding is authorized before and after six roster writes; a target |
| 29 | // stamped with the roster numeral is refused, and after a quarantine and |
| 30 | // readmission the same reservation is refused. |
| 31 | func TestClaimBindingLaunchAuthorizeSurvivesRosterWrites(t *testing.T) { |
| 32 | ctx := t.Context() |
| 33 | roster := newBindingRosterFixture(t, "dst", "ie") |
| 34 | identity := roster.identity() |
| 35 | actor, err := identity.Verify(ctx) |
| 36 | if err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | dataDir := t.TempDir() |
| 40 | f := newROFixture(t, roOptions{}) |
| 41 | f.inputs.Launch = func(context.Context, registry.Route) (ClaimServingLaunch, error) { |
| 42 | return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/opt/carlos/versions/v1/app", ExecutableSHA256: strings.Repeat("b", 64), Version: "v1", |
| 43 | Args: []string{"--socket", filepath.Join(provision.InstanceRunDir(roHost), "app.sock"), "--db", provision.NestedDBPath(dataDir, roHost)}, Env: []string{"CARLOS_ADMIN_TOKEN=test"}}, nil |
| 44 | } |
| 45 | in, err := f.inputs.Select(ctx, f.route, f.read(), roProbe) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | admitted := f.admit(f.read(), in) |
| 50 | h := admitted.Claim.Holder |
| 51 | boot, err := execlease.BootID() |
| 52 | if err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | start, err := claimConstructionProcessStart() |
| 56 | if err != nil { |
| 57 | t.Fatal(err) |
| 58 | } |
| 59 | peer := StopPeer{UID: 1001, PID: os.Getpid(), ProcessStart: start, BootID: boot, ControllerID: "controller-1"} |
| 60 | target := hostguard.FenceTarget{Region: "ie", Host: roHost, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Op: roOp, Phase: hostclaim.PhaseRelocatingProbing, Box: actor.Box, Enrollment: actor.Enrollment, BootID: peer.BootID, ControllerID: peer.ControllerID, ClaimETag: admitted.ETag, DirectoryRevision: actor.Binding} |
| 61 | attempts := &relocationProbeAttempts{Claims: f.claims, DataDir: dataDir, Box: actor.Box, NewID: f.inputs.NewID, Now: f.inputs.Now} |
| 62 | s := OperationStart{Admitted: admitted, Policy: roProbe, Inputs: in, Owner: target} |
| 63 | if err := attempts.Reserve(ctx, s, 0); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | a := attempts.CurrentAttempt(s) |
| 67 | exe := hostbox.LaunchFile{Device: 5, Inode: 6} |
| 68 | kernel := &launchKernel{path: in.Executable, want: exe, digest: in.ExecutableSHA256} |
| 69 | root := &ClaimLaunchRoot{ValidateExecutable: func(string, string, string) error { return nil }, Local: &rev2Local{}, Clock: &rev2Clock{wall: time.Now(), boot: 100}, DataRoot: dataDir, ExecLockDir: t.TempDir(), |
| 70 | Attempts: map[string]claimLaunchAttemptReader{hostclaim.KindRelocating: relocationAttemptReader{DataDir: dataDir}}, |
| 71 | Guard: hostguard.Guard{DataDir: dataDir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.claims}}, |
| 72 | Actor: identity.Verify, Kernel: kernel} |
| 73 | configureLaunchParticipation(t, &launchFixture{root: root, peer: peer, target: target, claims: &launchClaims{obs: admitted}}) |
| 74 | spec := &hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: in.Args, Env: in.Env} |
| 75 | reservation := hostbox.LaunchReservation{ID: a.ID + ":main", Role: "main", InvocationID: a.InvocationID, Inputs: a.LaunchInputs, Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(roHost)}, Executable: exe} |
| 76 | q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchStart, Spec: spec, Reservation: &reservation, Executable: in.Executable, MemoryMax: cgroup.TierDefault.MemoryMax} |
| 77 | if err := root.authorize(ctx, peer, q, admitted.Claim); err != nil { |
| 78 | t.Fatal("authorize at the install's binding:", err) |
| 79 | } |
| 80 | roster.sixWrites() |
| 81 | if err := root.authorize(ctx, peer, q, admitted.Claim); err != nil { |
| 82 | t.Fatal("authorize after six roster writes:", err) |
| 83 | } |
| 84 | numeral := reservation |
| 85 | numeral.Target.DirectoryRevision = fmt.Sprint(roster.read().Roster.Revision) |
| 86 | q.Reservation = &numeral |
| 87 | if err := root.authorize(ctx, peer, q, admitted.Claim); !errors.Is(err, hostbox.ErrChanged) { |
| 88 | t.Fatalf("numeral-stamped target: %v, want ErrChanged", err) |
| 89 | } |
| 90 | q.Reservation = &reservation |
| 91 | roster.quarantine("loss_1") |
| 92 | roster.readmit() |
| 93 | if err := root.authorize(ctx, peer, q, admitted.Claim); !errors.Is(err, hostbox.ErrChanged) { |
| 94 | t.Fatalf("after readmission: %v, want ErrChanged", err) |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | // L2, serving handover (claimservinghandover.go validateServingHandover): the |
| 99 | // prior installation and the successor are both stamped with the binding; |
| 100 | // after six roster writes the handover runs. A successor stamped with the |
| 101 | // roster numeral is refused before any successor state. |
| 102 | func TestClaimBindingServingHandoverSurvivesRosterWrites(t *testing.T) { |
| 103 | ctx := t.Context() |
| 104 | roster := newBindingRosterFixture(t, "box-1", "ie") |
| 105 | identity := roster.identity() |
| 106 | actor, err := identity.Verify(ctx) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | f := newServingRootFixtureFor(t, true, actor) |
| 111 | f.p.root.InventoryActor = identity.Verify |
| 112 | root := servingHandoverRoot(t, f) |
| 113 | roster.sixWrites() |
| 114 | before := f.f.read() |
| 115 | next := hostbox.ColdSuccessor{Host: f.current.Host, Incarnation: f.current.Incarnation, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: fmt.Sprint(roster.read().Roster.Revision), ETag: before.ETag, Generation: f.current.Generation, Stamp: f.current.Stamp, Session: f.current.Session} |
| 116 | if err := root.servingHandover().Run(ctx, f.receipt.Target, next); !errors.Is(err, hostbox.ErrChanged) { |
| 117 | t.Fatalf("numeral successor: %v, want ErrChanged", err) |
| 118 | } |
| 119 | if _, err := f.f.guard.Capture(f.current.Host); err != nil { |
| 120 | t.Fatal(err) |
| 121 | } |
| 122 | next.DirectoryRevision = actor.Binding |
| 123 | if err := root.servingHandover().Run(ctx, f.receipt.Target, next); err != nil { |
| 124 | t.Fatal("serving handover after six roster writes:", err) |
| 125 | } |
| 126 | installed, err := f.f.guard.Capture(f.current.Host) |
| 127 | if err != nil || installed != (hostguard.Captured{Incarnation: f.current.Incarnation, Stamp: f.current.Stamp}) { |
| 128 | t.Fatalf("successor installation: %+v %v", installed, err) |
| 129 | } |
| 130 | in, _, err := root.Reconcile.Inventory.Snapshot(ctx, f.current.Host, installed) |
| 131 | if err != nil || in.DirectoryRevision != actor.Binding { |
| 132 | t.Fatalf("successor inventory stamp %q: %v", in.DirectoryRevision, err) |
| 133 | } |
| 134 | } |
| 135 | |