Skip to content

feat(resources): import multi-resource YAML from a file or drop target - #1742

Merged
hisco merged 7 commits into
skyhook-io:mainfrom
kcaashish:feat/1580-import-yaml-files
Sep 15, 2026
Merged

hisco merged 7 commits into
skyhook-io:mainfrom
kcaashish:feat/1580-import-yaml-files

Conversation

@kcaashish

@kcaashish kcaashish commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Radar already applied multi-document YAML, but only from text pasted into the editor. This adds a file picker and a drop target that feed that same editor, plus the request bound the issue asked for.

What changed

An Import YAML file button beside a short hint, and the editor surface itself as the drop target — no extra panel to discover.

The file becomes editor text and nothing else. Client validation, schema loading, dry-run review, conflict handling and apply are untouched, so a multi-document file behaves exactly as a pasted one already did. There is no second apply path.

Accepts one .yaml / .yml, up to 6 MiB
Rejects empty, unreadable, multi-file, wrong extension — through the banner the dialog already uses for apply failures
Decides on the extension, never the MIME type

The extension, not the MIME type. Browsers report nothing dependable for YAML: macOS Chrome commonly sends "", Windows sends assorted octet-stream variants. A .yaml reported as application/octet-stream is accepted; a .txt claiming text/yaml is refused.

The drag overlay promises only what a drag can know. dragover exposes an item's kind and the item count but withholds file names, so it highlights for any file drag and calls out a multi-file one, leaving the extension check for the drop. Detection reads dataTransfer.types rather than items, because Safari keeps items empty for the whole drag — the overlay never appeared there otherwise.

Replacing the editor asks first only when there is work to lose. The dialog opens prefilled in both entry points — a kind skeleton from ResourcesView, duplicate YAML from WorkloadView — so a non-empty editor is the normal state, not a sign the user typed anything. The confirmation is gated on the content differing from what the dialog itself put there: dropping onto an untouched skeleton, or into an empty editor, loads with no prompt.

Server bounds

handleApplyResource read its body with an unbounded io.ReadAll. The limit is now one number — 6 MiB of YAML — enforced on every route, and stated on each in the terms that route actually carries:

  • apply caps the body directly, because there the body is the YAML.
  • preview checks the decoded yaml field. It never bounded the document at all before; its envelope cap only refused large files as a side effect,
    so the point it began failing moved with however much escaping a file happened to need.
  • preview's envelope is sized for worst-case escaping — twice the content limit, plus the wrapper's own bytes. (12.06 MiB) That is the ceiling the grammar allows: JSON doubles " and \, and YAML 1.2 admits no raw C0 control character in content except tab and newline, which also double. A ConfigMap holding minified JSON is close to half quotes, so a smaller envelope strands real files.
  • apply also caps at 100 documents, the limit preview already enforced, and validates the request fully before opening a cluster client.

Oversize returns 413, not 400: the cap fires before anything parses the body, so a 400 would blame content nobody has read yet. The convention is recorded alongside the existing status codes.

Correcting myself on the document cap

I said on the issue that capping documents on apply "would reject content that pastes fine today." That was wrong. YAMLReview: true is hardcoded, so preview always runs first and already caps at 100 — nothing pasted in the UI is affected, only direct API callers. That is why it is included here rather than left open.

Verification

Frontend: 3,668 tests, 23 covering the import module — extension over MIME, empty, oversize, unreadable, multi-file, multi-document passthrough, drag classification including Safari's shape, and confirm gating. Backend: handler tests for the 413 and the document cap, plus tests pinning apply's limits equal to preview's.

Against a live kind cluster: a dropped two-document file moves the editor status bar from "1 cluster schema active" to "2", and Review returns "2 accepted · 0 rejected · 0 not previewable" with admission-injected fields diffed — the existing pipeline, unchanged. A 7 MiB file is refused in 3 ms with the editor untouched.

Known limitation

Loading a large file blocks the main thread while the editor parses the buffer — in Chrome roughly 90 ms at 128 KiB, 460 ms at 1 MiB, 2.4 s at 5.5 MiB. The import covers that by naming the file it is loading, painted before the editor starts and retired only once frames arrive on time again, so the status describes when the editor is usable rather than when the content was handed over.

