| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "testing" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/hostfiles" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | // The release flush opens the files directory and reads the env file |
| 20 | // through instancefs (openat2), so this test is Linux's. |
| 21 | func TestReleaseFlushFilesCarriesTheLastWakeStateAndEnv(t *testing.T) { |
| 22 | ctx := context.Background() |
| 23 | data := release.NewDirStore(t.TempDir()) |
| 24 | reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | t.Cleanup(func() { reg.Close() }) |
| 29 | const host = "shop.example" |
| 30 | if err := reg.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Hibernate: true}); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | dir, envDir := t.TempDir(), t.TempDir() |
| 34 | if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello"), 0o644); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | if err := os.WriteFile(filepath.Join(envDir, host+".env"), []byte("KEY=v\n"), 0o600); err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | f := ReleaseFlushFiles{Data: data, Registry: reg, FilesDir: func(string) string { return dir }, EnvPath: func(h string) string { return filepath.Join(envDir, h+".env") }} |
| 41 | obs := hostclaim.Observed{Host: host, Claim: &hostclaim.Claim{Host: host, Holder: &hostclaim.Holder{Location: "instances/" + host + "/inc", Generation: "g6"}}} |
| 42 | first, err := f.Flush(ctx, obs) |
| 43 | if err != nil || first == 0 { |
| 44 | t.Fatalf("first flush: %d %v", first, err) |
| 45 | } |
| 46 | // A timer registered immediately before the release, and a new file. |
| 47 | if err := reg.ReplaceTimers(host, []registry.Timer{{Host: host, Name: "late", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, CreatedAt: 10, DueAt: 5000, Registration: "reg-late"}}); err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | if err := reg.SetNextDue(host, 7777); err != nil { |
| 51 | t.Fatal(err) |
| 52 | } |
| 53 | if err := os.WriteFile(filepath.Join(dir, "b.txt"), []byte("later"), 0o644); err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | seq, err := f.Flush(ctx, obs) |
| 57 | if err != nil || seq <= first { |
| 58 | t.Fatalf("second flush: %d %v", seq, err) |
| 59 | } |
| 60 | prefix := "instances/" + host + "/inc/files/g-g6/" |
| 61 | m, ok, err := hostfiles.Load(ctx, data, prefix) |
| 62 | if err != nil || !ok || m.Seq != seq { |
| 63 | t.Fatalf("manifest: %+v %v %v", m, ok, err) |
| 64 | } |
| 65 | if _, ok := m.Entries["b.txt"]; !ok { |
| 66 | t.Fatal("the file written before the release is not in the manifest") |
| 67 | } |
| 68 | ws, ok, err := hostfiles.LoadWake(ctx, data, prefix, m, nil) |
| 69 | if err != nil || !ok || len(ws.Timers) != 1 || ws.Timers[0].Name != "late" || ws.NextDue != 7777 { |
| 70 | t.Fatalf("wake state: %+v %v %v", ws, ok, err) |
| 71 | } |
| 72 | env, err := hostfiles.ReadPlatform(ctx, data, prefix, m, hostfiles.EnvItem, nil) |
| 73 | if err != nil || string(env) != "KEY=v\n" { |
| 74 | t.Fatalf("env: %q %v", env, err) |
| 75 | } |
| 76 | // Nothing changed: the same sequence, still the final state. |
| 77 | again, err := f.Flush(ctx, obs) |
| 78 | if err != nil || again != seq { |
| 79 | t.Fatalf("repeat: %d %v", again, err) |
| 80 | } |
| 81 | // The env file is removed before a later release: the flush stages its |
| 82 | // deletion, so the final state carries no env, not the old KEY=v. |
| 83 | if err := os.Remove(filepath.Join(envDir, host+".env")); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | gone, err := f.Flush(ctx, obs) |
| 87 | if err != nil || gone <= seq { |
| 88 | t.Fatalf("flush after the env file was removed: %d %v", gone, err) |
| 89 | } |
| 90 | m, ok, err = hostfiles.Load(ctx, data, prefix) |
| 91 | if err != nil || !ok || m.Seq != gone { |
| 92 | t.Fatalf("manifest after removal: %+v %v %v", m, ok, err) |
| 93 | } |
| 94 | if env, err := hostfiles.ReadPlatform(ctx, data, prefix, m, hostfiles.EnvItem, nil); !errors.Is(err, os.ErrNotExist) { |
| 95 | t.Fatalf("a removed env file was still published: %q %v", env, err) |
| 96 | } |
| 97 | if again, err := f.Flush(ctx, obs); err != nil || again != gone { |
| 98 | t.Fatalf("repeat after removal: %d %v", again, err) |
| 99 | } |
| 100 | var raw map[string]any |
| 101 | body, _, _ := data.Get(ctx, hostfiles.ManifestKey(prefix)) |
| 102 | _ = json.Unmarshal(body, &raw) |
| 103 | if raw["v"] != float64(hostfiles.ManifestVersion) { |
| 104 | t.Fatalf("manifest version: %v", raw["v"]) |
| 105 | } |
| 106 | } |
| 107 | |