diff --git a/aws/instance_profile_provider_config_test.go b/aws/instance_profile_provider_config_test.go index 258519573b..5b95034e79 100644 --- a/aws/instance_profile_provider_config_test.go +++ b/aws/instance_profile_provider_config_test.go @@ -30,17 +30,6 @@ func TestAccInstanceProfile_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccInstanceProfile_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: instanceProfileProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccInstanceProfile_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: instanceProfileProviderConfigTemplate(` diff --git a/aws/service_principal_role_provider_config_test.go b/aws/service_principal_role_provider_config_test.go index edd3eef2f6..4779d28dc3 100644 --- a/aws/service_principal_role_provider_config_test.go +++ b/aws/service_principal_role_provider_config_test.go @@ -33,17 +33,6 @@ func TestAccServicePrincipalRole_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccServicePrincipalRole_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: servicePrincipalRoleProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccServicePrincipalRole_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: servicePrincipalRoleProviderConfigTemplate(` diff --git a/aws/user_role_provider_config_test.go b/aws/user_role_provider_config_test.go index 04643f712a..56d8b40a4a 100644 --- a/aws/user_role_provider_config_test.go +++ b/aws/user_role_provider_config_test.go @@ -33,17 +33,6 @@ func TestAccUserRole_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccUserRole_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: userRoleProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccUserRole_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: userRoleProviderConfigTemplate(` diff --git a/catalog/catalog_test.go b/catalog/catalog_test.go index c038f512b4..ccf8cde276 100644 --- a/catalog/catalog_test.go +++ b/catalog/catalog_test.go @@ -272,17 +272,6 @@ func TestAccCatalog_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccCatalog_ProviderConfig_Required(t *testing.T) { - acceptance.UnityWorkspaceLevel(t, acceptance.Step{ - Template: catalogProviderConfigTemplate("test_catalog_{var.STICKY_RANDOM}", ` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccCatalog_ProviderConfig_EmptyID(t *testing.T) { acceptance.UnityWorkspaceLevel(t, acceptance.Step{ Template: catalogProviderConfigTemplate("test_catalog_{var.STICKY_RANDOM}", ` @@ -324,7 +313,7 @@ func TestAccCatalog_ProviderConfig_Match(t *testing.T) { `, workspaceIDStr)), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionNoop), }, }, }) @@ -354,7 +343,7 @@ func TestAccCatalog_ProviderConfig_Recreate(t *testing.T) { `), ConfigPlanChecks: resource.ConfigPlanChecks{ PostApplyPreRefresh: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionDestroyBeforeCreate), }, }, PlanOnly: true, @@ -382,7 +371,7 @@ func TestAccCatalog_ProviderConfig_Remove(t *testing.T) { Template: catalogProviderConfigTemplate(catalogName, ""), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_catalog.this", plancheck.ResourceActionNoop), }, }, }) diff --git a/catalog/resource_catalog_test.go b/catalog/resource_catalog_test.go index 4c8d3d4c0a..d77aca631e 100644 --- a/catalog/resource_catalog_test.go +++ b/catalog/resource_catalog_test.go @@ -844,7 +844,8 @@ func TestCatalogSuppressCaseSensitivity(t *testing.T) { "storage_location": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "provisioning_info.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "effective_predictive_optimization_flag.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, - "full_name": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "full_name": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "provider_config.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, }, HCL: ` name = "A" diff --git a/catalog/resource_sql_table_test.go b/catalog/resource_sql_table_test.go index 530a15676b..6d79c5a6f1 100644 --- a/catalog/resource_sql_table_test.go +++ b/catalog/resource_sql_table_test.go @@ -1883,6 +1883,8 @@ func TestResourceSqlTable_Diff_ExistingResource(t *testing.T) { if expectedDiff == nil { expectedDiff = make(map[string]*terraform.ResourceAttrDiff) } + // provider_config is Optional+Computed, so it always appears as computed in the diff + expectedDiff["provider_config.#"] = &terraform.ResourceAttrDiff{Old: "", New: "", NewComputed: true} qa.ResourceFixture{ HCL: ` catalog_name = "main" diff --git a/catalog/table_provider_config_test.go b/catalog/table_provider_config_test.go index c572ac61ca..0625a39f52 100644 --- a/catalog/table_provider_config_test.go +++ b/catalog/table_provider_config_test.go @@ -39,17 +39,6 @@ func TestAccTable_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccTable_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: tableProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccTable_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: tableProviderConfigTemplate(` diff --git a/common/client.go b/common/client.go index 499903474f..d84e928b5d 100644 --- a/common/client.go +++ b/common/client.go @@ -124,11 +124,14 @@ func (c *DatabricksClient) GetWorkspaceClientForUnifiedProvider( func (c *DatabricksClient) getWorkspaceClientForAccountUnifiedHost( ctx context.Context, workspaceID string, ) (*databricks.WorkspaceClient, error) { - // Workspace ID must be set in a workspace level resource if - // the provider is configured at account level. - // TODO: Link to the documentation once migration guide is published + // If workspace_id is not provided in provider_config, use the provider-level + // workspace_id from SDK config as fallback if workspaceID == "" { - return nil, fmt.Errorf("workspace_id is not set, please set the workspace_id in the provider_config") + workspaceID = c.Config.WorkspaceID + } + if workspaceID == "" { + return nil, fmt.Errorf("managing workspace-level resources requires a workspace_id, " + + "but none was found in the resource's provider_config block or the provider's workspace_id attribute") } // Parse the workspace ID to int. @@ -187,6 +190,23 @@ func parseWorkspaceID(workspaceID string) (int64, error) { return workspaceIDInt, nil } +// CurrentWorkspaceID returns the workspace ID for a workspace-level provider. +// It uses the cached value if available, otherwise makes an API call to resolve it. +func (c *DatabricksClient) CurrentWorkspaceID(ctx context.Context) (int64, error) { + if c.cachedWorkspaceID != 0 { + return c.cachedWorkspaceID, nil + } + w, err := c.WorkspaceClient() + if err != nil { + return 0, err + } + err = c.setCachedWorkspaceID(ctx, w) + if err != nil { + return 0, err + } + return c.cachedWorkspaceID, nil +} + // validateWorkspaceIDFromProvider validates the workspace ID specified in the // resource or data soruce matches the workspace ID of the provider configured workspace client. func (c *DatabricksClient) validateWorkspaceIDFromProvider(ctx context.Context, workspaceID int64, @@ -328,6 +348,15 @@ func (c *DatabricksClient) SetAccountClient(a *databricks.AccountClient) { c.cachedAccountClient = a } +// SetCachedWorkspaceID sets the cached workspace ID directly. +// This is used by test infrastructure to pre-populate the cache and prevent +// lazy CurrentWorkspaceID API calls during unit tests. +func (c *DatabricksClient) SetCachedWorkspaceID(id int64) { + c.mu.Lock() + defer c.mu.Unlock() + c.cachedWorkspaceID = id +} + func (c *DatabricksClient) setAccountId(accountId string) error { c.mu.Lock() defer c.mu.Unlock() diff --git a/common/resource.go b/common/resource.go index 8230b3ab80..ec1fe0d3f2 100644 --- a/common/resource.go +++ b/common/resource.go @@ -6,13 +6,66 @@ import ( "fmt" "log" "regexp" + "strconv" "strings" "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/config" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +// populateProviderConfigInState writes the effective workspace ID into +// provider_config in the resource state. +// +// During refresh reads (terraform plan), the prior state value must be preserved +// so that CustomizeDiff can compare the old effective workspace ID against the +// new one and trigger ForceNew when they differ. If this hook resolved the "new +// effective" workspace ID and wrote it during refresh, it would overwrite the old +// value before CustomizeDiff runs, making workspace-change detection impossible. +// +// Therefore this hook only resolves from provider-level sources (workspace_id, +// host) on the first time — when no workspace ID exists in state yet (after Create). +// On subsequent reads, it preserves whatever is already in state. +func populateProviderConfigInState(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error { + // If provider_config.workspace_id already exists in state, preserve it. + // During refresh reads the state value reflects the workspace the Read + // actually targeted. Overwriting it would prevent CustomizeDiff from + // detecting workspace changes. + if existing := d.Get(workspaceIDSchemaKey); existing != nil { + if existingStr, ok := existing.(string); ok && existingStr != "" { + if err := d.Set("provider_config", []map[string]any{{"workspace_id": existingStr}}); err != nil { + return fmt.Errorf("failed to set provider_config in state: %w", err) + } + return nil + } + } + + // No workspace ID in state yet (first time — after Create/Import). + // Resolve from provider config to populate state for the first time: + // 1. provider_config.workspace_id from raw config + // 2. workspace_id from provider + // 3. Lazy resolution from workspace host via CurrentWorkspaceID API call + wsID, _ := workspaceIDFromRawConfig(d) + if wsID == "" && c.DatabricksClient != nil && c.Config != nil { + wsID = c.Config.WorkspaceID + } + if wsID == "" && c.DatabricksClient != nil && c.Config != nil && c.Config.HostType() == config.WorkspaceHost { + resolvedID, err := c.CurrentWorkspaceID(ctx) + if err != nil { + return fmt.Errorf("failed to resolve workspace_id from workspace host: %w", err) + } + wsID = strconv.FormatInt(resolvedID, 10) + } + + if wsID != "" { + if err := d.Set("provider_config", []map[string]any{{"workspace_id": wsID}}); err != nil { + return fmt.Errorf("failed to set provider_config in state: %w", err) + } + } + return nil +} + // Resource aims to simplify things like error & deleted entities handling type Resource struct { Create func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error @@ -85,6 +138,9 @@ func (r Resource) saferCustomizeDiff() schema.CustomizeDiffFunc { // ToResource converts to Terraform resource definition func (r Resource) ToResource() *schema.Resource { + // Check if this resource has provider_config in its schema (unified provider resource) + _, hasProviderConfig := r.Schema["provider_config"] + var update func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics if r.Update != nil { @@ -102,6 +158,10 @@ func (r Resource) ToResource() *schema.Resource { err = nicerError(ctx, err, "read") return diag.FromErr(err) } + // No post-Read hook needed for Update: provider_config.workspace_id is + // already in state from a previous Create, and r.Read() never touches it. + // If the workspace ID had changed, CustomizeDiff would have triggered + // ForceNew (destroy+create), so Update only runs when it's unchanged. return nil } } else { @@ -132,7 +192,8 @@ func (r Resource) ToResource() *schema.Resource { m any) diag.Diagnostics { return func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { - err := recoverable(r.Read)(ctx, d, m.(*DatabricksClient)) + c := m.(*DatabricksClient) + err := recoverable(r.Read)(ctx, d, c) // TODO: https://github.com/databricks/terraform-provider-databricks/issues/2021 if ignoreMissing && apierr.IsMissing(err) { log.Printf("[INFO] %s[id=%s] is removed on backend", @@ -144,6 +205,16 @@ func (r Resource) ToResource() *schema.Resource { err = nicerError(ctx, err, "read") return diag.FromErr(err) } + // Post-Read hook for refresh reads and imports: populate provider_config in state. + // During normal refresh, provider_config is already in prior state and r.Read() + // doesn't touch it, so the hook preserves the existing value (no-op). + // During import (no prior state), the hook resolves the effective workspace ID + // from workspace_id / cached host for the first time. + if hasProviderConfig && d.Id() != "" { + if hookErr := populateProviderConfigInState(ctx, d, c); hookErr != nil { + return diag.FromErr(nicerError(ctx, hookErr, "populate provider_config for")) + } + } return nil } } @@ -167,12 +238,29 @@ func (r Resource) ToResource() *schema.Resource { return diag.FromErr(err) } if r.CanSkipReadAfterCreateAndUpdate != nil && r.CanSkipReadAfterCreateAndUpdate(d) { + // Resources that skip Read after Create (e.g. notebooks with SOURCE + // format) still need provider_config populated for CustomizeDiff to + // detect workspace changes. Populate it here since Read won't run. + if hasProviderConfig && d.Id() != "" { + if hookErr := populateProviderConfigInState(ctx, d, c); hookErr != nil { + return diag.FromErr(nicerError(ctx, hookErr, "populate provider_config for")) + } + } return nil } if err = recoverable(r.Read)(ctx, d, c); err != nil { err = nicerError(ctx, err, "read") return diag.FromErr(err) } + // Post-Create hook: populate provider_config in state after Read. + // Must run after Read so that Read's DatabricksClientForUnifiedProvider + // sees an empty workspace_id and uses the original provider client + // (which has the cached workspace ID) rather than creating a new client. + if hasProviderConfig && d.Id() != "" { + if hookErr := populateProviderConfigInState(ctx, d, c); hookErr != nil { + return diag.FromErr(nicerError(ctx, hookErr, "populate provider_config for")) + } + } return nil } } diff --git a/common/unified_provider.go b/common/unified_provider.go index 865e47aeb7..9ce1b527b6 100644 --- a/common/unified_provider.go +++ b/common/unified_provider.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "regexp" + "strconv" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/client" "github.com/databricks/databricks-sdk-go/config" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -26,7 +28,7 @@ type Namespace struct { // ProviderConfig is used to store the provider configurations for unified terraform provider // across resources onboarded to SDKv2. type ProviderConfig struct { - WorkspaceID string `json:"workspace_id"` + WorkspaceID string `json:"workspace_id,omitempty"` } // workspaceIDValidateFunc is used to validate the workspace ID for the provider configuration @@ -48,12 +50,14 @@ func AddNamespaceInSchema(m map[string]*schema.Schema) map[string]*schema.Schema m["provider_config"] = &schema.Schema{ Type: schema.TypeList, Optional: true, + Computed: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "workspace_id": { Type: schema.TypeString, - Required: true, + Optional: true, + Computed: true, }, }, }, @@ -64,7 +68,8 @@ func AddNamespaceInSchema(m map[string]*schema.Schema) map[string]*schema.Schema // NamespaceCustomizeSchema is used to customize the schema for the provider configuration // for a single schema. func NamespaceCustomizeSchema(s *CustomizableSchema) { - s.SchemaPath("provider_config", "workspace_id").SetValidateFunc(workspaceIDValidateFunc()) + s.SchemaPath("provider_config").SetComputed() + s.SchemaPath("provider_config", "workspace_id").SetOptional().SetComputed().SetValidateFunc(workspaceIDValidateFunc()) } // NamespaceCustomizeSchemaMap is used to customize the schema for the provider configuration @@ -74,21 +79,82 @@ func NamespaceCustomizeSchemaMap(m map[string]*schema.Schema) map[string]*schema if !ok { panic("provider_config not found in schema") } + providerConfig.Computed = true elem, ok := providerConfig.Elem.(*schema.Resource) if !ok { panic("provider_config.Elem is not a *schema.Resource") } if workspaceID, ok := elem.Schema["workspace_id"]; ok { + workspaceID.Optional = true + workspaceID.Required = false + workspaceID.Computed = true workspaceID.ValidateFunc = workspaceIDValidateFunc() } return m } -// namespaceForceNew marks the workspace_id field as ForceNew if it changed. -func namespaceForceNew(d *schema.ResourceDiff) error { - oldWorkspaceID, newWorkspaceID := d.GetChange(workspaceIDSchemaKey) - if oldWorkspaceID != "" && newWorkspaceID != "" && oldWorkspaceID != newWorkspaceID { - if err := d.ForceNew(workspaceIDSchemaKey); err != nil { +// namespaceForceNew is used to customize the diff for the provider configuration +// in a resource diff. It resolves effective workspace IDs (accounting for +// workspace_id fallback) and triggers ForceNew when the effective +// workspace changes. +// +// With provider_config marked as Optional+Computed, Terraform preserves the state +// value when the config doesn't specify provider_config. This means d.GetChange() +// shows no change in that case. We use GetRawConfigAt to inspect the actual user +// config and determine the true effective new workspace ID. +func namespaceForceNew(ctx context.Context, d *schema.ResourceDiff, c *DatabricksClient) error { + workspaceIDKey := workspaceIDSchemaKey + + // Get the old (state) workspace ID. + oldWsID, _ := d.GetChange(workspaceIDKey) + oldEffective, _ := oldWsID.(string) + + // Determine the new effective workspace ID by inspecting the raw config. + // With Optional+Computed, d.GetChange may return the preserved state value + // rather than reflecting the actual config, so we check the raw config directly. + configWsID, configHasProviderConfig := workspaceIDFromRawDiffConfig(d) + + var newEffective string + if configHasProviderConfig { + // Config explicitly sets provider_config.workspace_id + newEffective = configWsID + } else { + // Config does not have provider_config; use workspace_id + newEffective = c.Config.WorkspaceID + } + // Lazy resolution from workspace host: if newEffective is still empty and + // there's an old value in state to compare against, resolve the workspace ID + // from the host. The oldEffective guard prevents unnecessary API calls when + // both sides are empty (fresh resource with no provider_config). + if newEffective == "" && oldEffective != "" && c.Config != nil && c.Config.HostType() == config.WorkspaceHost { + resolvedID, err := c.CurrentWorkspaceID(ctx) + if err != nil { + return fmt.Errorf("failed to resolve workspace_id from workspace host: %w", err) + } + newEffective = strconv.FormatInt(resolvedID, 10) + } + + // If a resource has a workspace ID in state but the new effective + // workspace ID is empty (user removed workspace_id and there's no + // explicit provider_config), error out. This prevents silently continuing + // to operate against a workspace the user thought they disconnected from. + if oldEffective != "" && newEffective == "" && c.Config.HostType() != config.WorkspaceHost { + return fmt.Errorf("resource has provider_config.workspace_id = %q in state, "+ + "but managing workspace-level resources requires a workspace_id and "+ + "none was found in the resource's provider_config block or the provider's workspace_id attribute", oldEffective) + } + + if oldEffective != "" && newEffective != "" && oldEffective != newEffective { + // When Optional+Computed preserves the old state value (config removes + // provider_config), the SDK sees no diff for workspace_id. ForceNew + // requires HasChange, so we SetNew on the top-level provider_config key + // to create the diff entry before calling ForceNew. + if !d.HasChange(workspaceIDKey) { + if err := d.SetNew("provider_config", []map[string]interface{}{{"workspace_id": newEffective}}); err != nil { + return err + } + } + if err := d.ForceNew(workspaceIDKey); err != nil { return err } } @@ -127,10 +193,29 @@ func NamespaceValidateWorkspaceID(ctx context.Context, d *schema.ResourceDiff, c return nil } +// workspaceIDFromRawDiffConfig extracts the workspace ID from the raw config +// of a ResourceDiff. Returns the workspace ID string and whether provider_config +// is present in the config. +func workspaceIDFromRawDiffConfig(d *schema.ResourceDiff) (string, bool) { + path := cty.Path{ + cty.GetAttrStep{Name: "provider_config"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "workspace_id"}, + } + rawValue, diags := d.GetRawConfigAt(path) + if diags.HasError() || rawValue.IsNull() || !rawValue.IsKnown() { + return "", false + } + if rawValue.Type() == cty.String { + return rawValue.AsString(), true + } + return "", false +} + // NamespaceCustomizeDiff is used to customize the diff for the provider configuration // in a resource diff. func NamespaceCustomizeDiff(ctx context.Context, d *schema.ResourceDiff, c *DatabricksClient) error { - if err := namespaceForceNew(d); err != nil { + if err := namespaceForceNew(ctx, d, c); err != nil { return err } return NamespaceValidateWorkspaceID(ctx, d, c) @@ -168,9 +253,17 @@ func (c *DatabricksClient) DatabricksClientForUnifiedProvider(ctx context.Contex if !ok { return nil, fmt.Errorf("workspace_id must be a string") } - // If the workspace_id is not passed in the resource configuration, we don't need to create a new client - // and can return the current client. + // If the workspace_id is not passed in the resource configuration, + // fall back to the provider-level workspace_id. This is safe for all host + // types: workspace hosts with workspace_id are rejected at provider init, + // so Config.WorkspaceID being set here implies a non-workspace host. + // We don't error when no workspace_id is found because the caller may + // use AccountOrWorkspaceRequest to route to account APIs, which doesn't need + // a workspace-scoped client. if workspaceID == "" { + if c.DatabricksClient != nil && c.Config.WorkspaceID != "" { + return c.getDatabricksClientForUnifiedProvider(ctx, c.Config.WorkspaceID) + } return c, nil } return c.getDatabricksClientForUnifiedProvider(ctx, workspaceID) @@ -209,6 +302,22 @@ func (c *DatabricksClient) getDatabricksClientForUnifiedProvider(ctx context.Con }, nil } +func workspaceIDFromRawConfig(d *schema.ResourceData) (string, bool) { + path := cty.Path{ + cty.GetAttrStep{Name: "provider_config"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "workspace_id"}, + } + rawValue, diags := d.GetRawConfigAt(path) + if diags.HasError() || rawValue.IsNull() || !rawValue.IsKnown() { + return "", false + } + if rawValue.Type() == cty.String { + return rawValue.AsString(), true + } + return "", false +} + // setCachedDatabricksClient sets the cached Databricks Client. func (c *DatabricksClient) setCachedDatabricksClient(ctx context.Context, workspaceID string) error { // Acquire the lock to avoid race conditions. diff --git a/common/unified_provider_test.go b/common/unified_provider_test.go index e844b81480..78259e1554 100644 --- a/common/unified_provider_test.go +++ b/common/unified_provider_test.go @@ -2,11 +2,13 @@ package common import ( "context" + "fmt" "testing" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/client" "github.com/databricks/databricks-sdk-go/config" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/stretchr/testify/assert" @@ -203,7 +205,8 @@ func TestAddNamespaceInSchema(t *testing.T) { assert.True(t, exists, "workspace_id should exist") require.NotNil(t, workspaceID) assert.Equal(t, schema.TypeString, workspaceID.Type) - assert.True(t, workspaceID.Required) + assert.True(t, workspaceID.Optional) + assert.True(t, workspaceID.Computed) // Verify existing fields are preserved assert.Len(t, result, 3, "Should have 3 fields: name, description, provider_config") @@ -361,7 +364,7 @@ func TestWorkspaceClientUnifiedProvider(t *testing.T) { }, }, expectError: true, - errorContains: "workspace_id is not set, please set the workspace_id in the provider_config", + errorContains: "managing workspace-level resources requires a workspace_id", description: "Account-level provider requires workspace_id to be set", }, } @@ -569,7 +572,7 @@ func TestDatabricksClientForUnifiedProvider(t *testing.T) { }, expectError: false, expectSameClient: true, - description: "Account-level provider without workspace_id should return current client", + description: "Account-level provider without workspace_id returns current client for AccountOrWorkspaceRequest routing", }, { name: "workspace_id mismatch - returns error", @@ -779,51 +782,6 @@ func TestNamespaceCustomizeDiff_AccountLevelProvider_ValidWorkspace(t *testing.T assert.NoError(t, err) } -func TestNamespaceCustomizeDiff_AccountLevelProvider_ForceNewOnChange(t *testing.T) { - resource := newTestResourceForCustomizeDiff() - mockWS123 := &databricks.WorkspaceClient{ - Config: &config.Config{ - Host: "https://ws-123.cloud.databricks.com", - Token: "test-token", - }, - } - mockWS456 := &databricks.WorkspaceClient{ - Config: &config.Config{ - Host: "https://ws-456.cloud.databricks.com", - Token: "test-token", - }, - } - c := &DatabricksClient{ - DatabricksClient: &client.DatabricksClient{ - Config: &config.Config{ - Host: "https://accounts.cloud.databricks.com", - AccountID: "test-account-id", - Token: "test-token", - }, - }, - } - c.SetWorkspaceClientForWorkspace(123, mockWS123) - c.SetWorkspaceClientForWorkspace(456, mockWS456) - - diff, err := diffCustomizeDiff(t, resource, map[string]string{ - "name": "test", - "provider_config.#": "1", - "provider_config.0.workspace_id": "123", - }, map[string]interface{}{ - "name": "test", - "provider_config": []interface{}{ - map[string]interface{}{ - "workspace_id": "456", - }, - }, - }, c) - assert.NoError(t, err) - require.NotNil(t, diff) - wsAttr, ok := diff.Attributes[workspaceIDSchemaKey] - require.True(t, ok, "workspace_id should be in diff attributes") - assert.True(t, wsAttr.RequiresNew, "changing workspace_id should require new resource") -} - func TestNamespaceCustomizeDiff_AccountLevelProvider_InvalidWorkspace(t *testing.T) { resource := newTestResourceForCustomizeDiff() c := &DatabricksClient{ @@ -878,51 +836,6 @@ func TestNamespaceCustomizeDiff_UnifiedHost_ValidWorkspace(t *testing.T) { assert.NoError(t, err) } -func TestNamespaceCustomizeDiff_UnifiedHost_ForceNewOnChange(t *testing.T) { - resource := newTestResourceForCustomizeDiff() - mockWS123 := &databricks.WorkspaceClient{ - Config: &config.Config{ - Host: "https://ws-123.cloud.databricks.com", - Token: "test-token", - }, - } - mockWS456 := &databricks.WorkspaceClient{ - Config: &config.Config{ - Host: "https://ws-456.cloud.databricks.com", - Token: "test-token", - }, - } - c := &DatabricksClient{ - DatabricksClient: &client.DatabricksClient{ - Config: &config.Config{ - Host: "https://unified.cloud.databricks.com", - Token: "test-token", - Experimental_IsUnifiedHost: true, - }, - }, - } - c.SetWorkspaceClientForWorkspace(123, mockWS123) - c.SetWorkspaceClientForWorkspace(456, mockWS456) - - diff, err := diffCustomizeDiff(t, resource, map[string]string{ - "name": "test", - "provider_config.#": "1", - "provider_config.0.workspace_id": "123", - }, map[string]interface{}{ - "name": "test", - "provider_config": []interface{}{ - map[string]interface{}{ - "workspace_id": "456", - }, - }, - }, c) - assert.NoError(t, err) - require.NotNil(t, diff) - wsAttr, ok := diff.Attributes[workspaceIDSchemaKey] - require.True(t, ok, "workspace_id should be in diff attributes") - assert.True(t, wsAttr.RequiresNew, "changing workspace_id should require new resource") -} - func TestNamespaceCustomizeDiff_UnifiedHost_DirectFallback(t *testing.T) { resource := newTestResourceForCustomizeDiff() c := &DatabricksClient{ @@ -948,7 +861,7 @@ func TestNamespaceCustomizeDiff_UnifiedHost_DirectFallback(t *testing.T) { assert.NoError(t, err) } -func TestNamespaceCustomizeDiff_ForceNew(t *testing.T) { +func TestNamespaceCustomizeDiff_ForceNewOnChange(t *testing.T) { resource := newTestResourceForCustomizeDiff() mockWS := &databricks.WorkspaceClient{ Config: &config.Config{ @@ -969,7 +882,7 @@ func TestNamespaceCustomizeDiff_ForceNew(t *testing.T) { diff, err := diffCustomizeDiff(t, resource, map[string]string{ "name": "test", "provider_config.#": "1", - "provider_config.0.workspace_id": "789", + "provider_config.0.workspace_id": "456", }, map[string]interface{}{ "name": "test", "provider_config": []interface{}{ @@ -979,22 +892,205 @@ func TestNamespaceCustomizeDiff_ForceNew(t *testing.T) { }, }, c) assert.NoError(t, err) - // No change in workspace_id, so no ForceNew - if diff != nil { - for _, v := range diff.Attributes { - assert.False(t, v.RequiresNew, "should not require new when workspace_id is unchanged") - } - } + require.NotNil(t, diff) + wsAttr, ok := diff.Attributes[workspaceIDSchemaKey] + require.True(t, ok, "workspace_id should be in diff attributes") + assert.True(t, wsAttr.RequiresNew, "changing workspace_id should require new resource") } -func TestNamespaceCustomizeDiff_ForceNewOnChange(t *testing.T) { - resource := newTestResourceForCustomizeDiff() - mockWS := &databricks.WorkspaceClient{ - Config: &config.Config{ - Host: "https://test.cloud.databricks.com", - Token: "test-token", +func TestWorkspaceClientUnifiedProviderWithWorkspaceID(t *testing.T) { + testSchema := map[string]*schema.Schema{ + "provider_config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "workspace_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, }, + "name": { + Type: schema.TypeString, + Required: true, + }, + } + + ctx := context.Background() + mockWorkspaceClient := &databricks.WorkspaceClient{} + + testCases := []struct { + name string + resourceData map[string]interface{} + client *DatabricksClient + expectError bool + errorContains string + expectedHost string // Host of the workspace client we expect to be returned + description string + }{ + { + name: "account-level with workspace_id and no provider_config - uses default", + resourceData: map[string]interface{}{ + "name": "test", + }, + client: func() *DatabricksClient { + c := &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://accounts.cloud.databricks.com", + AccountID: "test-account-id", + Token: "test-token", + WorkspaceID: "123456", + }, + }, + } + c.Config = c.Config.WithTesting() + // Create mock workspace client for DEFAULT workspace + defaultWsClient := &databricks.WorkspaceClient{ + Config: &config.Config{ + Host: "https://default-workspace.test.databricks.com", + Token: "test-token", + }, + } + defaultWsClient.Config = defaultWsClient.Config.WithTesting() + // Create mock workspace client for OVERRIDE workspace + overrideWsClient := &databricks.WorkspaceClient{ + Config: &config.Config{ + Host: "https://override-workspace.test.databricks.com", + Token: "test-token", + }, + } + overrideWsClient.Config = overrideWsClient.Config.WithTesting() + // Cache BOTH workspace clients + c.cachedWorkspaceClients = map[int64]*databricks.WorkspaceClient{ + 123456: defaultWsClient, // workspace_id + 789012: overrideWsClient, // potential override + } + return c + }(), + expectError: false, + expectedHost: "https://default-workspace.test.databricks.com", + description: "Should use workspace_id from provider when provider_config is not set", + }, + { + name: "account-level with workspace_id and provider_config override - uses override", + resourceData: map[string]interface{}{ + "name": "test", + "provider_config": []interface{}{ + map[string]interface{}{ + "workspace_id": "789012", + }, + }, + }, + client: func() *DatabricksClient { + c := &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://accounts.cloud.databricks.com", + AccountID: "test-account-id", + Token: "test-token", + WorkspaceID: "123456", + }, + }, + } + c.Config = c.Config.WithTesting() + // Create mock workspace client for DEFAULT workspace + defaultWsClient := &databricks.WorkspaceClient{ + Config: &config.Config{ + Host: "https://default-workspace.test.databricks.com", + Token: "test-token", + }, + } + defaultWsClient.Config = defaultWsClient.Config.WithTesting() + // Create mock workspace client for OVERRIDE workspace + overrideWsClient := &databricks.WorkspaceClient{ + Config: &config.Config{ + Host: "https://override-workspace.test.databricks.com", + Token: "test-token", + }, + } + overrideWsClient.Config = overrideWsClient.Config.WithTesting() + // Cache BOTH workspace clients + c.cachedWorkspaceClients = map[int64]*databricks.WorkspaceClient{ + 123456: defaultWsClient, // workspace_id - should NOT be used + 789012: overrideWsClient, // provider_config override - SHOULD be used + } + return c + }(), + expectError: false, + expectedHost: "https://override-workspace.test.databricks.com", + description: "Should use workspace_id from provider_config over provider workspace_id", + }, + { + name: "account-level without workspace_id and no provider_config - returns error", + resourceData: map[string]interface{}{ + "name": "test", + }, + client: &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://accounts.cloud.databricks.com", + AccountID: "test-account-id", + Token: "test-token", + }, + }, + }, + expectError: true, + errorContains: "managing workspace-level resources requires a workspace_id", + description: "Should return error when neither workspace_id nor provider_config.workspace_id is set", + }, + { + name: "workspace-level with workspace_id - ignores it and uses workspace client", + resourceData: map[string]interface{}{ + "name": "test", + }, + client: &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://workspace.test.databricks.com", + Token: "test-token", + WorkspaceID: "999999", + }, + }, + cachedWorkspaceClient: mockWorkspaceClient, + cachedWorkspaceID: 123456, + }, + expectError: false, + description: "Workspace-level provider should ignore workspace_id and use configured workspace", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create resource data + d := schema.TestResourceDataRaw(t, testSchema, tc.resourceData) + + // Call WorkspaceClientUnifiedProvider + result, err := tc.client.WorkspaceClientUnifiedProvider(ctx, d) + + // Verify results + if tc.expectError { + assert.Error(t, err, tc.description) + if tc.errorContains != "" { + assert.Contains(t, err.Error(), tc.errorContains) + } + } else { + assert.NoError(t, err, tc.description) + assert.NotNil(t, result) + // Verify the correct workspace client was returned by checking its Host + if tc.expectedHost != "" { + assert.Equal(t, tc.expectedHost, result.Config.Host, + "Expected workspace client with host %s but got %s", tc.expectedHost, result.Config.Host) + } + } + }) } +} + +func TestConfigWorkspaceID(t *testing.T) { c := &DatabricksClient{ DatabricksClient: &client.DatabricksClient{ Config: &config.Config{ @@ -1002,26 +1098,523 @@ func TestNamespaceCustomizeDiff_ForceNewOnChange(t *testing.T) { Token: "test-token", }, }, - cachedWorkspaceClient: mockWS, - cachedWorkspaceID: 789, } - diff, err := diffCustomizeDiff(t, resource, map[string]string{ - "name": "test", - "provider_config.#": "1", - "provider_config.0.workspace_id": "456", - }, map[string]interface{}{ - "name": "test", - "provider_config": []interface{}{ - map[string]interface{}{ - "workspace_id": "789", + + // Initially, WorkspaceID should be empty + assert.Empty(t, c.Config.WorkspaceID) + + // Set the workspace ID + c.Config.WorkspaceID = "123456" + + // Verify it was set correctly + assert.Equal(t, "123456", c.Config.WorkspaceID) + + // Set it to a different value + c.Config.WorkspaceID = "789012" + + // Verify it was updated + assert.Equal(t, "789012", c.Config.WorkspaceID) +} + +// testCustomizeDiffForceNew is a helper that tests whether NamespaceCustomizeDiff +// triggers ForceNew for a given workspace_id transition. +// It builds a schema.Resource with provider_config, wires NamespaceCustomizeDiff as +// the CustomizeDiff, and runs the full diff pipeline to check ForceNew flags. +// It creates a proper cty-valued ResourceConfig so that GetRawConfigAt works in +// CustomizeDiff (terraform.NewResourceConfigRaw does not set cty values). +func testCustomizeDiffForceNew(t *testing.T, instanceState map[string]string, newConfig map[string]any, c *DatabricksClient) (bool, error) { + t.Helper() + + testSchema := map[string]*schema.Schema{ + "name": {Type: schema.TypeString, Required: true}, + } + AddNamespaceInSchema(testSchema) + + r := &schema.Resource{ + Schema: testSchema, + CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, m interface{}) error { + dc, ok := m.(*DatabricksClient) + if !ok { + return fmt.Errorf("expected *DatabricksClient, got %T", m) + } + return namespaceForceNew(ctx, d, dc) + }, + } + + // Build a cty.Value from the config map to ensure GetRawConfigAt works + // in CustomizeDiff. NewResourceConfigRaw doesn't set cty values. + ctyVal := configMapToCtyValue(newConfig) + block := schema.InternalMap(testSchema).CoreConfigSchema() + rc := terraform.NewResourceConfigShimmed(ctyVal, block) + + is := &terraform.InstanceState{ + ID: "test-resource-id", + Attributes: instanceState, + // Set RawConfig to the new config's cty value, matching the real gRPC + // PlanResourceChange path (grpc_provider.go:1081) where + // priorState.RawConfig = configVal. schemaMapWithIdentity.Diff() copies + // s.RawConfig to result.RawConfig, which ResourceDiff.GetRawConfig reads. + RawConfig: ctyVal, + } + + diff, err := r.Diff(context.Background(), is, rc, c) + if err != nil { + return false, err + } + if diff == nil { + return false, nil + } + + for _, v := range diff.Attributes { + if v.RequiresNew { + return true, nil + } + } + return false, nil +} + +// configMapToCtyValue converts a Go map (as used in test configs) to a cty.Value +// suitable for use with terraform.NewResourceConfigShimmed. It handles the +// provider_config TypeList structure used in unified provider tests. +func configMapToCtyValue(config map[string]any) cty.Value { + providerConfigType := cty.Object(map[string]cty.Type{ + "workspace_id": cty.String, + }) + + vals := map[string]cty.Value{} + if name, ok := config["name"]; ok { + vals["name"] = cty.StringVal(name.(string)) + } else { + vals["name"] = cty.NullVal(cty.String) + } + + if pc, ok := config["provider_config"]; ok { + pcList := pc.([]any) + if len(pcList) > 0 { + pcMap := pcList[0].(map[string]any) + vals["provider_config"] = cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "workspace_id": cty.StringVal(pcMap["workspace_id"].(string)), + }), + }) + } else { + vals["provider_config"] = cty.ListValEmpty(providerConfigType) + } + } else { + vals["provider_config"] = cty.NullVal(cty.List(providerConfigType)) + } + + return cty.ObjectVal(vals) +} + +func TestNamespaceCustomizeDiff_ForceNew(t *testing.T) { + makeClient := func(defaultWSID string, cachedWSID int64) *DatabricksClient { + cfg := &config.Config{ + Host: "https://test.cloud.databricks.com", + Token: "test-token", + WorkspaceID: defaultWSID, + } + return &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: cfg.WithTesting(), + }, + cachedWorkspaceID: cachedWSID, + } + } + + tests := []struct { + name string + instanceState map[string]string + newConfig map[string]any + defaultWSID string + cachedWSID int64 + expectForceNew bool + expectError bool + errorContains string + }{ + { + name: "workspace_id changes A to B - ForceNew", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + "provider_config": []any{ + map[string]any{"workspace_id": "200"}, + }, }, + expectForceNew: true, }, - }, c) - assert.NoError(t, err) - require.NotNil(t, diff) - wsAttr, ok := diff.Attributes[workspaceIDSchemaKey] - require.True(t, ok, "workspace_id should be in diff attributes") - assert.True(t, wsAttr.RequiresNew, "changing workspace_id should require new resource") + { + name: "workspace_id same A to A - no ForceNew", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + "provider_config": []any{ + map[string]any{"workspace_id": "100"}, + }, + }, + expectForceNew: false, + }, + { + name: "workspace_id added empty to A - no ForceNew", + instanceState: map[string]string{ + "name": "test", + }, + newConfig: map[string]any{ + "name": "test", + "provider_config": []any{ + map[string]any{"workspace_id": "100"}, + }, + }, + expectForceNew: false, + }, + { + // Workspace host with old state value but no provider_config, no default, + // and no cached ID: lazy resolution attempts CurrentWorkspaceID which fails + // because the test client has no reachable workspace server. + name: "workspace_id removed A to empty no default workspace host - error", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + expectError: true, + errorContains: "failed to resolve workspace_id from workspace host", + }, + { + name: "workspace_id removed A to empty default=A - no ForceNew same effective", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + defaultWSID: "100", + expectForceNew: false, + }, + { + name: "workspace_id removed A to empty default=B - ForceNew effective changes", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + defaultWSID: "200", + expectForceNew: true, + }, + { + name: "workspace_id added empty to A default=A - no ForceNew same effective", + instanceState: map[string]string{ + "name": "test", + }, + newConfig: map[string]any{ + "name": "test", + "provider_config": []any{ + map[string]any{"workspace_id": "100"}, + }, + }, + defaultWSID: "100", + expectForceNew: false, + }, + { + name: "workspace_id added empty to A default=B - no ForceNew without state", + instanceState: map[string]string{ + "name": "test", + }, + newConfig: map[string]any{ + "name": "test", + "provider_config": []any{ + map[string]any{"workspace_id": "100"}, + }, + }, + defaultWSID: "200", + expectForceNew: false, + }, + { + name: "both empty no default - no ForceNew", + instanceState: map[string]string{ + "name": "test", + }, + newConfig: map[string]any{ + "name": "test", + }, + expectForceNew: false, + }, + { + name: "both empty with default - no ForceNew", + instanceState: map[string]string{ + "name": "test", + }, + newConfig: map[string]any{ + "name": "test", + }, + defaultWSID: "100", + expectForceNew: false, + }, + // Workspace host change scenarios: cachedWorkspaceID reflects the + // workspace ID resolved from the current provider host during init. + // When the user changes the provider host (e.g. ws-A → ws-B), + // cachedWorkspaceID changes, triggering ForceNew. + { + name: "host change - cachedWSID differs from state - ForceNew", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + cachedWSID: 200, + expectForceNew: true, + }, + { + name: "host unchanged - cachedWSID matches state - no ForceNew", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + cachedWSID: 100, + expectForceNew: false, + }, + { + // Same as "workspace_id removed" above but explicitly sets cachedWSID=0. + // Lazy resolution fails on workspace host without a reachable server. + name: "no provider_config no default cachedWSID=0 workspace host - error", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + cachedWSID: 0, + expectError: true, + errorContains: "failed to resolve workspace_id from workspace host", + }, + { + name: "default takes precedence over cachedWSID - ForceNew from default", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + defaultWSID: "300", + cachedWSID: 200, + expectForceNew: true, + }, + { + name: "default matches state - cachedWSID ignored - no ForceNew", + instanceState: map[string]string{ + "name": "test", + "provider_config.#": "1", + "provider_config.0.workspace_id": "100", + }, + newConfig: map[string]any{ + "name": "test", + }, + defaultWSID: "100", + cachedWSID: 200, + expectForceNew: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + c := makeClient(tc.defaultWSID, tc.cachedWSID) + forceNew, err := testCustomizeDiffForceNew(t, tc.instanceState, tc.newConfig, c) + if tc.expectError { + require.Error(t, err) + if tc.errorContains != "" { + assert.Contains(t, err.Error(), tc.errorContains) + } + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectForceNew, forceNew, + "ForceNew mismatch: expected %v, got %v", tc.expectForceNew, forceNew) + }) + } +} + +func TestValidateWorkspaceID(t *testing.T) { + tests := []struct { + name string + host string + accountID string + workspaceID string + expectError bool + }{ + { + name: "workspace provider with workspace_id - error", + host: "https://workspace.cloud.databricks.com", + workspaceID: "123456", + expectError: true, + }, + { + name: "account provider with workspace_id - success", + host: "https://accounts.cloud.databricks.com", + accountID: "test-account-id", + workspaceID: "123456", + expectError: false, + }, + { + name: "workspace provider without workspace_id - success", + host: "https://workspace.cloud.databricks.com", + workspaceID: "", + expectError: false, + }, + { + name: "account provider without workspace_id - success", + host: "https://accounts.cloud.databricks.com", + accountID: "test-account-id", + workspaceID: "", + expectError: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg := &config.Config{ + Host: tc.host, + AccountID: tc.accountID, + Token: "test-token", + WorkspaceID: tc.workspaceID, + } + // Replicate the validation logic from provider init + hasError := cfg.WorkspaceID != "" && cfg.HostType() == config.WorkspaceHost + assert.Equal(t, tc.expectError, hasError) + }) + } +} + +func TestPopulateProviderConfigInState(t *testing.T) { + makeResourceData := func(t *testing.T, existingWSID string) *schema.ResourceData { + t.Helper() + testSchema := map[string]*schema.Schema{ + "name": {Type: schema.TypeString, Required: true}, + } + AddNamespaceInSchema(testSchema) + r := schema.Resource{Schema: testSchema} + d := r.TestResourceData() + d.SetId("test-resource") + if existingWSID != "" { + d.Set("provider_config", []map[string]any{{"workspace_id": existingWSID}}) + } + return d + } + + tests := []struct { + name string + existingWSID string // workspace_id already in state (simulates prior state) + providerWSID string // provider-level workspace_id (Config.WorkspaceID) + cachedWorkspaceID int64 + host string // defaults to workspace host if empty + expectedWSID string + }{ + // --- First time (no state) scenarios: resolve from provider --- + { + name: "first time - from provider workspace_id", + existingWSID: "", + providerWSID: "9876543210", + expectedWSID: "9876543210", + }, + { + name: "first time - from cachedWorkspaceID (workspace host)", + existingWSID: "", + cachedWorkspaceID: 1234567890, + expectedWSID: "1234567890", + }, + { + name: "first time - provider workspace_id takes precedence over cached", + existingWSID: "", + providerWSID: "1111111111", + cachedWorkspaceID: 2222222222, + expectedWSID: "1111111111", + }, + { + // For account-level providers without workspace_id or provider_config, + // the workspace_id in state remains empty. Lazy resolution only triggers + // for workspace hosts, so account hosts correctly skip resolution. + name: "first time - no sources available (account host)", + existingWSID: "", + host: "https://accounts.cloud.databricks.com", + expectedWSID: "", + }, + // --- Subsequent reads (has state) scenarios: preserve state --- + { + name: "subsequent read - preserves state, ignores different provider workspace_id", + existingWSID: "111", + providerWSID: "222", + expectedWSID: "111", + }, + { + name: "subsequent read - preserves state, ignores different cached", + existingWSID: "111", + cachedWorkspaceID: 222, + expectedWSID: "111", + }, + { + name: "subsequent read - preserves state even when all sources differ", + existingWSID: "111", + providerWSID: "222", + cachedWorkspaceID: 333, + expectedWSID: "111", + }, + { + name: "subsequent read - preserves state with no other sources", + existingWSID: "111", + expectedWSID: "111", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + d := makeResourceData(t, tc.existingWSID) + host := tc.host + if host == "" { + host = "https://my-workspace.databricks.com" + } + c := &DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: host, + WorkspaceID: tc.providerWSID, + }, + }, + cachedWorkspaceID: tc.cachedWorkspaceID, + } + + err := populateProviderConfigInState(context.Background(), d, c) + require.NoError(t, err) + + wsID := d.Get("provider_config.0.workspace_id") + assert.Equal(t, tc.expectedWSID, wsID) + }) + } } func TestGetDatabricksClientForUnifiedProvider_CopiesCommandFactory(t *testing.T) { diff --git a/internal/acceptance/workspace_id_sdkv2_gosdk_acc_test.go b/internal/acceptance/workspace_id_sdkv2_gosdk_acc_test.go new file mode 100644 index 0000000000..58ae05092e --- /dev/null +++ b/internal/acceptance/workspace_id_sdkv2_gosdk_acc_test.go @@ -0,0 +1,572 @@ +package acceptance + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// ========================================== +// Environment Variables +// ========================================== +// +// Workspace-level tests require: +// - THIS_WORKSPACE_ID: numeric ID of the workspace the provider points to +// +// Account-level tests require: +// - TEST_WORKSPACE_ID: primary workspace ID (account must have access) +// - TEST_WORKSPACE_ID_2: second workspace ID (for ForceNew / change tests) +// - TEST_WORKSPACE_URL: deploy URL of the primary workspace (for migration tests) +// +// TEST_WORKSPACE_URL must correspond to TEST_WORKSPACE_ID. +// Account credentials (OAuth) must work against TEST_WORKSPACE_URL directly. +// +// These tests use databricks_directory (SDKv2 resource using Go SDK via +// WorkspaceClientUnifiedProvider) to validate the unified provider plumbing. +// It has no dependencies and is fast to create. + +// ========================================== +// State Check Helpers +// ========================================== + +// checkProviderConfigWSIDFromEnv verifies provider_config.0.workspace_id matches the given env var. +// The env var lookup is deferred to check time (after LoadAccountEnv/LoadWorkspaceEnv runs). +func checkProviderConfigWSIDFromEnv(resourceAddr, envVar string) func(*terraform.State) error { + return func(s *terraform.State) error { + expected := os.Getenv(envVar) + if expected == "" { + return fmt.Errorf("env var %s is not set", envVar) + } + return resource.TestCheckResourceAttr(resourceAddr, "provider_config.0.workspace_id", expected)(s) + } +} + +// ========================================== +// Template Generators +// ========================================== + +const directoryResource = "databricks_directory.test" + +// directoryTemplate generates HCL for a databricks_directory without a provider block. +func directoryTemplate(providerConfig string) string { + return fmt.Sprintf(` + resource "databricks_directory" "test" { + path = "/Shared/dwsid-test-{var.STICKY_RANDOM}" + %s + } + `, providerConfig) +} + +// directoryWithProviderBlock generates HCL for a databricks_directory with an explicit provider block. +func directoryWithProviderBlock(providerAttrs, providerConfig string) string { + return fmt.Sprintf(` + provider "databricks" { + %s + } + resource "databricks_directory" "test" { + path = "/Shared/dwsid-test-{var.STICKY_RANDOM}" + %s + } + `, providerAttrs, providerConfig) +} + +// ========================================== +// Validation Tests +// ========================================== + +// TestAccWorkspaceID_InvalidWorkspaceID tests that +// invalid workspace_id values in the provider block are rejected. +// The SDK's workspace_id field does not have schema-level validation, +// so invalid values are caught during workspace client creation. +func TestMwsAccWorkspaceID_InvalidWorkspaceID(t *testing.T) { + AccountLevel(t, Step{ + Template: directoryWithProviderBlock(`workspace_id = "invalid"`, ""), + ExpectError: regexp.MustCompile(`failed to parse workspace_id`), + }) +} + +// ========================================== +// Workspace-Level Lifecycle Tests +// ========================================== + +// TestAccWorkspaceID_WS_AddProviderConfig tests adding provider_config to an existing resource. +// Step 1: Create without provider_config. Post-Read hook populates state with workspace ID. +// Step 2: Add provider_config with matching workspace_id -> Noop (same value already in state). +func TestAccWorkspaceID_WS_AddProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: directoryTemplate(""), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: directoryTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// TestAccWorkspaceID_WS_RemoveProviderConfig tests removing provider_config from a resource. +// Step 1: Create with provider_config. +// Step 2: Remove provider_config -> Noop. Optional+Computed preserves state value, +// and effective workspace is unchanged (same workspace). +func TestAccWorkspaceID_WS_RemoveProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: directoryTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: directoryTemplate(""), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// TestAccWorkspaceID_WS_ChangeProviderConfig tests that changing workspace_id +// to a different workspace on a workspace-level provider produces an error. +// Step 1: Create with workspace_id matching current workspace. +// Step 2: Change to different workspace_id -> error (workspace-level provider +// cannot target a different workspace). +func TestAccWorkspaceID_WS_ChangeProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: directoryTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: directoryTemplate(` + provider_config { + workspace_id = "123" + } + `), + ExpectError: regexp.MustCompile(`workspace_id mismatch`), + }, + ) +} + +// ========================================== +// Account-Level: New Setup +// ========================================== +// +// Account provider + workspace_id → create workspace-level resource. +// Verifies: resource created, state has correct provider_config.0.workspace_id. + +func TestMwsAccWorkspaceID_AccountNewSetup(t *testing.T) { + AccountLevel(t, Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }) +} + +// TestAccWorkspaceID_AccountNewSetupWithOverride tests that provider_config.workspace_id +// takes precedence over the provider-level workspace_id. +func TestMwsAccWorkspaceID_AccountNewSetupWithOverride(t *testing.T) { + AccountLevel(t, Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID_2"), + }) +} + +// ========================================== +// Migration: Same Workspace +// ========================================== +// +// Existing resource on workspace-level provider. Switch to account provider +// with workspace_id pointing to the SAME workspace. +// Expected: no changes (noop). +// +// Step 1: provider { host = TEST_WORKSPACE_URL } → workspace-level create. +// Step 2: provider { workspace_id = TEST_WORKSPACE_ID } → account-level, same ws. +// +// Requires: account-level OAuth credentials that also work against the workspace host. + +func TestMwsAccWorkspaceID_MigrationSameWorkspace(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `host = "{env.TEST_WORKSPACE_URL}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Migration: Different Workspace +// ========================================== +// +// Same as MigrationSameWorkspace, but workspace_id points to a DIFFERENT workspace. +// Expected: ForceNew (destroy in old workspace, recreate in new). +// +// Step 1: provider { host = TEST_WORKSPACE_URL } → create in workspace 1. +// Step 2: provider { workspace_id = TEST_WORKSPACE_ID_2 } → ForceNew. + +func TestMwsAccWorkspaceID_MigrationDiffWorkspace(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `host = "{env.TEST_WORKSPACE_URL}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Add provider_config Override (Same Workspace) +// ========================================== +// +// Resource was created using workspace_id. User adds explicit +// provider_config with the SAME workspace ID. +// Expected: Noop. State already has the same value from the default; no diff. + +func TestMwsAccWorkspaceID_AddOverrideSame(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Add provider_config Override (Different Workspace) +// ========================================== +// +// Resource was created using workspace_id = TEST_WORKSPACE_ID. +// User adds provider_config { workspace_id = TEST_WORKSPACE_ID_2 }. +// Expected: ForceNew (effective workspace changes). + +func TestMwsAccWorkspaceID_AddOverrideDiff(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Remove provider_config Override (Falls Back to Same Default) +// ========================================== +// +// Resource has provider_config { workspace_id = X }. User removes it. +// workspace_id is also X. +// Expected: Noop. Optional+Computed preserves state value, and effective +// workspace is unchanged (default fallback is the same value). + +func TestMwsAccWorkspaceID_RemoveOverrideSame(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Remove provider_config Override (Falls Back to Different Default) +// ========================================== +// +// Resource has provider_config { workspace_id = TEST_WORKSPACE_ID_2 }. +// workspace_id = TEST_WORKSPACE_ID (different). +// User removes provider_config → effective workspace changes → ForceNew. +// +// Step 1: Create in TEST_WORKSPACE_ID_2 (override wins over default). +// Step 2: Remove override → falls back to TEST_WORKSPACE_ID → ForceNew. + +func TestMwsAccWorkspaceID_RemoveOverrideDiff(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID_2"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Change workspace_id (No provider_config) +// ========================================== +// +// User changes workspace_id from TEST_WORKSPACE_ID to TEST_WORKSPACE_ID_2. +// Resources have no explicit provider_config. +// Expected: ForceNew for all affected resources. + +func TestMwsAccWorkspaceID_ChangeDefault(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Change workspace_id (Resource Has Explicit Override) +// ========================================== +// +// User changes workspace_id from TEST_WORKSPACE_ID to TEST_WORKSPACE_ID_2. +// Resource has explicit provider_config { workspace_id = TEST_WORKSPACE_ID }. +// Expected: Noop. The explicit override is the same value in both steps; +// the default changed but the override shields the resource from any diff. + +func TestMwsAccWorkspaceID_ChangeDefaultWithOverride(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Set workspace_id on Workspace-Level Provider +// ========================================== +// +// User accidentally sets workspace_id on a workspace-level provider. +// Expected: configuration error at provider initialization. + +func TestAccWorkspaceID_DefaultOnWorkspaceProvider(t *testing.T) { + WorkspaceLevel(t, Step{ + Template: directoryWithProviderBlock(`workspace_id = "12345"`, ""), + ExpectError: regexp.MustCompile(`workspace_id cannot be used with a workspace-level provider; it is only supported when the provider is configured at the account level`), + PlanOnly: true, + }) +} + +// ========================================== +// Account Provider Without workspace_id or provider_config +// ========================================== +// +// Account-level provider with no workspace_id. Resource has no provider_config. +// Expected: error during CRUD — no workspace_id available for routing. + +func TestMwsAccWorkspaceID_NoDefaultNoOverride(t *testing.T) { + AccountLevel(t, Step{ + Template: directoryWithProviderBlock("", ""), + ExpectError: regexp.MustCompile( + `managing workspace-level resources requires a workspace_id, but none was found in the resource's provider_config block or the provider's workspace_id attribute`, + ), + }) +} + +// ========================================== +// Provider Upgrade (Existing Resources, No Config Changes) +// ========================================== +// +// Approximation: create a resource at workspace level without any unified +// provider features (no provider_config, no workspace_id). Then verify +// a second plan/apply with the same config produces no changes. +// This validates backward compatibility — the new provider_config schema +// field doesn't cause unexpected diffs on existing resources. + +func TestAccWorkspaceID_ProviderUpgrade(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: directoryTemplate(""), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: directoryTemplate(""), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(directoryResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkProviderConfigWSIDFromEnv(directoryResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Remove workspace_id from Provider Config +// ========================================== +// +// User had workspace_id and resources relying on it (no explicit provider_config). +// User removes workspace_id. +// Expected: error — provider_config.workspace_id is in state but no source for it +// in config and no workspace_id set. +// +// Step 1: Create with workspace_id. +// Step 2: Remove workspace_id → plan error. + +func TestMwsAccWorkspaceID_RemoveDefault(t *testing.T) { + AccountLevel(t, + Step{ + Template: directoryWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkProviderConfigWSIDFromEnv(directoryResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: directoryWithProviderBlock("", ""), + ExpectError: regexp.MustCompile( + `resource has provider_config.workspace_id = .* in state, but managing workspace-level resources requires a workspace_id`, + ), + }, + ) +} diff --git a/internal/acceptance/workspace_id_sdkv2_http_acc_test.go b/internal/acceptance/workspace_id_sdkv2_http_acc_test.go new file mode 100644 index 0000000000..b7c3ee1685 --- /dev/null +++ b/internal/acceptance/workspace_id_sdkv2_http_acc_test.go @@ -0,0 +1,581 @@ +package acceptance + +import ( + "encoding/base64" + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// ========================================== +// Environment Variables (same as workspace_id_sdkv2_gosdk_acc_test.go) +// ========================================== +// +// Workspace-level tests require: +// - THIS_WORKSPACE_ID: numeric ID of the workspace the provider points to +// +// Account-level tests require: +// - TEST_WORKSPACE_ID: primary workspace ID (account must have access) +// - TEST_WORKSPACE_ID_2: second workspace ID (for ForceNew / change tests) +// - TEST_WORKSPACE_URL: deploy URL of the primary workspace (for migration tests) +// +// These tests use databricks_notebook (SDKv2 resource using HTTP paths via +// DatabricksClientForUnifiedProvider, NOT Go SDK) to validate the unified +// provider plumbing for non-Go-SDK resources. +// It has no dependencies, is strictly workspace-level, and requires no special entitlements. + +// ========================================== +// State Check Helpers +// ========================================== + +// checkNotebookProviderConfigWSIDFromEnv verifies provider_config.0.workspace_id matches the given env var. +func checkNotebookProviderConfigWSIDFromEnv(resourceAddr, envVar string) func(*terraform.State) error { + return func(s *terraform.State) error { + expected := os.Getenv(envVar) + if expected == "" { + return fmt.Errorf("env var %s is not set", envVar) + } + return resource.TestCheckResourceAttr(resourceAddr, "provider_config.0.workspace_id", expected)(s) + } +} + +// ========================================== +// Template Generators +// ========================================== + +const notebookResource = "databricks_notebook.test" + +var notebookContent = base64.StdEncoding.EncodeToString([]byte("# Databricks notebook source\nprint('hello')")) + +// notebookTemplate generates HCL for a databricks_notebook without a provider block. +func notebookTemplate(providerConfig string) string { + return fmt.Sprintf(` + resource "databricks_notebook" "test" { + path = "/Shared/dwsid-test-{var.STICKY_RANDOM}" + content_base64 = "%s" + language = "PYTHON" + %s + } + `, notebookContent, providerConfig) +} + +// notebookWithProviderBlock generates HCL for a databricks_notebook with an explicit provider block. +func notebookWithProviderBlock(providerAttrs, providerConfig string) string { + return fmt.Sprintf(` + provider "databricks" { + %s + } + resource "databricks_notebook" "test" { + path = "/Shared/dwsid-test-{var.STICKY_RANDOM}" + content_base64 = "%s" + language = "PYTHON" + %s + } + `, providerAttrs, notebookContent, providerConfig) +} + +// ========================================== +// Validation Tests +// ========================================== + +// TestMwsAccWorkspaceIDHttp_InvalidWorkspaceID tests that +// invalid workspace_id values in the provider block are rejected. +func TestMwsAccWorkspaceIDHttp_InvalidWorkspaceID(t *testing.T) { + AccountLevel(t, Step{ + Template: notebookWithProviderBlock(`workspace_id = "invalid"`, ""), + ExpectError: regexp.MustCompile(`failed to parse workspace_id`), + }) +} + +// ========================================== +// Workspace-Level Lifecycle Tests +// ========================================== + +// TestAccWorkspaceIDHttp_WS_AddProviderConfig tests adding provider_config to an existing resource. +// Step 1: Create without provider_config. Post-Read hook populates state with workspace ID. +// Step 2: Add provider_config with matching workspace_id -> Noop (same value already in state). +func TestAccWorkspaceIDHttp_WS_AddProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: notebookTemplate(""), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: notebookTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// TestAccWorkspaceIDHttp_WS_RemoveProviderConfig tests removing provider_config from a resource. +// Step 1: Create with provider_config. +// Step 2: Remove provider_config -> Noop. Optional+Computed preserves state value, +// and effective workspace is unchanged (same workspace). +func TestAccWorkspaceIDHttp_WS_RemoveProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: notebookTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: notebookTemplate(""), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// TestAccWorkspaceIDHttp_WS_ChangeProviderConfig tests that changing workspace_id +// to a different workspace on a workspace-level provider produces an error. +// Step 1: Create with workspace_id matching current workspace. +// Step 2: Change to different workspace_id -> error (workspace-level provider +// cannot target a different workspace). +func TestAccWorkspaceIDHttp_WS_ChangeProviderConfig(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: notebookTemplate(` + provider_config { + workspace_id = "{env.THIS_WORKSPACE_ID}" + } + `), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: notebookTemplate(` + provider_config { + workspace_id = "123" + } + `), + ExpectError: regexp.MustCompile(`workspace_id mismatch`), + }, + ) +} + +// ========================================== +// Account-Level: New Setup +// ========================================== +// +// Account provider + workspace_id → create workspace-level resource. +// Verifies: resource created, state has correct provider_config.0.workspace_id. + +func TestMwsAccWorkspaceIDHttp_AccountNewSetup(t *testing.T) { + AccountLevel(t, Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }) +} + +// TestMwsAccWorkspaceIDHttp_AccountNewSetupWithOverride tests that provider_config.workspace_id +// takes precedence over the provider-level workspace_id. +func TestMwsAccWorkspaceIDHttp_AccountNewSetupWithOverride(t *testing.T) { + AccountLevel(t, Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID_2"), + }) +} + +// ========================================== +// Migration: Same Workspace +// ========================================== +// +// Existing resource on workspace-level provider. Switch to account provider +// with workspace_id pointing to the SAME workspace. +// Expected: no changes (noop). +// +// Step 1: provider { host = TEST_WORKSPACE_URL } → workspace-level create. +// Step 2: provider { workspace_id = TEST_WORKSPACE_ID } → account-level, same ws. +// +// Requires: account-level OAuth credentials that also work against the workspace host. + +func TestMwsAccWorkspaceIDHttp_MigrationSameWorkspace(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `host = "{env.TEST_WORKSPACE_URL}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Migration: Different Workspace +// ========================================== +// +// Same as MigrationSameWorkspace, but workspace_id points to a DIFFERENT workspace. +// Expected: ForceNew (destroy in old workspace, recreate in new). +// +// Step 1: provider { host = TEST_WORKSPACE_URL } → create in workspace 1. +// Step 2: provider { workspace_id = TEST_WORKSPACE_ID_2 } → ForceNew. + +func TestMwsAccWorkspaceIDHttp_MigrationDiffWorkspace(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `host = "{env.TEST_WORKSPACE_URL}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Add provider_config Override (Same Workspace) +// ========================================== +// +// Resource was created using workspace_id. User adds explicit +// provider_config with the SAME workspace ID. +// Expected: Noop. State already has the same value from the default; no diff. + +func TestMwsAccWorkspaceIDHttp_AddOverrideSame(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Add provider_config Override (Different Workspace) +// ========================================== +// +// Resource was created using workspace_id = TEST_WORKSPACE_ID. +// User adds provider_config { workspace_id = TEST_WORKSPACE_ID_2 }. +// Expected: ForceNew (effective workspace changes). + +func TestMwsAccWorkspaceIDHttp_AddOverrideDiff(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Remove provider_config Override (Falls Back to Same Default) +// ========================================== +// +// Resource has provider_config { workspace_id = X }. User removes it. +// workspace_id is also X. +// Expected: Noop. Optional+Computed preserves state value, and effective +// workspace is unchanged (default fallback is the same value). + +func TestMwsAccWorkspaceIDHttp_RemoveOverrideSame(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Remove provider_config Override (Falls Back to Different Default) +// ========================================== +// +// Resource has provider_config { workspace_id = TEST_WORKSPACE_ID_2 }. +// workspace_id = TEST_WORKSPACE_ID (different). +// User removes provider_config → effective workspace changes → ForceNew. +// +// Step 1: Create in TEST_WORKSPACE_ID_2 (override wins over default). +// Step 2: Remove override → falls back to TEST_WORKSPACE_ID → ForceNew. + +func TestMwsAccWorkspaceIDHttp_RemoveOverrideDiff(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID_2}" + }`, + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID_2"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Change workspace_id (No provider_config) +// ========================================== +// +// User changes workspace_id from TEST_WORKSPACE_ID to TEST_WORKSPACE_ID_2. +// Resources have no explicit provider_config. +// Expected: ForceNew for all affected resources. + +func TestMwsAccWorkspaceIDHttp_ChangeDefault(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + "", + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID_2"), + }, + ) +} + +// ========================================== +// Change workspace_id (Resource Has Explicit Override) +// ========================================== +// +// User changes workspace_id from TEST_WORKSPACE_ID to TEST_WORKSPACE_ID_2. +// Resource has explicit provider_config { workspace_id = TEST_WORKSPACE_ID }. +// Expected: Noop. The explicit override is the same value in both steps; +// the default changed but the override shields the resource from any diff. + +func TestMwsAccWorkspaceIDHttp_ChangeDefaultWithOverride(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID_2}"`, + `provider_config { + workspace_id = "{env.TEST_WORKSPACE_ID}" + }`, + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Set workspace_id on Workspace-Level Provider +// ========================================== +// +// User accidentally sets workspace_id on a workspace-level provider. +// Expected: configuration error at provider initialization. + +func TestAccWorkspaceIDHttp_DefaultOnWorkspaceProvider(t *testing.T) { + WorkspaceLevel(t, Step{ + Template: notebookWithProviderBlock(`workspace_id = "12345"`, ""), + ExpectError: regexp.MustCompile(`workspace_id cannot be used with a workspace-level provider; it is only supported when the provider is configured at the account level`), + PlanOnly: true, + }) +} + +// ========================================== +// Account Provider Without workspace_id or provider_config +// ========================================== +// +// Account-level provider with no workspace_id. Resource has no provider_config. +// Expected: error during CRUD — no workspace_id available for routing. +// +// Unlike the Go SDK path (TestMwsAccWorkspaceID_NoDefaultNoOverride), which +// returns a clear "no workspace_id" error via GetWorkspaceClientForUnifiedProvider, +// the HTTP path (DatabricksClientForUnifiedProvider) cannot validate early because +// it doesn't know whether the caller needs a workspace-scoped or account-scoped +// client. It returns the account-level client, which then fails at the API layer +// when the resource attempts a workspace-level operation. + +func TestMwsAccWorkspaceIDHttp_NoDefaultNoOverride(t *testing.T) { + AccountLevel(t, Step{ + Template: notebookWithProviderBlock("", ""), + ExpectError: regexp.MustCompile( + `cannot create notebook`, + ), + }) +} + +// ========================================== +// Provider Upgrade (Existing Resources, No Config Changes) +// ========================================== +// +// Approximation: create a resource at workspace level without any unified +// provider features (no provider_config, no workspace_id). Then verify +// a second plan/apply with the same config produces no changes. +// This validates backward compatibility — the new provider_config schema +// field doesn't cause unexpected diffs on existing resources. + +func TestAccWorkspaceIDHttp_ProviderUpgrade(t *testing.T) { + WorkspaceLevel(t, + Step{ + Template: notebookTemplate(""), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + Step{ + Template: notebookTemplate(""), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(notebookResource, plancheck.ResourceActionNoop), + }, + }, + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "THIS_WORKSPACE_ID"), + }, + ) +} + +// ========================================== +// Remove workspace_id from Provider Config +// ========================================== +// +// User had workspace_id and resources relying on it (no explicit provider_config). +// User removes workspace_id. +// Expected: error — provider_config.workspace_id is in state but no source for it +// in config and no workspace_id set. +// +// Step 1: Create with workspace_id. +// Step 2: Remove workspace_id → plan error. + +func TestMwsAccWorkspaceIDHttp_RemoveDefault(t *testing.T) { + AccountLevel(t, + Step{ + Template: notebookWithProviderBlock( + `workspace_id = "{env.TEST_WORKSPACE_ID}"`, + "", + ), + Check: checkNotebookProviderConfigWSIDFromEnv(notebookResource, "TEST_WORKSPACE_ID"), + }, + Step{ + Template: notebookWithProviderBlock("", ""), + ExpectError: regexp.MustCompile( + `resource has provider_config.workspace_id = .* in state, but managing workspace-level resources requires a workspace_id`, + ), + }, + ) +} diff --git a/internal/providers/pluginfw/products/sharing/resource_share_acc_test.go b/internal/providers/pluginfw/products/sharing/resource_share_acc_test.go index 628500c315..f4ea3227cb 100644 --- a/internal/providers/pluginfw/products/sharing/resource_share_acc_test.go +++ b/internal/providers/pluginfw/products/sharing/resource_share_acc_test.go @@ -673,17 +673,6 @@ func TestAccShare_ProviderConfig_Multiple(t *testing.T) { }) } -func TestAccShare_ProviderConfig_Required(t *testing.T) { - acceptance.UnityWorkspaceLevel(t, acceptance.Step{ - Template: preTestTemplateSchema + shareTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`(?s).*workspace_id.*is required`), - PlanOnly: true, - }) -} - func TestAccShare_ProviderConfig_EmptyID(t *testing.T) { acceptance.UnityWorkspaceLevel(t, acceptance.Step{ Template: preTestTemplateSchema + shareTemplate(` diff --git a/internal/providers/sdkv2/sdkv2.go b/internal/providers/sdkv2/sdkv2.go index cf8884105d..6ba968b723 100644 --- a/internal/providers/sdkv2/sdkv2.go +++ b/internal/providers/sdkv2/sdkv2.go @@ -373,6 +373,11 @@ func ConfigureDatabricksClient(ctx context.Context, d *schema.ResourceData, conf if err != nil { return nil, diag.FromErr(err) } + // Validate workspace_id is not used with workspace-level providers + if databricksClient.Config.WorkspaceID != "" && databricksClient.Config.HostType() == config.WorkspaceHost { + return nil, diag.FromErr(fmt.Errorf("workspace_id cannot be used with a workspace-level provider; " + + "it is only supported when the provider is configured at the account level")) + } return databricksClient, nil } diff --git a/internal/providers/sdkv2/sdkv2_test.go b/internal/providers/sdkv2/sdkv2_test.go index ab768b1eaa..320bc2b411 100644 --- a/internal/providers/sdkv2/sdkv2_test.go +++ b/internal/providers/sdkv2/sdkv2_test.go @@ -43,6 +43,8 @@ func TestConfigureDatabricksClient(t *testing.T) { { name: "workspace_id can be set in provider config", config: map[string]interface{}{ + "host": "https://accounts.cloud.databricks.com", + "account_id": "00000000-0000-0000-0000-000000000001", "workspace_id": "1234567890", }, validateResourceData: func(dc *common.DatabricksClient) { diff --git a/jobs/job_test.go b/jobs/job_test.go index f6d30baaf4..f7f8022c0b 100644 --- a/jobs/job_test.go +++ b/jobs/job_test.go @@ -118,17 +118,6 @@ func TestAccJobCluster_ProviderConfig_Mismatched(t *testing.T) { }) } -func TestAccJobCluster_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: jobClusterTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccJobCluster_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: jobClusterTemplate(` @@ -158,7 +147,7 @@ func TestAccJobCluster_ProviderConfig_Match(t *testing.T) { `, workspaceIDStr)), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_job.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_job.this", plancheck.ResourceActionNoop), }, }, }) @@ -209,7 +198,7 @@ func TestAccJobCluster_ProviderConfig_Remove(t *testing.T) { Template: jobClusterTemplate(""), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_job.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_job.this", plancheck.ResourceActionNoop), }, }, }) diff --git a/permissions/access_control_rule_set_provider_config_test.go b/permissions/access_control_rule_set_provider_config_test.go index 6e6ef060f4..8a7e5134e5 100644 --- a/permissions/access_control_rule_set_provider_config_test.go +++ b/permissions/access_control_rule_set_provider_config_test.go @@ -33,17 +33,6 @@ func TestAccAccessControlRuleSet_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccAccessControlRuleSet_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: accessControlRuleSetProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccAccessControlRuleSet_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: accessControlRuleSetProviderConfigTemplate(` diff --git a/permissions/permissions_provider_config_test.go b/permissions/permissions_provider_config_test.go index bb219c33cf..6bb789ebf4 100644 --- a/permissions/permissions_provider_config_test.go +++ b/permissions/permissions_provider_config_test.go @@ -33,17 +33,6 @@ func TestAccPermissions_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccPermissions_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: permissionsProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccPermissions_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: permissionsProviderConfigTemplate(` diff --git a/pipelines/pipeline_provider_config_test.go b/pipelines/pipeline_provider_config_test.go index 10190b4705..bf7c256b33 100644 --- a/pipelines/pipeline_provider_config_test.go +++ b/pipelines/pipeline_provider_config_test.go @@ -34,17 +34,6 @@ func TestAccPipeline_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccPipeline_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: pipelineProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccPipeline_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: pipelineProviderConfigTemplate(` diff --git a/policies/cluster_policy_provider_config_test.go b/policies/cluster_policy_provider_config_test.go index 14ae700b84..4eb1711d18 100644 --- a/policies/cluster_policy_provider_config_test.go +++ b/policies/cluster_policy_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccClusterPolicy_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccClusterPolicy_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: clusterPolicyProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccClusterPolicy_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: clusterPolicyProviderConfigTemplate(` diff --git a/pools/instance_pool_provider_config_test.go b/pools/instance_pool_provider_config_test.go index 60fd721fef..3eff89bc5e 100644 --- a/pools/instance_pool_provider_config_test.go +++ b/pools/instance_pool_provider_config_test.go @@ -31,17 +31,6 @@ func TestAccInstancePool_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccInstancePool_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: instancePoolProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccInstancePool_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: instancePoolProviderConfigTemplate(` diff --git a/qa/testing.go b/qa/testing.go index 6f4d3a42be..6458b1b14e 100644 --- a/qa/testing.go +++ b/qa/testing.go @@ -14,6 +14,7 @@ import ( "reflect" "regexp" "sort" + "strconv" "strings" "testing" @@ -124,6 +125,10 @@ type ResourceFixture struct { Token string // new resource New bool + + // ProviderWorkspaceID sets the workspace_id on the client config for testing + // unified provider behavior with provider-level workspace ID fallback. + ProviderWorkspaceID string } // wrapper type for calling resource methords @@ -223,6 +228,14 @@ func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, se } if f.Fixtures != nil { client, s, err := HttpFixtureClientWithToken(t, f.Fixtures, token) + if err == nil && f.ProviderWorkspaceID != "" { + client.Config.WorkspaceID = f.ProviderWorkspaceID + // Override default cached workspace ID (12345) to match ProviderWorkspaceID + // so that validateWorkspaceIDFromProvider sees a consistent value. + if wsID, parseErr := strconv.ParseInt(f.ProviderWorkspaceID, 10, 64); parseErr == nil { + client.SetCachedWorkspaceID(wsID) + } + } ss := server{ Close: s.Close, URL: s.URL, @@ -245,6 +258,19 @@ func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, se c.SetWorkspaceClient(mw.WorkspaceClient) c.SetAccountClient(ma.AccountClient) c.Config.Credentials = testCredentialsProvider{token: token} + if f.ProviderWorkspaceID != "" { + c.Config.WorkspaceID = f.ProviderWorkspaceID + } + // Pre-populate cached workspace ID to prevent lazy CurrentWorkspaceID + // API calls in unit tests. When ProviderWorkspaceID is set, use that + // value so validateWorkspaceIDFromProvider sees a consistent ID. + cachedWsID := int64(12345) + if f.ProviderWorkspaceID != "" { + if wsID, parseErr := strconv.ParseInt(f.ProviderWorkspaceID, 10, 64); parseErr == nil { + cachedWsID = wsID + } + } + c.SetCachedWorkspaceID(cachedWsID) return c, server{ Close: func() {}, URL: "does-not-matter", @@ -619,9 +645,13 @@ func HttpFixtureClientWithToken(t *testing.T, fixtures []HTTPFixture, token stri if err != nil { return nil, nil, err } - return &common.DatabricksClient{ + dc := &common.DatabricksClient{ DatabricksClient: c, - }, server, nil + } + // Pre-populate cached workspace ID to prevent lazy CurrentWorkspaceID + // API calls in unit tests. + dc.SetCachedWorkspaceID(12345) + return dc, server, nil } // HTTPFixturesApply is a helper method @@ -641,6 +671,7 @@ func MockWorkspaceApply(t *testing.T, mockWorkspaceClient func(*mocks.MockWorksp }, } client.SetWorkspaceClient(mw.WorkspaceClient) + client.SetCachedWorkspaceID(12345) callback(context.Background(), client) } diff --git a/repos/repo_provider_config_test.go b/repos/repo_provider_config_test.go index b174b2fdae..7f368659ff 100644 --- a/repos/repo_provider_config_test.go +++ b/repos/repo_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccRepo_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccRepo_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: repoProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccRepo_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: repoProviderConfigTemplate(` diff --git a/scim/entitlement_provider_config_test.go b/scim/entitlement_provider_config_test.go index 9695d73718..a7c46996bd 100644 --- a/scim/entitlement_provider_config_test.go +++ b/scim/entitlement_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccEntitlements_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccEntitlements_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: entitlementProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccEntitlements_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: entitlementProviderConfigTemplate(` diff --git a/scim/group_member_provider_config_test.go b/scim/group_member_provider_config_test.go index 555185afe4..c805b39276 100644 --- a/scim/group_member_provider_config_test.go +++ b/scim/group_member_provider_config_test.go @@ -36,17 +36,6 @@ func TestAccGroupMember_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccGroupMember_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: groupMemberProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccGroupMember_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: groupMemberProviderConfigTemplate(` diff --git a/scim/group_provider_config_test.go b/scim/group_provider_config_test.go index e8b63efc5c..c295d93444 100644 --- a/scim/group_provider_config_test.go +++ b/scim/group_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccGroup_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccGroup_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: groupProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccGroup_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: groupProviderConfigTemplate(` diff --git a/scim/group_role_provider_config_test.go b/scim/group_role_provider_config_test.go index fd0b33dba8..1ffc50c00f 100644 --- a/scim/group_role_provider_config_test.go +++ b/scim/group_role_provider_config_test.go @@ -33,17 +33,6 @@ func TestAccGroupRole_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccGroupRole_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: groupRoleProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccGroupRole_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: groupRoleProviderConfigTemplate(` diff --git a/scim/service_principal_provider_config_test.go b/scim/service_principal_provider_config_test.go index b0b36425a3..b05b6ba85d 100644 --- a/scim/service_principal_provider_config_test.go +++ b/scim/service_principal_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccServicePrincipal_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccServicePrincipal_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: servicePrincipalProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccServicePrincipal_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: servicePrincipalProviderConfigTemplate(` diff --git a/scim/user_provider_config_test.go b/scim/user_provider_config_test.go index c757a0827d..b0ea99c3ea 100644 --- a/scim/user_provider_config_test.go +++ b/scim/user_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccUser_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccUser_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: userProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccUser_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: userProviderConfigTemplate(` diff --git a/secrets/secret_scope_provider_config_test.go b/secrets/secret_scope_provider_config_test.go index 6282b13a27..6d62ac3cd3 100644 --- a/secrets/secret_scope_provider_config_test.go +++ b/secrets/secret_scope_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccSecretScope_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSecretScope_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: secretScopeProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSecretScope_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: secretScopeProviderConfigTemplate(` diff --git a/serving/resource_model_serving_provisioned_throughput_test.go b/serving/resource_model_serving_provisioned_throughput_test.go index 29b968111c..221934b012 100644 --- a/serving/resource_model_serving_provisioned_throughput_test.go +++ b/serving/resource_model_serving_provisioned_throughput_test.go @@ -462,7 +462,9 @@ func TestModelServingProvisionedThroughputUpdate_RemoveConfigIsNoOp(t *testing.T } } `, - ExpectedDiff: map[string]*terraform.ResourceAttrDiff{}, + ExpectedDiff: map[string]*terraform.ResourceAttrDiff{ + "provider_config.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + }, }.ApplyNoError(t) } diff --git a/serving/resource_model_serving_test.go b/serving/resource_model_serving_test.go index 5bdec14367..fe476dc622 100644 --- a/serving/resource_model_serving_test.go +++ b/serving/resource_model_serving_test.go @@ -629,7 +629,9 @@ func TestModelServingUpdate_RemoveConfigIsNoOp(t *testing.T) { HCL: ` name = "test-endpoint" `, - ExpectedDiff: map[string]*terraform.ResourceAttrDiff{}, + ExpectedDiff: map[string]*terraform.ResourceAttrDiff{ + "provider_config.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + }, }.ApplyNoError(t) } diff --git a/settings/default_namespace_test.go b/settings/default_namespace_test.go index aabfcd27b1..40f43c1520 100644 --- a/settings/default_namespace_test.go +++ b/settings/default_namespace_test.go @@ -55,17 +55,6 @@ func TestAccDefaultNamespaceSetting_ProviderConfig_Mismatched(t *testing.T) { }) } -func TestAccDefaultNamespaceSetting_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: defaultNamespaceSettingTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccDefaultNamespaceSetting_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: defaultNamespaceSettingTemplate(` @@ -95,7 +84,7 @@ func TestAccDefaultNamespaceSetting_ProviderConfig_Match(t *testing.T) { `, workspaceIDStr)), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_default_namespace_setting.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_default_namespace_setting.this", plancheck.ResourceActionNoop), }, }, }) @@ -146,7 +135,7 @@ func TestAccDefaultNamespaceSetting_ProviderConfig_Remove(t *testing.T) { Template: defaultNamespaceSettingTemplate(""), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_default_namespace_setting.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_default_namespace_setting.this", plancheck.ResourceActionNoop), }, }, }) diff --git a/settings/notification_destination_provider_config_test.go b/settings/notification_destination_provider_config_test.go index 00f2f186e9..df8d277283 100644 --- a/settings/notification_destination_provider_config_test.go +++ b/settings/notification_destination_provider_config_test.go @@ -34,17 +34,6 @@ func TestAccNotificationDestination_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccNotificationDestination_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: notificationDestinationProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccNotificationDestination_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: notificationDestinationProviderConfigTemplate(` diff --git a/sharing/provider_provider_config_test.go b/sharing/provider_provider_config_test.go index 73956bf136..9a62315664 100644 --- a/sharing/provider_provider_config_test.go +++ b/sharing/provider_provider_config_test.go @@ -31,17 +31,6 @@ func TestAccProvider_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccProvider_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: providerProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccProvider_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: providerProviderConfigTemplate(` diff --git a/sql/resource_sql_endpoint_test.go b/sql/resource_sql_endpoint_test.go index e784829ee4..1397af8789 100644 --- a/sql/resource_sql_endpoint_test.go +++ b/sql/resource_sql_endpoint_test.go @@ -326,6 +326,7 @@ func TestResourceSQLEndpointUpdateHealthNoDiff(t *testing.T) { }, ExpectedDiff: map[string]*terraform.ResourceAttrDiff{ "state": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "provider_config.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "odbc_params.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "num_clusters": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "num_active_sessions": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, @@ -357,6 +358,7 @@ func TestResourceSQLEndpointUpdateNoAutoTermination(t *testing.T) { ExpectedDiff: map[string]*terraform.ResourceAttrDiff{ "auto_stop_mins": {Old: "120", New: "0", NewComputed: false, NewRemoved: false, RequiresNew: false, Sensitive: false}, "state": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "provider_config.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "odbc_params.#": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "num_clusters": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, "num_active_sessions": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, diff --git a/sql/sql_dashboard_provider_config_test.go b/sql/sql_dashboard_provider_config_test.go index 775ff23e0e..5aed056385 100644 --- a/sql/sql_dashboard_provider_config_test.go +++ b/sql/sql_dashboard_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccSqlDashboard_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSqlDashboard_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: sqlDashboardProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSqlDashboard_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: sqlDashboardProviderConfigTemplate(` diff --git a/sql/sql_global_config_provider_config_test.go b/sql/sql_global_config_provider_config_test.go index aec9b30d1a..7a5a1e5da7 100644 --- a/sql/sql_global_config_provider_config_test.go +++ b/sql/sql_global_config_provider_config_test.go @@ -28,17 +28,6 @@ func TestAccSqlGlobalConfig_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSqlGlobalConfig_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: sqlGlobalConfigProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSqlGlobalConfig_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: sqlGlobalConfigProviderConfigTemplate(` diff --git a/sql/sql_query_provider_config_test.go b/sql/sql_query_provider_config_test.go index 5a03964b38..84a74fbf02 100644 --- a/sql/sql_query_provider_config_test.go +++ b/sql/sql_query_provider_config_test.go @@ -31,17 +31,6 @@ func TestAccSqlQuery_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSqlQuery_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: sqlQueryProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSqlQuery_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: sqlQueryProviderConfigTemplate(` diff --git a/sql/sql_visualization_provider_config_test.go b/sql/sql_visualization_provider_config_test.go index 58975c7302..16d8608a88 100644 --- a/sql/sql_visualization_provider_config_test.go +++ b/sql/sql_visualization_provider_config_test.go @@ -32,17 +32,6 @@ func TestAccSqlVisualization_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSqlVisualization_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: sqlVisualizationProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSqlVisualization_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: sqlVisualizationProviderConfigTemplate(` diff --git a/sql/sql_widget_provider_config_test.go b/sql/sql_widget_provider_config_test.go index cc1236b3db..d7ba95d954 100644 --- a/sql/sql_widget_provider_config_test.go +++ b/sql/sql_widget_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccSqlWidget_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccSqlWidget_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: sqlWidgetProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccSqlWidget_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: sqlWidgetProviderConfigTemplate(` diff --git a/storage/dbfs_file_provider_config_test.go b/storage/dbfs_file_provider_config_test.go index 48ee347ad4..f7ff019a4c 100644 --- a/storage/dbfs_file_provider_config_test.go +++ b/storage/dbfs_file_provider_config_test.go @@ -30,17 +30,6 @@ func TestAccDbfsFile_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccDbfsFile_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: dbfsFileProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccDbfsFile_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: dbfsFileProviderConfigTemplate(` diff --git a/storage/mount_provider_config_test.go b/storage/mount_provider_config_test.go index 193ed4deef..9f02cce706 100644 --- a/storage/mount_provider_config_test.go +++ b/storage/mount_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccMount_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccMount_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: mountProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccMount_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: mountProviderConfigTemplate(` diff --git a/tokens/obo_token_provider_config_test.go b/tokens/obo_token_provider_config_test.go index 4b7065c8e0..142852776b 100644 --- a/tokens/obo_token_provider_config_test.go +++ b/tokens/obo_token_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccOboToken_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccOboToken_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: oboTokenProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccOboToken_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: oboTokenProviderConfigTemplate(` diff --git a/tokens/service_principal_secret_provider_config_test.go b/tokens/service_principal_secret_provider_config_test.go index ea2ec170d3..9a027954d1 100644 --- a/tokens/service_principal_secret_provider_config_test.go +++ b/tokens/service_principal_secret_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccServicePrincipalSecret_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccServicePrincipalSecret_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: servicePrincipalSecretProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccServicePrincipalSecret_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: servicePrincipalSecretProviderConfigTemplate(` diff --git a/tokens/token_provider_config_test.go b/tokens/token_provider_config_test.go index 8a90b4cabe..b2c5d016a5 100644 --- a/tokens/token_provider_config_test.go +++ b/tokens/token_provider_config_test.go @@ -29,17 +29,6 @@ func TestAccToken_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccToken_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: tokenProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccToken_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: tokenProviderConfigTemplate(` diff --git a/workspace/data_notebook_acc_test.go b/workspace/data_notebook_acc_test.go index d72986ae29..56c5a3d8cf 100644 --- a/workspace/data_notebook_acc_test.go +++ b/workspace/data_notebook_acc_test.go @@ -50,17 +50,6 @@ func TestAccNotebookData_ProviderConfig_Mismatched(t *testing.T) { }) } -func TestAccNotebookData_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: notebookDataTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccNotebookData_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: notebookDataTemplate(` diff --git a/workspace/notebook_test.go b/workspace/notebook_test.go index 0fab68d98f..d026fa32fe 100644 --- a/workspace/notebook_test.go +++ b/workspace/notebook_test.go @@ -118,17 +118,6 @@ func TestAccNotebook_ProviderConfig_Mismatched(t *testing.T) { }) } -func TestAccNotebook_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: notebookTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccNotebook_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: notebookTemplate(` @@ -158,7 +147,7 @@ func TestAccNotebook_ProviderConfig_Match(t *testing.T) { `, workspaceIDStr)), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_notebook.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_notebook.this", plancheck.ResourceActionNoop), }, }, }) @@ -209,7 +198,7 @@ func TestAccNotebook_ProviderConfig_Remove(t *testing.T) { Template: notebookTemplate(""), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("databricks_notebook.this", plancheck.ResourceActionUpdate), + plancheck.ExpectResourceAction("databricks_notebook.this", plancheck.ResourceActionNoop), }, }, }) diff --git a/workspace/workspace_conf_provider_config_test.go b/workspace/workspace_conf_provider_config_test.go index 9431589489..38e12f38c2 100644 --- a/workspace/workspace_conf_provider_config_test.go +++ b/workspace/workspace_conf_provider_config_test.go @@ -28,17 +28,6 @@ func TestAccWorkspaceConf_ProviderConfig_Invalid(t *testing.T) { }) } -func TestAccWorkspaceConf_ProviderConfig_Required(t *testing.T) { - acceptance.WorkspaceLevel(t, acceptance.Step{ - Template: workspaceConfProviderConfigTemplate(` - provider_config { - } - `), - ExpectError: regexp.MustCompile(`The argument "workspace_id" is required, but no definition was found.`), - PlanOnly: true, - }) -} - func TestAccWorkspaceConf_ProviderConfig_EmptyID(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: workspaceConfProviderConfigTemplate(`