| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostguard" |
| 14 | "github.com/carlosframework/platform/internal/instancectl" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | func TestPlan14RetiredGenerationBoundary(t *testing.T) { |
| 19 | for _, mode := range []string{"retired", "active sibling", "live installation", "pending pin", "open pin", "named", "legacy process", "open writers", "pending unit", "foreign receipt", "bad receipt", "barrier", "missing installation", "missing launches", "missing reservation"} { |
| 20 | t.Run(mode, func(t *testing.T) { |
| 21 | target := hostguard.FenceTarget{Host: "shop.example", Incarnation: "inc", Stamp: 3, Session: 4, Box: "box", Enrollment: "en", BootID: "boot", ControllerID: "controller", FenceID: "fence", Op: "delete", ObligationID: "park", RosterRevision: 1} |
| 22 | at := time.Now().UTC() |
| 23 | s := StopReceipt{Kind: "stopped", DirectoryRevision: testReceiptNumeral(target), Host: target.Host, Incarnation: target.Incarnation, InstallationStamp: target.Stamp, WriterSession: target.Session, Box: target.Box, Enrollment: target.Enrollment, BootID: target.BootID, ControllerID: target.ControllerID, FenceID: target.FenceID, OperationID: target.Op, ObligationID: target.ObligationID, EvidenceAt: at} |
| 24 | view := hostguard.FenceView{Host: target.Host, Installation: hostguard.RootInstallation{Host: target.Host, Incarnation: target.Incarnation, Stamp: target.Stamp}, Retired: []hostguard.Captured{{Incarnation: target.Incarnation, Stamp: target.Stamp}}, Receipts: []hostguard.FenceReceipt{{Target: target, Bytes: mustMarshalStopReceipt(s), EvidenceAt: at}}} |
| 25 | inv := generationInventory{Installations: []hostguard.FenceView{view}, Launches: []hostbox.LaunchInventory{{Target: target, WritersClosed: true, Reservations: []hostbox.LaunchReservation{{Target: target, State: hostbox.UnitSettled, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(target.Host)}}}}}} |
| 26 | records := []release.GenerationReservation{{Binding: release.GenerationBinding{Target: generationOwner(target)}, Closed: true, Retirement: strings.Repeat("a", 64)}} |
| 27 | switch mode { |
| 28 | case "active sibling": |
| 29 | sibling := hostguard.FenceView{Host: "sibling.example", Installation: hostguard.RootInstallation{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}} |
| 30 | inv.Installations = append(inv.Installations, sibling) |
| 31 | inv.Launches = append(inv.Launches, hostbox.LaunchInventory{Target: hostguard.FenceTarget{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}}) |
| 32 | records = append(records, release.GenerationReservation{Binding: release.GenerationBinding{Target: release.GenerationOwner{Host: "sibling.example", Incarnation: "sibling", Stamp: 1}}}) |
| 33 | case "live installation": |
| 34 | inv.Installations[0].Retired = nil |
| 35 | case "pending pin": |
| 36 | records[0].Retirement = "" |
| 37 | case "open pin": |
| 38 | records[0].Closed = false |
| 39 | case "named": |
| 40 | records[0].Binding.Unit = "console.service" |
| 41 | case "legacy process": |
| 42 | records[0].Process.PID = 10 |
| 43 | case "open writers": |
| 44 | inv.Launches[0].WritersClosed = false |
| 45 | case "pending unit": |
| 46 | inv.Launches[0].Reservations[0].State = hostbox.UnitIntended |
| 47 | case "foreign receipt": |
| 48 | inv.Installations[0].Receipts[0].Target.Session++ |
| 49 | case "bad receipt": |
| 50 | inv.Installations[0].Receipts[0].Bytes = []byte("{}") |
| 51 | case "barrier": |
| 52 | inv.Installations[0].Barrier = &target |
| 53 | case "missing installation": |
| 54 | inv.Installations = nil |
| 55 | case "missing launches": |
| 56 | inv.Launches = nil |
| 57 | case "missing reservation": |
| 58 | records = nil |
| 59 | } |
| 60 | if got := retiredGenerationSnapshot(t.Context(), inv, records); got != (mode == "retired" || mode == "active sibling") { |
| 61 | t.Fatalf("boundary = %v", got) |
| 62 | } |
| 63 | }) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | func TestPlan14RetiredGenerationGroups(t *testing.T) { |
| 68 | root := filepath.Join(t.TempDir(), "groups") |
| 69 | if err := emptyGenerationGroups(t.Context(), root); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | if err := os.MkdirAll(filepath.Join(root, "unknown"), 0700); err != nil { |
| 73 | t.Fatal(err) |
| 74 | } |
| 75 | for _, group := range []string{root, filepath.Join(root, "unknown")} { |
| 76 | if err := os.WriteFile(filepath.Join(group, "cgroup.procs"), nil, 0600); err != nil { |
| 77 | t.Fatal(err) |
| 78 | } |
| 79 | } |
| 80 | if err := emptyGenerationGroups(t.Context(), root); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | path := filepath.Join(root, "unknown", "cgroup.procs") |
| 84 | if err := os.WriteFile(path, []byte("42\n"), 0600); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | if err := emptyGenerationGroups(t.Context(), root); err == nil { |
| 88 | t.Fatal("unrecorded managed process ignored") |
| 89 | } |
| 90 | if err := os.Remove(path); err != nil { |
| 91 | t.Fatal(err) |
| 92 | } |
| 93 | if err := emptyGenerationGroups(t.Context(), root); err == nil { |
| 94 | t.Fatal("missing kernel evidence accepted") |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func TestPlan14GenerationEarlyBootStart(t *testing.T) { |
| 99 | for _, start := range []string{"0", "1", "-1", "not-a-number", "18446744073709551616"} { |
| 100 | got, err := generationStart([]byte("1 (init) S " + strings.Repeat("0 ", 18) + start)) |
| 101 | if start == "0" || start == "1" { |
| 102 | if err != nil || got != start { |
| 103 | t.Fatalf("start %s: %s %v", start, got, err) |
| 104 | } |
| 105 | } else if err == nil { |
| 106 | t.Fatalf("invalid start accepted: %s", start) |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |