carlos / platform Public

edges file: the seeds protocol, floor and verdict (Plan 3 Task 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev a7535941655e084e78066118a33eb0dac1401265 parent f22c608
6 files changed, +76 −4
  • internal/console/instancesplacement.go +5 −0
  • internal/console/instancesplacement_test.go +21 −0
  • internal/host/edgesfile.go +10 −0
  • internal/host/edgesfile_test.go +4 −4
  • internal/host/seedsfloor.go +12 −0
  • internal/host/seedsfloor_linux_test.go +24 −0

CI passed — run details

diff --git a/internal/console/instancesplacement.go b/internal/console/instancesplacement.go
index 711005ee9..20d400cce 100644
--- a/internal/console/instancesplacement.go
+++ b/internal/console/instancesplacement.go
@@ -84,6 +84,11 @@ type edgesFilePlacementView struct {
MovesFloorMinRelease string `json:"moves_floor_min_release"`
PrevRelease string `json:"prev_release"`
PrevAllowedFor string `json:"prev_allowed_for"`
+ // The seeds floor (seeded-instances spec §3.3), read only by
+ // seedRegionReady.
+ SeedsProtocol int `json:"seeds_protocol"`
+ SeedsFloorMinRelease string `json:"seeds_floor_min_release"`
+ PrevSeedsAllowedFor string `json:"prev_seeds_allowed_for"`
// Fresh is not on the wire: readLiveEdgesSnapshot sets it when the
// box's own status snapshot is still fresh (status.BoxStale). An edges
// file is not liveness, see readLiveEdgesSnapshot.
diff --git a/internal/console/instancesplacement_test.go b/internal/console/instancesplacement_test.go
index 748c91100..2a75c6bc6 100644
--- a/internal/console/instancesplacement_test.go
+++ b/internal/console/instancesplacement_test.go
@@ -2,6 +2,7 @@ package console
import (
"context"
+ "encoding/json"
"testing"
)
@@ -101,3 +102,23 @@ func TestPlacementsDormantWithoutAnyPlacementCode(t *testing.T) {
t.Error("published = true with no placement code anywhere — the gate must stay dormant")
}
}
+
+// The seeds fields are additive: an edges file from a box that predates
+// them parses, and reads as no protocol, no floor and no verdict.
+func TestEdgesFilePlacementViewSeedsFields(t *testing.T) {
+ var old edgesFilePlacementView
+ if err := json.Unmarshal([]byte(`{"box":"kass-1","moves_protocol":1}`), &old); err != nil {
+ t.Fatal(err)
+ }
+ if old.SeedsProtocol != 0 || old.SeedsFloorMinRelease != "" || old.PrevSeedsAllowedFor != "" {
+ t.Fatalf("old writer = %+v", old)
+ }
+ var cur edgesFilePlacementView
+ body := `{"box":"kass-1","seeds_protocol":2,"seeds_floor_min_release":"v0.27.0","prev_seeds_allowed_for":"v0.27.0"}`
+ if err := json.Unmarshal([]byte(body), &cur); err != nil {
+ t.Fatal(err)
+ }
+ if cur.SeedsProtocol != 2 || cur.SeedsFloorMinRelease != "v0.27.0" || cur.PrevSeedsAllowedFor != "v0.27.0" {
+ t.Fatalf("current writer = %+v", cur)
+ }
+}
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index 337e146fd..bc848fa75 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -303,6 +303,15 @@ type edgesFile struct {
// carlos.prev was judged at or above. Absent: no verdict.
PrevAllowedFor string `json:"prev_allowed_for,omitempty"`
+ // The seeds protocol (seeded-instances spec §3.3), read by the console's
+ // seedRegionReady. SeedsProtocol is not omitempty: 0 is a real answer
+ // (a build that cannot place a seed). SeedsFloorMinRelease is the
+ // armed seeds floor's min_release (absent: not armed), and
+ // PrevSeedsAllowedFor the floor carlos.prev was judged at or above.
+ SeedsProtocol int `json:"seeds_protocol"`
+ SeedsFloorMinRelease string `json:"seeds_floor_min_release,omitempty"`
+ PrevSeedsAllowedFor string `json:"prev_seeds_allowed_for,omitempty"`
+
Generation string `json:"generation"`
Hosts map[string]edgeHost `json:"hosts"`
}
@@ -815,6 +824,7 @@ func edgesFileLoop(reg *registry.Registry) {
}
ident.Headroom = headroomFor(ident.Capabilities, dataDir, reg)
ident = withMovesFields(ident, dataDir, optDir)
+ ident = withSeedsFields(ident, dataDir, optDir)
err = edgesFileWriteClaims(ctx, store, reg, ident, eip, &last, state.timed(), claims)
if err != nil {
// Throttled: a bucket that refuses every write (an IAM Deny, an
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index 1faa27ac3..8e771b148 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -317,7 +317,7 @@ func TestEdgesFileRawWireBodyMatchesSchema(t *testing.T) {
t.Fatalf("generation %q is not RFC3339: %v", probe.Generation, err)
}
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body =\n%s\nwant\n%s", got, want)
@@ -359,7 +359,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
t.Fatal(err)
}
gen := genOf(t, got)
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region unset) =\n%s\nwant\n%s", got, want)
@@ -378,7 +378,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
t.Fatal(err)
}
gen := genOf(t, got)
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-1","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-1","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (region set) =\n%s\nwant\n%s", got, want)
@@ -931,7 +931,7 @@ func TestEdgesFileRawWireBodyPlacementField(t *testing.T) {
if err := json.Unmarshal(got, &probe); err != nil {
t.Fatal(err)
}
- want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-2","placement":"uk","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"generation":"` +
+ want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-2","placement":"uk","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
if string(got) != want {
t.Fatalf("raw wire body (placement set) =\n%s\nwant\n%s", got, want)
diff --git a/internal/host/seedsfloor.go b/internal/host/seedsfloor.go
index 00edaac8f..4ef17e39a 100644
--- a/internal/host/seedsfloor.go
+++ b/internal/host/seedsfloor.go
@@ -52,6 +52,18 @@ func readLocalSeedsFloor(dataDir string) (*seeds.Floor, error) {
return &fl, nil
}
+// withSeedsFields sets the edges file's seeds fields from this box's own
+// files, read at each write, as withMovesFields does for the moves floor.
+func withSeedsFields(f edgesFile, dataDir, optDir string) edgesFile {
+ f.SeedsProtocol = seeds.ProtocolVersion
+ f.SeedsFloorMinRelease = ""
+ if fl, err := readLocalSeedsFloor(dataDir); err == nil && fl != nil {
+ f.SeedsFloorMinRelease = fl.MinRelease
+ }
+ f.PrevSeedsAllowedFor, _ = readLine(seedsPrevAllowedPath(optDir))
+ return f
+}
+
// seedsFloorArmed: a local seeds floor that reads, or one that is there and
// cannot be read.
func seedsFloorArmed(dataDir string) bool {
diff --git a/internal/host/seedsfloor_linux_test.go b/internal/host/seedsfloor_linux_test.go
index a1ba5a22d..a85a068c8 100644
--- a/internal/host/seedsfloor_linux_test.go
+++ b/internal/host/seedsfloor_linux_test.go
@@ -4,8 +4,10 @@ package host
import (
"context"
+ "encoding/json"
"os"
"path/filepath"
+ "strings"
"testing"
"github.com/carlosframework/platform/internal/release"
@@ -118,3 +120,25 @@ func TestJudgeSeedsPrev(t *testing.T) {
t.Fatal("a verdict for a carlos.prev below the floor")
}
}
+
+func TestWithSeedsFields(t *testing.T) {
+ dd, opt := t.TempDir(), t.TempDir()
+ f := withSeedsFields(edgesFile{Box: "kass-1"}, dd, opt)
+ if f.SeedsProtocol != seeds.ProtocolVersion || f.SeedsFloorMinRelease != "" || f.PrevSeedsAllowedFor != "" {
+ t.Fatalf("unarmed = %+v", f)
+ }
+ s := release.NewDirStore(t.TempDir())
+ publishSeedsFloor(t, s, "v0.27.0")
+ if _, err := syncLocalSeedsFloor(context.Background(), s, dd); err != nil {
+ t.Fatal(err)
+ }
+ os.WriteFile(seedsPrevAllowedPath(opt), []byte("v0.27.0\n"), 0o644)
+ f = withSeedsFields(f, dd, opt)
+ if f.SeedsFloorMinRelease != "v0.27.0" || f.PrevSeedsAllowedFor != "v0.27.0" {
+ t.Fatalf("armed = %+v", f)
+ }
+ body, _ := json.Marshal(f)
+ if !strings.Contains(string(body), `"seeds_protocol":0`) {
+ t.Fatalf("seeds_protocol must be on the wire even at 0: %s", body)
+ }
+}