| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 8 | "github.com/carlosframework/platform/internal/rootstate" |
| 9 | "io" |
| 10 | "os" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/hostbox" |
| 16 | "github.com/carlosframework/platform/internal/hostclaim" |
| 17 | "github.com/carlosframework/platform/internal/hostguard" |
| 18 | "github.com/carlosframework/platform/internal/instancectl" |
| 19 | "github.com/carlosframework/platform/internal/provision" |
| 20 | "github.com/carlosframework/platform/internal/release" |
| 21 | ) |
| 22 | |
| 23 | func TestPlan14FreshInstallRegistration(t *testing.T) { |
| 24 | t.Parallel() |
| 25 | for _, mode := range []string{"new", "writer", "relocation", "landing", "store-failure", "replay", "deleted-history"} { |
| 26 | t.Run(mode, func(t *testing.T) { |
| 27 | kind, phase := hostclaim.KindServing, "" |
| 28 | if mode == "relocation" { |
| 29 | kind, phase = hostclaim.KindRelocating, hostclaim.PhaseInstalling |
| 30 | } |
| 31 | if mode == "landing" { |
| 32 | kind, phase = hostclaim.KindLanding, provision.LandChecking |
| 33 | } |
| 34 | f, root, q := plan14InstallRegistration(t, kind, phase) |
| 35 | if mode == "writer" { |
| 36 | o, err := hostclaim.Update(t.Context(), f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.WriterBox = "box-1"; c.Holder.Session = 1; return nil }) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | q.Target.ClaimETag, q.Target.Session = o.ETag, 1 |
| 41 | q = plan14PreparedInventory(t, root, q) |
| 42 | } |
| 43 | disk := &rollbackDisk{dir: t.TempDir()} |
| 44 | history := &release.RootHistory{Store: disk} |
| 45 | calls := 0 |
| 46 | root.history = func(ctx context.Context, fn func(*release.RootHistory) error) error { |
| 47 | calls++ |
| 48 | if mode == "store-failure" { |
| 49 | return os.ErrPermission |
| 50 | } |
| 51 | return fn(history) |
| 52 | } |
| 53 | out, err := root.commitRootInventory(t.Context(), q) |
| 54 | if mode == "store-failure" { |
| 55 | if err == nil { |
| 56 | t.Fatal("registration ignored history failure") |
| 57 | } |
| 58 | if _, err = root.Stop.Coordinator.Guard.Capture(q.Target.Host); err == nil { |
| 59 | t.Fatal("registered before release exclusion") |
| 60 | } |
| 61 | return |
| 62 | } |
| 63 | if err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if out.Revision != 1 || calls != 1 { |
| 67 | t.Fatalf("out=%+v calls=%d", out, calls) |
| 68 | } |
| 69 | in, err := root.Stop.Coordinator.Guard.Root.View(t.Context(), q.Target.Host, hostguard.Captured{Incarnation: q.Target.Incarnation, Stamp: q.Target.Stamp}) |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | b := release.HistoryBinding{Host: in.Installation.Host, Account: in.Installation.Account, App: in.Installation.App} |
| 74 | id := rootHistoryInstallation(in.Installation) |
| 75 | if mode == "replay" || mode == "deleted-history" { |
| 76 | if mode == "deleted-history" { |
| 77 | names, e := disk.Names() |
| 78 | if e != nil { |
| 79 | t.Fatal(e) |
| 80 | } |
| 81 | for _, name := range names { |
| 82 | if e = disk.Remove(name); e != nil { |
| 83 | t.Fatal(e) |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | if _, err = root.commitRootInventory(t.Context(), q); err != nil { |
| 88 | t.Fatal(err) |
| 89 | } |
| 90 | } |
| 91 | fresh, err := history.FreshInstallation(b, id) |
| 92 | want := mode == "new" || mode == "replay" |
| 93 | if want && (err != nil || !fresh) { |
| 94 | t.Fatalf("new creation not recorded: %v %v", fresh, err) |
| 95 | } |
| 96 | if !want && err == nil && fresh { |
| 97 | t.Fatal("old installation became fresh") |
| 98 | } |
| 99 | }) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | func TestPlan14FreshInstallLaunchObservation(t *testing.T) { |
| 104 | for _, mode := range []string{"ready", "not-ready", "history-failure", "record-failure"} { |
| 105 | t.Run(mode, func(t *testing.T) { |
| 106 | f := newLaunchFixture(t, servingOp()) |
| 107 | f.claims.set(func(c *hostclaim.Claim) { |
| 108 | c.Holder.Account = "bab" |
| 109 | c.Holder.App = "app" |
| 110 | c.Holder.Key = "record" |
| 111 | c.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: f.target.Box, Region: f.target.Region, Session: f.target.Session, Generation: c.Holder.Generation}} |
| 112 | }) |
| 113 | f.root.Guard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) |
| 114 | in := hostguard.RootInstallation{Host: f.target.Host, Account: "bab", App: "app", Record: "record", Incarnation: f.target.Incarnation, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, InventoryRevision: 1} |
| 115 | if err := f.root.Guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | h := &release.RootHistory{Store: &rollbackDisk{dir: t.TempDir()}} |
| 119 | b := release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App} |
| 120 | if err := h.RecordCreation(b, rootHistoryInstallation(in)); err != nil { |
| 121 | t.Fatal(err) |
| 122 | } |
| 123 | f.root.history = func(ctx context.Context, fn func(*release.RootHistory) error) error { |
| 124 | if mode == "history-failure" { |
| 125 | return os.ErrPermission |
| 126 | } |
| 127 | return fn(h) |
| 128 | } |
| 129 | observed := 0 |
| 130 | f.root.installedProgram = func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) { |
| 131 | observed++ |
| 132 | if mode == "record-failure" { |
| 133 | return "", os.ErrPermission |
| 134 | } |
| 135 | return "actual/program", nil |
| 136 | } |
| 137 | if mode == "not-ready" { |
| 138 | f.kernel.observeErr = os.ErrPermission |
| 139 | } |
| 140 | _, err := f.root.Serve(t.Context(), f.peer, f.req) |
| 141 | if (err == nil) != (mode == "ready") { |
| 142 | t.Fatalf("launch: %v", err) |
| 143 | } |
| 144 | programs, e := h.Programs(b) |
| 145 | if e != nil { |
| 146 | t.Fatal(e) |
| 147 | } |
| 148 | if mode == "ready" { |
| 149 | if len(programs) != 1 || programs[0] != "actual/program" { |
| 150 | t.Fatalf("programs: %v", programs) |
| 151 | } |
| 152 | } else if len(programs) != 0 { |
| 153 | t.Fatal("unobserved program recorded", programs) |
| 154 | } |
| 155 | fresh, e := h.FreshInstallation(b, rootHistoryInstallation(in)) |
| 156 | if e != nil || fresh != (mode == "history-failure") { |
| 157 | t.Fatalf("fresh after attempt: %v %v", fresh, e) |
| 158 | } |
| 159 | if mode == "not-ready" && observed != 0 { |
| 160 | t.Fatal("recorded unready launch") |
| 161 | } |
| 162 | }) |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | func TestPlan14InstalledLaunchProgramEvidence(t *testing.T) { |
| 167 | path, err := os.Executable() |
| 168 | if err != nil { |
| 169 | t.Fatal(err) |
| 170 | } |
| 171 | file, err := os.Open(path) |
| 172 | if err != nil { |
| 173 | t.Fatal(err) |
| 174 | } |
| 175 | id, err := rootstate.Identity(file) |
| 176 | if err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | raw, err := io.ReadAll(file) |
| 180 | file.Close() |
| 181 | if err != nil { |
| 182 | t.Fatal(err) |
| 183 | } |
| 184 | digest := sha256.Sum256(raw) |
| 185 | for _, mode := range []string{"observed", "staged-only", "digest", "inode", "process", "unit", "authorization"} { |
| 186 | t.Run(mode, func(t *testing.T) { |
| 187 | f := newLaunchFixture(t, servingOp()) |
| 188 | f.req.Executable = path |
| 189 | f.req.Spec.Digest = hex.EncodeToString(digest[:]) |
| 190 | f.req.Reservation.Executable = hostbox.LaunchFile{Device: id.Device, Inode: id.Inode} |
| 191 | seen := f.kernel.observed |
| 192 | seen.Executable = f.req.Reservation.Executable |
| 193 | seen.Running = seen.Executable |
| 194 | f.kernel.observed = seen |
| 195 | switch mode { |
| 196 | case "staged-only": |
| 197 | seen.TenantPID = 0 |
| 198 | f.kernel.observed = seen |
| 199 | case "digest": |
| 200 | f.req.Spec.Digest = strings.Repeat("0", 64) |
| 201 | case "inode": |
| 202 | f.req.Reservation.Executable.Inode++ |
| 203 | case "process": |
| 204 | f.kernel.observed.TenantStart = "successor" |
| 205 | case "unit": |
| 206 | f.units.sameErr = os.ErrPermission |
| 207 | case "authorization": |
| 208 | f.root.ValidateExecutable = func(string, string, string) error { return os.ErrPermission } |
| 209 | } |
| 210 | program, err := f.root.rootInstalledProgram(t.Context(), f.req, f.units.started, seen) |
| 211 | if mode == "observed" { |
| 212 | if err != nil || program != release.FileProgram(path) { |
| 213 | t.Fatalf("program=%q: %v", program, err) |
| 214 | } |
| 215 | } else if err == nil { |
| 216 | t.Fatal("accepted", mode) |
| 217 | } |
| 218 | }) |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | func TestPlan14FreshInstallCanonicalIdentity(t *testing.T) { |
| 223 | t.Parallel() |
| 224 | for _, mode := range []string{"canonical", "account", "app", "host", "record", "census", "inventory", "service-files"} { |
| 225 | t.Run(mode, func(t *testing.T) { |
| 226 | f := newRestartConsumptionFixture(t) |
| 227 | rt, err := f.e.root.reg.Get(f.claim.Host) |
| 228 | if err != nil { |
| 229 | t.Fatal(err) |
| 230 | } |
| 231 | id, err := f.p.source.creation(t.Context(), *rt) |
| 232 | if err != nil || id == "" { |
| 233 | t.Fatal(id, err) |
| 234 | } |
| 235 | switch mode { |
| 236 | case "account": |
| 237 | rt.Account = "bap" |
| 238 | case "app": |
| 239 | rt.App = "other" |
| 240 | case "host": |
| 241 | rt.Host = "other.example" |
| 242 | case "record": |
| 243 | rec, err := provision.GetRecord(t.Context(), f.e.store, rt.Account, rt.App, rt.Host) |
| 244 | if err != nil { |
| 245 | t.Fatal(err) |
| 246 | } |
| 247 | rec.CreatedAt = rec.CreatedAt.Add(time.Second) |
| 248 | plan14PutRecord(t, f.e.store, *rec) |
| 249 | case "census": |
| 250 | f.p.source.census = func() (rootparticipation.Census, error) { return rootparticipation.Census{}, os.ErrPermission } |
| 251 | case "inventory": |
| 252 | if err := f.p.source.state.Remove(rt.Host + ".json"); err != nil { |
| 253 | t.Fatal(err) |
| 254 | } |
| 255 | case "service-files": |
| 256 | if err := f.e.root.reg.Delete(rt.Host); err != nil { |
| 257 | t.Fatal(err) |
| 258 | } |
| 259 | rt.Caps = "host-role" |
| 260 | rt.GoodVersion = "invented" |
| 261 | rt.Unit = "forged.service" |
| 262 | } |
| 263 | got, err := f.p.source.creation(t.Context(), *rt) |
| 264 | if mode == "canonical" || mode == "service-files" { |
| 265 | if err != nil || got != id { |
| 266 | t.Fatalf("canonical identity changed: %q %v", got, err) |
| 267 | } |
| 268 | } else if err == nil { |
| 269 | t.Fatal("accepted", mode) |
| 270 | } |
| 271 | }) |
| 272 | } |
| 273 | } |
| 274 | |