carlos / platform Public

host, provision: read CARLOS_INBOUND, refuse routed mints on a no-inbound box

Paul Campbell pushed by paul@keymail.dev 04c886f4e4392a0c85c463f1f58fdc46b6a39ed8 parent 8c3f2df
4 files changed, +109 −2
  • internal/host/agent.go +13 −1
  • internal/host/host_test.go +23 −0
  • internal/provision/reconcile.go +19 −1
  • internal/provision/reconcile_test.go +54 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 4cfbaf395..c0ce683fe 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -1530,7 +1530,10 @@ func domainsTickConfig(getenv func(string) string, publicIP string) (domains.Con
// hasn't been told its region/pools still serves only ""-scoped records,
// never someone else's residency promise. So this always returns a
// populated Config, and CARLOS_DATA_DIR is honored (default "/data")
-// regardless of whether region/pools are set.
+// regardless of whether region/pools are set. CARLOS_INBOUND (design §8) is
+// the same shape again: unset/"any" is today's behaviour exactly, and it is
+// a declaration about what work THIS BOX will accept, independent of a
+// record's own "no-inbound" placement request — see ReconcileConfig.Inbound.
func provisionTickConfig(getenv func(string) string) provision.ReconcileConfig {
region := strings.TrimSpace(getenv("CARLOS_REGION"))
var pools []string
@@ -1543,11 +1546,20 @@ func provisionTickConfig(getenv func(string) string) provision.ReconcileConfig {
if dd == "" {
dd = "/data"
}
+ // CARLOS_INBOUND is the same apps-domain-style inert gate as
+ // CARLOS_REGION/CARLOS_POOLS above: unset (or "any") is today's
+ // behaviour exactly, a legal "no constraint" state, never a "feature not
+ // configured" one. Read verbatim, not trimmed against a closed
+ // vocabulary here — provision.Reconcile/ReconcileSidecars are what
+ // compare it against InboundNone, so this stays a pure passthrough like
+ // Region above it.
+ inbound := strings.TrimSpace(getenv("CARLOS_INBOUND"))
return provision.ReconcileConfig{
Region: region,
Pools: pools,
DataDir: dd,
Now: time.Now,
+ Inbound: inbound,
}
}
diff --git a/internal/host/host_test.go b/internal/host/host_test.go
index 6f442c850..be918e582 100644
--- a/internal/host/host_test.go
+++ b/internal/host/host_test.go
@@ -784,6 +784,29 @@ func TestProvisionTickConfig(t *testing.T) {
}
}
+// TestProvisionTickConfigReadsInbound pins CARLOS_INBOUND's contract: an
+// unset var is the SAME apps-domain-style inert gate CARLOS_REGION/
+// CARLOS_POOLS already use — a legal "no constraint" state, never a
+// "feature not configured" one — so it must read back as "" (today's
+// behaviour, byte for byte), and only an explicit "none" changes Config.
+func TestProvisionTickConfigReadsInbound(t *testing.T) {
+ env := func(m map[string]string) func(string) string {
+ return func(k string) string { return m[k] }
+ }
+
+ got := provisionTickConfig(env(map[string]string{
+ "CARLOS_REGION": "ie", "CARLOS_POOLS": "eleven", "CARLOS_INBOUND": "none",
+ }))
+ if got.Inbound != "none" {
+ t.Errorf("Inbound = %q, want none", got.Inbound)
+ }
+
+ got = provisionTickConfig(env(map[string]string{}))
+ if got.Inbound != "" {
+ t.Fatal("an unset CARLOS_INBOUND must be an inert default — today's behaviour exactly")
+ }
+}
+
func TestBootstrapIdempotent(t *testing.T) {
root := t.TempDir()
if err := bootstrap(root, nil); err != nil {
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go
index fee9be573..075db51bb 100644
--- a/internal/provision/reconcile.go
+++ b/internal/provision/reconcile.go
@@ -44,7 +44,10 @@ type ReconcileConfig struct {
// default, the apps-domain-style gate this repo already uses — and
// InboundNone means the box serves sidecar work only: no certs, no
// inbound ports, possibly a private subnet with egress-only networking.
- // Read by ReconcileSidecars' placement gate.
+ // Read by ReconcileSidecars' placement gate (matches a record's own
+ // "no-inbound" placement request against this box) AND by Reconcile's
+ // mint gate below (refuses ALL routed instance minting outright — an
+ // ordinary instance record carries no placement vocabulary to match).
Inbound string
// DryRun reports what the pass would do and writes nothing — not to the
// registry, not anywhere. The backfill mitigation (design §4.3): a
@@ -519,6 +522,21 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
existing, ok := byHost[rec.Host]
switch {
case !ok:
+ // A box that declared CARLOS_INBOUND=none (design §8) has told
+ // this pass what work it will ACCEPT, not made a claim about its
+ // network — fleet boxes are already outbound-only dial in this
+ // estate. Ordinary instance records carry no placement
+ // vocabulary of their own (that is ReconcileSidecars' gate,
+ // Task 9, over SidecarRecord.Placement); every routed instance
+ // this pass mints needs inbound service, so a no-inbound box
+ // must refuse ALL of them outright, loudly, rather than mint
+ // something it can never serve — a misfiled record becomes a
+ // log line here, never a silent outage.
+ if cfg.Inbound == InboundNone {
+ log.Printf("provision: refusing to mint route %s (app %q acct %q): this box declares "+
+ "CARLOS_INBOUND=none (no-inbound) and cannot serve routed instance traffic", rec.Host, rec.App, rec.Account)
+ continue
+ }
// The route's whole shape now comes from the record (routeFor):
// Kind, Addr, Unit, Environment, StatusNamed, Hibernate and the
// path derivation. A record carrying none of them still mints the
diff --git a/internal/provision/reconcile_test.go b/internal/provision/reconcile_test.go
index f9193a80d..099aee210 100644
--- a/internal/provision/reconcile_test.go
+++ b/internal/provision/reconcile_test.go
@@ -1,11 +1,15 @@
package provision
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
+ "log"
+ "os"
"path/filepath"
"reflect"
+ "strings"
"testing"
"time"
@@ -1898,3 +1902,53 @@ func TestShapeMismatchComparesAddrsAndHealth(t *testing.T) {
t.Fatal("equal shapes must match")
}
}
+
+// --- inbound placement (design §8, box half) --------------------------------
+
+// TestReconcileNoInboundBoxRefusesRoutedInstances pins the instance pass's
+// half of design §8: a box that has declared CARLOS_INBOUND=none must never
+// mint a routed instance row, no matter what an account's own record asks
+// for — a box that cannot serve inbound traffic must never be handed
+// something to serve. This is independent of ReconcileSidecars' placement
+// gate (Task 9): that gate matches a RECORD's placement request against the
+// box; this one refuses ALL ordinary instance minting outright, because an
+// ordinary instance record carries no placement vocabulary at all.
+func TestReconcileNoInboundBoxRefusesRoutedInstances(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ reg := regWith(t)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data", Inbound: InboundNone}
+
+ var logged bytes.Buffer
+ log.SetOutput(&logged)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("acme.go.tito.io"); err != nil || rt != nil {
+ t.Fatalf("a no-inbound box minted a routed instance: %+v %v", rt, err)
+ }
+ if !strings.Contains(logged.String(), "acme.go.tito.io") || !strings.Contains(logged.String(), "no-inbound") {
+ t.Fatalf("refusal must be logged loudly (host + reason), got: %s", logged.String())
+ }
+}
+
+// TestReconcileUnsetInboundMintsAsBefore pins the inert-default half: a box
+// whose CARLOS_INBOUND is unset (the zero value read by provisionTickConfig)
+// must mint exactly as it always has — the new guard must never fire for
+// "any"/"" boxes, which is every box that exists today.
+func TestReconcileUnsetInboundMintsAsBefore(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ reg := regWith(t)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, "acme.go.tito.io")
+}