Skip to content

Cloud connect: when the in-app install is blocked, say what Radar tried, why it stopped, and what to do - #1772

Merged
nadaverell merged 22 commits into
mainfrom
fix/preflight-blocked-copy
Sep 16, 2026
Merged

nadaverell merged 22 commits into
mainfrom
fix/preflight-blocked-copy

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When the in-app Cloud connect stops before installing, the person sees a card in place of the plan they clicked for. It used to be a one-line verdict — often "your Kubernetes identity can't perform this", sometimes a raw Go error — with no way forward. Now it tells them three things: what Radar tried, why it stopped (in the cause's own terms, with the raw refusals underneath), and the one remedy every stop shares — a cluster admin gets the install command from Radar Cloud's install page, choosing Helm, Argo CD or Flux to match how the cluster is normally deployed to. The CLI prints the same three parts.

What changed

Blockers are classified where they're found (internal/cloudinstall/preflight.go). PreflightResult.Blocking stays the full list (CLI and existing consumers unchanged) and gains two subsets:

  • Denied — the caller's identity lacks a permission: a denied access review, or a dry-run 403 written in the apiserver's own permission sentences (User "x" cannot <verb> resource …, or RBAC escalation prevention's is attempting to grant RBAC permissions not currently held). Any other 403 is an admission webhook or policy, and is not a denial — more permission wouldn't change its answer.
  • Unverifiable — Radar can't prove the exact mutation. Today this only fires when Radar's own render of Radar's own chart hides a Secret (the chart renders none in this flow; the guard fails closed against a future chart doing so), so it's presented as a Radar version limitation.
  • Everything else is the cluster refusing the change: admission, an object owned by something else, an API the cluster doesn't serve.

Cause() reduces that to one word — cluster outranks permissions (permission alone wouldn't clear a refusal), permissions outranks verification. A generated RoleBinding's NotFound for a prepared role is never its own blocker: the role's outcome is the real one, and a dependent error must not outrank a permission denial.

Errors before the dry run get the same treatment. Looking for the existing install reads Helm state from Secrets the identity may not be allowed to read; that permission denial now gets the permissions card rather than an "unsupported" verdict quoting the error. A Kubernetes API error at that stage is Radar failing to inspect and surfaces as a retryable failure. What remains as unsupported is the inspection itself refusing (several Radars, one already connected, ownership Radar won't guess at, an incompatible existing release).

The card (CloudConnectFlow.tsx). The server puts cause and the attempted operation (mode, namespace, release) on the blocked response — additive; reason and the blocked_* handoff outcomes are unchanged. Three labeled sections, one button, Back at the right:

Cause Title Why it stopped
permissions Your Kubernetes identity can't do this install Your credentials lack permissions this needs (below). Anyone with them can complete this exact install.
cluster The cluster blocked part of this install The cluster refused: something already there conflicts with the install, or a policy rejects it. More permission wouldn't change that.
verification This version of Radar can't install this chart version from here The chart renders something this Radar build can't check before applying, so it won't install it unseen. A limitation of this Radar, not of your cluster or your access.

What Radar tried states the Helm install/upgrade, release, namespace and identity, and that nothing changed (or, before a plan existed, that Radar looked for an existing install). What to do: "Have a cluster admin get the install command from Radar Cloud. One page: pick Helm, Argo CD or Flux — whatever this cluster normally uses — and see exactly what it changes before running it." Button: Get the install command. The GitOps card keeps its own instructions (its remedy lives in the repo); every other blocked card, including unsupported, offers the button.

Every blocked card ends with one exit. The button's destination follows what Radar established, never a guess — the Hub's fresh command would reset an existing release's values, and its script writes the token Secret before Helm runs:

Radar established Button Destination
a release to adopt — natively Helm-owned, found by discovery or in a plan Get the install command /install?existing=1&ns&release&method=helm
a fresh install — complete plan, discovery saw the whole cluster; or a complete scan found no Radar running and only Helm's release records were unread (the card says to confirm nothing is installed first) Get the install command /install?method=helm
a GitOps-managed release with a verified Argo CD / Flux owner Get the Argo CD / Flux values patch /install?existing=1&ns&release&method=argocd|flux — the in-cluster lane's own link
nothing it can act on — denied before discovery finished, a partial scan, a Radar found without Helm ownership, an unverified GitOps owner, every unsupported refusal Open Radar Cloud the signup/login entry → Home

/install is what the Hub stashes across sign-in and replays (it does so for a link carrying a target or a method, hence method=helm on every install link). The copy says what the person will meet: Radar Cloud asks for a cluster name, then shows the instructions to review. The GitOps and unsupported cards use the same what / why / what-to-do shape, their server message as the why. The footer's standing "or set up in the browser" link stays on /signup: it is built from whatever the last attempt left behind, and a wrong guess there must not become an install. A prepare error's reason stays on the pitch under the CTA after its toast is gone.

Testing

  • preflight_test.go: Cause() precedence; a dry-run 403 with admission wording classifies as cluster while the authorizer sentence and both RBAC escalation messages classify as permissions. fresh_preflight_test.go: a denied generated role's binding NotFound leaves a single permissions blocker
  • cloudConnectHandoff.test.ts: one assertion per exit row — adopt pinned to Helm; fresh only from a complete plan and scan (partial scan → Radar Cloud); unknown target → Radar Cloud; GitOps → its verified owner's tab, unverified → Radar Cloud; unsupported → Radar Cloud even with a known release. cloud_install_test.go: the GitOps block picks the verified owner when a stale candidate is listed first; an adopt target survives a block after inspection; a fresh plan from a partial scan is flagged
  • go test ./internal/cloudinstall/ ./internal/server/ ./cmd/explorer/, make tsc
  • Visual, on throwaway kind clusters: a view-only ServiceAccount (permissions card from a denial before inspection finished — ends with "Open Radar Cloud" and the admin handoff) and a ValidatingAdmissionPolicy denying the radar namespace as admin (cluster card, correctly not classified as permissions despite arriving as is forbidden:)

Note

Medium Risk
Changes install planning, preflight classification, and Hub handoff URLs; wrong exit targeting could suggest a fresh install over an existing release, though tests explicitly guard partial scans and unsupported refusals.

Overview
When in-app Cloud connect stops before installing, the experience is reworked so users see what Radar tried, why it stopped, and a single actionable exit—in the web blocked card and in radar cloud install CLI output.

Preflight and pre-dry-run errors are classified in internal/cloudinstall: blockers are recorded as permission denials (blockDenied), cluster refusals (blockRefused), or unverifiable chart output (blockUnverifiable), with Cause() returning permissions, cluster, or verification. IsAuthorizationDenial separates apiserver RBAC 403s from admission/policy forbids. A generated RoleBinding’s dry-run NotFound no longer masks the parent Role’s permission denial.

The server blocked response is richer: cause, attempted (mode, namespace, release, stage, partial scan, unread Helm state, GitOps method), and ReleaseInspectError so permission failures while reading Helm Secrets get the permissions card while still deep-linking adopt when discovery found a native Helm release.

The UI replaces the generic signup link with exitFor()—Hub /install deep links only when the target is established (adopt, safe fresh, verified GitOps tab); otherwise Open Radar Cloud. The blocked card uses three labeled sections and a primary CTA; prepare errors persist on the pitch footer after the toast.

Reviewed by Cursor Bugbot for commit 92625e8. Bugbot is set up for automated code reviews on this repo. Configure here.

…ty problem

The blocked headline, the server's message and the CLI's summary all said
"your Kubernetes identity cannot perform this" whenever preflight blocked.
Preflight also blocks on an object that exists but is not owned by the Helm
release, on admission refusals, and on a chart Secret or manifest Radar
could not verify — none of which is about who the caller is, and telling
that person to ask for broader permissions sends them the wrong way. Say
what actually happened: the dry run was refused or could not be verified;
the blocking lines underneath already name which.
@nadaverell
nadaverell requested a review from hisco as a code owner September 15, 2026 23:47
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Describe blocked Cloud preflights as dry-run failures

🐞 Bug fix 🕐 Less than 10 minutes

Grey Divider

AI Description

• Reframes blocked preflights as refused or unverifiable dry runs, not identity failures.
• Aligns server, CLI, and dialog messaging while preserving reason codes and guidance.
• Keeps detailed blocking reasons visible so operators can identify the actual cause.
Diagram

graph TD
  P["Preflight result"] --> S["Server response"] --> W["Connect dialog"] --> U(["Operator"])
  P --> C["CLI output"] --> U
Loading
High-Level Assessment

The targeted presenter-specific copy changes are appropriate because behavior, reason codes, and detailed blockers remain correct. A shared message catalog was considered, but it would add disproportionate coupling and would not naturally span both Go and TypeScript for this small correction.

Files changed (3) +4 / -4

Bug fix (3) +4 / -4
cloud_install_output.goClarify blocked preflight CLI output +2/-2

Clarify blocked preflight CLI output

• Replaces the identity-specific failure statement with a neutral explanation that the planned operation was refused or could not be verified. Renames the detail heading to “Blocked on:” so the following lines communicate the specific cause.

cmd/explorer/cloud_install_output.go

cloud_install.goReturn neutral blocked-preflight guidance +1/-1

Return neutral blocked-preflight guidance

• Changes the preflight-blocked server message to describe the failed dry run without assuming insufficient Kubernetes permissions. The existing 'preflight' reason, blocking details, and platform-operator guidance remain unchanged.

internal/server/cloud_install.go

CloudConnectFlow.tsxReframe the blocked preflight dialog title +1/-1

Reframe the blocked preflight dialog title

• Changes the preflight card title from an identity-based assertion to a statement that the planned install did not pass its dry run. Existing icons, detailed blockers, and browser-wizard action are preserved.

web/src/components/CloudConnectFlow.tsx

Preflight records each blocker by cause at the site that finds it: a
denied access review or a 403 from the dry run is the caller's identity
lacking a permission; a hidden chart Secret is Radar unable to prove the
mutation; anything else — admission, an object owned by something else,
an API the cluster does not serve — is the cluster refusing the change.
Cause() picks one word for the result, with a refusal outranking denials
(permission alone would not clear it) and denials outranking verification.

The blocked card, the server's message and the CLI summary each present
that word: missing permissions → ask an operator, they have them; the
cluster refused → an operator has to clear it; can't verify → use the
browser wizard. The browser link is offered only where it changes the
outcome, so a cluster refusal no longer suggests a path that would meet
the same refusal.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread internal/cloudinstall/adoption_preflight.go
Admission webhooks answer 403 too, and more permission would not change
their answer, so only the apiserver's fixed authorizer sentence ("User
cannot <verb> resource") counts as the caller lacking a permission; any
other Forbidden is the cluster refusing the change. The CLI's verification
follow-up no longer sends the operator back through the same preflight the
hidden-Secret marker just blocked; it points at the wizard's Helm command.
Card copy names the cause in one line and says who clears it.
@nadaverell
nadaverell force-pushed the fix/preflight-blocked-copy branch from e3f9937 to 5d79325 Compare September 16, 2026 00:05
@nadaverell nadaverell changed the title Cloud connect: a failed preflight is a refused dry run, not an identity problem Cloud connect: when preflight blocks, say what would actually unblock it Sep 16, 2026
The marker preflight looks for is written by Radar's own Helm render of
Radar's own chart; it can only appear when a chart version renders
something this binary's installer refuses to apply unseen. Blaming "the
chart" read as the person's problem. Name it as a Radar version limit and
point at the wizard, which installs the same chart with Helm directly.
…, and what to do

The card replaces the plan the person never saw, so it now states the plan's
facts first — which Helm operation, which release and namespace, as whose
identity, and that nothing changed — then why it stopped in the cause's own
terms with the refusals underneath, then the one remedy every stop shares:
a cluster admin connects it from the browser wizard, which sets the
connection up the way the cluster is normally deployed to (Helm, or a values
patch for Argo CD or Flux). The wizard link is offered on every blocked card
except GitOps, whose remedy lives in the repo.

A permission denial while looking for the existing install — Helm keeps
release state in Secrets the identity may not be allowed to read — is the
same story as a denied preflight and gets the same card; a Kubernetes API
error there is Radar failing to inspect, not a refusal, and surfaces as a
retryable failure instead of an "unsupported" verdict. The CLI prints the
same three parts.
The remedy is a page with three tabs, not a flow, so it is called the
install page and the card's one action is a button: "Get the install
command". Back sits at the right. Copy trimmed by about a fifth.

A binding's NotFound for a prepared role whose own dry run was refused is
not recorded as a blocker: the role's refusal already is, and recording the
dependent error let it outrank a plain permission denial.
@nadaverell nadaverell changed the title Cloud connect: when preflight blocks, say what would actually unblock it Cloud connect: when the in-app install is blocked, say what Radar tried, why it stopped, and what to do Sep 16, 2026
The blocked card's button and the footer's "or set up in the browser" went
to /signup, which the Hub does not carry across sign-in: the person landed
on Home and had to find the install page. /install is what the Hub stashes
and replays after sign-in, and its sign-in card names the pending install.
An adopt carries existing=1 with the release and namespace Radar planned;
a fresh install takes the page's defaults, which are Radar's defaults too.
When inspection identified an existing release but chart preparation was
refused or denied, the blocked response dropped the plan, so the install
link opened a fresh install over a release that should be adopted. The
target now rides along whether or not the dry run ran.
…ton on unsupported

The release Radar planned to adopt now rides on the handoff itself, so the
footer's browser link after Back — and after a flow that failed with a plan
in hand — adopts the same release the blocked card did instead of opening a
fresh install over it. The unsupported card loses the install button: its
refusals (several Radars, one already connected, ownership Radar will not
guess at) say in the message what to do, and a fresh-install link there
would contradict it.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread internal/server/cloud_install.go
Comment thread web/src/components/CloudFunnelButton.tsx
Reading a release's Helm state can be refused after discovery has already
found the Radar Deployment carrying it; the error now carries that target
so the permissions card still adopts the release instead of offering a
fresh install over it. On the frontend, every handoff built from a status
with a plan — a failure, a canceled plan — keeps the plan's release the same
way the blocked card does.
…emedy

The footer's browser link, after Back from a GitOps or unsupported
refusal, opened the Hub's fresh install over the very release the card
said to recover or leave to the repo. Those handoffs fall back to the
signup pitch instead. The adoption dry run now treats a binding's NotFound
for a refused role as dependent, the way the fresh path does, so a
permission denial is not outranked by its own echo.
A refusal whose remedy is elsewhere never does, whatever release it names.
A failure the server marked not safe to retry never does either: a Hub
pairing or a Kubernetes install may already exist, its recovery is on the
failed card, and a second install would compound it. A blocked preflight
and a canceled plan created nothing, so the install page is the way forward.
…e plan it had

A target can be known from discovery before any chart was rendered or
dry-run. The attempted plan now carries the stage — inspect, prepare,
preflight — and the card's "what Radar tried" reads from it, so it never
claims a dry run that did not happen.
The card is the one moment Radar has just established what is in the
cluster, so its button may point at the install page with the release it
found. The footer's standing "or set up in the browser" link is built from
whatever the last attempt left behind — a canceled plan, a refusal, a
half-finished install, an inspection that never completed — and every one
of those states had a case where a fresh-install link was the wrong answer.
It stays on the signup pitch, where a wrong guess about the cluster cannot
become a fresh install over an existing release.
…ished

Reading Helm state after discovery ran establishes the target either way:
the release discovery found, or a fresh install where it found none. A
denial before discovery finished leaves it unknown, and that card tells
the admin to check before installing instead of linking a fresh install
that could land on a Radar already there. The fresh link names the default
Helm tab so the Hub stashes it across sign-in, which it does only for a
link carrying an intent.
…stall

Discovery that could only see the default namespace may have missed a
Radar elsewhere; "found none" then means nothing, and the card withholds
the install link the way it does for a denial before discovery ran.
The plan card discloses an incomplete discovery and asks for an
acknowledgement before a fresh install; the blocked card has no such step,
so it carries no target and offers no install link in that case.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a8dc4d9. Configure here.

Comment thread internal/server/cloud_install.go
Discovery can establish the release it found; it can never establish that
none exists, because a Helm release outlives a deleted or unlabeled
Deployment and the Hub's fresh command would reset its values. Before
inspection completes the card points at a release to adopt or at nothing.
A fresh plan from a discovery that saw only the default namespace kept its
stage but was sent as no attempt at all, so the card described Radar as
still searching above refusals that came from a dry run. The attempt now
carries a partialScan flag: the stage is told truthfully, the install link
is withheld, and the card says which namespace was checked so the admin
knows what to look at before installing.
A stop that leaves the person nowhere to go is a dead end, however well it
is explained. Every blocked card now ends the same way — an admin with
cluster access, in Radar Cloud — with one button whose destination follows
what Radar established: the install page, deep-linked, when it knows the
release to adopt (pinned to the Helm tab), the fresh install from a
complete plan and a complete scan, or a GitOps-managed release's values
patch for its verified controller; Radar Cloud itself everywhere else — an
unknown target, an unverified owner, and every unsupported refusal, whose
own message says what to resolve or recover. The Hub's fresh command would
reset an existing release's values and its script writes the token Secret
before Helm runs, so a guessed target is never linked.

The GitOps and unsupported cards join the what / why / what-to-do shape,
the server message as their why. A prepare error's reason stays on the
pitch under the CTA after its toast is gone.
…resh install, and says so

When a complete discovery scan finds no Radar running anywhere but reading
Helm's release records is refused, the card offers the fresh install
command and tells the admin to confirm nothing is installed first — the
one thing not ruled out is a leftover release whose Deployment was deleted,
and a fresh install over it would reset values for a Radar that was not
running anyway. A partial scan, or a denial before discovery finished,
still establishes nothing and exits to Radar Cloud.
A Radar Deployment found without matching native-Helm ownership would be
refused as unmanaged had inspection succeeded; a denied inspection must not
turn it into an adoption link, nor into a fresh install over it. Such a
find offers neither and exits to Radar Cloud.
@nadaverell
nadaverell merged commit a9c6f3d into main Sep 16, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant