carlos / platform Public

cli: carlos route -hibernate refuses on a provisioned sidecar row

Paul Campbell pushed by paul@keymail.dev 5945601ce3e7381c751117822dccb6d869379334 parent 4e28ec9
2 files changed, +81 −0
  • cli.go +28 −0
  • e2e_test.go +53 −0
diff --git a/cli.go b/cli.go
index 186c87d30..4b1f7ed66 100644
--- a/cli.go
+++ b/cli.go
@@ -1307,6 +1307,34 @@ func runRouteHibernate(r *registry.Registry, rt registry.Route, on, dryRun bool)
log.Fatalf("route: %s: Platform+Canary route — the domains reconciler rebuilds this row from values it derives itself, "+
"and a hibernation flip here is reverted on the next reconcile pass. This verb does not own this row", rt.Host)
}
+ if rt.IsSidecar() && rt.Provisioned {
+ // Task 11 item 5 (carried from Task 9's review), the same lesson
+ // this verb has already paid for on the Channel and Environment
+ // axes (recordOwnedRow) — one column later. Unlike an ORDINARY
+ // provisioned instance route, where Hibernate is the instance
+ // record's OPTIONAL, tri-state column (absent means this verb is
+ // still free to set it — internal/provision/reconcile.go's
+ // convergeSidecar-adjacent instance path), a sidecar row's
+ // Hibernate is unconditional: convergeSidecar
+ // (internal/provision/sidecar_reconcile.go) re-stamps it from
+ // SidecarManifest.HibernateOrDefault EVERY pass, with no "the
+ // record is silent, this column is free" escape. A flip here lands
+ // and is reverted within seconds of the next serial bump, with
+ // nothing said anywhere.
+ //
+ // Defence in depth, not the only door: every genuine sidecar host
+ // is minted under provision.SidecarSuffix, so suffixRail already
+ // refuses this axis (and every other) before this function is ever
+ // reached in practice. This check is what fires for a row that
+ // reaches this far anyway — a Kind=="sidecar" row outside the
+ // reserved suffix, which suffixRail's host-string check alone
+ // cannot see — and it is what gives an operator who does meet it
+ // the actual reason, not just "reserved suffix".
+ log.Fatalf("route: %s: provisioned sidecar row — its hibernation is owned by the app's sidecars manifest at\n%s, "+
+ "which the sidecar reconcile pass converges onto this row on every pass; a flip here is reverted within\n"+
+ "seconds of the next serial bump. This verb does not own this column",
+ rt.Host, provision.SidecarManifestKey(release.RouteAccount(rt), rt.App))
+ }
if on {
store, pub := deploymentReadSeam()
daemon, note, proven := routeDaemonCheck(context.Background(), store, pub, release.RouteAccount(rt), rt)
diff --git a/e2e_test.go b/e2e_test.go
index 9e63d7cb3..545fba187 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -36,6 +36,7 @@ import (
"github.com/carlosframework/platform/internal/appconfig"
"github.com/carlosframework/platform/internal/domains"
"github.com/carlosframework/platform/internal/edge"
+ "github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
"github.com/carlosframework/platform/internal/status"
@@ -2917,6 +2918,58 @@ func TestRouteEnvironmentRefusedOnProvisionedRoute(t *testing.T) {
}
}
+// Task 11 item 5 (carried from Task 9's review): the sidecar reconcile pass
+// (internal/provision/sidecar_reconcile.go, convergeSidecar) converges
+// Hibernate from the app's sidecars manifest on EVERY pass — unconditionally,
+// not the ordinary instance record's tri-state (absent-means-free) column a
+// Provisioned instance route carries. A `-hibernate` write on such a row
+// lands and is reverted within seconds of the next serial bump, with nothing
+// said anywhere — the same class of bug #130 was, on the axis this repo has
+// not yet paid the lesson for.
+//
+// The reserved sidecar suffix (suffixRail) already refuses every genuine
+// sidecar row through a different door — every one is minted under
+// provision.SidecarSuffix by construction — so this test uses a host OUTSIDE
+// that suffix to isolate the NEW check from that existing one: this is
+// deliberately the shape suffixRail's own doc comment calls out as not
+// hypothetical (an ordinary-kind row's Provisioned+Kind combination is what
+// this rail reads, not the hostname).
+func TestRouteHibernateRefusedOnProvisionedSidecarRow(t *testing.T) {
+ f := newRouteTestFixture(t)
+ f.addRoute(registry.Route{Host: "bot.eleven.bdm.example.com", Kind: "sidecar", App: "eleven", Account: "bdm",
+ Status: "ready", Provisioned: true})
+
+ out, err := f.run(f.fullEnv(), "route", "-registry", f.regPath, "-host", "bot.eleven.bdm.example.com", "-hibernate=false")
+ if err == nil {
+ t.Fatalf("route -hibernate on a provisioned sidecar row was accepted:\n%s", out)
+ }
+ if !strings.Contains(out, "sidecar") || !strings.Contains(out, provision.SidecarManifestKey("bdm", "eleven")) {
+ t.Errorf("refusal does not name the sidecar manifest as the door:\n%s", out)
+ }
+ if got := f.routeOf("bot.eleven.bdm.example.com").Hibernate; got != false {
+ t.Fatalf("hibernate = %v, want it unwritten after the refusal", got)
+ }
+}
+
+// TestRouteHibernateRefusedOnGenuineSidecarSuffixRow is the end-to-end proof
+// against a REAL sidecar host shape (under provision.SidecarSuffix): both
+// rails agree, and the operator-facing message names the reconcile pass, not
+// just "reserved suffix".
+func TestRouteHibernateRefusedOnGenuineSidecarSuffixRow(t *testing.T) {
+ f := newRouteTestFixture(t)
+ host := provision.SidecarHost("bot", "eleven", "bdm")
+ f.addRoute(registry.Route{Host: host, Kind: "sidecar", App: "eleven", Account: "bdm",
+ Status: "ready", Provisioned: true})
+
+ out, err := f.run(f.fullEnv(), "route", "-registry", f.regPath, "-host", host, "-hibernate")
+ if err == nil {
+ t.Fatalf("route -hibernate on a genuine sidecar row was accepted:\n%s", out)
+ }
+ if got := f.routeOf(host).Hibernate; got != false {
+ t.Fatalf("hibernate = %v, want it unwritten after the refusal", got)
+ }
+}
+
// The box-scoped axes are untouched: they name resources that exist on one
// machine, the record does not own them, and an operator must still be able
// to grant a port or stamp a unit on a provisioned route.