Skip to content

filer: switch workspace upload from import-file to /workspace/import - #6149

Open
Sankalp-Mittal wants to merge 21 commits into
mainfrom
sankalp-mittal/workspace-import-migration
Open

filer: switch workspace upload from import-file to /workspace/import#6149
Sankalp-Mittal wants to merge 21 commits into
mainfrom
sankalp-mittal/workspace-import-migration

Conversation

@Sankalp-Mittal

@Sankalp-Mittal Sankalp-Mittal commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What

DABs uploads workspace files one file at a time. This switches the upload call in libs/filer from the old, deprecated endpoint to the new one:

  • Old: POST /api/2.0/workspace-files/import-file/{path}
  • New: POST /api/2.0/workspace/import (multipart form)

Uploads stay one-file-at-a-time. Nothing else about deploy changes.

Why

The old endpoint is deprecated. The new one allows 30 uploads/sec per workspace instead of 20, has its own rate-limit budget, and scales better — so deploys hit fewer 429 (rate-limited) errors.

We do not use zip folder upload (the other option discussed): a zip upload can't overwrite a folder that already exists, and bundle deploy re-uploads into existing folders every time. So per-file on the new endpoint is the right choice.

Changes

  • workspace_files_client.go — build the multipart request by hand instead of using the SDK's Workspace.Upload. The SDK helper sends the wrong workspace-routing header for the CLI's internal "none" value; building it ourselves routes correctly. format=AUTO keeps the server deciding file-vs-notebook exactly as before.
  • Error handling — the new endpoint reports "path already taken" with different status codes and messages. We now check the API's structured error reason first (WORKSPACE_OBJECT_TYPE_MISMATCH), and fall back to matching the message text. The fallback is still needed: some workspaces don't send the structured reason yet, and one collision case comes from an older code path that never will. Both confirmed against a real workspace.
  • Test server — updated the fake server to accept the new upload format. Also added a way for tests to make one specific file's upload fail, since all uploads now share the same URL.

Size limits (nothing gets smaller)

file ipynb notebook source notebook
New /workspace/import 500 MB 100 MB 10 MB
Old import-file ~400 MB 100 MB 10 MB

Notebook limits are the same on both. The max file size goes up.

Testing

Unit and acceptance tests updated for the new endpoint.

Verified against a real workspace via bundle deploy:

  • All object types classify correctly — Python / Scala / SQL notebooks become notebooks, a .py file with no notebook header stays a file, a .ipynb becomes a notebook, and a .lvdash.json becomes a dashboard.
  • File edge cases — empty file, filename with spaces, unicode filename, and a binary file all upload correctly.
  • Byte-for-byte integrity — uploaded then downloaded and compared by checksum; every file (including a 12 MB binary and unicode content) comes back identical.
  • Re-deploy — uploading over existing objects (overwrite) reads back the new content, not the stale one; re-deploying unchanged files re-uploads nothing.
  • Concurrency — deploys of 1→20 files, and separately 1→20 files at 20 MB each (up to ~540 MB in one deploy), completed without error across ~140 deploys.
  • Error path — the "path already taken" / type-mismatch errors are detected and surfaced correctly; other upload failures now name the file that failed instead of a bare error.
  • Permissions — writing to a non-writable path surfaces a clean "access denied", distinct from a missing-file error.

Size limits verified against a real workspace:

  • Regular file: uploads succeed well past the old endpoint's ceiling — confirmed at 100 MB (19s) and 450 MB (41s) via multipart, up near the 500 MB limit.
  • Notebook: rejected above 10 MB with exceeded max size (10485760 bytes) — the same cap the old endpoint enforced, so no regression.

