diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 880cf8c743..95cadec169 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -5,6 +5,7 @@ ### New Features and Improvements ### Bug Fixes +* Bug fixed in `databricks_mws_workspaces` resource due to migration of resource to Go SDK ### Documentation diff --git a/docs/resources/mws_workspaces.md b/docs/resources/mws_workspaces.md index df833388d9..a252302da1 100644 --- a/docs/resources/mws_workspaces.md +++ b/docs/resources/mws_workspaces.md @@ -323,7 +323,6 @@ The following arguments are available: * `deployment_name` - (Optional) part of URL as in `https://-.cloud.databricks.com`. Deployment name cannot be used until a deployment name prefix is defined. Please contact your Databricks representative. Once a new deployment prefix is added/updated, it only will affect the new workspaces created. * `workspace_name` - name of the workspace, will appear on UI. * `network_id` - (Optional) `network_id` from [networks](mws_networks.md). -* `credentials_id` - (AWS only) ID of the workspace's credential configuration object. * `aws_region` - (AWS only) region of VPC. * `storage_configuration_id` - (AWS only)`storage_configuration_id` from [storage configuration](mws_storage_configurations.md). * `managed_services_customer_managed_key_id` - (Optional) `customer_managed_key_id` from [customer managed keys](mws_customer_managed_keys.md) with `use_cases` set to `MANAGED_SERVICES`. This is used to encrypt the workspace's notebook and secret data in the control plane. @@ -332,9 +331,7 @@ The following arguments are available: * `cloud_resource_container` - (GCP only) A block that specifies GCP workspace configurations, consisting of following blocks: * `gcp` - A block that consists of the following field: * `project_id` - The Google Cloud project ID, which the workspace uses to instantiate cloud resources for your workspace. -* `gcp_managed_network_config` - (GCP only) A block that describes the network configuration for workspaces with Databricks-managed networks. - * `subnet_cidr` - The IP range from which to allocate GKE cluster nodes. No bigger than `/9` and no smaller than `/29`. -* `gke_config` - (GCP only, deprecated) A block that specifies GKE configuration for the Databricks workspace: +* `gke_config` - (GCP only) A block that specifies GKE configuration for the Databricks workspace: * `connectivity_type`: Specifies the network connectivity types for the GKE nodes and the GKE master network. Possible values are: `PRIVATE_NODE_PUBLIC_MASTER`, `PUBLIC_NODE_PUBLIC_MASTER`. * `master_ip_range`: The IP range from which to allocate GKE cluster master resources. This field will be ignored if GKE private cluster is not enabled. It must be exactly as big as `/28`. * `private_access_settings_id` - (Optional) Canonical unique identifier of [databricks_mws_private_access_settings](mws_private_access_settings.md) in Databricks Account. @@ -354,25 +351,24 @@ You can specify a `token` block in the body of the workspace resource, so that T On AWS, the following arguments could be modified after the workspace is running: -* `credentials_id` -* `custom_tags` -* `managed_services_customer_managed_key_id` * `network_id` - Modifying [networks on running workspaces](mws_networks.md#modifying-networks-on-running-workspaces-aws-only) would require three separate `terraform apply` steps. -* `private_access_settings_id` +* `credentials_id` * `storage_customer_managed_key_id` +* `private_access_settings_id` +* `custom_tags` ## Attribute Reference In addition to all arguments above, the following attributes are exported: * `id` - (String) Canonical unique identifier for the workspace, of the format `/` -* `creation_time` - (Integer) time when workspace was created -* `custom_tags` - (Map) Custom Tags (if present) added to workspace -* `gcp_workspace_sa` - (String, GCP only) identifier of a service account created for the workspace in form of `db-@prod-gcp-.iam.gserviceaccount.com` * `workspace_id` - (String) workspace id * `workspace_status_message` - (String) updates on workspace status * `workspace_status` - (String) workspace status +* `creation_time` - (Integer) time when workspace was created * `workspace_url` - (String) URL of the workspace +* `custom_tags` - (Map) Custom Tags (if present) added to workspace +* `gcp_workspace_sa` - (String, GCP only) identifier of a service account created for the workspace in form of `db-@prod-gcp-.iam.gserviceaccount.com` ## Timeouts diff --git a/internal/acceptance/expect_not_destroyed.go b/internal/acceptance/expect_not_destroyed.go deleted file mode 100644 index 430437e035..0000000000 --- a/internal/acceptance/expect_not_destroyed.go +++ /dev/null @@ -1,29 +0,0 @@ -package acceptance - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-testing/plancheck" -) - -type expectNotDestroyed struct { - addr string -} - -func ExpectNotDestroyed(addr string) expectNotDestroyed { - return expectNotDestroyed{addr: addr} -} - -func (e expectNotDestroyed) CheckPlan(ctx context.Context, req plancheck.CheckPlanRequest, resp *plancheck.CheckPlanResponse) { - for _, resource := range req.Plan.ResourceChanges { - if resource.Address != e.addr { - continue - } - actions := resource.Change.Actions - if actions.DestroyBeforeCreate() || actions.CreateBeforeDestroy() || actions.Delete() { - resp.Error = fmt.Errorf("resource %s is marked for destruction", e.addr) - return - } - } -} diff --git a/internal/providers/pluginfw/products/dashboards/data_dashboards_acc_test.go b/internal/providers/pluginfw/products/dashboards/data_dashboards_acc_test.go index 806cb0ac7a..3c5cef0a15 100644 --- a/internal/providers/pluginfw/products/dashboards/data_dashboards_acc_test.go +++ b/internal/providers/pluginfw/products/dashboards/data_dashboards_acc_test.go @@ -27,7 +27,7 @@ func TestAccDashboardsDataSource(t *testing.T) { acceptance.WorkspaceLevel(t, acceptance.Step{ Template: ` resource "databricks_dashboard" "dashboard" { - display_name = "New Dashboard" + display_name = "dashboard-{var.RANDOM}" warehouse_id = "{env.TEST_DEFAULT_WAREHOUSE_ID}" serialized_dashboard = "{\"pages\":[{\"name\":\"new_name\",\"displayName\":\"New Page\"}]}" embed_credentials = false // Optional diff --git a/internal/service/provisioning_tf/legacy_model.go b/internal/service/provisioning_tf/legacy_model.go index 38994beccd..a47ef38e2f 100755 --- a/internal/service/provisioning_tf/legacy_model.go +++ b/internal/service/provisioning_tf/legacy_model.go @@ -2789,7 +2789,7 @@ func (c Network_SdkV2) ApplySchemaCustomizations(attrs map[string]tfschema.Attri attrs["vpc_endpoints"] = attrs["vpc_endpoints"].SetComputed() attrs["vpc_endpoints"] = attrs["vpc_endpoints"].(tfschema.ListNestedAttributeBuilder).AddValidator(listvalidator.SizeAtMost(1)).(tfschema.AttributeBuilder) attrs["vpc_id"] = attrs["vpc_id"].SetOptional() - attrs["vpc_status"] = attrs["vpc_status"].SetOptional() + attrs["vpc_status"] = attrs["vpc_status"].SetComputed() attrs["warning_messages"] = attrs["warning_messages"].SetComputed() attrs["workspace_id"] = attrs["workspace_id"].SetOptional() @@ -4166,7 +4166,7 @@ func (c Workspace_SdkV2) ApplySchemaCustomizations(attrs map[string]tfschema.Att attrs["storage_customer_managed_key_id"] = attrs["storage_customer_managed_key_id"].SetOptional() attrs["workspace_id"] = attrs["workspace_id"].SetOptional() attrs["workspace_name"] = attrs["workspace_name"].SetOptional() - attrs["workspace_status"] = attrs["workspace_status"].SetOptional() + attrs["workspace_status"] = attrs["workspace_status"].SetComputed() attrs["workspace_status_message"] = attrs["workspace_status_message"].SetComputed() return attrs diff --git a/mws/data_mws_workspaces.go b/mws/data_mws_workspaces.go index 30792cad18..4c2d48a9a4 100755 --- a/mws/data_mws_workspaces.go +++ b/mws/data_mws_workspaces.go @@ -16,17 +16,13 @@ func DataSourceMwsWorkspaces() common.Resource { if c.Config.AccountID == "" { return fmt.Errorf("provider block is missing `account_id` property") } - a, err := c.AccountClient() - if err != nil { - return err - } - workspaces, err := a.Workspaces.List(ctx) + workspaces, err := NewWorkspacesAPI(ctx, c).List(c.Config.AccountID) if err != nil { return err } data.Ids = map[string]int64{} for _, v := range workspaces { - data.Ids[v.WorkspaceName] = v.WorkspaceId + data.Ids[v.WorkspaceName] = v.WorkspaceID } return nil }) diff --git a/mws/data_mws_workspaces_test.go b/mws/data_mws_workspaces_test.go index 9f4a65be99..1284b9fc9c 100755 --- a/mws/data_mws_workspaces_test.go +++ b/mws/data_mws_workspaces_test.go @@ -1,28 +1,29 @@ package mws import ( - "errors" "testing" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/service/provisioning" "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/mock" ) func TestDataSourceMwsWorkspaces(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().List(mock.Anything).Return([]provisioning.Workspace{ - { - WorkspaceName: "bcd", - WorkspaceId: 123, - }, - { - WorkspaceName: "def", - WorkspaceId: 456, + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces", + + Response: []Workspace{ + { + WorkspaceName: "bcd", + WorkspaceID: 123, + }, + { + WorkspaceName: "def", + WorkspaceID: 456, + }, }, - }, nil) + }, }, AccountID: "abc", Resource: DataSourceMwsWorkspaces(), @@ -37,12 +38,10 @@ func TestDataSourceMwsWorkspaces(t *testing.T) { }) } -func TestDataSourceMwsWorkspaces_Error(t *testing.T) { +func TestCatalogsData_Error(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().List(mock.Anything).Return(nil, errors.New("i'm a teapot")) - }, AccountID: "abc", + Fixtures: qa.HTTPFailures, Resource: DataSourceMwsWorkspaces(), Read: true, NonWritable: true, @@ -52,8 +51,13 @@ func TestDataSourceMwsWorkspaces_Error(t *testing.T) { func TestDataSourceMwsWorkspaces_Empty(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().List(mock.Anything).Return([]provisioning.Workspace{}, nil) + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces", + + Response: []Workspace{}, + }, }, AccountID: "abc", Resource: DataSourceMwsWorkspaces(), diff --git a/mws/mws_workspaces_test.go b/mws/mws_workspaces_test.go index 6e49d84c29..9c3dc2bbaa 100644 --- a/mws/mws_workspaces_test.go +++ b/mws/mws_workspaces_test.go @@ -10,16 +10,14 @@ import ( "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/logger" - "github.com/databricks/databricks-sdk-go/service/provisioning" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/internal/acceptance" "github.com/databricks/terraform-provider-databricks/internal/providers" "github.com/databricks/terraform-provider-databricks/internal/providers/sdkv2" + "github.com/databricks/terraform-provider-databricks/tokens" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stretchr/testify/assert" ) @@ -78,137 +76,153 @@ func TestMwsAccWorkspaces(t *testing.T) { }) } -func TestMwsAccWorkspaces_TokenUpdate(t *testing.T) { - tokenUpdateTemplate := func(token, customTags string) string { - tokenBlock := `` - if token != "" { - tokenBlock = fmt.Sprintf(` - token { - %s - }`, token) - } - customTagsBlock := `` - if customTags != "" { - customTagsBlock = fmt.Sprintf(` - custom_tags = { - %s - }`, customTags) - } - return fmt.Sprintf(` +func TestMwsAccWorkspacesTokenUpdate(t *testing.T) { + acceptance.AccountLevel(t, acceptance.Step{ + Template: ` resource "databricks_mws_credentials" "this" { account_id = "{env.DATABRICKS_ACCOUNT_ID}" - credentials_name = "credentials-ws-{var.STICKY_RANDOM}" + credentials_name = "credentials-ws-{var.RANDOM}" role_arn = "{env.TEST_CROSSACCOUNT_ARN}" } + resource "databricks_mws_customer_managed_keys" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + aws_key_info { + key_arn = "{env.TEST_MANAGED_KMS_KEY_ARN}" + key_alias = "{env.TEST_MANAGED_KMS_KEY_ALIAS}" + } + use_cases = ["MANAGED_SERVICES"] + } resource "databricks_mws_storage_configurations" "this" { account_id = "{env.DATABRICKS_ACCOUNT_ID}" - storage_configuration_name = "storage-ws-{var.STICKY_RANDOM}" + storage_configuration_name = "storage-ws-{var.RANDOM}" bucket_name = "{env.TEST_ROOT_BUCKET}" } + resource "databricks_mws_networks" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + network_name = "network-ws-{var.RANDOM}" + vpc_id = "{env.TEST_VPC_ID}" + subnet_ids = [ + "{env.TEST_SUBNET_PRIVATE}", + "{env.TEST_SUBNET_PRIVATE2}", + ] + security_group_ids = [ + "{env.TEST_SECURITY_GROUP}", + ] + } resource "databricks_mws_workspaces" "this" { account_id = "{env.DATABRICKS_ACCOUNT_ID}" - workspace_name = "terra-{var.STICKY_RANDOM}" + workspace_name = "terra-{var.RANDOM}" aws_region = "{env.AWS_REGION}" + network_id = databricks_mws_networks.this.network_id credentials_id = databricks_mws_credentials.this.credentials_id storage_configuration_id = databricks_mws_storage_configurations.this.storage_configuration_id + managed_services_customer_managed_key_id = databricks_mws_customer_managed_keys.this.customer_managed_key_id - %s - %s - }`, tokenBlock, customTagsBlock) - } - - checkWorkspace := func(f func(instanceState map[string]string, w *databricks.WorkspaceClient) error) func(*terraform.State) error { - return func(s *terraform.State) error { - state, ok := s.RootModule().Resources["databricks_mws_workspaces.this"] - if !ok { - return fmt.Errorf("resource not found in state") + token { + comment = "test foo" } - a := databricks.Must(databricks.NewAccountClient()) - ctx := context.Background() - workspace, err := a.Workspaces.Get(ctx, provisioning.GetWorkspaceRequest{WorkspaceId: common.MustInt64(state.Primary.Attributes["workspace_id"])}) - assert.NoError(t, err) + }`, + Check: acceptance.ResourceCheckWithState("databricks_mws_workspaces.this", + func(ctx context.Context, client *common.DatabricksClient, state *terraform.InstanceState) error { + workspaceUrl, ok := state.Attributes["workspace_url"] + assert.True(t, ok, "workspace_url is absent from databricks_mws_workspaces instance state") - w, err := a.GetWorkspaceClient(*workspace) - assert.NoError(t, err) + workspaceClient, err := client.ClientForHost(ctx, workspaceUrl) + assert.NoError(t, err) - return f(state.Primary.Attributes, w) - } - } + tokensAPI := tokens.NewTokensAPI(ctx, workspaceClient) + tokens, err := tokensAPI.List() + assert.NoError(t, err) - checkTokenExists := checkWorkspace(func(instanceState map[string]string, w *databricks.WorkspaceClient) error { - tokenId := instanceState["token.0.token_id"] - assert.NotEmpty(t, tokenId) - tokens := w.Tokens.List(context.Background()) - ctx := context.Background() - for tokens.HasNext(ctx) { - token, err := tokens.Next(ctx) - if err != nil { - return fmt.Errorf("error fetching tokens: %w", err) - } - if token.TokenId == tokenId { + foundFoo := false + foundBar := false + for _, token := range tokens { + if token.Comment == "test foo" { + foundFoo = true + } + if token.Comment == "test bar" { + foundBar = true + } + } + assert.True(t, foundFoo) + assert.False(t, foundBar) return nil + }), + }, + acceptance.Step{ + Template: ` + resource "databricks_mws_credentials" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + credentials_name = "credentials-ws-{var.RANDOM}" + role_arn = "{env.TEST_CROSSACCOUNT_ARN}" + } + resource "databricks_mws_customer_managed_keys" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + aws_key_info { + key_arn = "{env.TEST_MANAGED_KMS_KEY_ARN}" + key_alias = "{env.TEST_MANAGED_KMS_KEY_ALIAS}" } + use_cases = ["MANAGED_SERVICES"] } - return fmt.Errorf("token %s not found", tokenId) - }) + resource "databricks_mws_storage_configurations" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + storage_configuration_name = "storage-ws-{var.RANDOM}" + bucket_name = "{env.TEST_ROOT_BUCKET}" + } + resource "databricks_mws_networks" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + network_name = "network-ws-{var.RANDOM}" + vpc_id = "{env.TEST_VPC_ID}" + subnet_ids = [ + "{env.TEST_SUBNET_PRIVATE}", + "{env.TEST_SUBNET_PRIVATE2}", + ] + security_group_ids = [ + "{env.TEST_SECURITY_GROUP}", + ] + } + resource "databricks_mws_workspaces" "this" { + account_id = "{env.DATABRICKS_ACCOUNT_ID}" + workspace_name = "terra-{var.RANDOM}" + aws_region = "{env.AWS_REGION}" + + network_id = databricks_mws_networks.this.network_id + credentials_id = databricks_mws_credentials.this.credentials_id + storage_configuration_id = databricks_mws_storage_configurations.this.storage_configuration_id + managed_services_customer_managed_key_id = databricks_mws_customer_managed_keys.this.customer_managed_key_id - var oldTokenId string - acceptance.AccountLevel(t, acceptance.Step{ - Template: tokenUpdateTemplate(`comment = "test foo"`, ""), - Check: checkTokenExists, - }, acceptance.Step{ - // Updating the comment causes the old token to be deleted and a new one to be created - Template: tokenUpdateTemplate(`comment = "test bar"`, ""), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, - Check: resource.ComposeAggregateTestCheckFunc( - checkTokenExists, - // Capture the token ID at the end of this step to verify it is not changed in future steps. - func(s *terraform.State) error { - state, ok := s.RootModule().Resources["databricks_mws_workspaces.this"] - if !ok { - return fmt.Errorf("resource not found in state") - } - instanceState := state.Primary.Attributes - oldTokenId = instanceState["token.0.token_id"] - return nil - }, - ), - }, acceptance.Step{ - // Modifying the tags doesn't change the token but does modify the workspace. - Template: tokenUpdateTemplate(`comment = "test bar"`, `"Key" = "Value"`), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, - Check: func(s *terraform.State) error { - state, ok := s.RootModule().Resources["databricks_mws_workspaces.this"] - if !ok { - return fmt.Errorf("resource not found in state") - } - instanceState := state.Primary.Attributes - assert.Equal(t, instanceState["custom_tags.Key"], "Value") - assert.Equal(t, instanceState["token.0.token_id"], oldTokenId) - return nil - }, - }, acceptance.Step{ - // It is also possible to modify the token comment and tags at the same time. - Template: tokenUpdateTemplate(`comment = "test quux"`, `"Key" = "Value2"`), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, - Check: func(s *terraform.State) error { - state, ok := s.RootModule().Resources["databricks_mws_workspaces.this"] - if !ok { - return fmt.Errorf("resource not found in state") + token { + comment = "test bar" } - instanceState := state.Primary.Attributes - assert.Equal(t, instanceState["custom_tags.Key"], "Value2") - assert.NotEqual(t, instanceState["token.0.token_id"], oldTokenId) - return nil - }, - }) + }`, + Check: acceptance.ResourceCheckWithState("databricks_mws_workspaces.this", + func(ctx context.Context, client *common.DatabricksClient, state *terraform.InstanceState) error { + workspaceUrl, ok := state.Attributes["workspace_url"] + assert.True(t, ok, "workspace_url is absent from databricks_mws_workspaces instance state") + + workspaceClient, err := client.ClientForHost(ctx, workspaceUrl) + assert.NoError(t, err) + + tokensAPI := tokens.NewTokensAPI(ctx, workspaceClient) + tokens, err := tokensAPI.List() + assert.NoError(t, err) + + foundFoo := false + foundBar := false + for _, token := range tokens { + if token.Comment == "test foo" { + foundFoo = true + } + if token.Comment == "test bar" { + foundBar = true + } + } + assert.False(t, foundFoo) + assert.True(t, foundBar) + return nil + }), + }) } func TestMwsAccGcpWorkspaces(t *testing.T) { @@ -306,7 +320,6 @@ func TestMwsAccGcpPscWorkspaces(t *testing.T) { } func TestMwsAccAwsChangeToServicePrincipal(t *testing.T) { - acceptance.LoadDebugEnvIfRunsFromIDE(t, "account") if !acceptance.IsAws(t) { acceptance.Skipf(t)("TestMwsAccAwsChangeToServicePrincipal should only run on AWS") } @@ -422,26 +435,17 @@ func TestMwsAccAwsChangeToServicePrincipal(t *testing.T) { // Tolerate existing token Template: workspaceTemplate(`token { comment = "Test {var.STICKY_RANDOM}" }`) + servicePrincipal, ProtoV6ProviderFactories: providerFactory, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, }, acceptance.Step{ // Allow the token to be removed Template: workspaceTemplate(``) + servicePrincipal, ProtoV6ProviderFactories: providerFactory, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, }, acceptance.Step{ // Fail when adding the token back Template: workspaceTemplate(`token { comment = "Test {var.STICKY_RANDOM}" }`) + servicePrincipal, ProtoV6ProviderFactories: providerFactory, ExpectError: regexp.MustCompile(`cannot create token: the principal used by Databricks \(client ID .*\) is not authorized to create a token in this workspace`), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{acceptance.ExpectNotDestroyed("databricks_mws_workspaces.this")}, - }, }, acceptance.Step{ // Use the original provider for a final step to clean up the newly created service principal - Template: workspaceTemplate(`token { comment = "Test {var.STICKY_RANDOM}" }`) + servicePrincipal, + Template: workspaceTemplate(``) + servicePrincipal, }) } diff --git a/mws/resource_mws_ncc_binding_test.go b/mws/resource_mws_ncc_binding_test.go index 82bcaea30e..4c7af575c6 100644 --- a/mws/resource_mws_ncc_binding_test.go +++ b/mws/resource_mws_ncc_binding_test.go @@ -2,33 +2,30 @@ package mws import ( "testing" - "time" - "github.com/databricks/databricks-sdk-go/experimental/mocks" "github.com/databricks/databricks-sdk-go/service/provisioning" "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/mock" ) -var mockWorkspace = &provisioning.Workspace{ - WorkspaceId: 123456789, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, -} - -var mockWaiter = &provisioning.WaitGetWorkspaceRunning[struct{}]{ - WorkspaceId: 123456789, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, -} - func TestResourceNccBindingCreate(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 123456789, - NetworkConnectivityConfigId: "ncc_id", - }).Return(mockWaiter, nil) + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/123456789", + ExpectedRequest: provisioning.UpdateWorkspaceRequest{ + NetworkConnectivityConfigId: "ncc_id", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/123456789?", + Response: Workspace{ + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceID: 123456789, + }, + }, }, Resource: ResourceMwsNccBinding(), AccountID: "abc", @@ -42,11 +39,23 @@ func TestResourceNccBindingCreate(t *testing.T) { func TestResourceNccBindingUpdate(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 123456789, - NetworkConnectivityConfigId: "new_ncc_id", - }).Return(mockWaiter, nil) + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/123456789", + ExpectedRequest: provisioning.UpdateWorkspaceRequest{ + NetworkConnectivityConfigId: "new_ncc_id", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/123456789?", + Response: Workspace{ + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceID: 123456789, + }, + }, }, Resource: ResourceMwsNccBinding(), AccountID: "abc", diff --git a/mws/resource_mws_workspaces.go b/mws/resource_mws_workspaces.go index e9ec0e78ab..eacf73d0d8 100644 --- a/mws/resource_mws_workspaces.go +++ b/mws/resource_mws_workspaces.go @@ -3,24 +3,25 @@ package mws import ( "bytes" "context" + "encoding/json" "errors" "fmt" "log" "net" + "net/url" "strings" "time" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/retries" - "github.com/databricks/databricks-sdk-go/service/provisioning" - "github.com/databricks/databricks-sdk-go/service/settings" "github.com/databricks/terraform-provider-databricks/common" "github.com/databricks/terraform-provider-databricks/internal/docs" + "github.com/databricks/terraform-provider-databricks/tokens" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) // DefaultProvisionTimeout is the amount of minutes terraform will wait @@ -28,56 +29,328 @@ import ( // this may help with local DNS cache issues. const DefaultProvisionTimeout = 20 * time.Minute -// workspaceReachableRequestTimeout is the amount of seconds to wait when polling the -// newly created workspace's SCIM Me endpoint to check if the workspace is reachable. -const workspaceReachableRequestTimeout = 10 * time.Second - -// workspace is the object that contains all the information for deploying a workspace -type workspace struct { - provisioning.Workspace - CustomerManagedKeyID string `json:"customer_managed_key_id,omitempty"` // just for compatibility, will be removed - GcpWorkspaceSa string `json:"gcp_workspace_sa" tf:"computed"` - Token *Token `json:"token,omitempty"` - WorkspaceURL string `json:"workspace_url,omitempty" tf:"computed"` +// NewWorkspacesAPI creates MWSWorkspacesAPI instance from provider meta +func NewWorkspacesAPI(ctx context.Context, m any) WorkspacesAPI { + return WorkspacesAPI{m.(*common.DatabricksClient), ctx} } -// wait for DNS caches to refresh, as sometimes we cannot make -// API calls to new workspaces immediately after it's created -func verifyWorkspaceReachable(ctx context.Context, w *databricks.WorkspaceClient) error { - return retries.New[struct{}](retries.WithRetryFunc(func(err error) bool { - var dnsError *net.DNSError - if errors.As(err, &dnsError) { - log.Printf("[INFO] workspace is not yet reachable: %s", dnsError) - // expected to retry on: dial tcp: lookup XXX: no such host - return true +// WorkspacesAPI exposes the mws workspaces API +type WorkspacesAPI struct { + client *common.DatabricksClient + context context.Context +} + +// List of workspace statuses for provisioning the workspace +const ( + WorkspaceStatusNotProvisioned = "NOT_PROVISIONED" + WorkspaceStatusProvisioning = "PROVISIONING" + WorkspaceStatusRunning = "RUNNING" + WorkspaceStatusFailed = "FAILED" + WorkspaceStatusCanceled = "CANCELLED" +) + +// WorkspaceStatusesNonRunnable is a list of statuses in which the workspace is not runnable +var WorkspaceStatusesNonRunnable = []string{WorkspaceStatusCanceled, WorkspaceStatusFailed} + +type GCP struct { + ProjectID string `json:"project_id"` +} + +type CloudResourceContainer struct { + GCP *GCP `json:"gcp"` +} + +type GCPManagedNetworkConfig struct { + SubnetCIDR string `json:"subnet_cidr" tf:"force_new"` + GKEClusterPodIPRange string `json:"gke_cluster_pod_ip_range,omitempty"` + GKEClusterServiceIPRange string `json:"gke_cluster_service_ip_range,omitempty"` +} + +type GkeConfig struct { + ConnectivityType string `json:"connectivity_type,omitempty"` + MasterIPRange string `json:"master_ip_range,omitempty"` +} + +type externalCustomerInfo struct { + CustomerName string `json:"customer_name"` + AuthoritativeUserEmail string `json:"authoritative_user_email"` + AuthoritativeUserFullName string `json:"authoritative_user_full_name"` +} + +// Workspace is the object that contains all the information for deploying a workspace +type Workspace struct { + AccountID string `json:"account_id"` + WorkspaceName string `json:"workspace_name"` + DeploymentName string `json:"deployment_name,omitempty"` + AwsRegion string `json:"aws_region,omitempty"` // required for AWS, not allowed for GCP + CredentialsID string `json:"credentials_id,omitempty"` // required for AWS, not allowed for GCP + CustomerManagedKeyID string `json:"customer_managed_key_id,omitempty"` // just for compatibility, will be removed + StorageConfigurationID string `json:"storage_configuration_id,omitempty"` // required for AWS, not allowed for GCP + ManagedServicesCustomerManagedKeyID string `json:"managed_services_customer_managed_key_id,omitempty"` + StorageCustomerManagedKeyID string `json:"storage_customer_managed_key_id,omitempty"` + PricingTier string `json:"pricing_tier,omitempty" tf:"computed"` + PrivateAccessSettingsID string `json:"private_access_settings_id,omitempty"` + NetworkID string `json:"network_id,omitempty" tf:"suppress_diff"` + IsNoPublicIPEnabled bool `json:"is_no_public_ip_enabled" tf:"optional,default:true"` + WorkspaceID int64 `json:"workspace_id,omitempty" tf:"computed"` + WorkspaceURL string `json:"workspace_url,omitempty" tf:"computed"` + WorkspaceStatus string `json:"workspace_status,omitempty" tf:"computed"` + WorkspaceStatusMessage string `json:"workspace_status_message,omitempty" tf:"computed"` + CreationTime int64 `json:"creation_time,omitempty" tf:"computed"` + ExternalCustomerInfo *externalCustomerInfo `json:"external_customer_info,omitempty"` + CloudResourceBucket *CloudResourceContainer `json:"cloud_resource_container,omitempty"` + GCPManagedNetworkConfig *GCPManagedNetworkConfig `json:"gcp_managed_network_config,omitempty" tf:"suppress_diff"` + GkeConfig *GkeConfig `json:"gke_config,omitempty" tf:"suppress_diff"` + Cloud string `json:"cloud,omitempty" tf:"computed"` + Location string `json:"location,omitempty"` + CustomTags map[string]string `json:"custom_tags,omitempty"` // Optional for AWS, not allowed for GCP +} + +// this type alias hack is required for Marshaller to work without an infinite loop +type aWorkspace Workspace + +// MarshalJSON is required to overcome the limitations of `omitempty` usage with reflect_resource.go +// for workspace creation in Accounts API for AWS and GCP. It exits early on AWS and picks only +// the relevant fields for GCP. +func (w *Workspace) MarshalJSON() ([]byte, error) { + if w.Cloud != "gcp" { + return json.Marshal(aWorkspace(*w)) + } + workspaceCreationRequest := map[string]any{ + "account_id": w.AccountID, + "cloud": w.Cloud, + "cloud_resource_container": w.CloudResourceBucket, + "location": w.Location, + "workspace_name": w.WorkspaceName, + } + if w.NetworkID != "" { + workspaceCreationRequest["network_id"] = w.NetworkID + } + if w.PrivateAccessSettingsID != "" { + workspaceCreationRequest["private_access_settings_id"] = w.PrivateAccessSettingsID + } + if w.GkeConfig != nil { + workspaceCreationRequest["gke_config"] = w.GkeConfig + } + if w.GCPManagedNetworkConfig != nil { + workspaceCreationRequest["gcp_managed_network_config"] = w.GCPManagedNetworkConfig + } + if w.ManagedServicesCustomerManagedKeyID != "" { + workspaceCreationRequest["managed_services_customer_managed_key_id"] = w.ManagedServicesCustomerManagedKeyID + } + if w.StorageCustomerManagedKeyID != "" { + workspaceCreationRequest["storage_customer_managed_key_id"] = w.StorageCustomerManagedKeyID + } + return json.Marshal(workspaceCreationRequest) +} + +// Create deploys the workspace and waits till it's properly running. +// In case of error, it removes the failed deployment and returns the message +func (a WorkspacesAPI) Create(ws *Workspace, timeout time.Duration) error { + if a.client.IsGcp() { + ws.Cloud = "gcp" + } + workspacesAPIPath := fmt.Sprintf("/accounts/%s/workspaces", ws.AccountID) + err := a.client.Post(a.context, workspacesAPIPath, ws, &ws) + if err != nil { + return err + } + if err = a.WaitForRunning(*ws, timeout); err != nil { + log.Printf("[ERROR] Deleting failed workspace: %s", err) + if derr := a.Delete(ws.AccountID, fmt.Sprintf("%d", ws.WorkspaceID)); derr != nil { + return fmt.Errorf("%s - %s", err, derr) } - return false - })).Wait(ctx, func(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, workspaceReachableRequestTimeout) - defer cancel() - _, err := w.CurrentUser.Me(ctx) return err - }) + } + if ws.WorkspaceURL == "" { + // WorkspaceURL is computed, yet very important field + host := generateWorkspaceHostname(a.client, *ws) + ws.WorkspaceURL = fmt.Sprintf("https://%s", host) + } + return nil } -func explainWorkspaceFailure(ctx context.Context, a *databricks.AccountClient, workspace *provisioning.Workspace) error { - errorBase := fmt.Sprintf("workspace status message: %s", workspace.WorkspaceStatusMessage) - if workspace.NetworkId == "" { - return errors.New(errorBase) +// generateWorkspaceHostname computes the hostname for the specified workspace, +// given the account console hostname. +func generateWorkspaceHostname(client *common.DatabricksClient, ws Workspace) string { + u, err := url.Parse(client.Config.Host) + if err != nil { + // Fallback. + log.Printf("[WARN] Unable to parse URL from client host: %v", err) + return ws.DeploymentName + ".cloud.databricks.com" + } + + // We expect the account console hostname to be of the form `accounts.foo[.bar]...` + // The workspace hostname can be generated by replacing `accounts` with the deployment name. + // If the hostname is an IP address, we're in testing mode and do fallback. + chunks := strings.Split(u.Hostname(), ".") + if len(chunks) == 0 || net.ParseIP(u.Hostname()) != nil { + // Fallback. + log.Printf("[WARN] Unable to split client host: %v", u.Hostname()) + return ws.DeploymentName + ".cloud.databricks.com" } - network, err := a.Networks.Get(ctx, provisioning.GetNetworkRequest{NetworkId: workspace.NetworkId}) + chunks[0] = ws.DeploymentName + return strings.Join(chunks, ".") +} + +func (a WorkspacesAPI) verifyWorkspaceReachable(ws Workspace) *resource.RetryError { + ctx, cancel := context.WithTimeout(a.context, 10*time.Second) + defer cancel() + // wait for DNS caches to refresh, as sometimes we cannot make + // API calls to new workspaces immediately after it's created + wsClient, err := a.client.ClientForHost(a.context, ws.WorkspaceURL) if err != nil { - return fmt.Errorf("%s; network error message: cannot read network: %w", errorBase, err) + return resource.NonRetryableError(err) + } + // make a request to SCIM API, just to verify there are no errors + var response map[string]any + err = wsClient.Get(ctx, "/preview/scim/v2/Me", nil, &response) + var dnsError *net.DNSError + if errors.As(err, &dnsError) { + err = fmt.Errorf("workspace %s is not yet reachable: %s", + ws.WorkspaceURL, dnsError) + log.Printf("[INFO] %s", err) + // expected to retry on: dial tcp: lookup XXX: no such host + return resource.RetryableError(err) + } + return nil +} + +func (a WorkspacesAPI) explainWorkspaceFailure(ws Workspace) error { + if ws.NetworkID == "" { + return errors.New(ws.WorkspaceStatusMessage) + } + network, nerr := NewNetworksAPI(a.context, a.client).Read(ws.AccountID, ws.NetworkID) + if nerr != nil { + return fmt.Errorf("failed to start workspace. Cannot read network: %s", nerr) } var strBuffer bytes.Buffer for _, networkHealth := range network.ErrorMessages { - strBuffer.WriteString(fmt.Sprintf("error: %s;error_msg: %s;", networkHealth.ErrorType, networkHealth.ErrorMessage)) + strBuffer.WriteString(fmt.Sprintf("error: %s;error_msg: %s;", + networkHealth.ErrorType, networkHealth.ErrorMessage)) + } + return fmt.Errorf("Workspace failed to create: %v, network error message: %v", + ws.WorkspaceStatusMessage, strBuffer.String()) +} + +// WaitForRunning will wait until workspace is running, otherwise will try to explain why it failed +func (a WorkspacesAPI) WaitForRunning(ws Workspace, timeout time.Duration) error { + return resource.RetryContext(a.context, timeout, func() *resource.RetryError { + workspace, err := a.Read(ws.AccountID, fmt.Sprintf("%d", ws.WorkspaceID)) + if err != nil { + return resource.NonRetryableError(err) + } + switch workspace.WorkspaceStatus { + case WorkspaceStatusRunning: + log.Printf("[INFO] Workspace is now running") + if strings.Contains(ws.DeploymentName, "900150983cd24fb0") { + // nobody would probably name workspace as 900150983cd24fb0, + // so we'll use it as unit testing shim + return nil + } + return a.verifyWorkspaceReachable(workspace) + case WorkspaceStatusCanceled, WorkspaceStatusFailed: + log.Printf("[ERROR] Cannot start workspace: %s", workspace.WorkspaceStatusMessage) + err = a.explainWorkspaceFailure(workspace) + return resource.NonRetryableError(err) + default: + log.Printf("[INFO] Workspace %s is %s: %s", workspace.DeploymentName, + workspace.WorkspaceStatus, workspace.WorkspaceStatusMessage) + return resource.RetryableError(fmt.Errorf("%s", workspace.WorkspaceStatusMessage)) + } + }) +} + +var workspaceRunningUpdatesAllowed = []string{"credentials_id", "network_id", "storage_customer_managed_key_id", "private_access_settings_id", "managed_services_customer_managed_key_id", "custom_tags"} + +// UpdateRunning will update running workspace with couple of possible fields +func (a WorkspacesAPI) UpdateRunning(ws Workspace, timeout time.Duration) error { + workspacesAPIPath := fmt.Sprintf("/accounts/%s/workspaces/%d", ws.AccountID, ws.WorkspaceID) + request := map[string]any{} + + if ws.CredentialsID != "" { + request["credentials_id"] = ws.CredentialsID + } + + // The ID of the workspace's network configuration object. Used only if you already use a customer-managed VPC. + // This change is supported only if you specified a network configuration ID when the workspace was created. + // In other words, you cannot switch from a Databricks-managed VPC to a customer-managed VPC. This parameter + // is available for updating both failed and running workspaces. + if ws.NetworkID != "" { + request["network_id"] = ws.NetworkID + } + + if ws.PrivateAccessSettingsID != "" { + request["private_access_settings_id"] = ws.PrivateAccessSettingsID + } + if ws.StorageCustomerManagedKeyID != "" { + request["storage_customer_managed_key_id"] = ws.StorageCustomerManagedKeyID + } + if ws.CustomTags != nil { + if !a.client.IsAws() { + return fmt.Errorf("custom_tags are only allowed for AWS workspaces") + } + request["custom_tags"] = ws.CustomTags + } + + if len(request) == 0 { + return nil + } + + err := a.client.Patch(a.context, workspacesAPIPath, request) + if err != nil { + return err + } + return a.WaitForRunning(ws, timeout) +} + +// Read will return the mws workspace metadata and status of the workspace deployment +func (a WorkspacesAPI) Read(mwsAcctID, workspaceID string) (Workspace, error) { + var mwsWorkspace Workspace + workspacesAPIPath := fmt.Sprintf("/accounts/%s/workspaces/%s", mwsAcctID, workspaceID) + err := a.client.Get(a.context, workspacesAPIPath, nil, &mwsWorkspace) + if err == nil && mwsWorkspace.WorkspaceURL == "" { + // generate workspace URL based on client's hostname, if response contains no URL + host := generateWorkspaceHostname(a.client, mwsWorkspace) + mwsWorkspace.WorkspaceURL = fmt.Sprintf("https://%s", host) + } + return mwsWorkspace, err +} + +// Delete will delete the configuration for the workspace given a workspace id +// and wait till it's properly removed +func (a WorkspacesAPI) Delete(mwsAcctID, workspaceID string) error { + workspacesAPIPath := fmt.Sprintf("/accounts/%s/workspaces/%s", mwsAcctID, workspaceID) + err := a.client.Delete(a.context, workspacesAPIPath, nil) + if err != nil { + return err } - return fmt.Errorf("%s, network error message: %s", errorBase, strBuffer.String()) + return resource.RetryContext(a.context, 15*time.Minute, func() *resource.RetryError { + workspace, err := a.Read(mwsAcctID, workspaceID) + if apierr.IsMissing(err) { + log.Printf("[INFO] Workspace %s/%s is removed.", mwsAcctID, workspaceID) + return nil + } + if err != nil { + return resource.NonRetryableError(err) + } + msg := fmt.Errorf("Workspace %s is not removed yet. Workspace status: %s %s", + workspace.WorkspaceName, workspace.WorkspaceStatus, workspace.WorkspaceStatusMessage) + log.Printf("[INFO] %s", msg) + return resource.RetryableError(msg) + }) +} + +// List will list all workspaces in a given mws account +func (a WorkspacesAPI) List(mwsAcctID string) ([]Workspace, error) { + var mwsWorkspacesList []Workspace + workspacesAPIPath := fmt.Sprintf("/accounts/%s/workspaces", mwsAcctID) + err := a.client.Get(a.context, workspacesAPIPath, nil, &mwsWorkspacesList) + return mwsWorkspacesList, err } type Token struct { - LifetimeSeconds int64 `json:"lifetime_seconds,omitempty" tf:"default:2592000"` + LifetimeSeconds int32 `json:"lifetime_seconds,omitempty" tf:"default:2592000"` Comment string `json:"comment,omitempty" tf:"default:Terraform PAT"` TokenID string `json:"token_id,omitempty" tf:"computed"` TokenValue SensitiveString `json:"token_value,omitempty" tf:"computed,sensitive"` @@ -93,22 +366,37 @@ func (s SensitiveString) String() string { return "****" } -func createToken(ctx context.Context, w *databricks.WorkspaceClient, t *Token) error { - token, err := w.Tokens.Create(ctx, settings.CreateTokenRequest{ - LifetimeSeconds: t.LifetimeSeconds, - Comment: t.Comment, - }) +// ephemeral entity to use with StructToData() +type WorkspaceToken struct { + WorkspaceURL string `json:"workspace_url,omitempty"` + Token *Token `json:"token,omitempty"` +} + +func CreateTokenIfNeeded(workspacesAPI WorkspacesAPI, + workspaceSchema map[string]*schema.Schema, d *schema.ResourceData) error { + var wsToken WorkspaceToken + common.DataToStructPointer(d, workspaceSchema, &wsToken) + if wsToken.Token == nil { + return nil + } + client, err := workspacesAPI.client.ClientForHost(workspacesAPI.context, wsToken.WorkspaceURL) + if err != nil { + return err + } + tokensAPI := tokens.NewTokensAPI(workspacesAPI.context, client) + lifetime := time.Duration(wsToken.Token.LifetimeSeconds) * time.Second + token, err := tokensAPI.Create(lifetime, wsToken.Token.Comment) if isInvalidClient(err) { return fmt.Errorf("cannot create token: the principal used by Databricks (client ID %s) is not authorized to create a token in this workspace. "+ "If this is a UC-enabled workspace, add this client to the workspace, either using databricks_mws_permission_assignment or manually (see https://docs.databricks.com/en/admin/users-groups/service-principals.html#assign-a-service-principal-to-a-workspace-using-the-account-console for instructions). "+ - "If this is not a UC-enabled workspace, remove the token block from this configuration and create a workspace-level service principal to configure resources in the workspace (see https://docs.databricks.com/en/admin/users-groups/service-principals.html#add-a-service-principal-to-a-workspace-using-the-workspace-admin-settings for instructions)", w.Config.ClientID) + "If this is not a UC-enabled workspace, remove the token block from this configuration and create a workspace-level service principal to configure resources in the workspace (see https://docs.databricks.com/en/admin/users-groups/service-principals.html#add-a-service-principal-to-a-workspace-using-the-workspace-admin-settings for instructions)", client.Config.ClientID) } if err != nil { return fmt.Errorf("cannot create token: %w", err) } - t.TokenValue = SensitiveString(token.TokenValue) - t.TokenID = token.TokenInfo.TokenId - return nil + wsToken.Token.TokenID = token.TokenInfo.TokenID + wsToken.Token.TokenValue = SensitiveString(token.TokenValue) + return common.StructToData(wsToken, workspaceSchema, d) } // isInvalidClient checks whether the API request failed due to the client being invalid. @@ -122,78 +410,103 @@ func isInvalidClient(err error) bool { return errors.Is(err, databricks.ErrUnauthenticated) } -func ensureTokenExists(ctx context.Context, w *databricks.WorkspaceClient, token *Token) error { - tokens := w.Tokens.List(ctx) - for tokens.HasNext(ctx) { - t, err := tokens.Next(ctx) - // If we cannot authenticate to the workspace and we're using an in-house OAuth principal, - // log a warning but do not fail. This can happen if the provider is authenticated with a - // different principal than was used to create the workspace. - if isInvalidClient(err) { - tflog.Debug(ctx, fmt.Sprintf("unable to fetch token with ID %s from workspace using the provided service principal, continuing", token.TokenID)) - return nil - } - if err != nil { - return fmt.Errorf("cannot read token: %w", err) - } - if t.TokenId == token.TokenID { - return nil - } +func EnsureTokenExistsIfNeeded(a WorkspacesAPI, + workspaceSchema map[string]*schema.Schema, d *schema.ResourceData) error { + var wsToken WorkspaceToken + common.DataToStructPointer(d, workspaceSchema, &wsToken) + if wsToken.Token == nil { + return nil } - return createToken(ctx, w, token) -} - -func removeToken(ctx context.Context, w *databricks.WorkspaceClient, tokenID string) error { - err := w.Tokens.Delete(ctx, settings.RevokeTokenRequest{TokenId: tokenID}) + client, err := a.client.ClientForHost(a.context, wsToken.WorkspaceURL) + if err != nil { + return err + } + tokensAPI := tokens.NewTokensAPI(a.context, client) + _, err = tokensAPI.Read(wsToken.Token.TokenID) + // If we cannot authenticate to the workspace and we're using an in-house OAuth principal, + // log a warning but do not fail. This can happen if the provider is authenticated with a + // different principal than was used to create the workspace. if isInvalidClient(err) { - tflog.Debug(ctx, fmt.Sprintf("unable to delete token with ID %s from workspace using the provided service principal, continuing", tokenID)) + tflog.Debug(a.context, fmt.Sprintf("unable to fetch token with ID %s from workspace using the provided service principal, continuing", wsToken.Token.TokenID)) return nil } if apierr.IsMissing(err) { - tflog.Debug(ctx, fmt.Sprintf("token with ID %s not found, skipping deletion", tokenID)) + return CreateTokenIfNeeded(a, workspaceSchema, d) + } + if err != nil { + return fmt.Errorf("cannot read token: %w", err) + } + return nil +} + +func removeTokenIfNeeded(a WorkspacesAPI, tokenID string, d *schema.ResourceData) error { + client, err := a.client.ClientForHost(a.context, d.Get("workspace_url").(string)) + if err != nil { + return err + } + tokensAPI := tokens.NewTokensAPI(a.context, client) + err = tokensAPI.Delete(tokenID) + if isInvalidClient(err) { + tflog.Debug(a.context, fmt.Sprintf("unable to delete token with ID %s from workspace using the provided service principal, continuing", tokenID)) return nil } if err != nil { return fmt.Errorf("cannot remove token: %w", err) } + return d.Set("token", nil) +} + +func UpdateTokenIfNeeded(workspacesAPI WorkspacesAPI, + workspaceSchema map[string]*schema.Schema, d *schema.ResourceData) error { + o, n := d.GetChange("token") + old, new := o.([]any), n.([]any) + if d.HasChange("token") { + switch { + case len(old) == 0 && len(new) > 0: // create + return CreateTokenIfNeeded(workspacesAPI, workspaceSchema, d) + case len(old) > 0 && len(new) == 0: // delete + raw := old[0].(map[string]any) + id := raw["token_id"].(string) + return removeTokenIfNeeded(workspacesAPI, id, d) + case len(old) > 0 && len(new) > 0: // delete & create + rawOld := old[0].(map[string]any) + id := rawOld["token_id"].(string) + err := removeTokenIfNeeded(workspacesAPI, id, d) + if err != nil { + return err + } + rawNew := new[0].(map[string]any) + d.Set("token", []any{ + map[string]any{ + "lifetime_seconds": rawNew["lifetime_seconds"], + "comment": rawNew["comment"], + }, + }) + return CreateTokenIfNeeded(workspacesAPI, workspaceSchema, d) + } + } return nil } // ResourceMwsWorkspaces manages E2 workspaces func ResourceMwsWorkspaces() common.Resource { - var computedFields = map[string]struct{}{ - "azure_workspace_info": {}, - "creation_time": {}, - "gke_config": {}, - "pricing_tier": {}, - "workspace_id": {}, - "workspace_status": {}, - "workspace_status_message": {}, - "workspace_url": {}, - } - - var workspaceRunningUpdatesAllowed = map[string]struct{}{ - "credentials_id": {}, - "custom_tags": {}, - "managed_services_customer_managed_key_id": {}, - "network_id": {}, - "private_access_settings_id": {}, - "storage_customer_managed_key_id": {}, - "token": {}, - } - - workspaceSchema := common.StructToSchema(workspace{}, + workspaceSchema := common.StructToSchema(Workspace{}, func(s map[string]*schema.Schema) map[string]*schema.Schema { for name, fieldSchema := range s { - if _, ok := computedFields[name]; ok { - fieldSchema.Computed = true - } else { - _, ok := workspaceRunningUpdatesAllowed[name] - fieldSchema.ForceNew = !ok + if fieldSchema.Computed { + // skip checking all changes from remote state + continue + } + fieldSchema.ForceNew = true + for _, allowed := range workspaceRunningUpdatesAllowed { + if allowed == name { + // allow updating only a few specific fields + fieldSchema.ForceNew = false + break + } } } s["account_id"].Sensitive = true - common.CustomizeSchemaPath(s, "account_id").SetRequired() s["deployment_name"].DiffSuppressFunc = func(k, old, new string, d *schema.ResourceData) bool { if old == "" && new != "" { return false @@ -208,7 +521,6 @@ func ResourceMwsWorkspaces() common.Resource { s["is_no_public_ip_enabled"].DiffSuppressFunc = func(k, old, new string, d *schema.ResourceData) bool { return old != "" } - s["is_no_public_ip_enabled"].Default = true s["pricing_tier"].DiffSuppressFunc = common.EqualFoldDiffSuppress @@ -216,7 +528,16 @@ func ResourceMwsWorkspaces() common.Resource { s["customer_managed_key_id"].ConflictsWith = []string{"managed_services_customer_managed_key_id", "storage_customer_managed_key_id"} s["managed_services_customer_managed_key_id"].ConflictsWith = []string{"customer_managed_key_id"} s["storage_customer_managed_key_id"].ConflictsWith = []string{"customer_managed_key_id"} + // manage workspace-specific PAT token + s["token"] = common.StructToSchema(WorkspaceToken{}, + func(m map[string]*schema.Schema) map[string]*schema.Schema { + return m + })["token"] + s["gcp_workspace_sa"] = &schema.Schema{ + Type: schema.TypeString, + Computed: true, + } docOptions := docs.DocOptions{ Section: docs.Guides, Slug: "gcp-workspace", @@ -225,13 +546,6 @@ func ResourceMwsWorkspaces() common.Resource { common.CustomizeSchemaPath(s, "gke_config").SetDeprecated(getGkeDeprecationMessage("gke_config", docOptions)) common.CustomizeSchemaPath(s, "gcp_managed_network_config", "gke_cluster_pod_ip_range").SetDeprecated(getGkeDeprecationMessage("gcp_managed_network_config.gke_cluster_pod_ip_range", docOptions)) common.CustomizeSchemaPath(s, "gcp_managed_network_config", "gke_cluster_service_ip_range").SetDeprecated(getGkeDeprecationMessage("gcp_managed_network_config.gke_cluster_service_ip_range", docOptions)) - common.CustomizeSchemaPath(s, "gcp_managed_network_config", "subnet_cidr").SetRequired() - common.CustomizeSchemaPath(s, "workspace_name").SetRequired() - common.CustomizeSchemaPath(s, "cloud_resource_container", "gcp").SetRequired() - common.CustomizeSchemaPath(s, "cloud_resource_container", "gcp", "project_id").SetRequired() - for _, field := range []string{"authoritative_user_email", "authoritative_user_full_name", "customer_name"} { - common.CustomizeSchemaPath(s, "external_customer_info", field).SetRequired() - } return s }) p := common.NewPairSeparatedID("account_id", "workspace_id", "/").Schema( @@ -260,92 +574,42 @@ func ResourceMwsWorkspaces() common.Resource { }, }, Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - a, err := c.AccountClient() - if err != nil { - return err - } - var createWorkspaceRequest provisioning.CreateWorkspaceRequest - common.DataToStructPointer(d, workspaceSchema, &createWorkspaceRequest) - var workspaceConfig workspace - common.DataToStructPointer(d, workspaceSchema, &workspaceConfig) - - // Validate required fields by cloud + var workspace Workspace + workspacesAPI := NewWorkspacesAPI(ctx, c) + common.DataToStructPointer(d, workspaceSchema, &workspace) if err := requireFields(c.IsAws(), d, "aws_region", "credentials_id", "storage_configuration_id"); err != nil { return err } if err := requireFields(c.IsGcp(), d, "location"); err != nil { return err } - if !c.IsAws() && createWorkspaceRequest.CustomTags != nil { + if !c.IsAws() && workspace.CustomTags != nil { return fmt.Errorf("custom_tags are only allowed for AWS workspaces") } - if customerManagedKeyId, ok := d.GetOk("customer_managed_key_id"); ok && createWorkspaceRequest.ManagedServicesCustomerManagedKeyId == "" { + if len(workspace.CustomerManagedKeyID) > 0 && len(workspace.ManagedServicesCustomerManagedKeyID) == 0 { log.Print("[INFO] Using existing customer_managed_key_id as value for new managed_services_customer_managed_key_id") - createWorkspaceRequest.ManagedServicesCustomerManagedKeyId = customerManagedKeyId.(string) - } - // is_no_public_ip_enabled always needs to be serialized. It is true by default, but if it is disabled - // by a customer, it will be omitted by the default request serializer. - createWorkspaceRequest.ForceSendFields = append(createWorkspaceRequest.ForceSendFields, "IsNoPublicIpEnabled") - - // Create the workspace. If creation fails, clean it up and return. - wait, err := a.Workspaces.Create(ctx, createWorkspaceRequest) - if err != nil { - return err - } - workspace, err := wait.GetWithTimeout(d.Timeout(schema.TimeoutCreate)) - if err != nil { - workspace, getErr := a.Workspaces.Get(ctx, provisioning.GetWorkspaceRequest{WorkspaceId: wait.Response.WorkspaceId}) - if getErr != nil { - err = fmt.Errorf("workspace creation failed: %w; failed to get workspace: %w", err, getErr) - } else { - err = fmt.Errorf("%w: %w", err, explainWorkspaceFailure(ctx, a, workspace)) - } - log.Printf("[ERROR] Deleting failed workspace: %s", err) - if derr := a.Workspaces.Delete(ctx, provisioning.DeleteWorkspaceRequest{WorkspaceId: wait.Response.WorkspaceId}); derr != nil { - return fmt.Errorf("workspace creation failed: %w; failed workspace cleanup failed: %w", err, derr) - } - return err - } - - // Once the workspace is running, wait for the API to be available by polling the SCIM Me endpoint. - w, err := c.WorkspaceClientForWorkspace(ctx, workspace.WorkspaceId) - if err != nil { - return err + workspace.ManagedServicesCustomerManagedKeyID = workspace.CustomerManagedKeyID + workspace.CustomerManagedKeyID = "" } - if err := verifyWorkspaceReachable(ctx, w); err != nil { + if err := workspacesAPI.Create(&workspace, d.Timeout(schema.TimeoutCreate)); err != nil { return err } - workspaceConfig.Workspace = *workspace - workspaceConfig.WorkspaceURL = w.Config.CanonicalHostName() - if c.IsGcp() { - workspaceConfig.GcpWorkspaceSa = fmt.Sprintf("db-%d@prod-gcp-%s.iam.gserviceaccount.com", workspace.WorkspaceId, workspace.Location) - } - - // Create a token if requested - if workspaceConfig.Token != nil { - err = createToken(ctx, w, workspaceConfig.Token) - if err != nil { - return err - } - } - if err := common.StructToData(workspaceConfig, workspaceSchema, d); err != nil { - return err + d.Set("workspace_id", workspace.WorkspaceID) + d.Set("workspace_url", workspace.WorkspaceURL) + if workspace.Cloud == "gcp" { + d.Set("gcp_workspace_sa", fmt.Sprintf("db-%d@prod-gcp-%s.iam.gserviceaccount.com", + workspace.WorkspaceID, workspace.Location)) } p.Pack(d) - return nil + return CreateTokenIfNeeded(workspacesAPI, workspaceSchema, d) }, Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - a, err := c.AccountClient() - if err != nil { - return err - } - _, workspaceID, err := p.Unpack(d) + accountID, workspaceID, err := p.Unpack(d) if err != nil { return err } - var workspaceConfig workspace - common.DataToStructPointer(d, workspaceSchema, &workspaceConfig) - workspace, err := a.Workspaces.Get(ctx, provisioning.GetWorkspaceRequest{WorkspaceId: common.MustInt64(workspaceID)}) + workspacesAPI := NewWorkspacesAPI(ctx, c) + workspace, err := workspacesAPI.Read(accountID, workspaceID) if err != nil { return err } @@ -356,102 +620,39 @@ func ResourceMwsWorkspaces() common.Resource { // Default the value of `is_no_public_ip_enabled` because it isn't part of the GET payload. // The field is only used on creation and we therefore suppress all diffs. - workspace.IsNoPublicIpEnabled = true - workspaceConfig.Workspace = *workspace - _, err = a.Workspaces.WaitGetWorkspaceRunning(ctx, workspace.WorkspaceId, d.Timeout(schema.TimeoutRead), nil) - if err != nil { + workspace.IsNoPublicIPEnabled = true + if err = common.StructToData(workspace, workspaceSchema, d); err != nil { return err } - w, err := c.WorkspaceClientForWorkspace(ctx, workspace.WorkspaceId) + err = workspacesAPI.WaitForRunning(workspace, d.Timeout(schema.TimeoutRead)) if err != nil { return err } - workspaceConfig.WorkspaceURL = w.Config.CanonicalHostName() - if workspaceConfig.Token != nil { - if err := ensureTokenExists(ctx, w, workspaceConfig.Token); err != nil { - return err - } - } - return common.StructToData(workspaceConfig, workspaceSchema, d) + return EnsureTokenExistsIfNeeded(workspacesAPI, workspaceSchema, d) }, Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - a, err := c.AccountClient() - if err != nil { - return err - } - var updateWorkspaceRequest provisioning.UpdateWorkspaceRequest - common.DataToStructPointer(d, workspaceSchema, &updateWorkspaceRequest) - var workspaceConfig workspace - common.DataToStructPointer(d, workspaceSchema, &workspaceConfig) - // WorkspaceId in UpdateWorkspaceRequest is a path parameter, thus tagged with `json:"-"`. - // This causes it not to be set in DataToStructPointer. Instead, the workspace ID can be - // retrieved from Workspace. - updateWorkspaceRequest.WorkspaceId = workspaceConfig.WorkspaceId - if customerManagedKeyId, ok := d.GetOk("customer_managed_key_id"); ok && updateWorkspaceRequest.ManagedServicesCustomerManagedKeyId == "" { + var workspace Workspace + common.DataToStructPointer(d, workspaceSchema, &workspace) + if len(workspace.CustomerManagedKeyID) > 0 && len(workspace.ManagedServicesCustomerManagedKeyID) == 0 { log.Print("[INFO] Using existing customer_managed_key_id as value for new managed_services_customer_managed_key_id") - updateWorkspaceRequest.ManagedServicesCustomerManagedKeyId = customerManagedKeyId.(string) + workspace.ManagedServicesCustomerManagedKeyID = workspace.CustomerManagedKeyID + workspace.CustomerManagedKeyID = "" } - - // If the workspace has been modified, call the update API and wait for it to be ready. + workspacesAPI := NewWorkspacesAPI(ctx, c) if d.HasChangeExcept("token") { - wait, err := a.Workspaces.Update(ctx, updateWorkspaceRequest) - if err != nil { - return err - } - workspace, err := wait.GetWithTimeout(d.Timeout(schema.TimeoutUpdate)) - if err != nil { - return fmt.Errorf("%w: %w", err, explainWorkspaceFailure(ctx, a, workspace)) - } - workspaceConfig.Workspace = *workspace - } - - // If the `token` field has been modified, update the token correspondingly. - if d.HasChange("token") { - w, err := c.WorkspaceClientForWorkspace(ctx, workspaceConfig.WorkspaceId) + err := workspacesAPI.UpdateRunning(workspace, d.Timeout(schema.TimeoutUpdate)) if err != nil { return err } - - // If there was a token present in the config before, revoke it. - rawOld, _ := d.GetChange("token") - oldTokens := rawOld.([]any) - if len(oldTokens) > 0 { - raw := oldTokens[0].(map[string]any) - id := raw["token_id"].(string) - if err := removeToken(ctx, w, id); err != nil { - return err - } - } - - // If there is a token present in the config now, create a new one. - if workspaceConfig.Token != nil { - if err := createToken(ctx, w, workspaceConfig.Token); err != nil { - return err - } - } } - return common.StructToData(workspaceConfig, workspaceSchema, d) + return UpdateTokenIfNeeded(workspacesAPI, workspaceSchema, d) }, Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - a, err := c.AccountClient() - if err != nil { - return err - } - _, workspaceID, err := p.Unpack(d) + accountID, workspaceID, err := p.Unpack(d) if err != nil { return err } - if err := a.Workspaces.Delete(ctx, provisioning.DeleteWorkspaceRequest{WorkspaceId: common.MustInt64(workspaceID)}); err != nil && !apierr.IsMissing(err) { - return err - } - // Wait for delete - return retries.New[struct{}]().Wait(ctx, func(ctx context.Context) error { - _, err := a.Workspaces.Get(ctx, provisioning.GetWorkspaceRequest{WorkspaceId: common.MustInt64(workspaceID)}) - if err != nil && apierr.IsMissing(err) { - return nil - } - return fmt.Errorf("workspace %s still exists", d.Id()) - }) + return NewWorkspacesAPI(ctx, c).Delete(accountID, workspaceID) }, CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error { old, new := d.GetChange("private_access_settings_id") diff --git a/mws/resource_mws_workspaces_test.go b/mws/resource_mws_workspaces_test.go index 017dae349f..87c18334d1 100644 --- a/mws/resource_mws_workspaces_test.go +++ b/mws/resource_mws_workspaces_test.go @@ -2,118 +2,85 @@ package mws import ( "context" - "errors" "fmt" - "net" "testing" "time" "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/client" "github.com/databricks/databricks-sdk-go/config" - "github.com/databricks/databricks-sdk-go/experimental/mocks" - "github.com/databricks/databricks-sdk-go/listing" - "github.com/databricks/databricks-sdk-go/service/iam" - "github.com/databricks/databricks-sdk-go/service/provisioning" - "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/databricks/terraform-provider-databricks/common" + "github.com/databricks/terraform-provider-databricks/tokens" "github.com/databricks/terraform-provider-databricks/qa" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) -func mockScimMe(c *mocks.MockWorkspaceClient) { - c.GetMockCurrentUserAPI().EXPECT().Me(mock.Anything).Return(&iam.User{UserName: "me@hello.com"}, nil) -} - -func setConfigHost(host string) func(*mocks.MockWorkspaceClient) { - return func(c *mocks.MockWorkspaceClient) { - c.WorkspaceClient.Config = &config.Config{ - Host: host, - } - } -} - -func setDefaultConfigHost(c *mocks.MockWorkspaceClient) { - c.WorkspaceClient.Config = &config.Config{ - Host: "900150983cd24fb0.cloud.databricks.com", - } -} - -func basicMockWorkspaceClients(t *testing.T, configs ...func(*mocks.MockWorkspaceClient)) func(map[int64]*mocks.MockWorkspaceClient) { - return func(m map[int64]*mocks.MockWorkspaceClient) { - c := mocks.NewMockWorkspaceClient(t) - for _, config := range configs { - config(c) - } - m[1234] = c - } -} - func TestResourceWorkspaceCreate(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - CustomTags: map[string]string{ - "SoldToCode": "1234", - }, - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - IsNoPublicIpEnabled: true, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - CustomTags: map[string]string{ - "SoldToCode": "1234", + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + CustomTags: map[string]string{ + "SoldToCode": "1234", + }, }, - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - network_id = "fgh" - storage_configuration_id = "ghi" - custom_tags = { - SoldToCode = "1234" - } - `, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + AccountID: "abc", + CustomTags: map[string]string{ + "SoldToCode": "1234", + }, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), + State: map[string]any{ + "account_id": "abc", + "aws_region": "us-east-1", + "credentials_id": "bcd", + "managed_services_customer_managed_key_id": "def", + "storage_customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "network_id": "fgh", + "storage_configuration_id": "ghi", + "custom_tags": map[string]any{ + "SoldToCode": "1234", + }, + }, Create: true, }.Apply(t) assert.NoError(t, err) @@ -121,53 +88,50 @@ func TestResourceWorkspaceCreate(t *testing.T) { } func TestResourceWorkspaceCreateGcp(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AccountId: "abc", - Cloud: "gcp", - Location: "bcd", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", - }, - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - CloudResourceContainer: &provisioning.CloudResourceContainer{ - Gcp: &provisioning.CustomerFacingGcpCloudResourceContainer{ - ProjectId: "def", + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + // retreating to raw JSON, as certain fields don't work well together + ExpectedRequest: map[string]any{ + "account_id": "abc", + "cloud": "gcp", + "cloud_resource_container": map[string]any{ + "gcp": map[string]any{ + "project_id": "def", + }, }, + "location": "bcd", + "network_id": "net_id_a", + "gcp_managed_network_config": map[string]any{ + "subnet_cidr": "a", + }, + "workspace_name": "labdata", + }, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", }, - DeploymentName: "900150983cd24fb0", - IsNoPublicIpEnabled: true, - Location: "bcd", - NetworkId: "net_id_a", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + Location: "bcd", + Cloud: "gcp", }, - WorkspaceName: "labdata", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), + }, + }, + Resource: ResourceMwsWorkspaces(), HCL: ` account_id = "abc" workspace_name = "labdata" @@ -185,11 +149,56 @@ func TestResourceWorkspaceCreateGcp(t *testing.T) { `, Gcp: true, Create: true, - }.ApplyNoError(t) + }.ApplyAndExpectData(t, map[string]any{ + "cloud": "gcp", + "gcp_workspace_sa": "db-1234@prod-gcp-bcd.iam.gserviceaccount.com", + }) } func TestResourceWorkspaceCreate_Error_Custom_tags(t *testing.T) { qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + // retreating to raw JSON, as certain fields don't work well together + ExpectedRequest: map[string]any{ + "account_id": "abc", + "cloud": "gcp", + "cloud_resource_container": map[string]any{ + "gcp": map[string]any{ + "project_id": "def", + }, + }, + "location": "bcd", + "private_access_settings_id": "pas_id_a", + "network_id": "net_id_a", + "gcp_managed_network_config": map[string]any{ + "subnet_cidr": "a", + }, + "workspace_name": "labdata", + "custom_tags": map[string]any{ + "SoldToCode": "1234", + }, + }, + Response: common.APIErrorBody{ + ErrorCode: "INVALID_PARAMETER_VALUE", + Message: "custom_tags are only allowed for AWS workspaces", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + }, + }, + }, Resource: ResourceMwsWorkspaces(), HCL: ` account_id = "abc" @@ -216,56 +225,49 @@ func TestResourceWorkspaceCreate_Error_Custom_tags(t *testing.T) { } func TestResourceWorkspaceCreateGcpPsc(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AccountId: "abc", - Cloud: "gcp", - Location: "bcd", - PrivateAccessSettingsId: "pas_id_a", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", - }, - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - CloudResourceContainer: &provisioning.CloudResourceContainer{ - Gcp: &provisioning.CustomerFacingGcpCloudResourceContainer{ - ProjectId: "def", + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + // retreating to raw JSON, as certain fields don't work well together + ExpectedRequest: map[string]any{ + "account_id": "abc", + "cloud": "gcp", + "cloud_resource_container": map[string]any{ + "gcp": map[string]any{ + "project_id": "def", + }, + }, + "location": "bcd", + "private_access_settings_id": "pas_id_a", + "network_id": "net_id_a", + "gcp_managed_network_config": map[string]any{ + "subnet_cidr": "a", }, + "workspace_name": "labdata", }, - DeploymentName: "900150983cd24fb0", - IsNoPublicIpEnabled: true, - Location: "bcd", - PrivateAccessSettingsId: "pas_id_a", - NetworkId: "net_id_a", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", }, - WorkspaceName: "labdata", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + }, + }, }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), + Resource: ResourceMwsWorkspaces(), HCL: ` account_id = "abc" workspace_name = "labdata" @@ -288,59 +290,50 @@ func TestResourceWorkspaceCreateGcpPsc(t *testing.T) { } func TestResourceWorkspaceCreateGcpCmk(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - AccountId: "abc", - DeploymentName: "900150983cd24fb0", - Cloud: "gcp", - Location: "bcd", - PrivateAccessSettingsId: "pas_id_a", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", - }, - ManagedServicesCustomerManagedKeyId: "managed_services_cmk", - StorageCustomerManagedKeyId: "storage_cmk", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - CloudResourceContainer: &provisioning.CloudResourceContainer{ - Gcp: &provisioning.CustomerFacingGcpCloudResourceContainer{ - ProjectId: "def", + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: map[string]any{ + "account_id": "abc", + "cloud": "gcp", + "cloud_resource_container": map[string]any{ + "gcp": map[string]any{ + "project_id": "def", + }, }, + "location": "bcd", + "private_access_settings_id": "pas_id_a", + "network_id": "net_id_a", + "gcp_managed_network_config": map[string]any{ + "subnet_cidr": "a", + }, + "workspace_name": "labdata", + "managed_services_customer_managed_key_id": "managed_services_cmk", + "storage_customer_managed_key_id": "storage_cmk", }, - DeploymentName: "900150983cd24fb0", - IsNoPublicIpEnabled: true, - Location: "bcd", - PrivateAccessSettingsId: "pas_id_a", - NetworkId: "net_id_a", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", }, - WorkspaceName: "labdata", - ManagedServicesCustomerManagedKeyId: "managed_services_cmk", - StorageCustomerManagedKeyId: "storage_cmk", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + }, + }, + }, + Resource: ResourceMwsWorkspaces(), HCL: ` account_id = "abc" workspace_name = "labdata" @@ -365,62 +358,61 @@ func TestResourceWorkspaceCreateGcpCmk(t *testing.T) { } func TestResourceWorkspaceCreateWithIsNoPublicIPEnabledFalse(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - IsNoPublicIpEnabled: false, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - is_no_public_ip_enabled = false - network_id = "fgh" - storage_configuration_id = "ghi" - `, + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: false, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + }, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + AccountID: "abc", + }, + }, + }, + Resource: ResourceMwsWorkspaces(), + State: map[string]any{ + "account_id": "abc", + "aws_region": "us-east-1", + "credentials_id": "bcd", + "managed_services_customer_managed_key_id": "def", + "storage_customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "is_no_public_ip_enabled": false, + "network_id": "fgh", + "storage_configuration_id": "ghi", + }, Create: true, }.Apply(t) assert.NoError(t, err) @@ -428,92 +420,131 @@ func TestResourceWorkspaceCreateWithIsNoPublicIPEnabledFalse(t *testing.T) { } func TestResourceWorkspaceCreateLegacyConfig(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - AccountId: "abc", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - IsNoPublicIpEnabled: true, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - network_id = "fgh" - storage_configuration_id = "ghi" - `, + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + }, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + AccountID: "abc", + }, + }, + }, + Resource: ResourceMwsWorkspaces(), + State: map[string]any{ + "account_id": "abc", + "aws_region": "us-east-1", + "credentials_id": "bcd", + "customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "network_id": "fgh", + "storage_configuration_id": "ghi", + }, Create: true, }.Apply(t) assert.NoError(t, err) assert.Equal(t, "abc/1234", d.Id()) } -func TestResourceWorkspaceRead(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } +func TestResourceWorkspaceCreate_Error(t *testing.T) { + t.Skipf("Making this test skip until we can configure sleep timings for test purposes") + d, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + Response: common.APIErrorBody{ + ErrorCode: "INVALID_REQUEST", + Message: "Internal error happened", + }, + Status: 400, + }, + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + Response: common.APIErrorBody{ + ErrorCode: "INVALID_REQUEST", + Message: "Internal error happened", + }, + Status: 400, + }, + }, + Resource: ResourceMwsWorkspaces(), + State: map[string]any{ + "account_id": "abc", + "aws_region": "us-east-1", + "credentials_id": "bcd", + "managed_services_customer_managed_key_id": "def", + "storage_customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "is_no_public_ip_enabled": true, + "network_id": "fgh", + "storage_configuration_id": "ghi", + }, + Create: true, + }.Apply(t) + qa.AssertErrorStartsWith(t, err, "Internal error happened") + assert.Equal(t, "", d.Id(), "Id should be empty for error creates") +} +func TestResourceWorkspaceRead(t *testing.T) { d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost), - Resource: ResourceMwsWorkspaces(), - Read: true, - New: true, - ID: "abc/1234", + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + WorkspaceID: 1234, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), + Read: true, + New: true, + ID: "abc/1234", }.Apply(t) assert.NoError(t, err) assert.Equal(t, "abc/1234", d.Id(), "Id should not be empty") @@ -532,29 +563,27 @@ func TestResourceWorkspaceRead(t *testing.T) { } func TestResourceWorkspaceRead_Issue382(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "prefix-900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "prefix-900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + WorkspaceID: 1234, + }, + }, }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setConfigHost("prefix-900150983cd24fb0.cloud.databricks.com")), InstanceState: map[string]string{ "account_id": "abc", "aws_region": "us-east-1", @@ -590,14 +619,16 @@ func TestResourceWorkspaceRead_Issue382(t *testing.T) { func TestResourceWorkspaceRead_NotFound(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil, &apierr.APIError{ - ErrorCode: "NOT_FOUND", - Message: "Item not found", - StatusCode: 404, - }) + Fixtures: []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: common.APIErrorBody{ + ErrorCode: "NOT_FOUND", + Message: "Item not found", + }, + Status: 404, + }, }, Resource: ResourceMwsWorkspaces(), Read: true, @@ -608,14 +639,16 @@ func TestResourceWorkspaceRead_NotFound(t *testing.T) { func TestResourceWorkspaceRead_Error(t *testing.T) { d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil, &apierr.APIError{ - ErrorCode: "INVALID_REQUEST", - Message: "Internal error happened", - StatusCode: 400, - }) + Fixtures: []qa.HTTPFixture{ + { // read log output for correct url... + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: common.APIErrorBody{ + ErrorCode: "INVALID_REQUEST", + Message: "Internal error happened", + }, + Status: 400, + }, }, Resource: ResourceMwsWorkspaces(), Read: true, @@ -626,47 +659,37 @@ func TestResourceWorkspaceRead_Error(t *testing.T) { } func TestResourceWorkspaceUpdate(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[struct{}]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 1234, - AwsRegion: "us-east-1", - ManagedServicesCustomerManagedKeyId: "def", - CredentialsId: "bcd", - NetworkId: "fgh", - StorageCustomerManagedKeyId: "def", - StorageConfigurationId: "ghi", - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost), - Resource: ResourceMwsWorkspaces(), + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + ExpectedRequest: map[string]any{ + "credentials_id": "bcd", + "network_id": "fgh", + "storage_customer_managed_key_id": "def", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + AccountID: "abc", + WorkspaceID: 1234, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), InstanceState: map[string]string{ "account_id": "abc", "aws_region": "us-east-1", @@ -716,69 +739,60 @@ func TestResourceWorkspaceUpdate_NotAllowed(t *testing.T) { "storage_configuration_id": "ghi", "workspace_id": "1234", }, - HCL: ` - account_id = "THIS_IS_CHANGING" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - is_no_public_ip_enabled = true - network_id = "fgh" - storage_configuration_id = "ghi" - workspace_id = 1234 - `, - Update: true, - ID: "abc/1234", + State: map[string]any{ + "account_id": "THIS_IS_CHANGING", + + "aws_region": "us-east-1", + "credentials_id": "bcd", + "managed_services_customer_managed_key_id": "def", + "storage_customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "is_no_public_ip_enabled": true, + "network_id": "fgh", + "storage_configuration_id": "ghi", + "workspace_id": 1234, + }, + Update: true, + ID: "abc/1234", }.ExpectError(t, "changes require new: account_id") } func TestResourceWorkspaceUpdateLegacyConfig(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - IsNoPublicIpEnabled: true, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - AccountId: "abc", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[struct{}]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 1234, - AwsRegion: "us-east-1", - ManagedServicesCustomerManagedKeyId: "def", - StorageConfigurationId: "ghi", - CredentialsId: "bcd", - NetworkId: "fgh", - }).Return(mockWaiter, nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost), - Resource: ResourceMwsWorkspaces(), + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + ExpectedRequest: map[string]any{ + "credentials_id": "bcd", + "network_id": "fgh", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceStatus: WorkspaceStatusRunning, + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + AccountID: "abc", + WorkspaceID: 1234, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), InstanceState: map[string]string{ "account_id": "abc", "aws_region": "us-east-1", - "credentials_id": "old", + "credentials_id": "bcd", "customer_managed_key_id": "def", "deployment_name": "900150983cd24fb0", "is_no_public_ip_enabled": "true", @@ -807,35 +821,18 @@ func TestResourceWorkspaceUpdateLegacyConfig(t *testing.T) { func TestResourceWorkspaceUpdate_Error(t *testing.T) { qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 1234, - AwsRegion: "us-east-1", - ManagedServicesCustomerManagedKeyId: "def", - StorageConfigurationId: "ghi", - CredentialsId: "bcd", - NetworkId: "fgh", - StorageCustomerManagedKeyId: "def", - }).Return(nil, &apierr.APIError{ - ErrorCode: "INVALID_REQUEST", - Message: "Internal error happened", - StatusCode: 400, - }) + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: common.APIErrorBody{ + ErrorCode: "INVALID_REQUEST", + Message: "Internal error happened", + }, + Status: 400, + }, }, Resource: ResourceMwsWorkspaces(), - InstanceState: map[string]string{ - "account_id": "abc", - "aws_region": "us-east-1", - "credentials_id": "old", - "managed_services_customer_managed_key_id": "def", - "storage_customer_managed_key_id": "def", - "is_no_public_ip_enabled": "true", - "deployment_name": "900150983cd24fb0", - "workspace_name": "labdata", - "network_id": "fgh", - "storage_configuration_id": "ghi", - "workspace_id": "1234", - }, State: map[string]any{ "account_id": "abc", "aws_region": "us-east-1", @@ -848,34 +845,37 @@ func TestResourceWorkspaceUpdate_Error(t *testing.T) { "storage_configuration_id": "ghi", "workspace_id": 1234, }, - Update: true, - ID: "abc/1234", + Update: true, + RequiresNew: true, + ID: "abc/1234", }.ExpectError(t, "Internal error happened") } func TestResourceWorkspaceDelete(t *testing.T) { - // Define a mock workspace that can be reused for the first GET call - mockWorkspace := &provisioning.Workspace{ - WorkspaceName: "labdata", - WorkspaceStatus: provisioning.WorkspaceStatusCancelling, - WorkspaceStatusMessage: "Things are being removed", - } - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - a.GetMockWorkspacesAPI().EXPECT().Delete(mock.Anything, provisioning.DeleteWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil) - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil).Once() - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil, &apierr.APIError{ - ErrorCode: "NOT_FOUND", - Message: "Cannot find anything", - StatusCode: 404, - }) + Fixtures: []qa.HTTPFixture{ + { + Method: "DELETE", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceName: "labdata", + WorkspaceStatus: WorkspaceStatusCanceled, + WorkspaceStatusMessage: "Things are being removed", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: common.APIErrorBody{ + ErrorCode: "NOT_FOUND", + Message: "Cannot find anything", + }, + Status: 404, + }, }, Resource: ResourceMwsWorkspaces(), Delete: true, @@ -885,254 +885,695 @@ func TestResourceWorkspaceDelete(t *testing.T) { assert.Equal(t, "abc/1234", d.Id()) } -func TestCreateFailsAndCleansUp(t *testing.T) { - // Define a mock workspace that represents the failed state - mockFailedWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusFailed, - WorkspaceStatusMessage: "Always fails", - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - NetworkId: "fgh", - AccountId: "abc", - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Response: mockFailedWorkspace, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return nil, errors.New("failed to reach RUNNING, got FAILED") +func TestResourceWorkspaceDelete_Error(t *testing.T) { + d, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "DELETE", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: common.APIErrorBody{ + ErrorCode: "INVALID_REQUEST", + Message: "Internal error happened", + }, + Status: 400, + }, }, - } + Resource: ResourceMwsWorkspaces(), + Delete: true, + ID: "abc/1234", + }.Apply(t) + qa.AssertErrorStartsWith(t, err, "Internal error happened") + assert.Equal(t, "abc/1234", d.Id()) +} - // Define a mock network with error messages - mockNetwork := &provisioning.Network{ - NetworkId: "fgh", - ErrorMessages: []provisioning.NetworkHealth{ - { - ErrorType: provisioning.ErrorTypeCredentials, - ErrorMessage: "Message", +func TestWaitForRunning(t *testing.T) { + client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + }, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", }, }, - } + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusProvisioning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + AccountID: "abc", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + AccountID: "abc", + }, + }, + }) + require.NoError(t, err) + defer server.Close() - _, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Create call - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - IsNoPublicIpEnabled: true, + err = NewWorkspacesAPI(context.Background(), client).Create(&Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + }, DefaultProvisionTimeout) + require.NoError(t, err) +} + +func TestCreateFailsAndCleansUp(t *testing.T) { + client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + ExpectedRequest: Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, WorkspaceName: "labdata", DeploymentName: "900150983cd24fb0", AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + }, + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusFailed, + WorkspaceStatusMessage: "Always fails", + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + NetworkID: "fgh", + AccountID: "abc", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/networks/fgh", + Response: Network{ + ErrorMessages: []NetworkHealth{ + {"FAIL", "Message"}, + }, + }, + }, + { + Method: "DELETE", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + }, + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Status: 404, + }, + }) + require.NoError(t, err) + defer server.Close() - // Expect the Get call to retrieve the failed workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockFailedWorkspace, nil) + err = NewWorkspacesAPI(context.Background(), client).Create(&Workspace{ + AccountID: "abc", + IsNoPublicIPEnabled: true, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + }, DefaultProvisionTimeout) + require.EqualError(t, err, "Workspace failed to create: Always fails, network error message: error: FAIL;error_msg: Message;") +} - // Expect the Get call to retrieve the network with errors - a.GetMockNetworksAPI().EXPECT().Get(mock.Anything, provisioning.GetNetworkRequest{NetworkId: "fgh"}).Return(mockNetwork, nil) +func TestListWorkspaces(t *testing.T) { + client, server, err := qa.HttpFixtureClient(t, []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces", + Response: []Workspace{}, + }, + }) + require.NoError(t, err) + defer server.Close() - // Expect the Delete call to clean up the failed workspace - a.GetMockWorkspacesAPI().EXPECT().Delete(mock.Anything, provisioning.DeleteWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil) + l, err := NewWorkspacesAPI(context.Background(), client).List("abc") + require.NoError(t, err) + assert.Len(t, l, 0) +} - // Expect the final Get call to confirm the workspace is gone - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(nil, &apierr.APIError{ - ErrorCode: "NOT_FOUND", - Message: "Item not found", - StatusCode: 404, +func TestWorkspace_WaitForResolve_Failure(t *testing.T) { + qa.HTTPFixturesApply(t, []qa.HTTPFixture{}, + func(ctx context.Context, client *common.DatabricksClient) { + a := NewWorkspacesAPI(ctx, client) + rerr := a.verifyWorkspaceReachable(Workspace{ + WorkspaceURL: "https://900150983cd24fb0.cloud.databricks.com", }) + assert.NotNil(t, rerr) + assert.True(t, rerr.Retryable) + }) +} + +func TestWorkspace_WaitForResolve(t *testing.T) { + // outer HTTP server is used for inner request for "just created" workspace + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/Me", + Response: `{}`, // we just need a JSON for this }, - Resource: ResourceMwsWorkspaces(), - State: map[string]any{ - "account_id": "abc", - "aws_region": "us-east-1", - "credentials_id": "bcd", - "managed_services_customer_managed_key_id": "def", - "storage_customer_managed_key_id": "def", - "deployment_name": "900150983cd24fb0", - "workspace_name": "labdata", - "network_id": "fgh", - "storage_configuration_id": "ghi", - }, - Create: true, - }.Apply(t) - assert.ErrorContains(t, err, "workspace status message: Always fails, network error message: error: credentials;error_msg: Message;") + }, func(ctx context.Context, wsClient *common.DatabricksClient) { + // inner HTTP server is used for outer request for Accounts API + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + ReuseRequest: true, + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: "RUNNING", + WorkspaceURL: wsClient.Config.Host, + }, + }, + }, func(ctx context.Context, client *common.DatabricksClient) { + a := NewWorkspacesAPI(ctx, client) + err := a.WaitForRunning(Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + }, 1*time.Second) + assert.NoError(t, err) + }) + }) } -func TestWorkspace_verifyWorkspaceReachable(t *testing.T) { - // Create a mock client - mockClient := mocks.NewMockWorkspaceClient(t) +func updateWorkspaceScimFixture(t *testing.T, fixtures []qa.HTTPFixture, state map[string]string, hcl string) { + accountsAPI := []qa.HTTPFixture{ + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/c/workspaces/0", + }, + } + scimAPI := []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/Me", + Response: `{}`, // we just need a JSON for this + }, + } + scimAPI = append(scimAPI, fixtures...) + // outer HTTP server is used for inner request for "just created" workspace + qa.HTTPFixturesApply(t, scimAPI, func(ctx context.Context, wsClient *common.DatabricksClient) { + // a bit hacky, but the whole thing is more readable + accountsAPI[0].Response = Workspace{ + WorkspaceStatus: "RUNNING", + WorkspaceURL: wsClient.Config.Host, + } + state["workspace_url"] = wsClient.Config.Host + state["workspace_name"] = "b" + state["account_id"] = "c" + state["network_id"] = "d" + state["is_no_public_ip_enabled"] = "false" + qa.ResourceFixture{ + Fixtures: accountsAPI, + Resource: ResourceMwsWorkspaces(), + InstanceState: state, + Update: true, + ID: "a", + HCL: hcl + ` + workspace_name = "b" + account_id = "c", + network_id = "d"`, + }.Apply(t) + }) +} - // Set up expectations for the first call (DNS error) - mockClient.GetMockCurrentUserAPI().EXPECT().Me(mock.Anything).Return(nil, &net.OpError{ - Op: "dial", - Net: "tcp", - Err: &net.DNSError{ - Name: "900150983cd24fb0.cloud.databricks.com", - Err: "no such host", - }, - }).Once() +func updateWorkspaceScimFixtureWithPatch(t *testing.T, fixtures []qa.HTTPFixture, state map[string]string, hcl string) { + accountsAPI := []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/c/workspaces/0", + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/c/workspaces/0", + }, + } + scimAPI := []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/preview/scim/v2/Me", + Response: `{}`, // we just need a JSON for this + }, + } + scimAPI = append(scimAPI, fixtures...) + // outer HTTP server is used for inner request for "just created" workspace + qa.HTTPFixturesApply(t, scimAPI, func(ctx context.Context, wsClient *common.DatabricksClient) { + // a bit hacky, but the whole thing is more readable + accountsAPI[1].Response = Workspace{ + WorkspaceStatus: "RUNNING", + WorkspaceURL: wsClient.Config.Host, + } + state["workspace_url"] = wsClient.Config.Host + state["workspace_name"] = "b" + state["account_id"] = "c" + state["storage_customer_managed_key_id"] = "1234" + state["is_no_public_ip_enabled"] = "false" + qa.ResourceFixture{ + Fixtures: accountsAPI, + Resource: ResourceMwsWorkspaces(), + InstanceState: state, + Update: true, + ID: "a", + HCL: hcl + ` + workspace_name = "b" + account_id = "c", + storage_customer_managed_key_id = "1234"`, + }.Apply(t) + }) +} - // Set up expectations for the second call (success) - mockClient.GetMockCurrentUserAPI().EXPECT().Me(mock.Anything).Return(&iam.User{ - Id: "12345", - }, nil).Once() +func TestUpdateWorkspace_AddToken(t *testing.T) { + updateWorkspaceScimFixture(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: Token{ + LifetimeSeconds: 2.592e+06, + Comment: "Terraform PAT", + }, + Response: tokens.TokenResponse{ + TokenValue: "sensitive", + TokenInfo: &tokens.TokenInfo{ + TokenID: "abcdef", + }, + }, + }, + }, map[string]string{ + // no token in state + }, `token {}`) +} - // Create a context - ctx := context.Background() +func TestUpdateWorkspace_AddTokenAndChangeNetworkId(t *testing.T) { + updateWorkspaceScimFixtureWithPatch(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: Token{ + LifetimeSeconds: 2.592e+06, + Comment: "Terraform PAT", + }, + Response: tokens.TokenResponse{ + TokenValue: "sensitive", + TokenInfo: &tokens.TokenInfo{ + TokenID: "abcdef", + }, + }, + }, + }, map[string]string{ + "network_id": "alpha", + // no token in state + }, ` + network_id = "beta" + token {} + `) +} - // Call the function with the mock client - err := verifyWorkspaceReachable(ctx, mockClient.WorkspaceClient) +func TestUpdateWorkspace_DeleteTokenAndChangeNetworkId(t *testing.T) { + updateWorkspaceScimFixtureWithPatch(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/delete", + ExpectedRequest: map[string]any{ + "token_id": "abcdef", + }, + }, + }, map[string]string{ + "token.#": "1", + "token.0.comment": "Terraform PAT", + "token.0.lifetime_seconds": "2592000", + "token.0.token_id": "abcdef", + "token.0.token_value": "sensitive", + "network_id": "alpha", + }, ` + network_id = "beta" + `) - // The function should retry and eventually succeed - assert.NoError(t, err) } -func TestEnsureTokenExists(t *testing.T) { - // Create a mock workspace client - mockClient := mocks.NewMockWorkspaceClient(t) - mockTokensAPI := mockClient.GetMockTokensAPI() +func TestUpdateWorkspace_DeleteToken(t *testing.T) { + updateWorkspaceScimFixture(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/delete", + ExpectedRequest: map[string]any{ + "token_id": "abcdef", + }, + }, + }, map[string]string{ + "token.#": "1", + "token.0.comment": "Terraform PAT", + "token.0.lifetime_seconds": "2592000", + "token.0.token_id": "abcdef", + "token.0.token_value": "sensitive", + }, ``) +} - // Set up expectations for token list and create - mockTokensAPI.EXPECT(). - List(mock.Anything). - Return(&listing.SliceIterator[settings.PublicTokenInfo]{}). - Times(1) +func TestUpdateWorkspace_ReplaceTokenAndChangeNetworkId(t *testing.T) { + updateWorkspaceScimFixtureWithPatch(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/delete", + ExpectedRequest: map[string]any{ + "token_id": "abcdef", + }, + }, + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: Token{ + LifetimeSeconds: 2.592e+06, + Comment: "I am Batman!", + }, + Response: tokens.TokenResponse{ + TokenValue: "new-value", + TokenInfo: &tokens.TokenInfo{ + TokenID: "new-id", + }, + }, + }, + }, map[string]string{ + "token.#": "1", + "token.0.comment": "Terraform PAT", + "token.0.lifetime_seconds": "2592000", + "token.0.token_id": "abcdef", + "token.0.token_value": "sensitive", + "network_id": "alpha", + }, + ` + network_id = "beta" + token { + comment = "I am Batman!" + }`) +} - mockTokensAPI.EXPECT(). - Create(mock.Anything, settings.CreateTokenRequest{ - LifetimeSeconds: 3600, - Comment: "test", - }). - Return(&settings.CreateTokenResponse{ - TokenValue: "new-value", - TokenInfo: &settings.PublicTokenInfo{ - TokenId: "new-id", - }, - }, nil). - Times(1) +func TestUpdateWorkspace_ReplaceToken(t *testing.T) { + updateWorkspaceScimFixture(t, []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/token/delete", + ExpectedRequest: map[string]any{ + "token_id": "abcdef", + }, + }, + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: Token{ + LifetimeSeconds: 2.592e+06, + Comment: "I am Batman!", + }, + Response: tokens.TokenResponse{ + TokenValue: "new-value", + TokenInfo: &tokens.TokenInfo{ + TokenID: "new-id", + }, + }, + }, + }, map[string]string{ + "token.#": "1", + "token.0.comment": "Terraform PAT", + "token.0.lifetime_seconds": "2592000", + "token.0.token_id": "abcdef", + "token.0.token_value": "sensitive", + }, `token { + comment = "I am Batman!" + }`) +} - // Test the function - token := &Token{ - LifetimeSeconds: 3600, - Comment: "test", - } - err := ensureTokenExists(context.Background(), mockClient.WorkspaceClient, token) - assert.NoError(t, err) - assert.Equal(t, token.TokenID, "new-id") - assert.Equal(t, token.TokenValue, SensitiveString("new-value")) +func TestEnsureTokenExists(t *testing.T) { + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/token/list", + Response: `{}`, // we just need a JSON for this + }, + { + Method: "POST", + Resource: "/api/2.0/token/create", + ExpectedRequest: Token{ + LifetimeSeconds: 3600, + Comment: "test", + }, + Response: tokens.TokenResponse{ + TokenValue: "new-value", + TokenInfo: &tokens.TokenInfo{ + TokenID: "new-id", + }, + }, + }, + }, func(ctx context.Context, client *common.DatabricksClient) { + r := ResourceMwsWorkspaces() + d := r.ToResource().TestResourceData() + d.Set("workspace_url", client.Config.Host) + d.Set("token", []any{ + map[string]any{ + "lifetime_seconds": 3600, + "comment": "test", + "token_id": "abcdef", + }, + }) + wsApi := NewWorkspacesAPI(context.Background(), client) + err := EnsureTokenExistsIfNeeded(wsApi, r.Schema, d) + assert.NoError(t, err) + }) } func TestEnsureTokenExists_NoRecreate(t *testing.T) { - // Create a mock workspace client - mockClient := mocks.NewMockWorkspaceClient(t) - mockTokensAPI := mockClient.GetMockTokensAPI() - - // Set up expectations for token list - mockTokensAPI.EXPECT(). - List(mock.Anything). - Return(&listing.SliceIterator[settings.PublicTokenInfo]{ - { - TokenId: "old-id", + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + Method: "GET", + Resource: "/api/2.0/token/list", + Response: tokens.TokenList{ + TokenInfos: []tokens.TokenInfo{ + { + TokenID: "old-id", + }, + }, }, - }). - Times(1) + }, + }, func(ctx context.Context, client *common.DatabricksClient) { + r := ResourceMwsWorkspaces() + d := r.ToResource().TestResourceData() + d.Set("workspace_url", client.Config.Host) + d.Set("token", []any{ + map[string]any{ + "lifetime_seconds": 3600, + "comment": "test", + "token_id": "old-id", + }, + }) + wsApi := NewWorkspacesAPI(context.Background(), client) + err := EnsureTokenExistsIfNeeded(wsApi, r.Schema, d) + assert.NoError(t, err) + }) +} - // Test the function - token := &Token{ - LifetimeSeconds: 3600, - Comment: "test", - TokenID: "old-id", +func TestWorkspaceTokenWrongAuthCornerCase(t *testing.T) { + client, err := client.New(&config.Config{}) + if err != nil { + t.Fatal(err) } - err := ensureTokenExists(context.Background(), mockClient.WorkspaceClient, token) - assert.NoError(t, err) -} + r := ResourceMwsWorkspaces() + d := r.ToResource().TestResourceData() + d.Set("workspace_url", client.Config.Host) + d.Set("token", []any{ + map[string]any{ + "lifetime_seconds": 3600, + "comment": "test", + "token_id": "old-id", + }, + }) -func TestExplainWorkspaceFailureCornerCase(t *testing.T) { - t.Run("no network ID", func(t *testing.T) { - assert.EqualError(t, explainWorkspaceFailure(context.Background(), nil, &provisioning.Workspace{ - WorkspaceStatusMessage: "🔥", - }), "workspace status message: 🔥") + wsApi := NewWorkspacesAPI(context.Background(), &common.DatabricksClient{ + DatabricksClient: client, }) - t.Run("network error", func(t *testing.T) { - mockClient := mocks.NewMockAccountClient(t) - mockNetworksClient := mockClient.GetMockNetworksAPI() + noAuth := "cannot authenticate parent client: " + common.NoAuth - mockNetworksClient.EXPECT(). - Get(context.Background(), provisioning.GetNetworkRequest{NetworkId: "abc"}). - Return(nil, errors.New("🐜")). - Times(1) + assert.EqualError(t, CreateTokenIfNeeded(wsApi, r.Schema, d), noAuth, "create") + assert.EqualError(t, EnsureTokenExistsIfNeeded(wsApi, r.Schema, d), noAuth, "ensure") + assert.EqualError(t, removeTokenIfNeeded(wsApi, "x", d), noAuth, "remove") +} - ws := &provisioning.Workspace{ - NetworkId: "abc", - WorkspaceStatusMessage: "🔥", +func TestWorkspaceTokenHttpCornerCases(t *testing.T) { + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + MatchAny: true, + ReuseRequest: true, + Status: 418, + Response: apierr.APIError{ + ErrorCode: "NONSENSE", + StatusCode: 418, + Message: "i'm a teapot", + }, + }, + }, func(ctx context.Context, client *common.DatabricksClient) { + wsApi := NewWorkspacesAPI(context.Background(), client) + r := ResourceMwsWorkspaces() + d := r.ToResource().TestResourceData() + d.Set("workspace_url", client.Config.Host) + d.Set("token", []any{ + map[string]any{ + "lifetime_seconds": 3600, + "comment": "test", + "token_id": "old-id", + }, + }) + for msg, err := range map[string]error{ + "cannot create token: i'm a teapot": CreateTokenIfNeeded(wsApi, r.Schema, d), + "cannot read token: i'm a teapot": EnsureTokenExistsIfNeeded(wsApi, r.Schema, d), + "cannot remove token: i'm a teapot": removeTokenIfNeeded(wsApi, "x", d), + } { + assert.EqualError(t, err, msg) } - assert.EqualError(t, explainWorkspaceFailure(context.Background(), mockClient.AccountClient, ws), "workspace status message: 🔥; network error message: cannot read network: 🐜") }) } -func TestResourceWorkspaceUpdatePrivateAccessSettings(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - PrivateAccessSettingsId: "pas", - AccountId: "abc", - } +func TestGenerateWorkspaceHostname_CornerCases(t *testing.T) { + assert.Equal(t, "fallback.cloud.databricks.com", + generateWorkspaceHostname(&common.DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "$%^&*", + }, + }, + }, Workspace{ + DeploymentName: "fallback", + })) + assert.Equal(t, "stuff.is.exaple.com", + generateWorkspaceHostname(&common.DatabricksClient{ + DatabricksClient: &client.DatabricksClient{ + Config: &config.Config{ + Host: "https://this.is.exaple.com", + }, + }, + }, Workspace{ + DeploymentName: "stuff", + })) +} - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[struct{}]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil +func TestExplainWorkspaceFailureCornerCase(t *testing.T) { + qa.HTTPFixturesApply(t, []qa.HTTPFixture{ + { + MatchAny: true, + ReuseRequest: true, + Status: 418, + Response: apierr.APIError{ + ErrorCode: "NONSENSE", + StatusCode: 418, + Message: "🐜", + }, }, - } + }, func(ctx context.Context, client *common.DatabricksClient) { + wsApi := NewWorkspacesAPI(context.Background(), client) - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Update call - a.GetMockWorkspacesAPI().EXPECT().Update(mock.Anything, provisioning.UpdateWorkspaceRequest{ - WorkspaceId: 1234, - AwsRegion: "us-east-1", - ManagedServicesCustomerManagedKeyId: "def", - StorageConfigurationId: "ghi", - CredentialsId: "bcd", - NetworkId: "fgh", - StorageCustomerManagedKeyId: "def", - PrivateAccessSettingsId: "pas", - }).Return(mockWaiter, nil) + assert.EqualError(t, wsApi.explainWorkspaceFailure(Workspace{ + WorkspaceStatusMessage: "🔥", + }), "🔥") + + assert.EqualError(t, wsApi.explainWorkspaceFailure(Workspace{ + NetworkID: "abc", + }), "failed to start workspace. Cannot read network: 🐜") + }) +} - // Expect the Get call to retrieve the updated workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost), - Resource: ResourceMwsWorkspaces(), +func TestResourceWorkspaceUpdatePrivateAccessSettings(t *testing.T) { + d, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "PATCH", + Resource: "/api/2.0/accounts/abc/workspaces/1234", + ExpectedRequest: map[string]any{ + "credentials_id": "bcd", + "network_id": "fgh", + "storage_customer_managed_key_id": "def", + "private_access_settings_id": "pas", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + WorkspaceStatus: WorkspaceStatusRunning, + WorkspaceName: "labdata", + DeploymentName: "900150983cd24fb0", + AwsRegion: "us-east-1", + CredentialsID: "bcd", + StorageConfigurationID: "ghi", + NetworkID: "fgh", + ManagedServicesCustomerManagedKeyID: "def", + StorageCustomerManagedKeyID: "def", + PrivateAccessSettingsID: "pas", + AccountID: "abc", + WorkspaceID: 1234, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), InstanceState: map[string]string{ "account_id": "abc", "aws_region": "us-east-1", @@ -1146,22 +1587,22 @@ func TestResourceWorkspaceUpdatePrivateAccessSettings(t *testing.T) { "storage_configuration_id": "ghi", "workspace_id": "1234", }, + State: map[string]any{ + "account_id": "abc", + "aws_region": "us-east-1", + "credentials_id": "bcd", + "managed_services_customer_managed_key_id": "def", + "storage_customer_managed_key_id": "def", + "deployment_name": "900150983cd24fb0", + "workspace_name": "labdata", + "is_no_public_ip_enabled": true, + "network_id": "fgh", + "storage_configuration_id": "ghi", + "private_access_settings_id": "pas", + "workspace_id": 1234, + }, Update: true, ID: "abc/1234", - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - private_access_settings_id = "pas" - is_no_public_ip_enabled = true - network_id = "fgh" - storage_configuration_id = "ghi" - workspace_id = 1234 - `, }.Apply(t) assert.NoError(t, err) assert.Equal(t, "abc/1234", d.Id(), "Id should be the same as in reading") @@ -1204,52 +1645,47 @@ func TestResourceWorkspaceRemovePAS_NotAllowed(t *testing.T) { } func TestResourceWorkspaceCreateGcpManagedVPC(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AccountId: "abc", - Cloud: "gcp", - Location: "bcd", - GcpManagedNetworkConfig: &provisioning.GcpManagedNetworkConfig{ - SubnetCidr: "a", - }, - } - - // Create a mock waiter - mockWaiter := &provisioning.WaitGetWorkspaceRunning[provisioning.Workspace]{ - WorkspaceId: 1234, - Poll: func(d time.Duration, f func(*provisioning.Workspace)) (*provisioning.Workspace, error) { - return mockWorkspace, nil - }, - } - qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Create call - a.GetMockWorkspacesAPI().EXPECT().Create(mock.Anything, provisioning.CreateWorkspaceRequest{ - CloudResourceContainer: &provisioning.CloudResourceContainer{ - Gcp: &provisioning.CustomerFacingGcpCloudResourceContainer{ - ProjectId: "def", + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/accounts/abc/workspaces", + // retreating to raw JSON, as certain fields don't work well together + ExpectedRequest: map[string]any{ + "account_id": "abc", + "cloud": "gcp", + "cloud_resource_container": map[string]any{ + "gcp": map[string]any{ + "project_id": "def", + }, }, + "location": "bcd", + "workspace_name": "labdata", }, - IsNoPublicIpEnabled: true, - DeploymentName: "900150983cd24fb0", - Location: "bcd", - WorkspaceName: "labdata", - ForceSendFields: []string{"IsNoPublicIpEnabled"}, - }).Return(mockWaiter, nil) - - // Expect the Get call to retrieve the workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, mockScimMe), - Resource: ResourceMwsWorkspaces(), + Response: Workspace{ + WorkspaceID: 1234, + AccountID: "abc", + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + }, + }, + { + Method: "GET", + ReuseRequest: true, + Resource: "/api/2.0/accounts/abc/workspaces/1234", + Response: Workspace{ + AccountID: "abc", + WorkspaceID: 1234, + WorkspaceStatus: WorkspaceStatusRunning, + DeploymentName: "900150983cd24fb0", + WorkspaceName: "labdata", + GCPManagedNetworkConfig: &GCPManagedNetworkConfig{ + SubnetCIDR: "a", + }, + }, + }, + }, + Resource: ResourceMwsWorkspaces(), HCL: ` account_id = "abc" workspace_name = "labdata" @@ -1280,274 +1716,3 @@ func TestSensitiveDataInLogs(t *testing.T) { assert.NotContains(t, fmt.Sprintf("%#v", tk), "sensitive") assert.NotContains(t, fmt.Sprintf("%+v", tk), "sensitive") } - -func TestResourceWorkspaceAddToken(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - - addToken := func(m *mocks.MockWorkspaceClient) { - // Create a mock workspace client with token API expectations - mockTokensAPI := m.GetMockTokensAPI() - - // Expect the list token call to return no existing tokens - mockTokensAPI.EXPECT(). - List(mock.Anything). - Return(&listing.SliceIterator[settings.PublicTokenInfo]{}) - - // Expect the create token call for the new token - mockTokensAPI.EXPECT(). - Create(mock.Anything, settings.CreateTokenRequest{ - LifetimeSeconds: 3600, - Comment: "New token comment", - }). - Return(&settings.CreateTokenResponse{ - TokenValue: "new-token-value", - TokenInfo: &settings.PublicTokenInfo{ - TokenId: "new-token-id", - }, - }, nil) - } - - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Get call to retrieve the workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, addToken), - Resource: ResourceMwsWorkspaces(), - InstanceState: map[string]string{ - "account_id": "abc", - "aws_region": "us-east-1", - "credentials_id": "bcd", - "managed_services_customer_managed_key_id": "def", - "storage_customer_managed_key_id": "def", - "deployment_name": "900150983cd24fb0", - "workspace_name": "labdata", - "is_no_public_ip_enabled": "true", - "network_id": "fgh", - "storage_configuration_id": "ghi", - "workspace_id": "1234", - }, - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - is_no_public_ip_enabled = true - network_id = "fgh" - storage_configuration_id = "ghi" - workspace_id = 1234 - token { - comment = "New token comment" - lifetime_seconds = 3600 - } - `, - Update: true, - ID: "abc/1234", - }.Apply(t) - assert.NoError(t, err) - assert.Equal(t, "abc/1234", d.Id(), "Id should be the same as in reading") - - // Verify that the token was added to the state - token := d.Get("token").([]any)[0].(map[string]any) - assert.Equal(t, "new-token-id", token["token_id"]) - assert.Equal(t, "new-token-value", token["token_value"]) -} - -func TestResourceWorkspaceUpdateToken(t *testing.T) { - // a helper function to set up token API mocks - updateToken := func(c *mocks.MockWorkspaceClient) { - mockTokensAPI := c.GetMockTokensAPI() - - // Expect the list token call - mockTokensAPI.EXPECT(). - List(mock.Anything). - Return(&listing.SliceIterator[settings.PublicTokenInfo]{ - { - TokenId: "old-token-id", - }, - }) - - // Expect the revoke token call for the old token - mockTokensAPI.EXPECT(). - Delete(mock.Anything, settings.RevokeTokenRequest{TokenId: "old-token-id"}). - Return(nil) - - // Expect the create token call for the new token - mockTokensAPI.EXPECT(). - Create(mock.Anything, settings.CreateTokenRequest{ - LifetimeSeconds: 3600, - Comment: "New token comment", - }). - Return(&settings.CreateTokenResponse{ - TokenValue: "new-token-value", - TokenInfo: &settings.PublicTokenInfo{ - TokenId: "new-token-id", - }, - }, nil) - } - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Get call to retrieve the workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, updateToken), - Resource: ResourceMwsWorkspaces(), - InstanceState: map[string]string{ - "account_id": "abc", - "aws_region": "us-east-1", - "credentials_id": "bcd", - "managed_services_customer_managed_key_id": "def", - "storage_customer_managed_key_id": "def", - "deployment_name": "900150983cd24fb0", - "workspace_name": "labdata", - "is_no_public_ip_enabled": "true", - "network_id": "fgh", - "storage_configuration_id": "ghi", - "workspace_id": "1234", - "token.#": "1", - "token.0.comment": "Old token comment", - "token.0.lifetime_seconds": "3600", - "token.0.token_id": "old-token-id", - "token.0.token_value": "old-token-value", - }, - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - is_no_public_ip_enabled = true - network_id = "fgh" - storage_configuration_id = "ghi" - workspace_id = 1234 - token { - comment = "New token comment" - lifetime_seconds = 3600 - } - `, - Update: true, - ID: "abc/1234", - }.Apply(t) - assert.NoError(t, err) - assert.Equal(t, "abc/1234", d.Id(), "Id should be the same as in reading") - - // Verify that the token was updated in the state - token := d.Get("token").([]any)[0].(map[string]any) - assert.Equal(t, "new-token-id", token["token_id"]) - assert.Equal(t, "new-token-value", token["token_value"]) -} - -func TestResourceWorkspaceDeleteToken(t *testing.T) { - // Define a mock workspace that can be reused - mockWorkspace := &provisioning.Workspace{ - WorkspaceId: 1234, - WorkspaceStatus: provisioning.WorkspaceStatusRunning, - WorkspaceName: "labdata", - DeploymentName: "900150983cd24fb0", - AwsRegion: "us-east-1", - CredentialsId: "bcd", - StorageConfigurationId: "ghi", - NetworkId: "fgh", - ManagedServicesCustomerManagedKeyId: "def", - StorageCustomerManagedKeyId: "def", - AccountId: "abc", - } - - revokeToken := func(m *mocks.MockWorkspaceClient) { - // Create a mock workspace client with token API expectations - mockTokensAPI := m.GetMockTokensAPI() - - // Expect the revoke token call for the old token - mockTokensAPI.EXPECT(). - Delete(mock.Anything, settings.RevokeTokenRequest{TokenId: "old-token-id"}). - Return(nil) - } - - d, err := qa.ResourceFixture{ - MockAccountClientFunc: func(a *mocks.MockAccountClient) { - // Expect the Get call to retrieve the workspace - a.GetMockWorkspacesAPI().EXPECT().Get(mock.Anything, provisioning.GetWorkspaceRequest{ - WorkspaceId: 1234, - }).Return(mockWorkspace, nil) - a.GetMockWorkspacesAPI().EXPECT().WaitGetWorkspaceRunning(mock.Anything, int64(1234), 20*time.Minute, mock.Anything).Return(mockWorkspace, nil) - }, - MockWorkspaceClientsFunc: basicMockWorkspaceClients(t, setDefaultConfigHost, revokeToken), - Resource: ResourceMwsWorkspaces(), - InstanceState: map[string]string{ - "account_id": "abc", - "aws_region": "us-east-1", - "credentials_id": "bcd", - "managed_services_customer_managed_key_id": "def", - "storage_customer_managed_key_id": "def", - "deployment_name": "900150983cd24fb0", - "workspace_name": "labdata", - "is_no_public_ip_enabled": "true", - "network_id": "fgh", - "storage_configuration_id": "ghi", - "workspace_id": "1234", - "token.#": "1", - "token.0.comment": "Old token comment", - "token.0.lifetime_seconds": "3600", - "token.0.token_id": "old-token-id", - "token.0.token_value": "old-token-value", - }, - HCL: ` - account_id = "abc" - aws_region = "us-east-1" - credentials_id = "bcd" - managed_services_customer_managed_key_id = "def" - storage_customer_managed_key_id = "def" - deployment_name = "900150983cd24fb0" - workspace_name = "labdata" - is_no_public_ip_enabled = true - network_id = "fgh" - storage_configuration_id = "ghi" - workspace_id = 1234 - `, - Update: true, - ID: "abc/1234", - }.Apply(t) - assert.NoError(t, err) - - // Verify that the token was removed from the state - assert.Len(t, d.Get("token"), 0) -} diff --git a/qa/testing.go b/qa/testing.go index 7cb00f81a8..6f4d3a42be 100644 --- a/qa/testing.go +++ b/qa/testing.go @@ -80,8 +80,6 @@ type ResourceFixture struct { MockWorkspaceClientFunc func(*mocks.MockWorkspaceClient) - MockWorkspaceClientsFunc func(map[int64]*mocks.MockWorkspaceClient) - MockAccountClientFunc func(*mocks.MockAccountClient) // The resource the unit test is testing. @@ -205,10 +203,10 @@ func (f ResourceFixture) setDatabricksEnvironmentForTest(client *common.Databric } func (f ResourceFixture) validateMocks() error { - isMockConfigured := f.MockAccountClientFunc != nil || f.MockWorkspaceClientFunc != nil || f.MockWorkspaceClientsFunc != nil + isMockConfigured := f.MockAccountClientFunc != nil || f.MockWorkspaceClientFunc != nil isFixtureConfigured := f.Fixtures != nil if isFixtureConfigured && isMockConfigured { - return fmt.Errorf("either (MockWorkspaceClientFunc, MockWorkspaceClientsFunc, MockAccountClientFunc) or Fixtures may be set, not both") + return fmt.Errorf("either (MockWorkspaceClientFunc, MockAccountClientFunc) or Fixtures may be set, not both") } return nil } @@ -232,14 +230,10 @@ func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, se return client, ss, err } mw := mocks.NewMockWorkspaceClient(t) - mws := map[int64]*mocks.MockWorkspaceClient{} ma := mocks.NewMockAccountClient(t) if f.MockWorkspaceClientFunc != nil { f.MockWorkspaceClientFunc(mw) } - if f.MockWorkspaceClientsFunc != nil { - f.MockWorkspaceClientsFunc(mws) - } if f.MockAccountClientFunc != nil { f.MockAccountClientFunc(ma) } @@ -250,9 +244,6 @@ func (f ResourceFixture) setupClient(t *testing.T) (*common.DatabricksClient, se } c.SetWorkspaceClient(mw.WorkspaceClient) c.SetAccountClient(ma.AccountClient) - for workspaceId, client := range mws { - c.SetWorkspaceClientForWorkspace(workspaceId, client.WorkspaceClient) - } c.Config.Credentials = testCredentialsProvider{token: token} return c, server{ Close: func() {},