package release import ( "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/rootstate" ) func generationFixture(t *testing.T) (*RootGenerations, *healthyRetryStore, GenerationBinding) { t.Helper() s := &healthyRetryStore{dir: t.TempDir(), fault: errors.New("directory sync failed")} b := GenerationBinding{Binding: HistoryBinding{Host: "shop.example", Account: "bab", App: "shop"}, Installation: "installation", Record: "record", RecordETag: "etag", Attempt: "attempt:main", Inputs: "inputs", Invocation: "invocation", Target: GenerationOwner{Host: "shop.example", Incarnation: "inc", Stamp: 1, Session: 2, Box: "box", Enrollment: "enrollment", BootID: "boot", ControllerID: "controller"}, ControllerPID: 42, ControllerStart: "start", Version: "v1", Digest: strings.Repeat("a", 64), Path: "/opt/carlos/versions/v1/bab/shop", Tree: "/opt/carlos/versions/v1/bab", Executable: rootstate.FileIdentity{Device: 1, Inode: 2}, TreeIdentity: rootstate.FileIdentity{Device: 1, Inode: 3}} return &RootGenerations{Store: s}, s, b } func TestPlan14GenerationReservationDurability(t *testing.T) { g, s, b := generationFixture(t) s.fail = 1 if _, err := g.Reserve(b); err == nil { t.Fatal("directory sync failure granted a reservation") } raw, err := s.Read(generationKey(b)) if err != nil { t.Fatal(err) } s.fail = 1 if _, err := g.Reserve(b); err == nil { t.Fatal("visible reservation bypassed directory sync retry") } r, err := g.Reserve(b) if err != nil || r.ID == "" || r.Binding != b || r.Revision != 1 { t.Fatalf("reserve: %+v %v", r, err) } after, err := s.Read(generationKey(b)) if err != nil || string(raw) != string(after) { t.Fatalf("retry changed reservation identity: %v", err) } for _, attack := range []string{"empty", "missing", "rename"} { t.Run(attack, func(t *testing.T) { dir := t.TempDir() path := GenerationsPath(dir) if err := WriteGenerations(path, Generations{Hosts: map[string][]string{b.Binding.Host: {b.Version}}}); err != nil { t.Fatal(err) } switch attack { case "empty": if err := WriteGenerations(path, Generations{}); err != nil { t.Fatal(err) } case "missing": if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Remove(GenerationsSeqPath(path)); err != nil { t.Fatal(err) } case "rename": if err := os.Rename(filepath.Dir(path), filepath.Dir(path)+".old"); err != nil { t.Fatal(err) } } got, err := g.Read(b) if err != nil || got != r { t.Fatalf("legacy edit changed root reservation: %+v %v", got, err) } }) } } func TestPlan14GenerationReservationConflicts(t *testing.T) { g, _, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } for name, change := range map[string]func(*GenerationBinding){ "account": func(b *GenerationBinding) { b.Binding.Account = "bap" }, "app": func(b *GenerationBinding) { b.Binding.App = "other" }, "version": func(b *GenerationBinding) { b.Version = "v2" }, "digest": func(b *GenerationBinding) { b.Digest = strings.Repeat("b", 64) }, "inode": func(b *GenerationBinding) { b.Executable.Inode++ }, "tree": func(b *GenerationBinding) { b.TreeIdentity.Inode++ }, "installation": func(b *GenerationBinding) { b.Installation = "replacement" }, "process": func(b *GenerationBinding) { b.ControllerStart = "replacement" }, "inputs": func(b *GenerationBinding) { b.Inputs = "replacement" }, } { t.Run(name, func(t *testing.T) { other := b change(&other) if _, err := g.Reserve(other); err == nil { t.Fatal("rebound existing reservation") } got, err := g.Read(b) if err != nil || got != r { t.Fatalf("conflict changed old pin: %+v %v", got, err) } }) } } func TestPlan14GenerationObservationKeepsReservation(t *testing.T) { g, s, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } o := GenerationProcess{PID: 51, Start: "51", TenantPID: 52, TenantStart: "52", Group: "system.slice/run.service", GroupIdentity: rootstate.FileIdentity{Device: 2, Inode: 3}, Invocation: "unit-invocation"} s.fail = 1 if err := g.Started(r, o); err == nil { t.Fatal("unsynced started record succeeded") } if err := g.Started(r, o); err != nil { t.Fatal(err) } got, err := g.Read(b) if err != nil || got.Binding != b || got.ID != r.ID || got.Process != o || got.Revision != 2 { t.Fatalf("started: %+v %v", got, err) } o.PID++ if err := g.Started(r, o); err == nil { t.Fatal("changed running lifetime") } if _, err := g.Reserve(b); err == nil { t.Fatal("running reservation authorized another launch") } } func TestPlan14GenerationRecordRefusals(t *testing.T) { for _, attack := range []string{"missing", "malformed", "unknown field", "oversize"} { t.Run(attack, func(t *testing.T) { g, s, b := generationFixture(t) if _, err := g.Reserve(b); err != nil { t.Fatal(err) } key := generationKey(b) raw, err := s.Read(key) if err != nil { t.Fatal(err) } switch attack { case "missing": if err := os.Remove(filepath.Join(s.dir, key)); err != nil { t.Fatal(err) } case "malformed": raw = []byte("{") case "unknown field": raw = append(raw[:len(raw)-1], []byte(`,"complete":true}`)...) case "oversize": raw = []byte(strings.Repeat(" ", rootstate.MaxRecord+1)) } if attack != "missing" { if err := s.Write(key, raw); err != nil { t.Fatal(err) } } if _, err := g.Read(b); err == nil { t.Fatal("unavailable reservation granted a launch") } }) } } func TestPlan14GenerationOldProcessStillParses(t *testing.T) { g, s, b := generationFixture(t) r, err := g.Reserve(b) if err != nil { t.Fatal(err) } p := GenerationProcess{PID: 100, TenantPID: 200, Start: "one", TenantStart: "two", Group: "unit", Invocation: "invocation", GroupIdentity: rootstate.FileIdentity{Device: 1, Inode: 2}} if err := g.Started(r, p); err != nil { t.Fatal(err) } raw, err := s.Read(generationKey(b)) if err != nil { t.Fatal(err) } if strings.Contains(string(raw), "LauncherIdentity") || strings.Contains(string(raw), "Containment") { t.Fatal("old process wire shape changed") } got, err := g.Read(b) if err != nil || got.Process != p { t.Fatalf("old process: %+v %v", got, err) } }