Not in this PR

  • Upload speed is unchanged (still 20 at a time).
  • Zip folder upload (can't overwrite folders).

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: af88c8f

Run: 31601402131

Env 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 4 4 303 1130 5:36
💚​ aws windows 4 4 305 1128 6:37
💚​ azure linux 4 4 302 1130 6:19
💚​ azure windows 4 4 304 1128 6:59
💚​ gcp linux 1 5 303 1130 5:48
💚​ gcp windows 1 5 305 1128 10:27
8 interesting tests: 4 RECOVERED, 4 SKIP
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S
💚​ TestFetchRepositoryInfoAPI_FromRepo/root 💚​R 💚​R 💚​R 💚​R
💚​ TestFetchRepositoryInfoAPI_FromRepo/subdir 💚​R 💚​R 💚​R 💚​R
Top 10 slowest tests (at least 2 minutes):
duration env testname
6:12 gcp windows TestAccept
3:31 aws windows TestAccept
3:18 azure windows TestAccept
2:31 azure linux TestAccept/bundle/resources/quality_monitors/change_output_schema_name/DATABRICKS_BUNDLE_ENGINE=direct
2:14 azure windows TestAccept/bundle/resources/quality_monitors/change_assets_dir/DATABRICKS_BUNDLE_ENGINE=terraform
2:10 azure linux TestAccept/bundle/resources/quality_monitors/change_assets_dir/DATABRICKS_BUNDLE_ENGINE=terraform
2:08 azure windows TestAccept/bundle/resources/quality_monitors/change_assets_dir/DATABRICKS_BUNDLE_ENGINE=direct
2:04 gcp windows TestAccept/bundle/resources/quality_monitors/change_output_schema_name/DATABRICKS_BUNDLE_ENGINE=direct
2:04 azure windows TestAccept/bundle/resources/quality_monitors/change_output_schema_name/DATABRICKS_BUNDLE_ENGINE=terraform
2:01 aws windows TestAccept/bundle/resources/quality_monitors/change_output_schema_name/DATABRICKS_BUNDLE_ENGINE=direct

@Sankalp-Mittal
Sankalp-Mittal marked this pull request as ready for review August 4, 2026 08:22
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/bundle/ - needs approval

80 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @andrewnester, @shreyas-goenka, @anton-107, @lennartkats-db

/acceptance/experimental/air/ - needs approval

Files: acceptance/experimental/air/run-submit-deps/output.txt, acceptance/experimental/air/run-submit-deps/script
Suggested: @vinchenzo-db
Also eligible: @riddhibhagwat-db, @apeforest, @bfontain, @lu-wang-dl, @panchalhp-db, @maggiewang-db, @ben-hansen-db, @pardis-beikzadeh-db

/libs/filer/ - needs approval

Files: libs/filer/workspace_files_client.go, libs/filer/workspace_files_client_test.go
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @Divyansh-db, @hectorcast-db, @parthban-db, @tanmay-db, @tejaskochar-db, @mihaimitrea-db, @chrisst, @rauchy

/libs/sync/ - needs approval

Files: libs/sync/watchdog.go
Suggested: @simonfaltum
Also eligible: @renaudhartert-db, @Divyansh-db, @hectorcast-db, @parthban-db, @tanmay-db, @tejaskochar-db, @mihaimitrea-db, @chrisst, @rauchy

General files (require maintainer)

11 files changed
Based on git history:

  • @denik -- recent work in libs/testserver/, .nextchanges/bundles/, acceptance/bin/

Any maintainer (@andrewnester, @anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db, @janniklasrose, @lennartkats-db, @rugpanov, @rclarey) can approve all areas.
See OWNERS for ownership rules.

Replace POST /api/2.0/workspace-files/import-file/{path} with the multipart
variant of POST /api/2.0/workspace/import (via the SDK's Workspace.Upload +
format=AUTO). The previous endpoint is deprecated; the new one has a higher
rate limit (30 vs 20 rps/workspace) and is ~1.5-2x faster for typical bundle
deployments.

Error handling is rewritten to branch on SDK sentinels (errors.Is against
ErrNotFound / ErrResourceAlreadyExists / ErrAlreadyExists /
ErrInvalidParameterValue / ErrPermissionDenied) and the AIP-193 ErrorInfo
reason WORKSPACE_OBJECT_TYPE_MISMATCH, with a message-substring fallback for
workspaces where WP-6031 has not rolled out.

The testserver now decodes the multipart /workspace/import body and mirrors the
real endpoint's format=AUTO notebook detection. A new bodyContains option on the
fault mechanism lets a test target a single file's upload, which is no longer
possible by URL since every upload shares the /workspace/import path.

Co-authored-by: Isaac
The size limits in WorkspaceFilesClient.Write were described using internal
config flag names and figures measured by hand against one workspace, neither
of which a reader can verify. Replace them with the documented limits and link
the sources.

This corrects one figure: the notebook cap was described as a flat 10 MiB, but
IPYNB notebooks are documented at 100 MB. Only source-format notebooks are
capped at 10 MB, so the limit depends on how format=AUTO classifies the
payload.

Also restore the rate limit doc link to MaxRequestsInFlight, which the comment
carried when the constant was introduced in #81. The value is unchanged.

Add a comment above each test in workspace_files_client_test.go stating what it
covers.

Co-authored-by: Isaac
The local_code_source test filtered recorded uploads by URL path, which no
longer matches: /workspace/import carries the target filename in the multipart
body. Filter on the body instead, the same way auto-migrate-empty-tfstate does.

--del-field raw_body is dropped because the tarball is binary and the request
recorder already summarizes it as a size placeholder.

This test was added in #6110, after the upload migration branch was cut, so it
was not covered by the earlier fixture updates.

Co-authored-by: Isaac
Two error shapes stopped mapping to fileAlreadyExistsError when Write moved to
sentinel matching.

A 409 carrying no error_code unwraps to ErrResourceConflict only. ErrAlreadyExists
and ErrResourceAlreadyExists are siblings under it, not parents, so neither
matched. The workspace returns exactly this shape, and locker relies on
fs.ErrExist here to report which client holds the lock. Match the parent
sentinel instead.

A 400 with an empty error_code matches no sentinel but ErrBadRequest, so the
notebook already-exists check added in #5106 no longer fired. Restore it; that
PR notes the newer message format may not have reached all workspaces, and this
change has no evidence the rollout has since completed.

Both shapes now have test cases; each fails against the previous logic.

Co-authored-by: Isaac
The SDK's Workspace.Upload sets X-Databricks-Workspace-Id from cfg.WorkspaceID
behind a bare != "" check, so it forwards the CLI-only "none" sentinel as a
literal workspace ID. That sentinel is written to .databrickscfg by
`auth login --skip-workspace` and names no real workspace, so the upload is
misrouted.

Encode the same multipart body in newImportForm and post it through
apiClient.Do, which routes the header through auth.WorkspaceIDHeaders and maps
the sentinel to no header at all. Field names and layout match Upload; the
`language` field is omitted because it only applies to format=SOURCE.

Co-authored-by: Isaac
Write no longer calls Workspace.Upload, so the mock expectations on it asserted
against a call that is gone. Drive the tests through testserver and inspect the
multipart body the CLI actually sends, which pins the wire format rather than
the SDK call shape: path, content, format=AUTO, and overwrite present only when
requested.

Adds coverage for the routing header the previous commit fixes, including the
"none" sentinel case.

Co-authored-by: Isaac
Tighten the size-limit block to the classification rule and its two doc links,
and reorganize the path-conflict list so each bullet names the branch that
handles it, including the two shapes that carry no error_code.

Also move multipartContentLimit above the function that reads it.

Co-authored-by: Isaac
Match the notebook extension case-insensitively, so a ".R" source is detected
the way the real endpoint detects it.

Also require the format field to be AUTO rather than accepting an absent value.
The real endpoint defaults to SOURCE, which imports every payload as a notebook,
so a caller that omits the field is a bug the fake should surface. The rejection
message now reports what was received.

Co-authored-by: Isaac
Replace the hand-rolled sys.argv slicing with argparse. The manual parser had to
special-case --body-contains before the positionals, so the flag only worked in
that one position and the usage string was maintained in two places.

Behavior is unchanged: same positionals, same optional error_code default, same
payload.

Co-authored-by: Isaac
Guards the ToLower added to detectNotebookLanguage: the test fails against the
previous case-sensitive switch, where ".R" was stored as a FILE at the
unstripped path.

Co-authored-by: Isaac
…port

The test filtered recorded uploads by URL path, which no longer matches:
/workspace/import carries the target filename in the multipart body. Both
assertions still show exactly two uploads (the config and the command), so the
test's point -- that the requirements file is never uploaded -- is unchanged;
only the recorded request shape differs.

This test was added in #6166, which merged into this branch after the upload
migration, so it was not covered by the earlier fixture updates. Mirrors the
fix applied to ai_runtime_task/local_code_source for the same reason.

Co-authored-by: Isaac
The local_code_source test asserts the uploaded size of each .air_snapshots
tarball. On Windows, git checks the packaged files out with CRLF, which adds a
byte per newline and shifts every recorded size, so both Windows jobs failed
with 2-4 byte differences.

Pin the packaged files to eol=lf, following the per-directory pattern in
acceptance/experimental/air/run-submit-deps/.gitattributes. Only the files that
actually land in a tarball are listed; debug.log is excluded by .gitignore and
stays unpinned.

The recorded sizes only became load-bearing when uploads moved to
/workspace/import, which records the multipart body; the previous endpoint put
the filename in the URL and never recorded the payload.

Co-authored-by: Isaac
Python's text mode translates \n to \r\n on write under Windows, so editing a
file through this helper replaced its LF endings with CRLF. Pinning the checked
out bytes with .gitattributes does not help, because the rewrite happens after
checkout: in ai_runtime_task/local_code_source only the upload recorded after
the update_file.py step still reported a different size on Windows.

Pass newline="" on both the read and the write so the file's existing endings
survive verbatim, in either direction.

Co-authored-by: Isaac
fb99c27 wrapped unmapped upload failures with "failed to upload <path>",
which changed the message the upload/internal_server_error and upload/timeout
tests capture. Regenerate their goldens to match.

Co-authored-by: Isaac
print_requests.py --sort orders by the whole request, including the multipart
body. One synced file is out.requests.txt, whose recorded body differs between
runs, so its position in the list was nondeterministic — passing locally but
failing in CI.

Drop --sort and sort the displayed "METHOD path" lines instead, which are
stable. The contains.py assertions are unaffected.

Co-authored-by: Isaac
#6196 removed the Local config field and regenerated every out.test.toml
snapshot, but this test was added on this branch in parallel, so its committed
snapshot still carried Local = true. The post-test "no files changed" check
regenerates it and fails on the diff. Regenerate to match.

Co-authored-by: Isaac
The previous newline="" fix stopped Python from rewriting \n to \r\n on write,
but it also made the read preserve CRLF verbatim. Script search strings are
always LF, so a multi-line replacement (e.g. duplicate_yaml_merge_key) no longer
matched on Windows, where databricks.yml is checked out with CRLF, and
update_file.py errored with "old=... not found".

Normalize CRLF to LF on read so the LF search string matches, and keep the
newline="" write so the result stays LF. Net effect on Windows: the edited file
is normalized to LF, which is what every downstream upload/hash assertion
expects anyway.

Co-authored-by: Isaac
Replace the hand-built multipart form (newImportForm + apiClient.Do) with the
SDK's multipart Workspace.Upload. To keep the CLI-only "none" workspace-id
sentinel from being forwarded as a literal routing id by Upload's bare
cfg.WorkspaceID != "" check, normalize it to "" in NewWorkspaceFilesClient.

The error mapping and multipart wire format are unchanged, so the existing
Write tests still hold; add TestNewWorkspaceFilesClientNormalizesWorkspaceID
to pin the sentinel normalization.

Co-authored-by: Isaac
@Sankalp-Mittal
Sankalp-Mittal force-pushed the sankalp-mittal/workspace-import-migration branch from 2a99228 to af88c8f Compare August 12, 2026 13:25
@@ -0,0 +1 @@
Bundle file uploads now use the multipart `POST /api/2.0/workspace/import` endpoint instead of the deprecated `POST /api/2.0/workspace-files/import-file`. The new endpoint has a documented rate limit of 30 requests per second per workspace ([API rate limits](https://docs.databricks.com/aws/en/resources/limits)), higher than the limit that applied to the previous endpoint, and is ~1.5–2× faster for typical bundle deployments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

~1.5–2× faster for typical bundle deployments

This looks awesome! Did you validate this via some tests?

// workspaceObjectTypeMismatchReason is the AIP-193 ErrorInfo reason attached
// by /workspace/import when overwrite=true targets a path whose existing
// object's node type differs from the upload (FILE vs NOTEBOOK).
const workspaceObjectTypeMismatchReason = "WORKSPACE_OBJECT_TYPE_MISMATCH"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can be inlined. We don't need a constant for this.

func NewWorkspaceFilesClient(w *databricks.WorkspaceClient, root string) (Filer, error) {
// Workspace.Upload/Download forward cfg.WorkspaceID behind a bare != "" check,
// so normalize the "none" sentinel to "" to keep the SDK from routing on it.
if w.Config.WorkspaceID == auth.WorkspaceIDNone {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just curious, does not normalizing it cause the SDK to error? Maybe we should flag this with the SDK folks?

// is still needed, both verified against a live workspace on
// 2026-08-04:
//
// - Rollout lag: universe #2019174 merged 2026-06-03 with its

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need this? 2026-06-03 was more than 2 months ago. This should be rolled out by now.

// build still return WCS-worded collisions without details.
// That half is temporary.
//
// - Message (b) above is thrown by webapp, which #2019174 never

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we fix this in the backend? Kyle can help with this (we can send a PR or ask him to fix this). Checking the error message is always fragile and break easily if the backend changes the error message.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Honestly fixing this can be optional - if we can confirm that no CLI clients rely on exactly this error. We should try getting the error fixed though in the backend if it's an easy fix.

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.

3 participants