route: fix record-owned refusal — drop the console verb that doesn't exist
Both refusal messages told the operator to "Repoint/Bind the RECORD instead" via `carlos route -host <host> -channel <channel>` "against the console" — but carlos route has no console mode (main.go dispatches straight to runRoute, no resolveMode call) and no record-editing endpoint exists yet in the console or CLI. An operator who followed the sentence literally retyped the exact command they were just refused. Fixed by stating what's true (the column is converged from the record on every pass, a write here is reverted) and giving the concrete record key, then stopping — no substitute verb, and deliberately not "delete and recreate" either, since reg.Delete purges the route's capability grants. Also: fill the account into the record path (release.RouteAccount(rt) was already resolved a few lines below for the same row) so the channel message's key is copy-pasteable instead of carrying a literal <account> placeholder; give the environment message the same apps/<account>/<app>/ instances/<host>.json key so the two read alike; and merge the byte-identical vPlatformOwned/vRecordOwned and ePlatformOwned/eRecordOwned refusal bodies into combined case labels (same refusal, two owners). Strengthened two tests: the environment-clear half now asserts the column is still bound after the refusal, not just that the command errored; and the box-scoped test gained a -tunnel case, since -unit short-circuits before the verdict machinery this change touches while -tunnel flows through it downstream of the refusal-collection loops.
2 files changed,
+28
−17
cli.go+10 −17e2e_test.go+18 −0
diff --git a/cli.go b/cli.go| index c26bec15b..757dddd6c 100644 |
| --- a/cli.go |
| +++ b/cli.go |
| @@ -727,10 +727,9 @@ func runRoute(args []string) { |
| case recordOwnedRow(rt): |
| chanVerdicts[i] = verdict{rt: rt, from: from, kind: vRecordOwned, note: fmt.Sprintf( |
| "provisioned route on %s — its channel is owned by the instance record at\n"+ |
| - "apps/<account>/%s/instances/%s.json, which the provisioning reconciler converges onto this row\n"+ |
| - "on every pass; a change here is reverted within seconds of the next serial bump.\n"+ |
| - "Repoint the RECORD instead: `carlos route -host %s -channel <channel>` against the console", |
| - from, rt.App, rt.Host, rt.Host)} |
| + "apps/%s/%s/instances/%s.json, which the provisioning reconciler converges onto this row\n"+ |
| + "on every pass; a change here is reverted within seconds of the next serial bump.", |
| + from, release.RouteAccount(rt), rt.App, rt.Host)} |
| default: |
| chanVerdicts[i] = verdict{rt: rt, from: from, kind: vNeedsCheck} |
| anyNeedsCheck = true |
| @@ -826,10 +825,11 @@ func runRoute(args []string) { |
| "A binding already on such a route (made before this rail existed) survives re-mints and cannot be\n" + |
| "cleared here either: `carlos remove " + rt.Host + "` and let the next reconcile pass re-mint it clean"} |
| case recordOwnedRow(rt): |
| - envVerdicts[i] = envVerdict{rt: rt, from: from, kind: eRecordOwned, note: "provisioned route — its environment is owned by the\n" + |
| - "instance record, which the provisioning reconciler converges onto this row on every pass. A binding\n" + |
| - "made here (or cleared here) is reverted within seconds of the next serial bump.\n" + |
| - "Bind the RECORD instead: `carlos route -host " + rt.Host + " -environment <name>` against the console"} |
| + envVerdicts[i] = envVerdict{rt: rt, from: from, kind: eRecordOwned, note: fmt.Sprintf( |
| + "provisioned route — its environment is owned by the instance record at\n"+ |
| + "apps/%s/%s/instances/%s.json, which the provisioning reconciler converges onto this row on every\n"+ |
| + "pass. A binding made here (or cleared here) is reverted within seconds of the next serial bump.", |
| + release.RouteAccount(rt), rt.App, rt.Host)} |
| case envClear: |
| envVerdicts[i] = envVerdict{rt: rt, from: from, kind: eWillWrite} |
| default: |
| @@ -882,11 +882,7 @@ func runRoute(args []string) { |
| case vUnproven: |
| refusals = append(refusals, fmt.Sprintf( |
| "%s: %s — refusing regardless of -allow-version-change: the comparison itself is unproven", v.rt.Host, v.note)) |
| - case vPlatformOwned: |
| - refusals = append(refusals, fmt.Sprintf( |
| - "%s: %s — refusing regardless of -allow-version-change: no flag overrides a column this verb does not own", |
| - v.rt.Host, v.note)) |
| - case vRecordOwned: |
| + case vPlatformOwned, vRecordOwned: |
| refusals = append(refusals, fmt.Sprintf( |
| "%s: %s — refusing regardless of -allow-version-change: no flag overrides a column this verb does not own", |
| v.rt.Host, v.note)) |
| @@ -901,10 +897,7 @@ func runRoute(args []string) { |
| envToWrite = append(envToWrite, v.rt.Host) |
| case eMissing: |
| refusals = append(refusals, fmt.Sprintf("%s: %s", v.rt.Host, v.note)) |
| - case ePlatformOwned: |
| - refusals = append(refusals, fmt.Sprintf( |
| - "%s: %s — no flag overrides a row this verb does not own", v.rt.Host, v.note)) |
| - case eRecordOwned: |
| + case ePlatformOwned, eRecordOwned: |
| refusals = append(refusals, fmt.Sprintf( |
| "%s: %s — no flag overrides a row this verb does not own", v.rt.Host, v.note)) |
| } |
diff --git a/e2e_test.go b/e2e_test.go| index 13615561a..27a7fa66e 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -2874,6 +2874,9 @@ func TestRouteEnvironmentRefusedOnProvisionedRoute(t *testing.T) { |
| if err == nil { |
| t.Fatalf("clearing an environment on a Provisioned route was accepted:\n%s", out) |
| } |
| + if got := f.environmentOf("bound.go.tito.io"); got != "demo" { |
| + t.Fatalf("environment = %q, want it still bound after the refusal", got) |
| + } |
| } |
| // The box-scoped axes are untouched: they name resources that exist on one |
| @@ -2892,6 +2895,21 @@ func TestRouteBoxScopedAxesStillWorkOnProvisionedRoute(t *testing.T) { |
| if got := f.routeOf("acme.go.tito.io").Unit; got != "console.service" { |
| t.Errorf("unit = %q, want console.service", got) |
| } |
| + |
| + // -unit short-circuits before the verdict machinery this change touched |
| + // (the grantAxis/hibernateSet/backingSet/unitSet block returns early, well |
| + // above chanVerdicts/envVerdicts). -tunnel does not: it runs through the |
| + // same candidates slice and sits downstream of the refusal-collection |
| + // loops recordOwnedRow feeds, so it is the axis a future recordOwnedRow |
| + // case could plausibly catch by mistake. Exercise it directly. |
| + out, err = f.run(f.fullEnv(), "route", "-registry", f.regPath, |
| + "-host", "acme.go.tito.io", "-tunnel", "relay1") |
| + if err != nil { |
| + t.Fatalf("route -tunnel on a Provisioned route was refused:\n%s", out) |
| + } |
| + if got := f.routeOf("acme.go.tito.io").Tunnel; got != "relay1" { |
| + t.Errorf("tunnel = %q, want relay1", got) |
| + } |
| } |
| // re-review NEW 1, the reachable gap: moving a Platform+Canary route BETWEEN |