| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "fmt" |
| 7 | "os" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostclaim" |
| 11 | execlease "github.com/carlosframework/platform/internal/hostexec" |
| 12 | "github.com/carlosframework/platform/internal/hostguard" |
| 13 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 14 | ) |
| 15 | |
| 16 | func TestPlan14ExecutionMigrationIsIndependent(t *testing.T) { |
| 17 | t.Parallel() |
| 18 | for _, attack := range []string{"none", "missing", "legacy marker", "incomplete", "pending", "never enrolled", "enrollment", "box", "generation"} { |
| 19 | t.Run(attack, func(t *testing.T) { |
| 20 | _, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, "checking") |
| 21 | if _, err := root.commitRootInventory(t.Context(), iq); err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | disk := &rollbackDisk{dir: t.TempDir()} |
| 25 | census := rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| 26 | switch attack { |
| 27 | case "incomplete": |
| 28 | census.Complete = false |
| 29 | case "pending": |
| 30 | census.Pending = "acquisition" |
| 31 | case "never enrolled": |
| 32 | census.NeverEnrolled = true |
| 33 | case "enrollment": |
| 34 | census.Enrollment = "other" |
| 35 | case "box": |
| 36 | census.Box = "other" |
| 37 | case "generation": |
| 38 | census.Migration++ |
| 39 | } |
| 40 | if attack != "missing" && attack != "legacy marker" { |
| 41 | raw, _ := json.Marshal(census) |
| 42 | if err := disk.Write("census.json", raw); err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | } |
| 46 | if attack == "legacy marker" { |
| 47 | if err := disk.Write("migration.json", []byte(`{"complete":true}`)); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | } |
| 51 | q := execlease.ExecutionRequest{Schema: 1, ID: "begin", Operation: "executionBegin", Host: iq.Target.Host, Incarnation: iq.Target.Incarnation, Stamp: iq.Target.Stamp, Session: iq.Target.Session, Op: iq.Target.Op} |
| 52 | a, err := root.executionAuthorityWithMigration(t.Context(), disk, q, execlease.ControllerProcess{}) |
| 53 | if attack == "none" { |
| 54 | if err != nil || a.Migration != 1 { |
| 55 | t.Fatalf("valid migration: %+v %v", a, err) |
| 56 | } |
| 57 | } else if err == nil { |
| 58 | t.Fatal("invalid migration admitted", attack) |
| 59 | } |
| 60 | }) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | func TestPlan14ExecutionPreinstallationBindsMigration(t *testing.T) { |
| 65 | _, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, "checking") |
| 66 | disk := &rollbackDisk{dir: t.TempDir()} |
| 67 | raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| 68 | if err := disk.Write("census.json", raw); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | q := execlease.ExecutionRequest{Schema: 1, ID: "begin", Operation: "executionBegin", Host: iq.Target.Host, Incarnation: iq.Target.Incarnation, Stamp: iq.Target.Stamp, Session: iq.Target.Session, Op: iq.Target.Op} |
| 72 | a, err := root.executionAuthorityWithMigration(context.Background(), disk, q, execlease.ControllerProcess{}) |
| 73 | if err != nil || !a.ParticipationOnly || a.Migration != 1 || a.Installation != "" { |
| 74 | t.Fatalf("participation before registration: %+v %v", a, err) |
| 75 | } |
| 76 | if _, err := disk.Read(q.Host + ".json"); !os.IsNotExist(err) { |
| 77 | t.Fatal("authority read created participation", err) |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func TestPlan14ExecutionServingBeforeInstallation(t *testing.T) { |
| 82 | _, root, iq := plan14InstallRegistration(t, hostclaim.KindServing, "") |
| 83 | disk := &rollbackDisk{dir: t.TempDir()} |
| 84 | raw, _ := json.Marshal(rootparticipation.Census{Schema: 1, Box: "box-1", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| 85 | if err := disk.Write("census.json", raw); err != nil { |
| 86 | t.Fatal(err) |
| 87 | } |
| 88 | boot, err := execlease.BootID() |
| 89 | if err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | q := execlease.ExecutionRequest{Schema: 1, ID: "first-serving", Operation: "executionView", Host: iq.Target.Host, Incarnation: iq.Target.Incarnation, Stamp: iq.Target.Stamp, Session: iq.Target.Session} |
| 93 | peer := execlease.ControllerProcess{KernelBootID: boot, PID: os.Getpid(), Start: "fixture"} |
| 94 | broker, err := execlease.NewRootBroker(execlease.RootBrokerConfig{ |
| 95 | State: disk, Remote: root.Stop.Coordinator.Evidence, Clock: execlease.SystemClock{}, BootID: boot, Identity: "broker", |
| 96 | Authority: func(ctx context.Context, q execlease.ExecutionRequest, p execlease.ControllerProcess) (execlease.ExecutionAuthority, error) { |
| 97 | return root.executionAuthorityWithMigration(ctx, disk, q, p) |
| 98 | }, |
| 99 | Alive: func(context.Context, execlease.ControllerProcess) error { return nil }, |
| 100 | Cleanup: func(context.Context, execlease.ExecutionRecord) error { t.Error("serving cleanup"); return nil }, |
| 101 | Settle: func(context.Context, execlease.ExecutionRecord) error { t.Error("serving settlement"); return nil }, |
| 102 | Exclude: func(context.Context, func() error) error { |
| 103 | t.Error("serving acquired lease") |
| 104 | return execlease.ErrState |
| 105 | }, |
| 106 | }) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | t.Cleanup(func() { _ = broker.Close(context.Background()) }) |
| 111 | out, err := broker.Execute(t.Context(), peer, q) |
| 112 | if err != nil || out.Status != "serving" || out.Token != "" { |
| 113 | t.Fatalf("first serving participation: %+v %v", out, err) |
| 114 | } |
| 115 | if _, err := disk.Read(q.Host + ".json"); !os.IsNotExist(err) { |
| 116 | t.Fatal("serving created lease", err) |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | func TestPlan14ExecutionRetainedSourceParticipation(t *testing.T) { |
| 121 | t.Parallel() |
| 122 | for _, attack := range []string{"none", "source", "enrollment", "stamp"} { |
| 123 | t.Run(attack, func(t *testing.T) { |
| 124 | f, root, iq := plan14InstallRegistration(t, hostclaim.KindServing, "") |
| 125 | if _, err := root.commitRootInventory(t.Context(), iq); err != nil { |
| 126 | t.Fatal(err) |
| 127 | } |
| 128 | prior := f.read().Claim.Holder.Stamp |
| 129 | _, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { |
| 130 | c.Holder.Stamp++ |
| 131 | c.Op = hostclaim.Op{ID: "move", Kind: hostclaim.KindLanding, Phase: "checking", Exec: "box-2", Participants: []string{"box-1", "box-2"}, Intent: map[string]string{"source": "box-1", "source_enrollment": "en", "target": "box-2", "to": "ie"}, Journal: map[string]string{hostclaim.JournalSourceStamp: fmt.Sprint(prior)}} |
| 132 | switch attack { |
| 133 | case "source": |
| 134 | c.Op.Intent["source"] = "box-3" |
| 135 | case "enrollment": |
| 136 | c.Op.Intent["source_enrollment"] = "other" |
| 137 | case "stamp": |
| 138 | c.Op.Journal[hostclaim.JournalSourceStamp] = fmt.Sprint(prior + 1) |
| 139 | } |
| 140 | return nil |
| 141 | }) |
| 142 | if err != nil { |
| 143 | t.Fatal(err) |
| 144 | } |
| 145 | q := executionExpectation(f.read().Claim) |
| 146 | q.ID, q.Operation = "source", "executionBegin" |
| 147 | a, err := root.executionAuthority(t.Context(), q, execlease.ControllerProcess{}) |
| 148 | if attack != "none" { |
| 149 | if err == nil { |
| 150 | t.Fatal("foreign retained source admitted") |
| 151 | } |
| 152 | return |
| 153 | } |
| 154 | if err != nil || !a.ParticipationOnly || a.Installation == "" { |
| 155 | t.Fatalf("retained source: %+v %v", a, err) |
| 156 | } |
| 157 | }) |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func TestPlan14ExecutionInstalledWithoutLaunchCleanup(t *testing.T) { |
| 162 | t.Parallel() |
| 163 | for _, positive := range []bool{false, true} { |
| 164 | t.Run(fmt.Sprint(positive), func(t *testing.T) { |
| 165 | f, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, "checking") |
| 166 | if _, err := root.commitRootInventory(t.Context(), iq); err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | q := executionExpectation(f.read().Claim) |
| 170 | a, err := root.executionAuthority(t.Context(), q, execlease.ControllerProcess{}) |
| 171 | if err != nil { |
| 172 | t.Fatal(err) |
| 173 | } |
| 174 | calls := 0 |
| 175 | root.Stop.Coordinator.Kernel = executionNoLaunchKernel{stopKernelFunc(func(context.Context, hostguard.FenceTarget, StopInventory, DrainAcknowledgement) (StopKernelEvidence, error) { |
| 176 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 177 | }), func(_ context.Context, target hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, error) { |
| 178 | calls++ |
| 179 | if target.Host != q.Host || target.Stamp != q.Stamp || target.Session != 0 || inv.InstallationStamp != q.Stamp { |
| 180 | t.Fatal("wrong empty installation") |
| 181 | } |
| 182 | if !positive { |
| 183 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 184 | } |
| 185 | return StopKernelEvidence{}, nil |
| 186 | }} |
| 187 | err = root.cleanupExecution(t.Context(), execlease.ExecutionRecord{Request: q, Authority: a}) |
| 188 | if calls != 1 || (err == nil) != positive { |
| 189 | t.Fatalf("kernel completeness calls=%d err=%v", calls, err) |
| 190 | } |
| 191 | }) |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | type executionNoLaunchKernel struct { |
| 196 | stopKernelFunc |
| 197 | verify func(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) |
| 198 | } |
| 199 | |
| 200 | func (k executionNoLaunchKernel) VerifyNoProcess(ctx context.Context, target hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, error) { |
| 201 | return k.verify(ctx, target, inv) |
| 202 | } |
| 203 | |
| 204 | func TestPlan14ExecutionRecoveryStartsCanonicalFence(t *testing.T) { |
| 205 | for _, changed := range []bool{false, true} { |
| 206 | t.Run(fmt.Sprint(changed), func(t *testing.T) { |
| 207 | coordinator, req, _ := claimStopFixture(t) |
| 208 | root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: coordinator}} |
| 209 | target := hostguard.FenceTarget{Host: req.Host, Incarnation: req.Incarnation, Stamp: req.InstallationStamp, Session: req.WriterSession, Box: req.Box, Enrollment: req.Enrollment, BootID: req.BootID, ControllerID: req.ControllerID, DirectoryRevision: req.ExpectedDirectoryRevision, ObligationID: req.ObligationID} |
| 210 | if changed { |
| 211 | target.Session++ |
| 212 | } |
| 213 | got, err := root.beginExecutionRecoveryFence(t.Context(), target) |
| 214 | if changed { |
| 215 | if err == nil { |
| 216 | t.Fatal("fenced a different writer session") |
| 217 | } |
| 218 | return |
| 219 | } |
| 220 | if err != nil { |
| 221 | t.Fatal(err) |
| 222 | } |
| 223 | if got.FenceID == "" || got.ClaimETag != req.ExpectedClaimETag || got.Op != req.OperationID || got.Phase != req.Phase { |
| 224 | t.Fatalf("unbound fence: %+v", got) |
| 225 | } |
| 226 | if err := coordinator.Guard.ResumeFence(t.Context(), got); err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | }) |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | func TestPlan14ExecutionFirstSessionTransition(t *testing.T) { |
| 234 | t.Parallel() |
| 235 | f, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, "checking") |
| 236 | q := executionExpectation(f.read().Claim) |
| 237 | before, err := root.executionAuthority(t.Context(), q, execlease.ControllerProcess{}) |
| 238 | if err != nil { |
| 239 | t.Fatal(err) |
| 240 | } |
| 241 | if _, err := root.commitRootInventory(t.Context(), iq); err != nil { |
| 242 | t.Fatal(err) |
| 243 | } |
| 244 | if _, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.Session++; return nil }); err != nil { |
| 245 | t.Fatal(err) |
| 246 | } |
| 247 | nextQ := executionExpectation(f.read().Claim) |
| 248 | after, err := root.executionAuthority(t.Context(), nextQ, execlease.ControllerProcess{}) |
| 249 | if err != nil { |
| 250 | t.Fatal(err) |
| 251 | } |
| 252 | before.Migration = after.Migration |
| 253 | old := execlease.ExecutionRecord{Request: q, Authority: before} |
| 254 | if err := root.transitionExecution(t.Context(), old, nextQ, after); err != nil { |
| 255 | t.Fatal("first root-authorized session refused", err) |
| 256 | } |
| 257 | nextQ.Op = "other" |
| 258 | if err := root.transitionExecution(t.Context(), old, nextQ, after); err == nil { |
| 259 | t.Fatal("foreign operation admitted") |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | func TestPlan14ExecutionEstablishedSessionTransitionNeedsCleanup(t *testing.T) { |
| 264 | t.Parallel() |
| 265 | for _, clean := range []bool{false, true} { |
| 266 | t.Run(fmt.Sprint(clean), func(t *testing.T) { |
| 267 | f, root, iq := plan14InstallRegistration(t, hostclaim.KindLanding, "checking") |
| 268 | if _, err := root.commitRootInventory(t.Context(), iq); err != nil { |
| 269 | t.Fatal(err) |
| 270 | } |
| 271 | q := executionExpectation(f.read().Claim) |
| 272 | before, err := root.executionAuthority(t.Context(), q, execlease.ControllerProcess{}) |
| 273 | if err != nil { |
| 274 | t.Fatal(err) |
| 275 | } |
| 276 | if _, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.Session++; return nil }); err != nil { |
| 277 | t.Fatal(err) |
| 278 | } |
| 279 | nextQ := executionExpectation(f.read().Claim) |
| 280 | after, err := root.executionAuthority(t.Context(), nextQ, execlease.ControllerProcess{}) |
| 281 | if err != nil { |
| 282 | t.Fatal(err) |
| 283 | } |
| 284 | calls := 0 |
| 285 | root.Stop.Coordinator.Kernel = executionNoLaunchKernel{verify: func(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error) { |
| 286 | calls++ |
| 287 | if !clean { |
| 288 | return StopKernelEvidence{}, ErrClaimStopInventory |
| 289 | } |
| 290 | return StopKernelEvidence{}, nil |
| 291 | }} |
| 292 | err = root.transitionExecution(t.Context(), execlease.ExecutionRecord{Request: q, Authority: before}, nextQ, after) |
| 293 | if (err == nil) != clean || calls != 1 { |
| 294 | t.Fatalf("cleanup=%t calls=%d err=%v", clean, calls, err) |
| 295 | } |
| 296 | }) |
| 297 | } |
| 298 | } |
| 299 | |