//go:build linux package layout import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/registry" ) // The naming rules of extras.go: an entry is a host's when it is the host // or the host plus a dot or dash suffix (or the activator's prefix form), // and when two hosts on the box both match, the longer one owns it. func TestExtraOwnershipIsTheLongestMatchingHost(t *testing.T) { hosts := []string{"a.b.oncarlos.com", "a.b.oncarlos.com-x", "other.example.com"} for name, want := range map[string]string{ "a.b.oncarlos.com": "a.b.oncarlos.com", "a.b.oncarlos.com-blobs": "a.b.oncarlos.com", "a.b.oncarlos.com.cache": "a.b.oncarlos.com", ".activator-a.b.oncarlos.com.db-litestream": "a.b.oncarlos.com", "a.b.oncarlos.com-x": "a.b.oncarlos.com-x", "a.b.oncarlos.com-x-blobs": "a.b.oncarlos.com-x", "a.b.oncarlos.com-x.db": "a.b.oncarlos.com-x", "a.b.oncarlos.comx": "", "blobs": "", "registry.db": "", } { if got := ownerOf(name, hosts); got != want { t.Errorf("ownerOf(%q) = %q, want %q", name, got, want) } } } // A journal's extra names pass the scan's own rule or the journal is // refused: one path element, not one of the plan's own names, named for // the host, and never absent. Journals from before #256 have no extras. func TestValidateJudgesTheJournalsExtraNames(t *testing.T) { dd, rd := t.TempDir(), t.TempDir() good := func() *Journal { p, _ := PlanFor(dd, rd, host, ModeNested) present := make([]bool, len(artifactNames)) present[0] = true return p.Journal(present) } row := registry.Route{Host: host, DBPath: good().From.DB, Socket: good().From.Socket} j := good() j.Extra = []JournalArtifact{{Name: host, Present: true}, {Name: host + "-blobs", Present: true}} if _, err := Validate(j, dd, rd, row); err != nil { t.Fatalf("two good extras: %v", err) } for name, extra := range map[string]JournalArtifact{ "a path": {Name: "../" + host, Present: true}, "a fixed name": {Name: host + ".db", Present: true}, "another host's": {Name: replica + "-blobs", Present: true}, "the data dir": {Name: ".", Present: true}, "recorded absent": {Name: host + "-blobs", Present: false}, } { j := good() j.Extra = []JournalArtifact{extra} if _, err := Validate(j, dd, rd, row); err == nil || !strings.HasSuffix(err.Error(), "not acting") { t.Errorf("%s (%q): Validate = %v, want a refusal ending in not acting", name, extra.Name, err) } } j = good() j.Extra = []JournalArtifact{{Name: host, Present: true}, {Name: host, Present: true}} if _, err := Validate(j, dd, rd, row); err == nil || !strings.Contains(err.Error(), "twice") { t.Errorf("a name twice: Validate = %v", err) } // The wire shape is additive: a journal without the field reads as // one with no extras, and one with the field round-trips. j = good() j.Extra = []JournalArtifact{{Name: host + "-blobs", Present: true}} if err := WriteJournal(dd, j); err != nil { t.Fatal(err) } back, found, err := ReadJournal(dd, host) if err != nil || !found || len(back.Extra) != 1 || back.Extra[0].Name != host+"-blobs" { t.Fatalf("round trip = %+v %v %v", back, found, err) } } // UnattributedEntries names what no host on the box owns and the // platform did not make: the shared directories a flat app with cwd / // wrote, which no move can take. func TestUnattributedEntriesSkipsHostsAndPlatformNames(t *testing.T) { dd := t.TempDir() for _, name := range []string{"blobs", "registry.db", "layout", ".instances", "instances", ".offline-moves", host, host + "-blobs", "cache.tmp"} { if err := os.MkdirAll(filepath.Join(dd, name), 0o700); err != nil { t.Fatal(err) } } got := UnattributedEntries(dd, []string{host, replica}) want := []string{filepath.Join(dd, "blobs"), filepath.Join(dd, "cache.tmp")} if strings.Join(got, " ") != strings.Join(want, " ") { t.Fatalf("UnattributedEntries = %v, want %v", got, want) } } // Review (#256 B): ownership knows more than the rows. A host deleted or // moved away leaves its platform-named files behind, and when its name // extends this host's, the shorter host's scan must not sweep them in, // nor the app entries beside them that the files are evidence of. Each // platform shape, for a dotted and a dashed extension, with no row for // either: none is an extra, and each is reported with "no row". func TestScanExtrasLeavesAnotherHostsPlatformFilesAlone(t *testing.T) { dd, rd := t.TempDir(), t.TempDir() shapes := func(x string) []string { return []string{x + ".env", x + ".db", x + ".db-wal", x + ".db-shm", x + ".config.env", x + "-sidecar.env", ".activator-" + x + ".db-litestream", x + ".db.corrupt"} } var planted []string for _, x := range []string{host + ".au", host + "-staging"} { planted = append(planted, shapes(x)...) // The app entries of that host, which only its files evidence. planted = append(planted, x, x+"-blobs") } planted = append(planted, host, host+"-blobs") for _, name := range planted { if err := os.MkdirAll(filepath.Join(dd, name), 0o700); err != nil { t.Fatal(err) } } for _, mode := range []Mode{ModeNested, ModeFlat} { p, err := PlanFor(dd, rd, host, mode) if err != nil { t.Fatal(err) } var logged []string p.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } if mode == ModeFlat { // The revert scans the instance directory; stage the same // names there to show the rule holds in that direction too. raw := rawInstanceDir(dd, host) for _, name := range planted { if err := os.MkdirAll(filepath.Join(raw, name), 0o700); err != nil { t.Fatal(err) } } os.RemoveAll(filepath.Join(dd, host)) os.RemoveAll(filepath.Join(dd, host+"-blobs")) } got, err := ScanExtras(p, []string{replica}) if err != nil { t.Fatal(err) } if strings.Join(got, " ") != host+" "+host+"-blobs" { t.Errorf("%s: ScanExtras = %v, want only the host's own two entries", mode, got) } joined := strings.Join(logged, "\n") for _, x := range []string{host + ".au", host + "-staging"} { for _, name := range append(shapes(x), x, x+"-blobs") { if !strings.Contains(joined, name+" ("+x+"'s, which has no row on this box)") { t.Errorf("%s: %s not reported as %s's with no row:\n%s", mode, name, x, joined) } } } } // And a host that is a row: reported as its own, without the "no row". p, _ := PlanFor(dd, rd, host, ModeNested) var logged []string p.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } if _, err := ScanExtras(p, []string{host + ".au"}); err != nil { t.Fatal(err) } if j := strings.Join(logged, "\n"); !strings.Contains(j, host+".au.env ("+host+".au's)") || strings.Contains(j, host+".au.env ("+host+".au's, which") { t.Errorf("a host with a row is reported wrongly:\n%s", j) } } func TestFixedShapeHostReadsEveryPlatformShape(t *testing.T) { for name, want := range map[string]string{ "a.example.com.env": "a.example.com", "a.example.com.db": "a.example.com", "a.example.com.db-wal": "a.example.com", "a.example.com.db-shm": "a.example.com", "a.example.com.config.env": "a.example.com", "a.example.com-sidecar.env": "a.example.com", "a.example.com.db.corrupt": "a.example.com", ".activator-a.example.com.db-litestream": "a.example.com", "a.example.com": "", "a.example.com-blobs": "", "litestream.env": "", "registry.db": "", ".env": "", } { got, ok := fixedShapeHost(name) if got != want || ok != (want != "") { t.Errorf("fixedShapeHost(%q) = %q %v, want %q", name, got, ok, want) } } }