//go:build linux package host import ( "context" "encoding/json" "errors" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // The release flush opens the files directory and reads the env file // through instancefs (openat2), so this test is Linux's. func TestReleaseFlushFilesCarriesTheLastWakeStateAndEnv(t *testing.T) { ctx := context.Background() data := release.NewDirStore(t.TempDir()) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) const host = "shop.example" if err := reg.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Hibernate: true}); err != nil { t.Fatal(err) } dir, envDir := t.TempDir(), t.TempDir() if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("hello"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(envDir, host+".env"), []byte("KEY=v\n"), 0o600); err != nil { t.Fatal(err) } f := ReleaseFlushFiles{Data: data, Registry: reg, FilesDir: func(string) string { return dir }, EnvPath: func(h string) string { return filepath.Join(envDir, h+".env") }} obs := hostclaim.Observed{Host: host, Claim: &hostclaim.Claim{Host: host, Holder: &hostclaim.Holder{Location: "instances/" + host + "/inc", Generation: "g6"}}} first, err := f.Flush(ctx, obs) if err != nil || first == 0 { t.Fatalf("first flush: %d %v", first, err) } // A timer registered immediately before the release, and a new file. 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 { t.Fatal(err) } if err := reg.SetNextDue(host, 7777); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "b.txt"), []byte("later"), 0o644); err != nil { t.Fatal(err) } seq, err := f.Flush(ctx, obs) if err != nil || seq <= first { t.Fatalf("second flush: %d %v", seq, err) } prefix := "instances/" + host + "/inc/files/g-g6/" m, ok, err := hostfiles.Load(ctx, data, prefix) if err != nil || !ok || m.Seq != seq { t.Fatalf("manifest: %+v %v %v", m, ok, err) } if _, ok := m.Entries["b.txt"]; !ok { t.Fatal("the file written before the release is not in the manifest") } ws, ok, err := hostfiles.LoadWake(ctx, data, prefix, m, nil) if err != nil || !ok || len(ws.Timers) != 1 || ws.Timers[0].Name != "late" || ws.NextDue != 7777 { t.Fatalf("wake state: %+v %v %v", ws, ok, err) } env, err := hostfiles.ReadPlatform(ctx, data, prefix, m, hostfiles.EnvItem, nil) if err != nil || string(env) != "KEY=v\n" { t.Fatalf("env: %q %v", env, err) } // Nothing changed: the same sequence, still the final state. again, err := f.Flush(ctx, obs) if err != nil || again != seq { t.Fatalf("repeat: %d %v", again, err) } // The env file is removed before a later release: the flush stages its // deletion, so the final state carries no env, not the old KEY=v. if err := os.Remove(filepath.Join(envDir, host+".env")); err != nil { t.Fatal(err) } gone, err := f.Flush(ctx, obs) if err != nil || gone <= seq { t.Fatalf("flush after the env file was removed: %d %v", gone, err) } m, ok, err = hostfiles.Load(ctx, data, prefix) if err != nil || !ok || m.Seq != gone { t.Fatalf("manifest after removal: %+v %v %v", m, ok, err) } if env, err := hostfiles.ReadPlatform(ctx, data, prefix, m, hostfiles.EnvItem, nil); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a removed env file was still published: %q %v", env, err) } if again, err := f.Flush(ctx, obs); err != nil || again != gone { t.Fatalf("repeat after removal: %d %v", again, err) } var raw map[string]any body, _, _ := data.Get(ctx, hostfiles.ManifestKey(prefix)) _ = json.Unmarshal(body, &raw) if raw["v"] != float64(hostfiles.ManifestVersion) { t.Fatalf("manifest version: %v", raw["v"]) } }