Safari is materially slower, and at multi-megabyte sizes the pause is long enough to be a real problem. The cost is YamlEditor parsing the whole buffer two to three times per change (parseFallbackYamlDiagnosticsparseYamlDocumentIdentities, plus parseAllDocuments), which affects paste and the resource YAML editor equally — it is not introduced here. Happy to lower the import cap to whatever keeps the worst case tolerable, or open a separate issue for the parse cost, whichever you prefer.

There is also a ceiling above Radar's. The Kubernetes API server refuses request bodies over 3 MiB, and etcd's practical per-object limit is lower still — and preview's dry-run goes through the apiserver like any other write. So 6 MiB describes a file, not an object: a multi-document bundle reaches it fine, while a single object approaching it is refused by the cluster whatever Radar allows. That error surfaces unchanged rather than being masked.

Type of change

  • New feature (non-breaking change that adds functionality)
  • Bug fix (non-breaking: preview's oversize status, Safari drag detection)

How has this been tested?

  • Tested locally with minikube/kind
  • Added/updated unit tests

Related issues

Closes #1580.


Note

Medium Risk
Changes cluster YAML apply/preview HTTP handlers (body limits, status codes, document cap) that direct API callers may hit; UI import only affects the create dialog but loads full file content into the editor on the main thread.

Overview
Adds Import YAML file and drag-and-drop on the create-resource dialog so multi-document manifests land in the same editor and flow through the existing preview/apply pipeline—no new apply path. Client checks enforce one .yaml/.yml up to 6 MiB (extension-based), with a replace confirmation only when the buffer differs from the dialog’s initial skeleton, plus loading UI while the editor ingests large text.

On the server, apply and preview now cap request bodies before read (readBoundedTextBody / bounded JSON decode) and return 413 for oversize payloads instead of mislabeling size as malformed 400. Shared constants align 6 MiB YAML content, a preview envelope sized for worst-case JSON escaping, and a 100-document cap on apply (matching preview). Apply validates document count and reviewedVersions indices before opening a cluster client. CLAUDE.md documents the 413 convention.

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

/api/resources/apply read its body with an unbounded io.ReadAll, letting a
single request decide how much memory Radar spent on it. Both routes now cap
before reading rather than after:

- 6 MiB request body on apply and preview, returning 413. The cap fires
  before anything parses the body, so a 400 would blame content nobody has
  read yet and send the caller to debug syntax that may be fine.
- 100 YAML documents on apply, the limit preview already enforced. A byte
  cap alone still admits a small body holding tens of thousands of
  documents, and the two routes disagreeing about the same bundle is what
  made that reachable.

An oversize preview body previously answered 400 "invalid preview request:
http: request body too large" — a size problem reported as a malformed
request, and the one users actually hit, since preview always runs first.

Splitting the documents now happens before the cluster client is fetched, so
the request is fully validated before Radar reaches for a client.
Applying a rendered manifest bundle meant pasting it into the editor by hand.
The dialog now takes a file: an "Import YAML file" button, and the editor
surface itself as the drop target so the interaction is discoverable without
adding another panel.

The file becomes editor text and nothing else. Client validation, schema
loading, dry-run review, conflict handling and apply are untouched, so a
multi-document file behaves exactly as a pasted one already did.

Limits and rejections:

- one .yaml or .yml file, up to 6 MiB — the same limit the server enforces,
  refused at pick time instead of after an upload.
- the extension decides, not the MIME type. Browsers report nothing
  dependable for YAML: macOS Chrome commonly sends "", Windows sends
  assorted octet-stream variants.
- empty, unreadable and multi-file selections report through the error
  banner the dialog already uses for apply failures, and clear on the next
  successful import.

The drag overlay promises only what a drag can know. A dragover exposes an
item's kind and the item count but withholds file names, so it highlights for
any file drag and calls out a multi-file drag, leaving the extension check
for the drop.

Replacing the editor asks first only when there is work to lose. The dialog
opens prefilled in both of its entry points — a kind skeleton from
ResourcesView, duplicate YAML from WorkloadView — so a non-empty editor is
the normal state, not a signal the user has typed anything. The confirmation
is gated on the content differing from what the dialog itself put there:
dropping onto an untouched skeleton, or into an empty editor, loads straight
away with no prompt. Cancelling leaves the editor exactly as it was.
Two problems with the import, both about what the user can see.

The drag overlay never appeared in Safari. Detection read dataTransfer.items,
which Safari keeps empty for the whole drag — the dragged items stay protected
until the drop. Every browser advertises "Files" in dataTransfer.types, so that
is the signal for whether files are coming; items now only supplies the count,
where the browser is willing to give one.

Loading a file then handed the content straight to the editor, which parses the
whole buffer and blocks the main thread while it does — measured in Chrome at
about 90ms for 128 KiB, 460ms for 1 MiB and 2.4s for 5.5 MiB. Nothing said so,
and a pause that long reads as a hang.

The import now names the file it is loading. Two details that sound like
implementation but are the whole point:

- the status is committed and painted before the content reaches the editor,
  across two animation frames. One frame only commits it; the paint needs the
  second, and a status that arrives after the block has started is no status.
- it is retired when a frame arrives on time again, not when the content was
  handed over. Clearing it alongside the value batches both into the single
  render that blocks, which retires the message while the editor is still
  unusable — exactly the state it exists to describe.

No spinner. The thread it would animate on is the one that is blocked, so it
would freeze mid-turn and read as more broken than plain text.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Import multi-resource YAML files with bounded preview and apply

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Import single YAML bundles by picker or editor drop, preserving existing review and apply flow.
• Validate extension, size, readability, and replacement safety before loading editor content.
• Bound preview and apply requests to 6 MiB and 100 documents.
Diagram

graph TD
  A["Picker or drop"] --> B["File validation"] --> C{"Editor modified?"}
  C -- "No" --> E["YAML editor"] --> F["Preview route"] --> G["Apply route"] --> H["Cluster client"]
  C -- "Yes" --> D["Replace prompt"] --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated file upload API
  • ➕ Could stream uploaded manifests instead of first loading them into the editor.
  • ➕ Would separate file transport from raw YAML request handling.
  • ➖ Would duplicate the established preview and apply workflow.
  • ➖ Requires additional multipart APIs, validation behavior, and error handling.
  • ➖ Makes imported files behave differently from pasted YAML.
2. Parse imported YAML before loading
  • ➕ Could reject malformed documents before invoking the editor.
  • ➕ Could provide document-level import diagnostics immediately.
  • ➖ Duplicates validation already performed by the editor and preview API.
  • ➖ Risks inconsistent YAML parsing behavior across client and server.
  • ➖ Adds complexity without improving the apply path.

Recommendation: Keep the PR’s text-to-editor approach. It adds file convenience without creating a second apply path, while shared client/server limits constrain resource usage; a dedicated upload pipeline would add substantial duplication for little user benefit.

Files changed (7) +649 / -23

Enhancement (2) +274 / -9
CreateResourceDialog.tsxAdd YAML picker and editor drop target +179/-9

Add YAML picker and editor drop target

• Adds file selection, drag overlays, import progress feedback, and replacement confirmation for edited content. Imported text continues through the existing editor, preview, and apply workflow.

packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx

yaml-file-import.tsImplement YAML file validation and drag detection +95/-0

Implement YAML file validation and drag detection

• Introduces helpers for reading one '.yaml' or '.yml' file up to 6 MiB, returning actionable rejection messages, detecting file drags across browsers, and protecting user-edited content.

packages/k8s-ui/src/utils/yaml-file-import.ts

Bug fix (2) +56 / -14
server.goBound and fully validate apply requests before cluster access +16/-14

Bound and fully validate apply requests before cluster access

• Replaces the unbounded apply-body read with the shared capped reader. Enforces the 100-document limit and validates reviewed document indexes before obtaining a Kubernetes client.

internal/server/server.go

yaml_preview.goAlign preview and apply request limits +40/-0

Align preview and apply request limits

• Defines matching 6 MiB and 100-document limits for preview and apply. Returns 413 for oversized preview payloads and adds a bounded raw-text body reader for apply requests.

internal/server/yaml_preview.go

Tests (2) +318 / -0
yaml_preview_test.goTest request size and document-count enforcement +129/-0

Test request size and document-count enforcement

• Covers bounded body acceptance, oversized apply and preview responses, synchronized route limits, and rejection of apply bundles exceeding 100 documents.

internal/server/yaml_preview_test.go

yaml-file-import.test.tsTest YAML file import behavior across browsers +189/-0

Test YAML file import behavior across browsers

• Covers multi-document preservation, extension-based acceptance, size and content rejection, unreadable files, Safari drag detection, multi-file drops, and replacement confirmation rules.

packages/k8s-ui/src/utils/yaml-file-import.test.ts

Documentation (1) +1 / -0
CLAUDE.mdDocument HTTP 413 request-limit conventions +1/-0

Document HTTP 413 request-limit conventions

• Adds guidance that bodies exceeding route-level pre-read limits return 413, while parsed input violations remain 400.

CLAUDE.md

@qodo-code-review

qodo-code-review Bot commented Sep 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A closed dialog reopens with old YAML ✓ Resolved 🐞 Bug ≡ Correctness
Description
loadFiles and the nested animation-frame callbacks in loadIntoEditor remain live after
closeNow, which neither invalidates the asynchronous work, clears pendingImport, nor stores and
cancels the frames before they call setYaml(content). Closing during a read can therefore expose
the independently portaled confirmation over the closed parent, while closing and reopening before a
deferred callback runs lets the open effect restore the new initial YAML only for the prior import
to replace it.
Code

packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[R154-158]

+  const loadIntoEditor = useCallback((fileName: string, content: string) => {
+    setImporting(fileName)
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        setYaml(content)
Relevance

●●● Strong

Clear async lifecycle bug; team accepts cancellation and stale-update fixes for deferred UI
transitions.

PR-#1050

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The cited code shows that the loader performs asynchronous file reading and schedules three
uncancelled animation-frame stages, including the YAML content replacement, while closing only
resets some dialog state and does not invalidate the read or cancel the queued frames. It also shows
that the confirmation is independently portaled and controlled by pendingImport, so late
asynchronous updates can remain visible after closure, and a deferred setYaml can run after the
reopen effect has restored the next session’s initial state.

packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[94-108]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[111-117]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[154-176]
packages/k8s-ui/src/components/ui/DialogPortal.tsx[28-30]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[94-117]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[154-195]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[522-534]
packages/k8s-ui/src/components/ui/ConfirmDialog.tsx[43-48]
packages/k8s-ui/src/components/ui/DialogPortal.tsx[74-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Pending file reads and queued animation-frame callbacks from an import survive dialog closure, allowing an old import to expose its confirmation after the parent closes or commit stale YAML after a subsequent reopen resets the dialog.
## Fix Focus Areas
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[94-117]
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[154-195]
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[522-534]
## Recommended Fix
Introduce a dialog-session or import generation ref and retain every scheduled animation-frame identifier. Whenever the dialog closes or resets, invalidate the generation, clear pending import state, and cancel all scheduled frames; before every post-await or frame-driven state update, verify that the originating generation is still current and the dialog is open, including before calling `setYaml` or changing loading state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. File imports can overwrite newer work ✓ Resolved 🐞 Bug ≡ Correctness
Description
loadFiles resumes after readYamlFile(files) using the render-time yaml value and applies the
result without checking whether the editor state or import request is still current. If the user
edits during the read or starts another picker or drop import, an older completion can skip
confirmation against the latest buffer or arrive last and replace newer editor work.
Code

packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[R188-190]

+      if (needsReplaceConfirmation(yaml, initialYaml)) {
+        setPendingImport({ fileName: result.fileName, yaml: result.yaml })
+        return
Relevance

●●● Strong

Stale closure can overwrite user edits; repository history favors fixes for asynchronous state
races.

PR-#807

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
readYamlFile yields while awaiting file.text(), and both picker and drop paths can start
additional reads during that interval. When the callback resumes, it makes its replacement decision
from the captured yaml closure and applies the result without a freshness check; because the
editor is controlled directly by yaml, the late state update replaces its full buffer.

packages/k8s-ui/src/utils/yaml-file-import.ts[52-63]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[178-195]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[218-229]
packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[397-403]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Asynchronous file reads use stale editor state and can complete out of order, allowing an older import to replace newer user work without the intended replacement confirmation.
## Fix Focus Areas
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[178-195]
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[218-229]
- packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx[376-386]
## Recommended Fix
Track the latest editor value in a ref and assign each import attempt a monotonically increasing generation or request ID stored in a ref. Increment it whenever an import begins and invalidate it when the dialog is reset or closed; after `readYamlFile` resolves, return without applying the result unless that request is still current. Evaluate replacement confirmation against the latest editor value rather than the render-time closure before committing the result, so an older import cannot replace intervening edits or a newer import.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Accepted files fail Hub review ✓ Resolved 🔗 Cross-repo conflict ≡ Correctness
Description
readYamlFile and MAX_YAML_FILE_BYTES allow 6 MiB of raw YAML, but usePreviewResources wraps it
with metadata via JSON.stringify and handlePreviewResources/decodeBoundedJSONBody applies the
same limit to the larger encoded request rather than independently to its yaml field. In Radar Hub
Web, where RadarApp uses the Hub-proxied cluster API, wrapper syntax or escaped characters push
accepted files at or near the limit past the preview bound before review, even though the raw apply
path would accept them.
Code

packages/k8s-ui/src/utils/yaml-file-import.ts[R47-49]

+  if (file.size > MAX_YAML_FILE_BYTES) {
+    const limit = Math.round(MAX_YAML_FILE_BYTES / (1024 * 1024))
+    return reject('too-large', `${file.name} is larger than the ${limit} MiB limit.`)
Relevance

●● Moderate

Potential boundary mismatch is plausible, but the PR explicitly documents the envelope overhead as
intentional.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The picker permits raw YAML through exactly 6 MiB and the apply endpoint sends and bounds that raw
text, but preview always serializes the YAML and additional fields as a JSON object and caps the
complete encoded body at the same numeric limit. The JSON framing and escaping make preview's
effective YAML allowance smaller than the picker and apply allowances, while Radar Hub Web's
imported and mounted RadarApp with a Hub-proxied cluster API base establishes that its users
exercise this mismatched contract.

packages/k8s-ui/src/utils/yaml-file-import.ts[43-49]
web/src/api/client.ts[4203-4217]
internal/server/yaml_preview.go[77-87]
internal/server/yaml_preview.go[27-30]
internal/server/yaml_preview.go[76-86]
internal/server/server.go[3885-3893]
web/src/api/client.ts[4210-4223]
packages/k8s-ui/src/utils/yaml-file-import.ts[1-10]
packages/k8s-ui/src/utils/yaml-file-import.ts[1-6]
internal/server/yaml_preview.go[71-88]
internal/server/yaml_preview.go[214-228]
web/src/api/client.ts[4210-4217]
web/src/api/client.ts[4263-4267]
External repo: skyhook-dev/radar-hub-web, src/pages/ClusterView.tsx [1-12]
External repo: skyhook-dev/radar-hub-web, src/pages/ClusterView.tsx [166-175]
External repo: skyhook-dev/radar-hub-web, src/pages/ClusterView.tsx [248-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The importer and raw apply endpoint accept up to 6 MiB of YAML, but preview limits the larger JSON-encoded envelope to the same size. JSON metadata, framing, and escaping can therefore cause accepted files near the advertised limit to fail preview with HTTP 413 before review, including in the Radar Hub Web embedded application.
## Fix Focus Areas
- internal/server/yaml_preview.go[20-30]
- internal/server/yaml_preview.go[71-88]
- packages/k8s-ui/src/utils/yaml-file-import.ts[1-10]
- packages/k8s-ui/src/utils/yaml-file-import.ts[43-49]
## Recommended Fix
Introduce a bounded preview-envelope limit large enough for the worst-case JSON representation of a 6 MiB YAML string, while independently enforcing the shared 6 MiB limit against the decoded `req.YAML` byte length. Preserve HTTP 413 when either the safe envelope bound or decoded YAML limit is exceeded, keep the importer and raw apply-body limit aligned to the decoded-YAML limit, and add boundary tests proving that a 6 MiB YAML file can be previewed and applied.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


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

Comment thread packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx Outdated
Comment thread packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx Outdated
Comment thread packages/k8s-ui/src/utils/yaml-file-import.ts
@kcaashish

Copy link
Copy Markdown
Contributor Author

Previews:
image

image file-a543e23508e34eefdae15f55f5088a4e

An import spans an await and several animation frames, and nothing tied the
result back to the editor it was meant for.

Two ways that went wrong. Reading a file captured the buffer as it stood when
the read began, so an edit made while a large file was being read was judged
against the old value and could be replaced without the confirmation ever
appearing; two overlapping imports could also finish out of order and let the
older one land last. Separately, closing the dialog neither cancelled the
queued frames nor cleared the pending confirmation, so closing and reopening
quickly let a frame from the previous session overwrite the buffer the open
effect had just reset.

Every import now takes a generation on entry. The resume after the read and
each deferred frame check it before touching state, and closing, reopening or
unmounting bumps the generation and cancels whatever frames are still
outstanding. The replacement check reads the buffer as it stands now through a
ref rather than through the render-time closure.

Confirming a replacement deliberately takes a fresh generation rather than the
one its read started under: starting another import while the prompt is up
would otherwise retire it, and Replace would silently do nothing. Confirming is
a decision about the editor as it stands when the button is clicked.
Apply and the importer accept 6 MiB of YAML. Preview applied the same 6 MiB to
its JSON body, which carries that YAML escaped inside an envelope and is always
the larger of the two — so a manifest at the advertised limit was refused before
review, and the point it started failing moved with however much escaping the
file happened to need. Preview runs before apply, so this was the limit users
actually met.

The two are now separate concerns. maxYAMLContentBytes is the limit Radar
advertises, checked on the decoded yaml field, which preview never bounded at
all before — its envelope cap only refused large documents as a side effect.
Apply keeps stating it directly, because there the body is the content.
maxYAMLPreviewRequestBytes drops to being about transport and gets room for the
escaping, so the envelope stops deciding how much YAML fits.

Nothing a caller may submit grew: every route still refuses YAML over 6 MiB,
and preview now refuses it for that reason rather than incidentally. A document
made almost entirely of quotes could still exceed the envelope and is answered
with the same size error.

Boundary tests cover both directions — a document at exactly the limit whose
encoded envelope is provably larger survives review, and one over the limit
inside an envelope that fits is refused.

@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.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 21d08d2. Configure here.

Comment thread packages/k8s-ui/src/components/shared/CreateResourceDialog.tsx
…other

Taking a generation retires the frames the previous import queued, including
the one that would have cleared its loading status. When the new import then
returned early — a rejected file, or one waiting on replace confirmation — it
never reached the editor, so nothing cleared the status and the previous
import's overlay stayed over the editor with no frame left alive to retire it.
The overlay does not take pointer events, so starting a second import while one
is loading is easy to do.

An import now clears the status as soon as it takes ownership, before branching
on the result, so every path out leaves it consistent: handing off to the editor
sets it again in the same tick, and the paths that return early leave it clear.
The envelope held 8 MiB against a 6 MiB content limit, which covers the
escaping a manifest typically incurs and not the escaping one is allowed to.
A ConfigMap wrapping minified JSON is close to half quotes, and every quote
doubles when the document is encoded — so a 6 MiB bundle of that shape encoded
past 8 MiB and preview refused it with 413 while apply would have taken it.
Preview runs first, so the file simply could not be applied.

Twice the content limit is not a margin chosen for comfort, it is the ceiling
the grammar allows. JSON escaping doubles `"` and `\`, and YAML 1.2 admits no
raw C0 control character in content except tab and newline, which also escape
to two bytes; every other byte survives encoding unchanged. No valid document
within the content limit can fail to fit.

The slack on top is the envelope's own bytes. Doubling the content alone is
short by the field names and braces around it: a document of nothing but quotes
encodes to exactly twice its size, and the wrapper then pushes it over. That is
how the tests found it.

The limit callers meet is unchanged — 6 MiB of YAML on every route, still
checked against the decoded field. Only the transport allowance moved.
@hisco

hisco commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Thanks, merging as-is. Keeping 6 MiB.

The parse cost is worth its own PR rather than a smaller cap. It's the two or three parses per change you pointed at, so it's more than moving a number, and a lower import cap would only hide it on one path.

@hisco
hisco merged commit ac988fe into skyhook-io:main Sep 15, 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.

Import multi-resource YAML from a file or drop target

2 participants