carlos / platform Public

region at the door: edges files publish the placement code, creates validate against the armed set, exec children get CARLOS_REGION (#329)

Three additive pieces:

- Each box's edges file gains "placement" — CARLOS_REGION verbatim, the
  OTHER vocabulary from the AWS "region" field beside it — so the two
  travel in one file and any reader can join them.
- The instance-create door refuses a region no armed box serves, naming
  the real choices. Dormant until some box publishes a placement code:
  a console rolled ahead of its fleet changes nothing, so roll order
  does not matter. Placed apps and the unconstrained "" region are
  untouched rails.
- Every exec child gets CARLOS_REGION from its box, agent-owned in both
  directions like CARLOS_CONTROL_SOCKET — a tenant's config env cannot
  tell an app it runs somewhere the agent never vouched for. (titogo's
  boot-clears-freeze rule for #331 needs exactly this.)

The landing itself already worked: a record's region scopes reconcile,
and the region box's activator replicates against its own data bucket.
This closes the one gap — a typo'd region minting a record nothing will
ever serve.

Closes #329
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PEypt7de2X1G84aQ9rNkCf
Paul Campbell pushed by paul@keymail.dev 36073c919090c5a44bcf3fe7cfe81c5fa5634fcd parent 9a056e5
8 files changed, +270 −46
  • internal/activator/backend_exec.go +13 −0
  • internal/activator/backend_exec_test.go +47 −0
  • internal/console/apiinstances.go +20 −0
  • internal/console/apiinstances_test.go +49 −0
  • internal/console/instancesplacement.go +47 −12
  • internal/host/agent.go +6 −3
  • internal/host/edgesfile.go +20 −7
  • internal/host/edgesfile_test.go +68 −24
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index ddd0893b5..a8fac9f93 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -60,6 +60,14 @@ type ExecBackend struct {
// but a carlos app is a separate binary from the release store — there is
// nothing sensible to invent here, and the adopt pass owns the symlinks.
Materialize func(ctx context.Context, host string) error
+ // Region is the box's CARLOS placement code (CARLOS_REGION), handed to
+ // every exec child as its own CARLOS_REGION (issue #329, titogo's
+ // residence design: the app reads where it lives — e.g. to clear a move
+ // freeze after landing). Agent-owned in BOTH directions, exactly like
+ // the control-channel vars below: set from what the agent knows, and
+ // STRIPPED when the agent knows nothing — a tenant's config env must not
+ // be able to tell an app it runs somewhere it does not.
+ Region string
// Cache, when non-nil, adds the purge verb to each spawned instance's
// control socket so it can invalidate its own cached responses right
// after a write. Nil disables that ONE verb, not the socket: the control
@@ -626,6 +634,11 @@ func (b *ExecBackend) Start(ctx context.Context, rt registry.Route) (Proc, strin
} else {
childVars = withoutVars(childVars, "CARLOS_PURGE")
}
+ if b.Region != "" {
+ childVars = withVar(childVars, "CARLOS_REGION", b.Region)
+ } else {
+ childVars = withoutVars(childVars, "CARLOS_REGION")
+ }
// Record the build we're about to exec — read the symlink immediately before
// the start so it names the version the kernel will resolve. Reading it here,
// rather than back in wake() after the health wait, closes a real window: a
diff --git a/internal/activator/backend_exec_test.go b/internal/activator/backend_exec_test.go
index 6bc46a846..7e3fb3a8a 100644
--- a/internal/activator/backend_exec_test.go
+++ b/internal/activator/backend_exec_test.go
@@ -1,10 +1,13 @@
package activator
import (
+ "context"
"os"
"os/exec"
"path/filepath"
+ "strings"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/cgroup"
"github.com/carlosframework/platform/internal/registry"
@@ -56,3 +59,47 @@ func TestSpawnInCgroupAppliesTheTier(t *testing.T) {
t.Errorf("memory.max = %q, want the children tier", got)
}
}
+
+// The box's placement code reaches every exec child as CARLOS_REGION (issue
+// #329) — agent-owned in both directions: set from ExecBackend.Region, and
+// STRIPPED when the backend has none, even when a tenant's own env file
+// tries to supply it. Same rule and same reason as CARLOS_CONTROL_SOCKET /
+// CARLOS_PURGE: an app must not be told it runs somewhere the agent never
+// vouched for.
+func TestExecBackendStartChildRegion(t *testing.T) {
+ for _, tc := range []struct {
+ name, backendRegion, want string
+ wantSet bool
+ }{
+ {"set from the backend", "uk", "uk", true},
+ {"stripped when the backend has none", "", "", false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ b := testExecBackend(t)
+ b.Region = tc.backendRegion
+ host := "region-env.example.com"
+ _, envPath := stageRecordingLiveFullEnv(t, b, host, "v1")
+ // A tenant-supplied value that must never survive as-is.
+ if err := os.WriteFile(filepath.Join(b.DataDir, host+".config.env"), []byte("CARLOS_REGION=forged\n"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ rt := registry.Route{Host: host, Kind: "instance", Socket: filepath.Join(t.TempDir(), "instance.sock")}
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ proc, _, _ := b.Start(ctx, rt) // fake binary never binds the socket; health-wait error expected
+ if proc != nil {
+ defer proc.Stop(context.Background())
+ }
+ child := map[string]string{}
+ for _, line := range strings.Split(waitForFile(t, envPath, time.Second), "\n") {
+ if k, v, ok := strings.Cut(line, "="); ok {
+ child[k] = v
+ }
+ }
+ got, ok := child["CARLOS_REGION"]
+ if ok != tc.wantSet || got != tc.want {
+ t.Fatalf("child CARLOS_REGION = %q (set=%v), want %q (set=%v)", got, ok, tc.want, tc.wantSet)
+ }
+ })
+ }
+}
diff --git a/internal/console/apiinstances.go b/internal/console/apiinstances.go
index 64650e82b..954ca0e79 100644
--- a/internal/console/apiinstances.go
+++ b/internal/console/apiinstances.go
@@ -604,6 +604,26 @@ func (s *Server) handleAPIInstanceCreate(w http.ResponseWriter, r *http.Request)
jsonError(w, http.StatusBadRequest, msg)
return
}
+ // The armed-placement gate (issue #329): a non-empty region that names
+ // no armed box is an instance NOTHING will ever mint — the record would
+ // sit dark forever, which is worse than a refusal that lists the real
+ // choices. Placed apps skipped (their value is a fleet, validated by its
+ // own rail above); the unconstrained "" region skipped (it means "the
+ // deployment's home", which always exists); and the whole gate is
+ // dormant until some box publishes a placement code, so a console rolled
+ // ahead of its fleet changes nothing.
+ if placed == "" && region != "" {
+ if set, armed := armedPlacements(ctx, s.cfg.Releases); armed && !set[region] {
+ codes := make([]string, 0, len(set))
+ for c := range set {
+ codes = append(codes, c)
+ }
+ slices.Sort(codes)
+ jsonError(w, http.StatusBadRequest,
+ "region "+region+" is not armed in this deployment — armed: "+strings.Join(codes, ", "))
+ return
+ }
+ }
channel := strings.TrimSpace(req.Channel)
if channel == "" {
// The manifest's channel, then the documented default (§3.1). An
diff --git a/internal/console/apiinstances_test.go b/internal/console/apiinstances_test.go
index 8870c16df..b9a5bd7ff 100644
--- a/internal/console/apiinstances_test.go
+++ b/internal/console/apiinstances_test.go
@@ -2135,3 +2135,52 @@ func TestAPIInstanceListServedBy(t *testing.T) {
t.Errorf("get beta = %d %s, want 200 with no served_by key", code, out)
}
}
+
+// TestAPIInstanceCreateArmedPlacementGate (issue #329): once any box
+// publishes a placement code in its edges file, a create naming a region no
+// armed box serves is refused with the real choices — a record nothing will
+// ever mint is a dark instance, worse than a 400. Dormant before that
+// (legacy fleet: behaviour unchanged); a placement without an EIP is not a
+// landing target; the unconstrained "" region always passes.
+func TestAPIInstanceCreateArmedPlacementGate(t *testing.T) {
+ fx, token := instanceFixture(t, "code-inst-armed")
+ m := titoManifest()
+ m.Regions = nil // the manifest allow-list is a separate rail; keep it out
+ enableInstances(t, fx, "titogo", m)
+ u := instancesURL(fx, "bab", "titogo")
+ put := func(key, body string) {
+ t.Helper()
+ if _, err := fx.rel.Put(context.Background(), key, []byte(body), ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // DORMANT: no edges file carries a placement — any shape-valid region
+ // is accepted, exactly as before the gate existed.
+ put("control/edges/legacy.json", `{"v":1,"box":"legacy","eip":"9.9.9.9","hosts":{}}`)
+ if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.go.tito.io","region":"zz-typo"}`); code != http.StatusCreated {
+ t.Fatalf("dormant create = %d %s, want 201", code, out)
+ }
+
+ // ARMED: uk has an EIP, se does not, legacy still publishes none.
+ put("control/edges/uk-box.json", `{"v":1,"box":"uk-box","eip":"1.2.3.4","region":"eu-west-2","placement":"uk","hosts":{}}`)
+ put("control/edges/se-box.json", `{"v":1,"box":"se-box","eip":"","region":"eu-north-1","placement":"se","hosts":{}}`)
+
+ if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"beta.go.tito.io","region":"uk"}`); code != http.StatusCreated {
+ t.Fatalf("create region uk (armed) = %d %s, want 201", code, out)
+ }
+ if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"gamma.go.tito.io"}`); code != http.StatusCreated {
+ t.Fatalf("create with unconstrained region = %d %s, want 201", code, out)
+ }
+ code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"delta.go.tito.io","region":"zz-typo"}`)
+ if code != http.StatusBadRequest {
+ t.Fatalf("create region zz-typo (armed) = %d %s, want 400", code, out)
+ }
+ if !strings.Contains(out, "armed: uk") {
+ t.Fatalf("refusal %q does not name the armed set", out)
+ }
+ // se published a placement but no EIP: not a landing target.
+ if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"eps.go.tito.io","region":"se"}`); code != http.StatusBadRequest {
+ t.Fatalf("create region se (no EIP) = %d %s, want 400", code, out)
+ }
+}
diff --git a/internal/console/instancesplacement.go b/internal/console/instancesplacement.go
index 9ae896c8e..649325f8d 100644
--- a/internal/console/instancesplacement.go
+++ b/internal/console/instancesplacement.go
@@ -33,10 +33,12 @@ type servedBy struct {
// capacity, and "where is this instance" is the question, not "who is its
// write home" (that is the home map's question, internal/edge).
type edgesFilePlacementView struct {
- V int `json:"v"`
- Box string `json:"box"`
- Region string `json:"region"`
- Hosts map[string]json.RawMessage `json:"hosts"`
+ V int `json:"v"`
+ Box string `json:"box"`
+ EIP string `json:"eip"`
+ Region string `json:"region"`
+ Placement string `json:"placement"`
+ Hosts map[string]json.RawMessage `json:"hosts"`
}
// loadEdgesJoin folds every readable edges file into host → the boxes that
@@ -49,14 +51,31 @@ type edgesFilePlacementView struct {
// all, the same shape filter the other readers apply.
func loadEdgesJoin(ctx context.Context, s release.Store) map[string][]servedBy {
out := map[string][]servedBy{}
+ for _, f := range readEdgesFiles(ctx, s) {
+ for host := range f.Hosts {
+ out[host] = append(out[host], servedBy{Box: f.Box, Region: f.Region})
+ }
+ }
+ for host := range out {
+ sort.Slice(out[host], func(i, j int) bool { return out[host][i].Box < out[host][j].Box })
+ }
+ return out
+}
+
+// readEdgesFiles is the shared best-effort read under both consumers:
+// list, shape-filter, parse, skip anything unreadable or unversioned. A
+// failed list is an empty answer, logged — the callers each degrade to
+// their pre-#337/#329 behaviour rather than failing the request.
+func readEdgesFiles(ctx context.Context, s release.Store) []edgesFilePlacementView {
if s == nil {
- return out
+ return nil
}
keys, err := s.List(ctx, edgesFilePrefix)
if err != nil {
log.Printf("console: api instances: could not list %s for the placement join: %v", edgesFilePrefix, err)
- return out
+ return nil
}
+ var out []edgesFilePlacementView
for _, key := range keys {
name, ok := strings.CutPrefix(key, edgesFilePrefix)
if !ok || !strings.HasSuffix(name, ".json") || strings.Contains(name, "/") {
@@ -72,12 +91,28 @@ func loadEdgesJoin(ctx context.Context, s release.Store) map[string][]servedBy {
log.Printf("console: api instances: skipping %s in the placement join: not a versioned edges file", key)
continue
}
- for host := range f.Hosts {
- out[host] = append(out[host], servedBy{Box: f.Box, Region: f.Region})
- }
- }
- for host := range out {
- sort.Slice(out[host], func(i, j int) bool { return out[host][i].Box < out[host][j].Box })
+ out = append(out, f)
}
return out
}
+
+// armedPlacements folds the fleet's edges files into the set of placement
+// codes an instance may be created into (issue #329): every code published
+// by a box that also has a dial address (an EIP — a box nothing can reach
+// is not a landing target). armed reports whether ANY file carried a
+// placement code at all: false means the fleet predates the field (or the
+// read failed) and the gate stays DORMANT — the door then accepts what it
+// always accepted, so rolling boxes and console in either order is safe.
+func armedPlacements(ctx context.Context, s release.Store) (set map[string]bool, armed bool) {
+ set = map[string]bool{}
+ for _, f := range readEdgesFiles(ctx, s) {
+ if f.Placement == "" {
+ continue
+ }
+ armed = true
+ if f.EIP != "" {
+ set[f.Placement] = true
+ }
+ }
+ return set, armed
+}
diff --git a/internal/host/agent.go b/internal/host/agent.go
index bd8f84dbb..13bb1580c 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -270,9 +270,12 @@ func RunAgent(args []string) {
// gate is still the hibernate bucket env even though unit tenants
// never touch the bucket — one opt-in switch for the whole activator.
eb := &activator.ExecBackend{
- Cfg: *cfg,
- OptDir: release.OptDir(),
- DataDir: dd,
+ Cfg: *cfg,
+ OptDir: release.OptDir(),
+ DataDir: dd,
+ // The box's placement code, into every exec child's env
+ // (issue #329) — same env the reconcile pass scopes by.
+ Region: strings.TrimSpace(os.Getenv("CARLOS_REGION")),
Cache: cache,
Materialize: materializeHook(reg, dd),
Cgroups: cgm,
diff --git a/internal/host/edgesfile.go b/internal/host/edgesfile.go
index c2e724dcf..d57bbd1aa 100644
--- a/internal/host/edgesfile.go
+++ b/internal/host/edgesfile.go
@@ -73,10 +73,19 @@ type edgeHost struct {
// batch-wide if it ever reached here (plan review catch, 2026-08-23).
// edgesFileRegion resolves the real thing.
type edgesFile struct {
- V int `json:"v"`
- Box string `json:"box"`
- EIP string `json:"eip"`
- Region string `json:"region,omitempty"`
+ V int `json:"v"`
+ Box string `json:"box"`
+ EIP string `json:"eip"`
+ Region string `json:"region,omitempty"`
+ // Placement is this box's CARLOS placement code — CARLOS_REGION verbatim
+ // ("ie", "uk"), the OTHER region vocabulary (see Region above, issue
+ // #329). Additive and omitempty like Region: a pre-placement body parses
+ // unchanged, and a box with no CARLOS_REGION set (single-box
+ // deployments, the legal unset state) publishes no key at all. The
+ // console's create door folds every file's Placement into the armed set
+ // a requested instance region is validated against — dormant until at
+ // least one box publishes the field, so roll order does not matter.
+ Placement string `json:"placement,omitempty"`
Generation string `json:"generation"`
Hosts map[string]edgeHost `json:"hosts"`
}
@@ -114,7 +123,7 @@ type edgesFile struct {
// once per alias per process (cleared when the alias publishes).
var withheldAliasSaid sync.Map
-func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, box, eip, region string, last *[]byte) error {
+func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, box, eip, region, placement string, last *[]byte) error {
routes, err := reg.List()
if err != nil {
return fmt.Errorf("edges: list: %w", err)
@@ -248,7 +257,7 @@ func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry,
}
}
- f := edgesFile{V: 1, Box: box, EIP: eip, Region: region, Hosts: hosts}
+ f := edgesFile{V: 1, Box: box, EIP: eip, Region: region, Placement: placement, Hosts: hosts}
gate, err := json.Marshal(f)
if err != nil {
return fmt.Errorf("edges: marshal: %w", err)
@@ -359,6 +368,10 @@ func edgesFileLoop(reg *registry.Registry) {
// per pass). readPlacementRegion is the one IMDS call; edgesFileRegion
// applies the CARLOS_AWS_REGION override on top, pure.
region := edgesFileRegion(os.Getenv, readPlacementRegion())
+ // The placement code alongside it: CARLOS_REGION, the same env the
+ // reconcile pass scopes by — the two vocabularies travel in one file so
+ // a reader can join them without a second source.
+ placement := strings.TrimSpace(os.Getenv("CARLOS_REGION"))
log.Printf("agent: edges file loop armed (5min + jitter, %s)", edgesFileKey(box))
last := seedEdgesFileGate(ctx, store, box)
delay := time.Minute
@@ -366,7 +379,7 @@ func edgesFileLoop(reg *registry.Registry) {
time.Sleep(delay)
delay = 5*time.Minute + rand.N(time.Minute)
eip := readPublicIP()
- if err := edgesFilePass(ctx, store, reg, box, eip, region, &last); err != nil {
+ if err := edgesFilePass(ctx, store, reg, box, eip, region, placement, &last); err != nil {
log.Printf("agent: edges file pass: %v", err)
}
}
diff --git a/internal/host/edgesfile_test.go b/internal/host/edgesfile_test.go
index 11c140ffa..2c219db67 100644
--- a/internal/host/edgesfile_test.go
+++ b/internal/host/edgesfile_test.go
@@ -55,7 +55,7 @@ func TestEdgesFilePublishesHomedHosts(t *testing.T) {
s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
seedRoute(t, reg, "app.example.com")
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -86,7 +86,7 @@ func TestEdgesFilePublishesWithEmptyEIP(t *testing.T) {
s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
seedRoute(t, reg, "app.example.com")
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", "", &lastBody); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -112,7 +112,7 @@ func TestEdgesFileSkipsWriteWhenUnchanged(t *testing.T) {
seedRoute(t, reg, "app.example.com")
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
if s.puts != 1 {
@@ -122,7 +122,7 @@ func TestEdgesFileSkipsWriteWhenUnchanged(t *testing.T) {
// Second pass, identical registry state: a fresh generation timestamp
// would differ from the first pass's, but the change-gate excludes
// Generation, so this must not write.
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
if s.puts != 1 {
@@ -131,7 +131,7 @@ func TestEdgesFileSkipsWriteWhenUnchanged(t *testing.T) {
// A registry change makes the next pass write again.
seedRoute(t, reg, "second.example.com")
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
if s.puts != 2 {
@@ -151,12 +151,12 @@ func TestEdgesFileHoldsLastKnownGoodEIPOnTransientBlank(t *testing.T) {
seedRoute(t, reg, "app.example.com")
var last []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &last); err != nil {
t.Fatal(err)
}
// Simulate a transient IMDS blip: this tick's probe returns "".
- if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", "", &last); err != nil {
t.Fatal(err)
}
@@ -186,7 +186,7 @@ func TestEdgesFileExcludesNonReadyRows(t *testing.T) {
}
var last []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &last); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -219,7 +219,7 @@ func TestEdgesFileWildcardRowPublishesVerbatim(t *testing.T) {
}
var last []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &last); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -250,7 +250,7 @@ func TestEdgesFileRawWireBodyMatchesSchema(t *testing.T) {
seedRoute(t, reg, "app.example.com")
var last []byte
- if err := edgesFilePass(context.Background(), s, reg, "quiet-harbour", "203.0.113.10", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "quiet-harbour", "203.0.113.10", "", "", &last); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
@@ -302,7 +302,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
t.Run("unset region is omitted entirely", func(t *testing.T) {
var last []byte
- if err := edgesFilePass(context.Background(), s, reg, "quiet-harbour", "203.0.113.10", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "quiet-harbour", "203.0.113.10", "", "", &last); err != nil {
t.Fatal(err)
}
got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
@@ -321,7 +321,7 @@ func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
s2, reg2 := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
seedRoute(t, reg2, "app.example.com")
var last []byte
- if err := edgesFilePass(context.Background(), s2, reg2, "quiet-harbour", "203.0.113.10", "eu-west-1", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s2, reg2, "quiet-harbour", "203.0.113.10", "eu-west-1", "", &last); err != nil {
t.Fatal(err)
}
got, _, err := s2.Get(context.Background(), "control/edges/quiet-harbour.json")
@@ -423,7 +423,7 @@ func TestEdgesFileSeedGateSkipsWriteOnUnchangedBucketState(t *testing.T) {
// A prior process (or the prior pass of this one) already published.
var priming []byte
- if err := edgesFilePass(context.Background(), real, reg, "annie", "1.2.3.4", "", &priming); err != nil {
+ if err := edgesFilePass(context.Background(), real, reg, "annie", "1.2.3.4", "", "", &priming); err != nil {
t.Fatal(err)
}
@@ -435,7 +435,7 @@ func TestEdgesFileSeedGateSkipsWriteOnUnchangedBucketState(t *testing.T) {
}
counting := &putCountingStore{Store: real}
- if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "", "", &last); err != nil {
t.Fatal(err)
}
if counting.puts != 0 {
@@ -454,7 +454,7 @@ func TestEdgesFileSeedHoldsEIPThroughRestartBlip(t *testing.T) {
seedRoute(t, reg, "app.example.com")
var priming []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &priming); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &priming); err != nil {
t.Fatal(err)
}
@@ -466,7 +466,7 @@ func TestEdgesFileSeedHoldsEIPThroughRestartBlip(t *testing.T) {
// This tick both hits a transient IMDS blip AND sees a real registry
// change, so the pass must write.
seedRoute(t, reg, "second.example.com")
- if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", &last); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", "", &last); err != nil {
t.Fatal(err)
}
@@ -495,13 +495,13 @@ func TestEdgesFileUnseededLastPublishesBlankEIPOnFirstTickBlip(t *testing.T) {
seedRoute(t, reg, "app.example.com")
var priming []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &priming); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &priming); err != nil {
t.Fatal(err)
}
// The pre-fix shape: a fresh process's *last starts nil (no seed read).
var unseeded []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", &unseeded); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "", "", "", &unseeded); err != nil {
t.Fatal(err)
}
@@ -545,7 +545,7 @@ func TestEdgesFileSeedPreRegionBucketWritesOnceWhenRegionArrives(t *testing.T) {
// This build now has CARLOS_REGION set: the first pass must write, once.
counting := &putCountingStore{Store: real}
- if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "eu-west-1", &last); err != nil {
+ if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "eu-west-1", "", &last); err != nil {
t.Fatal(err)
}
if counting.puts != 1 {
@@ -565,7 +565,7 @@ func TestEdgesFileSeedPreRegionBucketWritesOnceWhenRegionArrives(t *testing.T) {
// A second, identical-region pass must not write again — the gate now
// includes region on both sides.
- if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "eu-west-1", &last); err != nil {
+ if err := edgesFilePass(context.Background(), counting, reg, "annie", "1.2.3.4", "eu-west-1", "", &last); err != nil {
t.Fatal(err)
}
if counting.puts != 1 {
@@ -588,7 +588,7 @@ func TestEdgesFileMarksPlatformRows(t *testing.T) {
}
}
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
body, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -654,7 +654,7 @@ func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) {
publish := func() map[string]hostView {
t.Helper()
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
body, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -734,7 +734,7 @@ func TestEdgesFilePublishesDelegatedWildcardParents(t *testing.T) {
}
}
var lastBody []byte
- if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", &lastBody); err != nil {
+ if err := edgesFilePass(context.Background(), s, reg, "annie", "1.2.3.4", "", "", &lastBody); err != nil {
t.Fatal(err)
}
body, _, err := s.Get(context.Background(), "control/edges/annie.json")
@@ -797,7 +797,7 @@ func TestEdgesFileBoxNameJoinsTheStatusSnapshotLabel(t *testing.T) {
s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
seedRoute(t, reg, "app.example.com")
var last []byte
- if err := edgesFilePass(ctx, s, reg, box, "203.0.113.7", "eu-west-1", &last); err != nil {
+ if err := edgesFilePass(ctx, s, reg, box, "203.0.113.7", "eu-west-1", "", &last); err != nil {
t.Fatal(err)
}
if err := status.WriteBox(ctx, s, snapshot); err != nil {
@@ -808,3 +808,47 @@ func TestEdgesFileBoxNameJoinsTheStatusSnapshotLabel(t *testing.T) {
t.Fatalf("fold = %v, want this box's own EIP — the two writers disagree about its name", got)
}
}
+
+// The placement code (issue #329): CARLOS_REGION verbatim, additive between
+// region and generation, omitted entirely when the box has none — the
+// console's armed-placement gate stays dormant on a fleet that never
+// publishes it, which is what makes roll order irrelevant.
+func TestEdgesFileRawWireBodyPlacementField(t *testing.T) {
+ s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
+ seedRoute(t, reg, "app.example.com")
+ var last []byte
+ if err := edgesFilePass(context.Background(), s, reg, "quiet-harbour", "203.0.113.10", "eu-west-2", "uk", &last); err != nil {
+ t.Fatal(err)
+ }
+ got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var probe struct {
+ Placement string `json:"placement"`
+ Generation string `json:"generation"`
+ }
+ 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","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)
+ }
+
+ // Unset placement publishes no key at all.
+ s2, reg2 := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
+ seedRoute(t, reg2, "app.example.com")
+ var last2 []byte
+ if err := edgesFilePass(context.Background(), s2, reg2, "quiet-harbour", "203.0.113.10", "", "", &last2); err != nil {
+ t.Fatal(err)
+ }
+ got2, _, err := s2.Get(context.Background(), "control/edges/quiet-harbour.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(got2), "placement") {
+ t.Fatalf("raw wire body with no CARLOS_REGION carries a placement key: %s", got2)
+ }
+}