//go:build linux package host import ( "context" "errors" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // O5 (instance-key plan rev 3): the release flush publishes the adoption // record in the same commit as the env item only when it binds exactly the // releasing writer; a stale record, or none, publishes none. A record that // does not parse refuses the flush (fix round 1, M9): it may be this // writer's, and absence is never evidence. func TestReleaseFlushPublishesOnlyTheReleasingWritersAdoption(t *testing.T) { t.Parallel() ctx := context.Background() const host = "shop.example" writer := instancekey.Binding{Host: host, Incarnation: "inc-1", Session: 6, Generation: "g6"} for _, tc := range []struct { name string record []byte // nil is no file want bool refuses bool }{ {"the releasing writer's record", instancekey.AdoptionRecord(writer, instancekey.SourcePlatform, instancekey.Digest("k1")), true, false}, {"a stale record from session 5", instancekey.AdoptionRecord(instancekey.Binding{Host: host, Incarnation: "inc-1", Session: 5, Generation: "g5"}, instancekey.SourcePlatform, instancekey.Digest("k1")), false, false}, {"a malformed record", []byte(`{"v":1}`), false, true}, {"no record", nil, false, false}, } { t.Run(tc.name, func(t *testing.T) { 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() }) 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(envDir, host+".env"), []byte("CARLOS_INSTANCE_KEY=k1\n"), 0o600); err != nil { t.Fatal(err) } if tc.record != nil { if err := os.WriteFile(filepath.Join(envDir, host+instancekey.AdoptionSuffix), tc.record, 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", Incarnation: "inc-1", Session: 6, Generation: "g6"}}} seq, err := f.Flush(ctx, obs) if tc.refuses { if !errors.Is(err, ErrClaimStopUnavailable) { t.Fatalf("a malformed record was flushed: %v", err) } if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g-g6/"); ok || err != nil { t.Fatalf("a refused flush committed a manifest: %v %v", ok, err) } return } if err != nil { t.Fatal(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", ok, err) } got, err := hostfiles.ReadPlatform(ctx, data, prefix, m, instancekey.AdoptionItem, nil) if tc.want && (err != nil || string(got) != string(tc.record)) { t.Fatalf("the writer's record was not published: %q %v", got, err) } if !tc.want && err == nil { t.Fatalf("a record not the releasing writer's was published: %q", got) } }) } } // Fix round 1, M6: a host with no paths (no route, or a route that could // not be read) is refused before anything is staged, never flushed with an // empty env path read as "no env file". func TestReleaseFlushRefusesAHostWithNoPaths(t *testing.T) { ctx := context.Background() const host = "shop.example" 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() }) paths := relocationHostPaths(t.TempDir(), reg) if p := paths(host); p.FilesDir != "" || p.EnvPath != "" { t.Fatalf("paths without a route: %+v", p) } f := ReleaseFlushFiles{Data: data, Registry: reg, FilesDir: func(h string) string { return paths(h).FilesDir }, EnvPath: func(h string) string { return paths(h).EnvPath }} obs := hostclaim.Observed{Host: host, Claim: &hostclaim.Claim{Host: host, Holder: &hostclaim.Holder{Location: "instances/" + host + "/inc", Incarnation: "inc-1", Session: 6, Generation: "g6"}}} if _, err := f.Flush(ctx, obs); !errors.Is(err, ErrClaimStopUnavailable) { t.Fatalf("flushed with no paths: %v", err) } if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g-g6/"); ok || err != nil { t.Fatalf("a refused flush committed a manifest: %v %v", ok, err) } }