From b35f287ce8b724afc966ca695fc13a67e893da4a Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 16:32:08 +0000 Subject: [PATCH 01/15] Fix wrong grants field for registered_model resource The grants converter for registered_model incorrectly set grants.Function instead of grants.Model, causing Terraform grants to target a function instead of the registered model. Task: 001.md Co-authored-by: Isaac --- bundle/deploy/terraform/tfdyn/convert_registered_model.go | 2 +- bundle/deploy/terraform/tfdyn/convert_registered_model_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/deploy/terraform/tfdyn/convert_registered_model.go b/bundle/deploy/terraform/tfdyn/convert_registered_model.go index 49e05b47e5..dc2be0ab30 100644 --- a/bundle/deploy/terraform/tfdyn/convert_registered_model.go +++ b/bundle/deploy/terraform/tfdyn/convert_registered_model.go @@ -38,7 +38,7 @@ func (registeredModelConverter) Convert(ctx context.Context, key string, vin dyn // Configure grants for this resource. if grants := convertGrantsResource(ctx, vin); grants != nil { - grants.Function = fmt.Sprintf("${databricks_registered_model.%s.id}", key) + grants.Model = fmt.Sprintf("${databricks_registered_model.%s.id}", key) out.Grants["registered_model_"+key] = grants } diff --git a/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go b/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go index dab20f45eb..dd390e51d9 100644 --- a/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go @@ -46,7 +46,7 @@ func TestConvertRegisteredModel(t *testing.T) { // Assert equality on the grants assert.Equal(t, &schema.ResourceGrants{ - Function: "${databricks_registered_model.my_registered_model.id}", + Model: "${databricks_registered_model.my_registered_model.id}", Grant: []schema.ResourceGrantsGrant{ { Privileges: []string{"EXECUTE"}, From f5a88689697fbd92a6bb422c1612ff52a5182da7 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 16:37:16 +0000 Subject: [PATCH 02/15] Fix registered_model grants in direct engine and acceptance tests The direct engine's grantResourceToSecurableType map also mapped registered_models to "function" instead of "model". Update the acceptance test script and outputs to use the correct securable type. Task: 001.md Co-authored-by: Isaac --- .../resources/grants/registered_models/out.plan1.direct.json | 2 +- acceptance/bundle/resources/grants/registered_models/output.txt | 2 +- acceptance/bundle/resources/grants/registered_models/script | 2 +- bundle/direct/dresources/grants.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json b/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json index 482faa449f..34230ba950 100644 --- a/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json +++ b/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json @@ -29,7 +29,7 @@ "action": "create", "new_state": { "value": { - "securable_type": "function", + "securable_type": "model", "full_name": "", "__embed__": [ { diff --git a/acceptance/bundle/resources/grants/registered_models/output.txt b/acceptance/bundle/resources/grants/registered_models/output.txt index cac10d7691..74b0a8b6a8 100644 --- a/acceptance/bundle/resources/grants/registered_models/output.txt +++ b/acceptance/bundle/resources/grants/registered_models/output.txt @@ -11,7 +11,7 @@ Deployment complete! >>> print_requests.py //permissions ->>> [CLI] grants get function main.myschema_[UNIQUE_NAME].mymodel +>>> [CLI] grants get model main.myschema_[UNIQUE_NAME].mymodel { "privilege_assignments": [ { diff --git a/acceptance/bundle/resources/grants/registered_models/script b/acceptance/bundle/resources/grants/registered_models/script index 98d73773af..a5f1a5f45d 100644 --- a/acceptance/bundle/resources/grants/registered_models/script +++ b/acceptance/bundle/resources/grants/registered_models/script @@ -6,7 +6,7 @@ trace $CLI bundle deploy trace print_requests.py //permissions > out.deploy1.requests.$DATABRICKS_BUNDLE_ENGINE.json # Since we're sending different requests between terraform and direct, assert that the end result is the same: -trace $CLI grants get function main.myschema_$UNIQUE_NAME.mymodel | jq --sort-keys +trace $CLI grants get model main.myschema_$UNIQUE_NAME.mymodel | jq --sort-keys trace $CLI bundle destroy --auto-approve trace print_requests.py //permissions > out.destroy.requests.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index 8bb1906122..652012134a 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -17,7 +17,7 @@ var grantResourceToSecurableType = map[string]string{ "schemas": "schema", "external_locations": "external_location", "volumes": "volume", - "registered_models": "function", + "registered_models": "model", } type GrantsState struct { From b8d0a7ea4d30bf64b423f4d3d7ef43fad14985ea Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 16:38:14 +0000 Subject: [PATCH 03/15] Update migration test expected outputs for registered_model grants fix After fixing the grants field from Function to Model, Terraform will store "model" instead of "function" in state, and the migrated direct state will also use "model" as the securable type. Note: these outputs need to be regenerated on cloud to confirm. Task: 001.md Co-authored-by: Isaac --- acceptance/bundle/migrate/grants/out.new_state.json | 4 ++-- acceptance/bundle/migrate/grants/out.original_state.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acceptance/bundle/migrate/grants/out.new_state.json b/acceptance/bundle/migrate/grants/out.new_state.json index 7a24ba0f3c..df35ab2cee 100644 --- a/acceptance/bundle/migrate/grants/out.new_state.json +++ b/acceptance/bundle/migrate/grants/out.new_state.json @@ -20,9 +20,9 @@ ] }, "resources.registered_models.my_registered_model.grants": { - "__id__": "function/main.schema_grants.mymodel", + "__id__": "model/main.schema_grants.mymodel", "state": { - "securable_type": "function", + "securable_type": "model", "full_name": "main.schema_grants.mymodel", "__embed__": [ { diff --git a/acceptance/bundle/migrate/grants/out.original_state.json b/acceptance/bundle/migrate/grants/out.original_state.json index a410fff585..2bfcb19345 100644 --- a/acceptance/bundle/migrate/grants/out.original_state.json +++ b/acceptance/bundle/migrate/grants/out.original_state.json @@ -18,7 +18,7 @@ "credential": null, "external_location": null, "foreign_connection": null, - "function": "main.schema_grants.mymodel", + "function": null, "grant": [ { "principal": "deco-test-user@databricks.com", @@ -27,9 +27,9 @@ ] } ], - "id": "function/main.schema_grants.mymodel", + "id": "model/main.schema_grants.mymodel", "metastore": null, - "model": null, + "model": "main.schema_grants.mymodel", "pipeline": null, "provider_config": [], "recipient": null, From 49c59b8ef74e3f2587657227ba601a45c67174b4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 16:46:47 +0000 Subject: [PATCH 04/15] Fix stale test assertion for registered_model grants in convert_test.go Change assert.NotEmpty(t, resource.Function) to assert.NotEmpty(t, resource.Model) in TestBundleToTerraformRegisteredModelGrants to match the actual grants field being set for registered model resources. Task: 002.md Co-authored-by: Isaac --- bundle/deploy/terraform/convert_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 564340190d..207e3c13db 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -563,7 +563,7 @@ func TestBundleToTerraformRegisteredModelGrants(t *testing.T) { out := produceTerraformConfiguration(t, config) convertToResourceStruct(t, &resource, out.Resource.Grants["registered_model_my_registered_model"]) - assert.NotEmpty(t, resource.Function) + assert.NotEmpty(t, resource.Model) assert.Len(t, resource.Grant, 1) assert.Equal(t, "jane@doe.com", resource.Grant[0].Principal) assert.Equal(t, "EXECUTE", resource.Grant[0].Privileges[0]) From c693ac224b80bfaefe0882fa3b3636af7158bc35 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 17:22:08 +0000 Subject: [PATCH 05/15] Revert registered_model grants changes: function is the correct securable type Investigation during task 003 revealed that the Unity Catalog API uses FUNCTION as the securable type for registered model grants. The SDK's SecurableType enum has no MODEL value. The Terraform provider maps the model attribute to FUNCTION internally, producing different state IDs (model/... vs function/...) that break migration and direct engine API calls. Reverting all changes from tasks 001, 002 as the original code was correct. Task: 003.md Co-authored-by: Isaac --- acceptance/bundle/migrate/grants/out.new_state.json | 4 ++-- acceptance/bundle/migrate/grants/out.original_state.json | 6 +++--- .../grants/registered_models/out.plan1.direct.json | 2 +- .../bundle/resources/grants/registered_models/output.txt | 2 +- acceptance/bundle/resources/grants/registered_models/script | 2 +- bundle/deploy/terraform/convert_test.go | 2 +- bundle/deploy/terraform/tfdyn/convert_registered_model.go | 2 +- .../deploy/terraform/tfdyn/convert_registered_model_test.go | 2 +- bundle/direct/dresources/grants.go | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/acceptance/bundle/migrate/grants/out.new_state.json b/acceptance/bundle/migrate/grants/out.new_state.json index df35ab2cee..7a24ba0f3c 100644 --- a/acceptance/bundle/migrate/grants/out.new_state.json +++ b/acceptance/bundle/migrate/grants/out.new_state.json @@ -20,9 +20,9 @@ ] }, "resources.registered_models.my_registered_model.grants": { - "__id__": "model/main.schema_grants.mymodel", + "__id__": "function/main.schema_grants.mymodel", "state": { - "securable_type": "model", + "securable_type": "function", "full_name": "main.schema_grants.mymodel", "__embed__": [ { diff --git a/acceptance/bundle/migrate/grants/out.original_state.json b/acceptance/bundle/migrate/grants/out.original_state.json index 2bfcb19345..a410fff585 100644 --- a/acceptance/bundle/migrate/grants/out.original_state.json +++ b/acceptance/bundle/migrate/grants/out.original_state.json @@ -18,7 +18,7 @@ "credential": null, "external_location": null, "foreign_connection": null, - "function": null, + "function": "main.schema_grants.mymodel", "grant": [ { "principal": "deco-test-user@databricks.com", @@ -27,9 +27,9 @@ ] } ], - "id": "model/main.schema_grants.mymodel", + "id": "function/main.schema_grants.mymodel", "metastore": null, - "model": "main.schema_grants.mymodel", + "model": null, "pipeline": null, "provider_config": [], "recipient": null, diff --git a/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json b/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json index 34230ba950..482faa449f 100644 --- a/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json +++ b/acceptance/bundle/resources/grants/registered_models/out.plan1.direct.json @@ -29,7 +29,7 @@ "action": "create", "new_state": { "value": { - "securable_type": "model", + "securable_type": "function", "full_name": "", "__embed__": [ { diff --git a/acceptance/bundle/resources/grants/registered_models/output.txt b/acceptance/bundle/resources/grants/registered_models/output.txt index 74b0a8b6a8..cac10d7691 100644 --- a/acceptance/bundle/resources/grants/registered_models/output.txt +++ b/acceptance/bundle/resources/grants/registered_models/output.txt @@ -11,7 +11,7 @@ Deployment complete! >>> print_requests.py //permissions ->>> [CLI] grants get model main.myschema_[UNIQUE_NAME].mymodel +>>> [CLI] grants get function main.myschema_[UNIQUE_NAME].mymodel { "privilege_assignments": [ { diff --git a/acceptance/bundle/resources/grants/registered_models/script b/acceptance/bundle/resources/grants/registered_models/script index a5f1a5f45d..98d73773af 100644 --- a/acceptance/bundle/resources/grants/registered_models/script +++ b/acceptance/bundle/resources/grants/registered_models/script @@ -6,7 +6,7 @@ trace $CLI bundle deploy trace print_requests.py //permissions > out.deploy1.requests.$DATABRICKS_BUNDLE_ENGINE.json # Since we're sending different requests between terraform and direct, assert that the end result is the same: -trace $CLI grants get model main.myschema_$UNIQUE_NAME.mymodel | jq --sort-keys +trace $CLI grants get function main.myschema_$UNIQUE_NAME.mymodel | jq --sort-keys trace $CLI bundle destroy --auto-approve trace print_requests.py //permissions > out.destroy.requests.$DATABRICKS_BUNDLE_ENGINE.json diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 207e3c13db..564340190d 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -563,7 +563,7 @@ func TestBundleToTerraformRegisteredModelGrants(t *testing.T) { out := produceTerraformConfiguration(t, config) convertToResourceStruct(t, &resource, out.Resource.Grants["registered_model_my_registered_model"]) - assert.NotEmpty(t, resource.Model) + assert.NotEmpty(t, resource.Function) assert.Len(t, resource.Grant, 1) assert.Equal(t, "jane@doe.com", resource.Grant[0].Principal) assert.Equal(t, "EXECUTE", resource.Grant[0].Privileges[0]) diff --git a/bundle/deploy/terraform/tfdyn/convert_registered_model.go b/bundle/deploy/terraform/tfdyn/convert_registered_model.go index dc2be0ab30..49e05b47e5 100644 --- a/bundle/deploy/terraform/tfdyn/convert_registered_model.go +++ b/bundle/deploy/terraform/tfdyn/convert_registered_model.go @@ -38,7 +38,7 @@ func (registeredModelConverter) Convert(ctx context.Context, key string, vin dyn // Configure grants for this resource. if grants := convertGrantsResource(ctx, vin); grants != nil { - grants.Model = fmt.Sprintf("${databricks_registered_model.%s.id}", key) + grants.Function = fmt.Sprintf("${databricks_registered_model.%s.id}", key) out.Grants["registered_model_"+key] = grants } diff --git a/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go b/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go index dd390e51d9..dab20f45eb 100644 --- a/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_registered_model_test.go @@ -46,7 +46,7 @@ func TestConvertRegisteredModel(t *testing.T) { // Assert equality on the grants assert.Equal(t, &schema.ResourceGrants{ - Model: "${databricks_registered_model.my_registered_model.id}", + Function: "${databricks_registered_model.my_registered_model.id}", Grant: []schema.ResourceGrantsGrant{ { Privileges: []string{"EXECUTE"}, diff --git a/bundle/direct/dresources/grants.go b/bundle/direct/dresources/grants.go index 652012134a..8bb1906122 100644 --- a/bundle/direct/dresources/grants.go +++ b/bundle/direct/dresources/grants.go @@ -17,7 +17,7 @@ var grantResourceToSecurableType = map[string]string{ "schemas": "schema", "external_locations": "external_location", "volumes": "volume", - "registered_models": "model", + "registered_models": "function", } type GrantsState struct { From 83b50d65324a7866e36ae21c9a47714d1e83e64c Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 17:29:46 +0000 Subject: [PATCH 06/15] Fix WorkspaceFilesClient.Stat() silently swallowing non-404 API errors Stat() checked for 404 status to return a file-not-found error, but fell through to return (zero-value, nil) for any other API error (403, 500, etc.). This caused callers to proceed with bogus file metadata instead of seeing the real error. Task: 001.md Co-authored-by: Isaac --- libs/filer/workspace_files_client.go | 2 + libs/filer/workspace_files_client_test.go | 75 +++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index a306843538..9862dc6567 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -372,6 +372,8 @@ func (w *WorkspaceFilesClient) Stat(ctx context.Context, name string) (fs.FileIn if aerr.StatusCode == http.StatusNotFound { return nil, fileDoesNotExistError{absPath} } + + return nil, err } return stat, nil diff --git a/libs/filer/workspace_files_client_test.go b/libs/filer/workspace_files_client_test.go index 2603d31d6d..bf4ad80dba 100644 --- a/libs/filer/workspace_files_client_test.go +++ b/libs/filer/workspace_files_client_test.go @@ -6,7 +6,9 @@ import ( "testing" "time" + "github.com/databricks/cli/libs/testserver" "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/config" "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/stretchr/testify/assert" @@ -131,3 +133,76 @@ func TestWorkspaceFilesClient_wsfsUnmarshal(t *testing.T) { assert.False(t, info.IsDir()) assert.NotNil(t, info.Sys()) } + +func TestWorkspaceFilesClientStatReturnsAPIErrors(t *testing.T) { + tests := []struct { + name string + statusCode int + errorCode string + }{ + {"forbidden", 403, "PERMISSION_DENIED"}, + {"internal_error", 500, "INTERNAL_ERROR"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := testserver.New(t) + + server.Handle("GET", "/api/2.0/workspace/get-status", func(req testserver.Request) any { + return testserver.Response{ + StatusCode: tt.statusCode, + Body: map[string]string{ + "error_code": tt.errorCode, + "message": "test error", + }, + } + }) + + testserver.AddDefaultHandlers(server) + + client, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + }) + require.NoError(t, err) + + f, err := NewWorkspaceFilesClient(client, "/test") + require.NoError(t, err) + + _, err = f.Stat(t.Context(), "file") + require.Error(t, err) + + var apiErr *apierr.APIError + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, tt.statusCode, apiErr.StatusCode) + }) + } +} + +func TestWorkspaceFilesClientStatReturnsNotFoundAsFileDoesNotExist(t *testing.T) { + server := testserver.New(t) + + server.Handle("GET", "/api/2.0/workspace/get-status", func(req testserver.Request) any { + return testserver.Response{ + StatusCode: 404, + Body: map[string]string{ + "error_code": "RESOURCE_DOES_NOT_EXIST", + "message": "not found", + }, + } + }) + + testserver.AddDefaultHandlers(server) + + client, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + }) + require.NoError(t, err) + + f, err := NewWorkspaceFilesClient(client, "/test") + require.NoError(t, err) + + _, err = f.Stat(t.Context(), "file") + assert.ErrorIs(t, err, fs.ErrNotExist) +} From 8c53c1f0a17118642b72d05b0416c8a031c487e8 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 19:13:56 +0000 Subject: [PATCH 07/15] Add changelog entry for WorkspaceFilesClient.Stat() fix Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0db4b902e1..882d2b8f72 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### CLI ### Bundles +* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Dependency updates From 9202e2450bfb64b5296980fefc5443a14446b053 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 20:37:04 +0000 Subject: [PATCH 08/15] Add acceptance test for WorkspaceFilesClient.Stat() error propagation Verify that non-404 API errors (e.g. 403 PERMISSION_DENIED) from workspace/get-status are surfaced to the user rather than silently swallowed. Before the fix in commit 58849af6b, Stat() returned (nil, nil) on non-404 API errors, causing a nil-pointer panic in the caller. Co-authored-by: Isaac --- .../export-dir-api-error-propagation/out.test.toml | 5 +++++ .../export-dir-api-error-propagation/output.txt | 14 ++++++++++++++ .../export-dir-api-error-propagation/script | 9 +++++++++ .../export-dir-api-error-propagation/test.toml | 12 ++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml create mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt create mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/script create mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml b/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt b/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt new file mode 100644 index 0000000000..ab3047c380 --- /dev/null +++ b/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt @@ -0,0 +1,14 @@ + +>>> errcode [CLI] workspace export-dir /test-dir [TEST_TMP_DIR]/export +Exporting files from /test-dir +Error: User does not have READ permission on /test-dir. + +Host: [DATABRICKS_URL] +Auth type: Personal Access Token (pat) + +Next steps: + - Verify you have the required permissions for this operation + - Check your identity: databricks auth describe + - Consider setting up a profile: databricks auth login --profile + +Exit code: 1 diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/script b/acceptance/cmd/workspace/export-dir-api-error-propagation/script new file mode 100644 index 0000000000..ae4e14300c --- /dev/null +++ b/acceptance/cmd/workspace/export-dir-api-error-propagation/script @@ -0,0 +1,9 @@ +# This tests that non-404 API errors from workspace/get-status are properly +# propagated to the user. Before the fix in WorkspaceFilesClient.Stat() +# (commit 58849af6b), Stat() silently swallowed non-404 API errors, returning +# (nil, nil). This caused a nil pointer panic in filerFS.Open() when it tried +# to call stat.IsDir() on the nil result. After the fix, the 403 error is +# properly returned and the user sees a clear error message. + +mkdir -p "$TEST_TMP_DIR/export" +trace errcode $CLI workspace export-dir /test-dir "$TEST_TMP_DIR/export" diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml b/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml new file mode 100644 index 0000000000..e5031814a4 --- /dev/null +++ b/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml @@ -0,0 +1,12 @@ +Local = true +Cloud = false + +[Env] +MSYS_NO_PATHCONV = "1" + +# Return 403 PERMISSION_DENIED for workspace/get-status. +# This is the first API call made by export-dir (via Stat() in filerFS.Open). +[[Server]] +Pattern = "GET /api/2.0/workspace/get-status" +Response.StatusCode = 403 +Response.Body = '{"error_code": "PERMISSION_DENIED", "message": "User does not have READ permission on /test-dir."}' From 7b7602e4557f315a90094d8100ef39b5d1a2a806 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Wed, 15 Apr 2026 20:42:58 +0000 Subject: [PATCH 09/15] Fix acceptance test comment: document actual pre-fix behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous comment incorrectly stated that the bug caused a nil pointer panic. In reality, since wsfsFileInfo is a value type (not a pointer), Stat() returned a zero-value struct with nil error. This caused export-dir to silently report success without actually exporting anything — worse than a panic because the user doesn't realize the operation failed. Task: 004.md Co-authored-by: Isaac --- .../export-dir-api-error-propagation/script | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/script b/acceptance/cmd/workspace/export-dir-api-error-propagation/script index ae4e14300c..6cdc8bfe56 100644 --- a/acceptance/cmd/workspace/export-dir-api-error-propagation/script +++ b/acceptance/cmd/workspace/export-dir-api-error-propagation/script @@ -1,9 +1,16 @@ # This tests that non-404 API errors from workspace/get-status are properly -# propagated to the user. Before the fix in WorkspaceFilesClient.Stat() -# (commit 58849af6b), Stat() silently swallowed non-404 API errors, returning -# (nil, nil). This caused a nil pointer panic in filerFS.Open() when it tried -# to call stat.IsDir() on the nil result. After the fix, the 403 error is -# properly returned and the user sees a clear error message. +# propagated to the user. Before the fix in WorkspaceFilesClient.Stat(), +# Stat() silently swallowed non-404 API errors, returning a zero-value +# wsfsFileInfo with nil error. Because wsfsFileInfo is a value type (not a +# pointer), filerFS.Open() didn't panic but treated the directory as a regular +# file (zero-value ObjectType makes IsDir() return false). The result was that +# export-dir silently reported success without exporting anything: +# +# Exporting files from /test-dir +# . -> /export (skipped; already exists) +# Export complete +# +# After the fix, the 403 error is properly returned to the user. mkdir -p "$TEST_TMP_DIR/export" trace errcode $CLI workspace export-dir /test-dir "$TEST_TMP_DIR/export" From 076ed08b13a5e34f6b68ddd4db6b79fce3a3d1de Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:37:46 +0000 Subject: [PATCH 10/15] Simplify Stat() error handling tests: remove changelog, acceptance test, consolidate unit test - Remove changelog entry (per task request) - Remove acceptance test for export-dir-api-error-propagation - Merge two test functions into a single table-driven test with 3 entries Task: 005.md Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 - .../out.test.toml | 5 -- .../output.txt | 14 ----- .../export-dir-api-error-propagation/script | 16 ------ .../test.toml | 12 ----- libs/filer/workspace_files_client_test.go | 51 ++++++------------- 6 files changed, 16 insertions(+), 83 deletions(-) delete mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml delete mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt delete mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/script delete mode 100644 acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 882d2b8f72..0db4b902e1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,7 +7,6 @@ ### CLI ### Bundles -* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Dependency updates diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml b/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml deleted file mode 100644 index d560f1de04..0000000000 --- a/acceptance/cmd/workspace/export-dir-api-error-propagation/out.test.toml +++ /dev/null @@ -1,5 +0,0 @@ -Local = true -Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt b/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt deleted file mode 100644 index ab3047c380..0000000000 --- a/acceptance/cmd/workspace/export-dir-api-error-propagation/output.txt +++ /dev/null @@ -1,14 +0,0 @@ - ->>> errcode [CLI] workspace export-dir /test-dir [TEST_TMP_DIR]/export -Exporting files from /test-dir -Error: User does not have READ permission on /test-dir. - -Host: [DATABRICKS_URL] -Auth type: Personal Access Token (pat) - -Next steps: - - Verify you have the required permissions for this operation - - Check your identity: databricks auth describe - - Consider setting up a profile: databricks auth login --profile - -Exit code: 1 diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/script b/acceptance/cmd/workspace/export-dir-api-error-propagation/script deleted file mode 100644 index 6cdc8bfe56..0000000000 --- a/acceptance/cmd/workspace/export-dir-api-error-propagation/script +++ /dev/null @@ -1,16 +0,0 @@ -# This tests that non-404 API errors from workspace/get-status are properly -# propagated to the user. Before the fix in WorkspaceFilesClient.Stat(), -# Stat() silently swallowed non-404 API errors, returning a zero-value -# wsfsFileInfo with nil error. Because wsfsFileInfo is a value type (not a -# pointer), filerFS.Open() didn't panic but treated the directory as a regular -# file (zero-value ObjectType makes IsDir() return false). The result was that -# export-dir silently reported success without exporting anything: -# -# Exporting files from /test-dir -# . -> /export (skipped; already exists) -# Export complete -# -# After the fix, the 403 error is properly returned to the user. - -mkdir -p "$TEST_TMP_DIR/export" -trace errcode $CLI workspace export-dir /test-dir "$TEST_TMP_DIR/export" diff --git a/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml b/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml deleted file mode 100644 index e5031814a4..0000000000 --- a/acceptance/cmd/workspace/export-dir-api-error-propagation/test.toml +++ /dev/null @@ -1,12 +0,0 @@ -Local = true -Cloud = false - -[Env] -MSYS_NO_PATHCONV = "1" - -# Return 403 PERMISSION_DENIED for workspace/get-status. -# This is the first API call made by export-dir (via Stat() in filerFS.Open). -[[Server]] -Pattern = "GET /api/2.0/workspace/get-status" -Response.StatusCode = 403 -Response.Body = '{"error_code": "PERMISSION_DENIED", "message": "User does not have READ permission on /test-dir."}' diff --git a/libs/filer/workspace_files_client_test.go b/libs/filer/workspace_files_client_test.go index bf4ad80dba..8c984a57ea 100644 --- a/libs/filer/workspace_files_client_test.go +++ b/libs/filer/workspace_files_client_test.go @@ -134,14 +134,26 @@ func TestWorkspaceFilesClient_wsfsUnmarshal(t *testing.T) { assert.NotNil(t, info.Sys()) } -func TestWorkspaceFilesClientStatReturnsAPIErrors(t *testing.T) { +func TestWorkspaceFilesClientStatErrorHandling(t *testing.T) { tests := []struct { name string statusCode int errorCode string + assertErr func(t *testing.T, err error) }{ - {"forbidden", 403, "PERMISSION_DENIED"}, - {"internal_error", 500, "INTERNAL_ERROR"}, + {"forbidden", 403, "PERMISSION_DENIED", func(t *testing.T, err error) { + var apiErr *apierr.APIError + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, 403, apiErr.StatusCode) + }}, + {"internal_error", 500, "INTERNAL_ERROR", func(t *testing.T, err error) { + var apiErr *apierr.APIError + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, 500, apiErr.StatusCode) + }}, + {"not_found", 404, "RESOURCE_DOES_NOT_EXIST", func(t *testing.T, err error) { + assert.ErrorIs(t, err, fs.ErrNotExist) + }}, } for _, tt := range tests { @@ -171,38 +183,7 @@ func TestWorkspaceFilesClientStatReturnsAPIErrors(t *testing.T) { _, err = f.Stat(t.Context(), "file") require.Error(t, err) - - var apiErr *apierr.APIError - require.ErrorAs(t, err, &apiErr) - assert.Equal(t, tt.statusCode, apiErr.StatusCode) + tt.assertErr(t, err) }) } } - -func TestWorkspaceFilesClientStatReturnsNotFoundAsFileDoesNotExist(t *testing.T) { - server := testserver.New(t) - - server.Handle("GET", "/api/2.0/workspace/get-status", func(req testserver.Request) any { - return testserver.Response{ - StatusCode: 404, - Body: map[string]string{ - "error_code": "RESOURCE_DOES_NOT_EXIST", - "message": "not found", - }, - } - }) - - testserver.AddDefaultHandlers(server) - - client, err := databricks.NewWorkspaceClient(&databricks.Config{ - Host: server.URL, - Token: "testtoken", - }) - require.NoError(t, err) - - f, err := NewWorkspaceFilesClient(client, "/test") - require.NoError(t, err) - - _, err = f.Stat(t.Context(), "file") - assert.ErrorIs(t, err, fs.ErrNotExist) -} From 8c347c8645033896f6330902898330d2aa768e99 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:49:24 +0000 Subject: [PATCH 11/15] Add changelog entry for WorkspaceFilesClient.Stat() fix Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0db4b902e1..22c885835c 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### Notable Changes ### CLI +* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Bundles From ef1b7cc5f3accf443a0b090e8b968ffb48173d17 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 13:51:36 +0000 Subject: [PATCH 12/15] Refactor Stat error tests: split table-driven test into 3 separate tests Replace the table-driven TestWorkspaceFilesClientStatErrorHandling with three focused test functions (TestWorkspaceFilesClientStatForbidden, TestWorkspaceFilesClientStatInternalError, TestWorkspaceFilesClientStatNotFound) and a shared statWithError helper. Task: 006.md Co-authored-by: Isaac --- libs/filer/workspace_files_client_test.go | 86 +++++++++++------------ 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/libs/filer/workspace_files_client_test.go b/libs/filer/workspace_files_client_test.go index 8c984a57ea..e7b5a337a7 100644 --- a/libs/filer/workspace_files_client_test.go +++ b/libs/filer/workspace_files_client_test.go @@ -134,56 +134,50 @@ func TestWorkspaceFilesClient_wsfsUnmarshal(t *testing.T) { assert.NotNil(t, info.Sys()) } -func TestWorkspaceFilesClientStatErrorHandling(t *testing.T) { - tests := []struct { - name string - statusCode int - errorCode string - assertErr func(t *testing.T, err error) - }{ - {"forbidden", 403, "PERMISSION_DENIED", func(t *testing.T, err error) { - var apiErr *apierr.APIError - require.ErrorAs(t, err, &apiErr) - assert.Equal(t, 403, apiErr.StatusCode) - }}, - {"internal_error", 500, "INTERNAL_ERROR", func(t *testing.T, err error) { - var apiErr *apierr.APIError - require.ErrorAs(t, err, &apiErr) - assert.Equal(t, 500, apiErr.StatusCode) - }}, - {"not_found", 404, "RESOURCE_DOES_NOT_EXIST", func(t *testing.T, err error) { - assert.ErrorIs(t, err, fs.ErrNotExist) - }}, - } +func statWithError(t *testing.T, statusCode int, errorCode string) error { + t.Helper() + + server := testserver.New(t) + server.Handle("GET", "/api/2.0/workspace/get-status", func(req testserver.Request) any { + return testserver.Response{ + StatusCode: statusCode, + Body: map[string]string{ + "error_code": errorCode, + "message": "test error", + }, + } + }) + testserver.AddDefaultHandlers(server) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := testserver.New(t) + client, err := databricks.NewWorkspaceClient(&databricks.Config{ + Host: server.URL, + Token: "testtoken", + }) + require.NoError(t, err) - server.Handle("GET", "/api/2.0/workspace/get-status", func(req testserver.Request) any { - return testserver.Response{ - StatusCode: tt.statusCode, - Body: map[string]string{ - "error_code": tt.errorCode, - "message": "test error", - }, - } - }) + f, err := NewWorkspaceFilesClient(client, "/test") + require.NoError(t, err) - testserver.AddDefaultHandlers(server) + _, err = f.Stat(t.Context(), "file") + require.Error(t, err) + return err +} - client, err := databricks.NewWorkspaceClient(&databricks.Config{ - Host: server.URL, - Token: "testtoken", - }) - require.NoError(t, err) +func TestWorkspaceFilesClientStatForbidden(t *testing.T) { + err := statWithError(t, 403, "PERMISSION_DENIED") + var apiErr *apierr.APIError + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, 403, apiErr.StatusCode) +} - f, err := NewWorkspaceFilesClient(client, "/test") - require.NoError(t, err) +func TestWorkspaceFilesClientStatInternalError(t *testing.T) { + err := statWithError(t, 500, "INTERNAL_ERROR") + var apiErr *apierr.APIError + require.ErrorAs(t, err, &apiErr) + assert.Equal(t, 500, apiErr.StatusCode) +} - _, err = f.Stat(t.Context(), "file") - require.Error(t, err) - tt.assertErr(t, err) - }) - } +func TestWorkspaceFilesClientStatNotFound(t *testing.T) { + err := statWithError(t, 404, "RESOURCE_DOES_NOT_EXIST") + assert.ErrorIs(t, err, fs.ErrNotExist) } From b3af15916f90e3ad0f4813a47579ff288c0d40c4 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:00:42 +0000 Subject: [PATCH 13/15] Remove changelog entry for WorkspaceFilesClient.Stat() fix Task: 007.md Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 22c885835c..0db4b902e1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,7 +5,6 @@ ### Notable Changes ### CLI -* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Bundles From 3aecd046a191971a6f8997b6e24f9bb2e96cbbcf Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:08:34 +0000 Subject: [PATCH 14/15] Add changelog entry for WorkspaceFilesClient.Stat() fix Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0db4b902e1..22c885835c 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### Notable Changes ### CLI +* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Bundles From 545da3524997e0d57ab6ed484c6a958ccc780c55 Mon Sep 17 00:00:00 2001 From: Denis Bilenko Date: Thu, 16 Apr 2026 14:13:03 +0000 Subject: [PATCH 15/15] Reverted by user via WEB UI --- NEXT_CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 22c885835c..0db4b902e1 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,7 +5,6 @@ ### Notable Changes ### CLI -* Fix `WorkspaceFilesClient.Stat()` silently swallowing non-404 API errors (denik/random-bugfixes-1) ### Bundles