diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 765eef12b6..6cdf837395 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -10,6 +10,7 @@ ### Bug Fixes +* Correctly handle the change `/Workspace/Path` -> `/Path` and back in workspace resources ([#5130](https://github.com/databricks/terraform-provider-databricks/pull/5130)) * Remove unnecessary `SetSuppressDiff()` for `workload_size` in `databricks_model_serving` ([#5152](https://github.com/databricks/terraform-provider-databricks/pull/5152)). ### Documentation diff --git a/sql/resource_alert.go b/sql/resource_alert.go index c7d71e8c61..64cc8811da 100644 --- a/sql/resource_alert.go +++ b/sql/resource_alert.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/databricks-sdk-go/service/sql" "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/workspace" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -87,10 +88,7 @@ func ResourceAlert() common.Resource { log.Printf("[WARN] error getting alert by ID: %v", err) return err } - parentPath := d.Get("parent_path").(string) - if parentPath != "" && strings.HasPrefix(apiAlert.ParentPath, "/Workspace") && !strings.HasPrefix(parentPath, "/Workspace") { - apiAlert.ParentPath = strings.TrimPrefix(parentPath, "/Workspace") - } + apiAlert.ParentPath = workspace.NormalizeWorkspacePath(d.Get("parent_path").(string), apiAlert.ParentPath) return common.StructToData(apiAlert, s, d) }, Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { diff --git a/sql/resource_query.go b/sql/resource_query.go index 4db7820d21..37793f4572 100644 --- a/sql/resource_query.go +++ b/sql/resource_query.go @@ -3,10 +3,10 @@ package sql import ( "context" "log" - "strings" "github.com/databricks/databricks-sdk-go/service/sql" "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/workspace" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -130,10 +130,7 @@ func ResourceQuery() common.Resource { log.Printf("[WARN] error getting query by ID: %v", err) return err } - parentPath := d.Get("parent_path").(string) - if parentPath != "" && strings.HasPrefix(apiQuery.ParentPath, "/Workspace") && !strings.HasPrefix(parentPath, "/Workspace") { - apiQuery.ParentPath = strings.TrimPrefix(parentPath, "/Workspace") - } + apiQuery.ParentPath = workspace.NormalizeWorkspacePath(d.Get("parent_path").(string), apiQuery.ParentPath) return common.StructToData(QueryStruct{Query: *apiQuery}, s, d) }, Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { diff --git a/workspace/file_resource.go b/workspace/file_resource.go index 93f51794ce..413e79d90a 100644 --- a/workspace/file_resource.go +++ b/workspace/file_resource.go @@ -194,3 +194,30 @@ func PathListHash(v any) int { } return c } + +// NormalizeWorkspacePath normalizes the path returned from the API to match the configured path. +// The Databricks API may add or remove the "/Workspace" prefix depending on the workspace configuration. +// This function ensures that the returned path matches the format used in the configuration to avoid +// Terraform detecting false changes. +// +// Examples: +// - If configured path is "/Users/..." and API returns "/Workspace/Users/...", it strips "/Workspace" +// - If configured path is "/Workspace/Users/..." and API returns "/Users/...", it adds "/Workspace" +func NormalizeWorkspacePath(configuredPath string, apiPath string) string { + if configuredPath == "" { + return apiPath + } + + // Case 1: API added /Workspace prefix, but config doesn't have it + if strings.HasPrefix(apiPath, "/Workspace") && !strings.HasPrefix(configuredPath, "/Workspace") { + return strings.TrimPrefix(apiPath, "/Workspace") + } + + // Case 2: Config has /Workspace prefix, but API doesn't have it + if !strings.HasPrefix(apiPath, "/Workspace") && strings.HasPrefix(configuredPath, "/Workspace") { + return "/Workspace" + apiPath + } + + // No normalization needed + return apiPath +} diff --git a/workspace/file_resource_test.go b/workspace/file_resource_test.go index cf7378e8d1..ea0b70cdba 100644 --- a/workspace/file_resource_test.go +++ b/workspace/file_resource_test.go @@ -59,3 +59,68 @@ func TestFileContentSchemaClean(t *testing.T) { assert.True(t, d.HasError()) assert.Equal(t, "Clean path required", d[0].Summary) } + +func TestNormalizeWorkspacePath(t *testing.T) { + testCases := []struct { + name string + configuredPath string + apiPath string + expected string + }{ + { + name: "API adds /Workspace prefix - should strip it", + configuredPath: "/Users/user@example.com/notebook.py", + apiPath: "/Workspace/Users/user@example.com/notebook.py", + expected: "/Users/user@example.com/notebook.py", + }, + { + name: "Config has /Workspace prefix but API doesn't - should add it", + configuredPath: "/Workspace/Users/user@example.com/notebook.py", + apiPath: "/Users/user@example.com/notebook.py", + expected: "/Workspace/Users/user@example.com/notebook.py", + }, + { + name: "Both have /Workspace prefix - no change", + configuredPath: "/Workspace/Users/user@example.com/notebook.py", + apiPath: "/Workspace/Users/user@example.com/notebook.py", + expected: "/Workspace/Users/user@example.com/notebook.py", + }, + { + name: "Neither has /Workspace prefix - no change", + configuredPath: "/Users/user@example.com/notebook.py", + apiPath: "/Users/user@example.com/notebook.py", + expected: "/Users/user@example.com/notebook.py", + }, + { + name: "Empty configured path - return API path as-is", + configuredPath: "", + apiPath: "/Workspace/Users/user@example.com/notebook.py", + expected: "/Workspace/Users/user@example.com/notebook.py", + }, + { + name: "Directory path without /Workspace in config, with /Workspace in API", + configuredPath: "/Shared/test", + apiPath: "/Workspace/Shared/test", + expected: "/Shared/test", + }, + { + name: "Directory path with /Workspace in config, without /Workspace in API", + configuredPath: "/Workspace/Shared/test", + apiPath: "/Shared/test", + expected: "/Workspace/Shared/test", + }, + { + name: "Service principal path - API adds /Workspace", + configuredPath: "/Users/0b66cdac-04f8-408e-9290-13c058a2ebe1/file.py", + apiPath: "/Workspace/Users/0b66cdac-04f8-408e-9290-13c058a2ebe1/file.py", + expected: "/Users/0b66cdac-04f8-408e-9290-13c058a2ebe1/file.py", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := NormalizeWorkspacePath(tc.configuredPath, tc.apiPath) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/workspace/resource_directory.go b/workspace/resource_directory.go index 2ed4bcc4df..42aca36b54 100644 --- a/workspace/resource_directory.go +++ b/workspace/resource_directory.go @@ -56,6 +56,7 @@ func ResourceDirectory() common.Resource { d.SetId("") return fmt.Errorf("different object type, %s, on this path other than a directory", objectStatus.ObjectType) } + objectStatus.Path = NormalizeWorkspacePath(d.Get("path").(string), objectStatus.Path) err = common.StructToData(objectStatus, s, d) if err != nil { return err diff --git a/workspace/resource_notebook.go b/workspace/resource_notebook.go index 1bff5895ff..8e03ffd954 100644 --- a/workspace/resource_notebook.go +++ b/workspace/resource_notebook.go @@ -351,6 +351,7 @@ func ResourceNotebook() common.Resource { if err != nil { return err } + objectStatus.Path = NormalizeWorkspacePath(d.Get("path").(string), objectStatus.Path) SetWorkspaceObjectComputedProperties(d, c) err = common.StructToData(objectStatus, s, d) if err != nil { diff --git a/workspace/resource_workspace_file.go b/workspace/resource_workspace_file.go index 8a72107e1d..7fb58b2f9c 100644 --- a/workspace/resource_workspace_file.go +++ b/workspace/resource_workspace_file.go @@ -78,6 +78,7 @@ func ResourceWorkspaceFile() common.Resource { if err != nil { return err } + objectStatus.Path = NormalizeWorkspacePath(d.Get("path").(string), objectStatus.Path) SetWorkspaceObjectComputedProperties(d, c) return common.StructToData(objectStatus, s, d) }, diff --git a/workspace/resource_workspace_file_test.go b/workspace/resource_workspace_file_test.go index 180fb448bc..a8c97cd79f 100644 --- a/workspace/resource_workspace_file_test.go +++ b/workspace/resource_workspace_file_test.go @@ -309,3 +309,56 @@ func TestResourceWorkspaceFileUpdate(t *testing.T) { Update: true, }.ApplyNoError(t) } + +func TestResourceWorkspaceFileRead_WorkspacePrefixNormalization(t *testing.T) { + objectID := int64(12345) + // Test case 1: Config without /Workspace prefix, API returns with prefix + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockWorkspaceAPI().EXPECT(). + GetStatusByPath(mock.Anything, "/Users/user@example.com/file.py"). + Return(&ws_api.ObjectInfo{ + ObjectId: objectID, + ObjectType: ws_api.ObjectTypeFile, + Path: "/Workspace/Users/user@example.com/file.py", + }, nil) + }, + Resource: ResourceWorkspaceFile(), + Read: true, + New: true, + ID: "/Users/user@example.com/file.py", + State: map[string]any{ + "path": "/Users/user@example.com/file.py", + }, + }.ApplyAndExpectData(t, map[string]any{ + "id": "/Users/user@example.com/file.py", + "path": "/Users/user@example.com/file.py", // Should match configured path + "workspace_path": "/Workspace/Users/user@example.com/file.py", + "object_id": int(objectID), + }) + + // Test case 2: Config with /Workspace prefix, API returns without prefix + qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockWorkspaceAPI().EXPECT(). + GetStatusByPath(mock.Anything, "/Workspace/Users/user@example.com/file.py"). + Return(&ws_api.ObjectInfo{ + ObjectId: objectID, + ObjectType: ws_api.ObjectTypeFile, + Path: "/Users/user@example.com/file.py", + }, nil) + }, + Resource: ResourceWorkspaceFile(), + Read: true, + New: true, + ID: "/Workspace/Users/user@example.com/file.py", + State: map[string]any{ + "path": "/Workspace/Users/user@example.com/file.py", + }, + }.ApplyAndExpectData(t, map[string]any{ + "id": "/Workspace/Users/user@example.com/file.py", + "path": "/Workspace/Users/user@example.com/file.py", // Should match configured path + "workspace_path": "/Workspace/Workspace/Users/user@example.com/file.py", + "object_id": int(objectID), + }) +}