carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package layout
4
5import (
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// The naming rules of extras.go: an entry is a host's when it is the host
16// or the host plus a dot or dash suffix (or the activator's prefix form),
17// and when two hosts on the box both match, the longer one owns it.
18func TestExtraOwnershipIsTheLongestMatchingHost(t *testing.T) {
19 hosts := []string{"a.b.oncarlos.com", "a.b.oncarlos.com-x", "other.example.com"}
20 for name, want := range map[string]string{
21 "a.b.oncarlos.com": "a.b.oncarlos.com",
22 "a.b.oncarlos.com-blobs": "a.b.oncarlos.com",
23 "a.b.oncarlos.com.cache": "a.b.oncarlos.com",
24 ".activator-a.b.oncarlos.com.db-litestream": "a.b.oncarlos.com",
25 "a.b.oncarlos.com-x": "a.b.oncarlos.com-x",
26 "a.b.oncarlos.com-x-blobs": "a.b.oncarlos.com-x",
27 "a.b.oncarlos.com-x.db": "a.b.oncarlos.com-x",
28 "a.b.oncarlos.comx": "",
29 "blobs": "",
30 "registry.db": "",
31 } {
32 if got := ownerOf(name, hosts); got != want {
33 t.Errorf("ownerOf(%q) = %q, want %q", name, got, want)
34 }
35 }
36}
37
38// A journal's extra names pass the scan's own rule or the journal is
39// refused: one path element, not one of the plan's own names, named for
40// the host, and never absent. Journals from before #256 have no extras.
41func TestValidateJudgesTheJournalsExtraNames(t *testing.T) {
42 dd, rd := t.TempDir(), t.TempDir()
43 good := func() *Journal {
44 p, _ := PlanFor(dd, rd, host, ModeNested)
45 present := make([]bool, len(artifactNames))
46 present[0] = true
47 return p.Journal(present)
48 }
49 row := registry.Route{Host: host, DBPath: good().From.DB, Socket: good().From.Socket}
50 j := good()
51 j.Extra = []JournalArtifact{{Name: host, Present: true}, {Name: host + "-blobs", Present: true}}
52 if _, err := Validate(j, dd, rd, row); err != nil {
53 t.Fatalf("two good extras: %v", err)
54 }
55 for name, extra := range map[string]JournalArtifact{
56 "a path": {Name: "../" + host, Present: true},
57 "a fixed name": {Name: host + ".db", Present: true},
58 "another host's": {Name: replica + "-blobs", Present: true},
59 "the data dir": {Name: ".", Present: true},
60 "recorded absent": {Name: host + "-blobs", Present: false},
61 } {
62 j := good()
63 j.Extra = []JournalArtifact{extra}
64 if _, err := Validate(j, dd, rd, row); err == nil || !strings.HasSuffix(err.Error(), "not acting") {
65 t.Errorf("%s (%q): Validate = %v, want a refusal ending in not acting", name, extra.Name, err)
66 }
67 }
68 j = good()
69 j.Extra = []JournalArtifact{{Name: host, Present: true}, {Name: host, Present: true}}
70 if _, err := Validate(j, dd, rd, row); err == nil || !strings.Contains(err.Error(), "twice") {
71 t.Errorf("a name twice: Validate = %v", err)
72 }
73 // The wire shape is additive: a journal without the field reads as
74 // one with no extras, and one with the field round-trips.
75 j = good()
76 j.Extra = []JournalArtifact{{Name: host + "-blobs", Present: true}}
77 if err := WriteJournal(dd, j); err != nil {
78 t.Fatal(err)
79 }
80 back, found, err := ReadJournal(dd, host)
81 if err != nil || !found || len(back.Extra) != 1 || back.Extra[0].Name != host+"-blobs" {
82 t.Fatalf("round trip = %+v %v %v", back, found, err)
83 }
84}
85
86// UnattributedEntries names what no host on the box owns and the
87// platform did not make: the shared directories a flat app with cwd /
88// wrote, which no move can take.
89func TestUnattributedEntriesSkipsHostsAndPlatformNames(t *testing.T) {
90 dd := t.TempDir()
91 for _, name := range []string{"blobs", "registry.db", "layout", ".instances", "instances", ".offline-moves", host, host + "-blobs", "cache.tmp"} {
92 if err := os.MkdirAll(filepath.Join(dd, name), 0o700); err != nil {
93 t.Fatal(err)
94 }
95 }
96 got := UnattributedEntries(dd, []string{host, replica})
97 want := []string{filepath.Join(dd, "blobs"), filepath.Join(dd, "cache.tmp")}
98 if strings.Join(got, " ") != strings.Join(want, " ") {
99 t.Fatalf("UnattributedEntries = %v, want %v", got, want)
100 }
101}
102
103// Review (#256 B): ownership knows more than the rows. A host deleted or
104// moved away leaves its platform-named files behind, and when its name
105// extends this host's, the shorter host's scan must not sweep them in,
106// nor the app entries beside them that the files are evidence of. Each
107// platform shape, for a dotted and a dashed extension, with no row for
108// either: none is an extra, and each is reported with "no row".
109func TestScanExtrasLeavesAnotherHostsPlatformFilesAlone(t *testing.T) {
110 dd, rd := t.TempDir(), t.TempDir()
111 shapes := func(x string) []string {
112 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"}
113 }
114 var planted []string
115 for _, x := range []string{host + ".au", host + "-staging"} {
116 planted = append(planted, shapes(x)...)
117 // The app entries of that host, which only its files evidence.
118 planted = append(planted, x, x+"-blobs")
119 }
120 planted = append(planted, host, host+"-blobs")
121 for _, name := range planted {
122 if err := os.MkdirAll(filepath.Join(dd, name), 0o700); err != nil {
123 t.Fatal(err)
124 }
125 }
126 for _, mode := range []Mode{ModeNested, ModeFlat} {
127 p, err := PlanFor(dd, rd, host, mode)
128 if err != nil {
129 t.Fatal(err)
130 }
131 var logged []string
132 p.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
133 if mode == ModeFlat {
134 // The revert scans the instance directory; stage the same
135 // names there to show the rule holds in that direction too.
136 raw := rawInstanceDir(dd, host)
137 for _, name := range planted {
138 if err := os.MkdirAll(filepath.Join(raw, name), 0o700); err != nil {
139 t.Fatal(err)
140 }
141 }
142 os.RemoveAll(filepath.Join(dd, host))
143 os.RemoveAll(filepath.Join(dd, host+"-blobs"))
144 }
145 got, err := ScanExtras(p, []string{replica})
146 if err != nil {
147 t.Fatal(err)
148 }
149 if strings.Join(got, " ") != host+" "+host+"-blobs" {
150 t.Errorf("%s: ScanExtras = %v, want only the host's own two entries", mode, got)
151 }
152 joined := strings.Join(logged, "\n")
153 for _, x := range []string{host + ".au", host + "-staging"} {
154 for _, name := range append(shapes(x), x, x+"-blobs") {
155 if !strings.Contains(joined, name+" ("+x+"'s, which has no row on this box)") {
156 t.Errorf("%s: %s not reported as %s's with no row:\n%s", mode, name, x, joined)
157 }
158 }
159 }
160 }
161 // And a host that is a row: reported as its own, without the "no row".
162 p, _ := PlanFor(dd, rd, host, ModeNested)
163 var logged []string
164 p.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
165 if _, err := ScanExtras(p, []string{host + ".au"}); err != nil {
166 t.Fatal(err)
167 }
168 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") {
169 t.Errorf("a host with a row is reported wrongly:\n%s", j)
170 }
171}
172
173func TestFixedShapeHostReadsEveryPlatformShape(t *testing.T) {
174 for name, want := range map[string]string{
175 "a.example.com.env": "a.example.com",
176 "a.example.com.db": "a.example.com",
177 "a.example.com.db-wal": "a.example.com",
178 "a.example.com.db-shm": "a.example.com",
179 "a.example.com.config.env": "a.example.com",
180 "a.example.com-sidecar.env": "a.example.com",
181 "a.example.com.db.corrupt": "a.example.com",
182 ".activator-a.example.com.db-litestream": "a.example.com",
183 "a.example.com": "",
184 "a.example.com-blobs": "",
185 "litestream.env": "",
186 "registry.db": "",
187 ".env": "",
188 } {
189 got, ok := fixedShapeHost(name)
190 if got != want || ok != (want != "") {
191 t.Errorf("fixedShapeHost(%q) = %q %v, want %q", name, got, ok, want)
192 }
193 }
194}
195