diff --git a/provider/provider.go b/provider/provider.go index b5210b8a55..65c4a4f53c 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -105,7 +105,6 @@ func DatabricksProvider() *schema.Provider { "databricks_cluster": clusters.ResourceCluster(), "databricks_cluster_policy": policies.ResourceClusterPolicy(), "databricks_dbfs_file": storage.ResourceDbfsFile(), - "databricks_default_namespace_setting": settings.ResourceDefaultNamespaceSetting(), "databricks_directory": workspace.ResourceDirectory(), "databricks_entitlements": scim.ResourceEntitlements(), "databricks_external_location": catalog.ResourceExternalLocation(), @@ -178,6 +177,9 @@ func DatabricksProvider() *schema.Provider { }, Schema: providerSchema(), } + for name, resource := range settings.AllSettingsResources() { + p.ResourcesMap[fmt.Sprintf("databricks_%s_setting", name)] = resource + } p.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { if p.TerraformVersion != "" { useragent.WithUserAgentExtra("terraform", p.TerraformVersion) diff --git a/settings/all_settings.go b/settings/all_settings.go new file mode 100644 index 0000000000..e424760a19 --- /dev/null +++ b/settings/all_settings.go @@ -0,0 +1,20 @@ +package settings + +import ( + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// Instructions for adding a new setting: +// +// 1. Create a new file named resource_.go in this directory. +// 2. In that file, create an instance of either the workspaceSettingDefinition or accountSettingDefinition interface for your setting. +// If the setting name is user-settable, it will be provided in the third argument to the updateFunc method. If not, you must set the +// SettingName field appropriately. You must also set AllowMissing: true and the field mask to the field to update. +// 3. Add a new entry to the AllSettingsResources map below. The final resource name will be "databricks__setting". +func AllSettingsResources() map[string]*schema.Resource { + return map[string]*schema.Resource{ + "default_namespace": makeSettingResource[settings.DefaultNamespaceSetting, *databricks.WorkspaceClient](defaultNamespaceSetting), + } +} diff --git a/settings/generic_setting.go b/settings/generic_setting.go new file mode 100644 index 0000000000..6095c88c4d --- /dev/null +++ b/settings/generic_setting.go @@ -0,0 +1,311 @@ +package settings + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/terraform-provider-databricks/common" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func retryOnEtagError[Req, Resp any](f func(req Req) (Resp, error), firstReq Req, updateReq func(req *Req, newEtag string), retriableErrors []error) (Resp, error) { + req := firstReq + // Retry once on etag error. + res, err := f(req) + if err == nil { + return res, nil + } + if !isRetriableError(err, retriableErrors) { + return res, err + } + etag, err := getEtagFromError(err) + if err != nil { + return res, err + } + updateReq(&req, etag) + return f(req) +} + +func isRetriableError(err error, retriableErrors []error) bool { + for _, retriableError := range retriableErrors { + if errors.Is(err, retriableError) { + return true + } + } + return false +} + +func getEtagFromError(err error) (string, error) { + errorInfos := apierr.GetErrorInfo(err) + if len(errorInfos) > 0 { + metadata := errorInfos[0].Metadata + if etag, ok := metadata["etag"]; ok { + return etag, nil + } + } + return "", fmt.Errorf("error fetching the default workspace namespace settings: %w", err) +} + +type genericSettingDefinition[T, U any] interface { + // Returns the struct corresponding to the setting. The schema of the Terraform resource will be generated from this struct. + SettingStruct() T + + // Read the setting from the server. The etag is provided as the third argument. + Read(ctx context.Context, c U, etag string) (*T, error) + + // Update the setting to the value specified by t, and return the new etag. + Update(ctx context.Context, c U, t T) (string, error) + + // Delete the setting with the given etag, and return the new etag. + Delete(ctx context.Context, c U, etag string) (string, error) + + // Get the etag from the setting. + GetETag(t *T) string + + // Update the etag in the setting. + SetETag(t *T, newEtag string) +} + +func getEtag[T any](t T) string { + rv := reflect.ValueOf(t) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + return rv.FieldByName("Etag").String() +} + +func setEtag[T any](t T, newEtag string) { + rv := reflect.ValueOf(t) + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + rv.FieldByName("Etag").SetString(newEtag) +} + +type workspaceSettingDefinition[T any] genericSettingDefinition[T, *databricks.WorkspaceClient] + +// A workspace setting is a setting that is scoped to a workspace. +type workspaceSetting[T any] struct { + // The struct corresponding to the setting. The schema of the Terraform resource will be generated from this struct. + // This struct must have an Etag field of type string. + settingStruct T + + // Read the setting from the server. The etag is provided as the third argument. + readFunc func(ctx context.Context, w *databricks.WorkspaceClient, etag string) (*T, error) + + // Update the setting to the value specified by t, and return the new etag. If the setting name is user-settable, + // it will be provided in the third argument. If not, you must set the SettingName field appropriately. You must + // also set AllowMissing: true and the field mask to the field to update. + updateFunc func(ctx context.Context, w *databricks.WorkspaceClient, setting T) (string, error) + + // Delete the setting with the given etag, and return the new etag. + deleteFunc func(ctx context.Context, w *databricks.WorkspaceClient, etag string) (string, error) +} + +func (w workspaceSetting[T]) SettingStruct() T { + return w.settingStruct +} +func (w workspaceSetting[T]) Read(ctx context.Context, c *databricks.WorkspaceClient, etag string) (*T, error) { + return w.readFunc(ctx, c, etag) +} +func (w workspaceSetting[T]) Update(ctx context.Context, c *databricks.WorkspaceClient, t T) (string, error) { + return w.updateFunc(ctx, c, t) +} +func (w workspaceSetting[T]) Delete(ctx context.Context, c *databricks.WorkspaceClient, etag string) (string, error) { + return w.deleteFunc(ctx, c, etag) +} +func (w workspaceSetting[T]) GetETag(t *T) string { + return getEtag(t) +} +func (w workspaceSetting[T]) SetETag(t *T, newEtag string) { + setEtag(t, newEtag) +} + +var _ workspaceSettingDefinition[struct{}] = workspaceSetting[struct{}]{} + +type accountSettingDefinition[T any] genericSettingDefinition[T, *databricks.AccountClient] + +// An account setting is a setting that is scoped to a workspace. +type accountSetting[T any] struct { + // The struct corresponding to the setting. The schema of the Terraform resource will be generated from this struct. + // This struct must have an Etag field of type string. + settingStruct T + + // Read the setting from the server. The etag is provided as the third argument. + readFunc func(ctx context.Context, w *databricks.AccountClient, etag string) (*T, error) + + // Update the setting to the value specified by t, and return the new etag. If the setting name is user-settable, + // it will be provided in the third argument. If not, you must set the SettingName field appropriately. You must + // also set AllowMissing: true and the field mask to the field to update. + updateFunc func(ctx context.Context, w *databricks.AccountClient, setting T) (string, error) + + // Delete the setting with the given etag, and return the new etag. + deleteFunc func(ctx context.Context, w *databricks.AccountClient, etag string) (string, error) +} + +func (w accountSetting[T]) SettingStruct() T { + return w.settingStruct +} +func (w accountSetting[T]) Read(ctx context.Context, c *databricks.AccountClient, etag string) (*T, error) { + return w.readFunc(ctx, c, etag) +} +func (w accountSetting[T]) Update(ctx context.Context, c *databricks.AccountClient, t T) (string, error) { + return w.updateFunc(ctx, c, t) +} +func (w accountSetting[T]) Delete(ctx context.Context, c *databricks.AccountClient, etag string) (string, error) { + return w.deleteFunc(ctx, c, etag) +} +func (w accountSetting[T]) GetETag(t *T) string { + return getEtag(t) +} +func (w accountSetting[T]) SetETag(t *T, newEtag string) { + setEtag(t, newEtag) +} + +var _ accountSettingDefinition[struct{}] = accountSetting[struct{}]{} + +func makeSettingResource[T, U any](defn genericSettingDefinition[T, U]) *schema.Resource { + resourceSchema := common.StructToSchema(defn.SettingStruct(), + func(s map[string]*schema.Schema) map[string]*schema.Schema { + s["etag"].Computed = true + // Note: this may not always be computed, but it is for the default namespace setting. If other settings + // are added for which setting_name is not computed, we'll need to expose this somehow as part of the setting + // definition. + s["setting_name"].Computed = true + return s + }) + createOrUpdateRetriableErrors := []error{apierr.ErrNotFound, apierr.ErrResourceConflict} + deleteRetriableErrors := []error{apierr.ErrResourceConflict} + createOrUpdate := func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient, setting T) error { + common.DataToStructPointer(d, resourceSchema, &setting) + var res string + switch defn := defn.(type) { + case workspaceSettingDefinition[T]: + w, err := c.WorkspaceClient() + if err != nil { + return err + } + res, err = retryOnEtagError[T, string]( + func(setting T) (string, error) { + return defn.Update(ctx, w, setting) + }, + setting, + defn.SetETag, + createOrUpdateRetriableErrors) + if err != nil { + return err + } + case accountSettingDefinition[T]: + a, err := c.AccountClient() + if err != nil { + return err + } + res, err = retryOnEtagError( + func(setting T) (string, error) { + return defn.Update(ctx, a, setting) + }, + setting, + defn.SetETag, + createOrUpdateRetriableErrors) + if err != nil { + return err + } + default: + return fmt.Errorf("unexpected setting type: %T", defn) + } + d.SetId(res) + return nil + } + + return common.Resource{ + Schema: resourceSchema, + Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + var setting T + return createOrUpdate(ctx, d, c, setting) + }, + Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + var res *T + switch defn := defn.(type) { + case workspaceSettingDefinition[T]: + w, err := c.WorkspaceClient() + if err != nil { + return err + } + res, err = defn.Read(ctx, w, d.Id()) + if err != nil { + return err + } + case accountSettingDefinition[T]: + a, err := c.AccountClient() + if err != nil { + return err + } + res, err = defn.Read(ctx, a, d.Id()) + if err != nil { + return err + } + default: + return fmt.Errorf("unexpected setting type: %T", defn) + } + err := common.StructToData(res, resourceSchema, d) + if err != nil { + return err + } + // Update the etag. The server will accept any etag and respond + // with a response which is at least as recent as the etag. + // Updating, while not always necessary, ensures that the + // server responds with an updated response. + d.SetId(defn.GetETag(res)) + return nil + }, + Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + var setting T + defn.SetETag(&setting, d.Id()) + return createOrUpdate(ctx, d, c, setting) + }, + Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { + var etag string + updateETag := func(req *string, newEtag string) { *req = newEtag } + switch defn := defn.(type) { + case workspaceSettingDefinition[T]: + w, err := c.WorkspaceClient() + if err != nil { + return err + } + etag, err = retryOnEtagError( + func(etag string) (string, error) { + return defn.Delete(ctx, w, etag) + }, + d.Id(), + updateETag, + deleteRetriableErrors) + if err != nil { + return err + } + case accountSettingDefinition[T]: + a, err := c.AccountClient() + if err != nil { + return err + } + etag, err = retryOnEtagError( + func(etag string) (string, error) { + return defn.Delete(ctx, a, etag) + }, + d.Id(), + updateETag, + deleteRetriableErrors) + if err != nil { + return err + } + default: + return fmt.Errorf("unexpected setting type: %T", defn) + } + d.SetId(etag) + return nil + }, + }.ToResource() +} diff --git a/settings/generic_setting_test.go b/settings/generic_setting_test.go new file mode 100644 index 0000000000..4a273c90c0 --- /dev/null +++ b/settings/generic_setting_test.go @@ -0,0 +1,278 @@ +package settings + +import ( + "testing" + + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/experimental/mocks" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/databricks/terraform-provider-databricks/qa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// Choose an arbitrary setting to test. +var testSetting = AllSettingsResources()["default_namespace"] + +func TestQueryCreateDefaultNameSetting(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockSettingsAPI().EXPECT() + e.UpdateDefaultWorkspaceNamespace(mock.Anything, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + FieldMask: "namespace.value", + Setting: &settings.DefaultNamespaceSetting{ + Etag: "", + Namespace: settings.StringMessage{ + Value: "namespace_value", + }, + SettingName: "default", + }, + }).Return(nil, &apierr.APIError{ + ErrorCode: "NOT_FOUND", + StatusCode: 404, + Message: "SomeMessage", + Details: []apierr.ErrorDetail{{ + Type: "type.googleapis.com/google.rpc.ErrorInfo", + Metadata: map[string]string{ + "etag": "etag1", + }, + }}, + }) + e.UpdateDefaultWorkspaceNamespace(mock.Anything, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + FieldMask: "namespace.value", + Setting: &settings.DefaultNamespaceSetting{ + Etag: "etag1", + Namespace: settings.StringMessage{ + Value: "namespace_value", + }, + SettingName: "default", + }, + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "namespace_value", + }, + SettingName: "default", + }, nil) + e.ReadDefaultWorkspaceNamespace(mock.Anything, settings.ReadDefaultWorkspaceNamespaceRequest{ + Etag: "etag2", + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "namespace_value", + }, + SettingName: "default", + }, nil) + }, + Resource: testSetting, + Create: true, + HCL: ` + namespace { + value = "namespace_value" + } + `, + }.Apply(t) + + assert.NoError(t, err) + + assert.Equal(t, "etag2", d.Id()) + assert.Equal(t, "namespace_value", d.Get("namespace.0.value")) +} + +func TestQueryReadDefaultNameSetting(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockSettingsAPI().EXPECT().ReadDefaultWorkspaceNamespace(mock.Anything, settings.ReadDefaultWorkspaceNamespaceRequest{ + Etag: "etag1", + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "namespace_value", + }, + SettingName: "default", + }, nil) + }, + Resource: testSetting, + Read: true, + HCL: ` + namespace { + value = "namespace_value" + } + `, + ID: "etag1", + }.Apply(t) + + assert.NoError(t, err) + + assert.Equal(t, "etag2", d.Id()) + res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) + assert.Equal(t, "namespace_value", res["value"]) +} + +func TestQueryUpdateDefaultNameSetting(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockSettingsAPI().EXPECT() + e.UpdateDefaultWorkspaceNamespace(mock.Anything, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + FieldMask: "namespace.value", + Setting: &settings.DefaultNamespaceSetting{ + Etag: "etag1", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, nil) + e.ReadDefaultWorkspaceNamespace(mock.Anything, settings.ReadDefaultWorkspaceNamespaceRequest{ + Etag: "etag2", + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, nil) + }, + Resource: testSetting, + Update: true, + HCL: ` + namespace { + value = "new_namespace_value" + } + `, + ID: "etag1", + }.Apply(t) + + assert.NoError(t, err) + + assert.Equal(t, "etag2", d.Id()) + res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) + assert.Equal(t, "new_namespace_value", res["value"]) +} + +func TestQueryUpdateDefaultNameSettingWithConflict(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + e := w.GetMockSettingsAPI().EXPECT() + e.UpdateDefaultWorkspaceNamespace(mock.Anything, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + FieldMask: "namespace.value", + Setting: &settings.DefaultNamespaceSetting{ + Etag: "etag1", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, + }).Return(nil, &apierr.APIError{ + ErrorCode: "RESOURCE_CONFLICT", + StatusCode: 409, + Message: "SomeMessage", + Details: []apierr.ErrorDetail{{ + Type: "type.googleapis.com/google.rpc.ErrorInfo", + Metadata: map[string]string{ + "etag": "etag2", + }, + }}, + }) + e.UpdateDefaultWorkspaceNamespace(mock.Anything, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + FieldMask: "namespace.value", + Setting: &settings.DefaultNamespaceSetting{ + Etag: "etag2", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag3", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, nil) + e.ReadDefaultWorkspaceNamespace(mock.Anything, settings.ReadDefaultWorkspaceNamespaceRequest{ + Etag: "etag3", + }).Return(&settings.DefaultNamespaceSetting{ + Etag: "etag3", + Namespace: settings.StringMessage{ + Value: "new_namespace_value", + }, + SettingName: "default", + }, nil) + }, + Resource: testSetting, + Update: true, + HCL: ` + namespace { + value = "new_namespace_value" + } + `, + ID: "etag1", + }.Apply(t) + + assert.NoError(t, err) + + assert.Equal(t, "etag3", d.Id()) + res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) + assert.Equal(t, "new_namespace_value", res["value"]) +} + +func TestQueryDeleteDefaultNameSetting(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockSettingsAPI().EXPECT().DeleteDefaultWorkspaceNamespace(mock.Anything, settings.DeleteDefaultWorkspaceNamespaceRequest{ + Etag: "etag1", + }).Return(&settings.DeleteDefaultWorkspaceNamespaceResponse{ + Etag: "etag2", + }, nil) + }, + Resource: testSetting, + Delete: true, + ID: "etag1", + }.Apply(t) + + assert.NoError(t, err) + assert.Equal(t, "etag2", d.Id()) +} + +func TestQueryDeleteDefaultNameSettingWithConflict(t *testing.T) { + d, err := qa.ResourceFixture{ + MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { + w.GetMockSettingsAPI().EXPECT().DeleteDefaultWorkspaceNamespace(mock.Anything, settings.DeleteDefaultWorkspaceNamespaceRequest{ + Etag: "etag1", + }).Return(nil, &apierr.APIError{ + ErrorCode: "RESOURCE_CONFLICT", + StatusCode: 409, + Message: "SomeMessage", + Details: []apierr.ErrorDetail{{ + Type: "type.googleapis.com/google.rpc.ErrorInfo", + Metadata: map[string]string{ + "etag": "etag2", + }, + }}, + }) + w.GetMockSettingsAPI().EXPECT().DeleteDefaultWorkspaceNamespace(mock.Anything, settings.DeleteDefaultWorkspaceNamespaceRequest{ + Etag: "etag2", + }).Return(&settings.DeleteDefaultWorkspaceNamespaceResponse{ + Etag: "etag3", + }, nil) + }, + Resource: testSetting, + Delete: true, + ID: "etag1", + }.Apply(t) + + assert.NoError(t, err) + assert.Equal(t, "etag3", d.Id()) +} diff --git a/settings/resource_default_namespace_setting.go b/settings/resource_default_namespace_setting.go index c7a1e44753..8457b72191 100644 --- a/settings/resource_default_namespace_setting.go +++ b/settings/resource_default_namespace_setting.go @@ -2,177 +2,38 @@ package settings import ( "context" - "errors" - "fmt" - "net/http" - "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/settings" - "github.com/databricks/terraform-provider-databricks/common" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -// NewDefaultNamespaceSettingsAPI creates a DefaultNamespaceSettingsAPI instance -func NewDefaultNamespaceSettingsAPI(ctx context.Context, m any) DefaultNamespaceSettingsAPI { - client := m.(*common.DatabricksClient) - return DefaultNamespaceSettingsAPI{client, ctx} -} - -// DefaultNamespaceSettingsAPI exposes the Default Namespace Settings API -type DefaultNamespaceSettingsAPI struct { - client *common.DatabricksClient - context context.Context -} - -func (a DefaultNamespaceSettingsAPI) isEtagVersionError(err error) bool { - var aerr *apierr.APIError - if !errors.As(err, &aerr) { - return false - } - return aerr.StatusCode == http.StatusNotFound || (aerr.StatusCode == http.StatusConflict && aerr.ErrorCode == "RESOURCE_CONFLICT") -} - -func (a DefaultNamespaceSettingsAPI) getEtagFromError(err error) (string, error) { - if !a.isEtagVersionError(err) { - return "", err - } - errorInfos := apierr.GetErrorInfo(err) - if len(errorInfos) > 0 { - metadata := errorInfos[0].Metadata - if etag, ok := metadata["etag"]; ok { - return etag, nil +// Default Namespace Setting +var defaultNamespaceSetting = workspaceSetting[settings.DefaultNamespaceSetting]{ + settingStruct: settings.DefaultNamespaceSetting{}, + readFunc: func(ctx context.Context, w *databricks.WorkspaceClient, etag string) (*settings.DefaultNamespaceSetting, error) { + return w.Settings.ReadDefaultWorkspaceNamespace(ctx, settings.ReadDefaultWorkspaceNamespaceRequest{ + Etag: etag, + }) + }, + updateFunc: func(ctx context.Context, w *databricks.WorkspaceClient, t settings.DefaultNamespaceSetting) (string, error) { + t.SettingName = "default" + res, err := w.Settings.UpdateDefaultWorkspaceNamespace(ctx, settings.UpdateDefaultWorkspaceNamespaceRequest{ + AllowMissing: true, + Setting: &t, + FieldMask: "namespace.value", + }) + if err != nil { + return "", err } - } - return "", fmt.Errorf("error fetching the default workspace namespace settings: %w", err) -} - -func (a DefaultNamespaceSettingsAPI) DeleteWithRetry(etag string) (string, error) { - etag, err := a.executeDelete(etag) - if err == nil { - return etag, nil - } - etag, err = a.getEtagFromError(err) - if err != nil { - return "", err - } - return a.executeDelete(etag) -} - -func (a DefaultNamespaceSettingsAPI) executeDelete(etag string) (string, error) { - var response settings.DefaultNamespaceSetting - err := a.client.DeleteWithResponse(a.context, "/settings/types/default_namespace_ws/names/default", map[string]string{ - "etag": etag, - }, &response) - if err != nil { - return "", err - } - return response.Etag, nil -} - -func (a DefaultNamespaceSettingsAPI) UpdateWithRetry(request settings.UpdateDefaultWorkspaceNamespaceRequest) (string, error) { - etag, err := a.executeUpdate(request) - if err == nil { - return etag, nil - } - etag, err = a.getEtagFromError(err) - if err != nil { - return "", err - } - request.Setting.Etag = etag - return a.executeUpdate(request) -} - -func (a DefaultNamespaceSettingsAPI) executeUpdate(request settings.UpdateDefaultWorkspaceNamespaceRequest) (string, error) { - var response settings.DefaultNamespaceSetting - err := a.client.PatchWithResponse(a.context, "/settings/types/default_namespace_ws/names/default", request, &response) - if err != nil { - return "", err - } - return response.Etag, nil -} - -func (a DefaultNamespaceSettingsAPI) Read(etag string) (settings.DefaultNamespaceSetting, error) { - var setting settings.DefaultNamespaceSetting - err := a.client.Get(a.context, "/settings/types/default_namespace_ws/names/default", map[string]string{ - "etag": etag, - }, &setting) - return setting, err -} - -var resourceSchema = common.StructToSchema(settings.DefaultNamespaceSetting{}, - func(s map[string]*schema.Schema) map[string]*schema.Schema { - s["etag"].Computed = true - s["setting_name"].Computed = true - - return s - }) - -func ResourceDefaultNamespaceSetting() *schema.Resource { - return common.Resource{ - Schema: resourceSchema, - Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - var setting settings.DefaultNamespaceSetting - common.DataToStructPointer(d, resourceSchema, &setting) - setting.SettingName = "default" - request := settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - Setting: &setting, - FieldMask: "namespace.value", - } - settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c) - etag, err := settingAPI.UpdateWithRetry(request) - if err != nil { - return err - } - d.SetId(etag) - return nil - }, - Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c) - res, err := settingAPI.Read(d.Id()) - if err != nil { - return err - } - err = common.StructToData(res, resourceSchema, d) - if err != nil { - return err - } - // Update the etag. The server will accept any etag and respond - // with a response which is at least as recent as the etag. - // Updating, while not always necessary, ensures that the - // server responds with an updated response. - d.SetId(res.Etag) - return nil - }, - Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - var setting settings.DefaultNamespaceSetting - common.DataToStructPointer(d, resourceSchema, &setting) - setting.SettingName = "default" - setting.Etag = d.Id() - request := settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - Setting: &setting, - FieldMask: "namespace.value", - } - settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c) - etag, err := settingAPI.UpdateWithRetry(request) - if err != nil { - return err - } - // Update the etag. The server will accept any etag and respond - // with a response which is at least as recent as the etag. - // Updating, while not always necessary, ensures that the - // server responds with an updated response. - d.SetId(etag) - return nil - }, - Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { - etag, err := NewDefaultNamespaceSettingsAPI(ctx, c).DeleteWithRetry(d.Id()) - if err != nil { - return err - } - d.SetId(etag) - return nil - }, - }.ToResource() + return res.Etag, err + }, + deleteFunc: func(ctx context.Context, w *databricks.WorkspaceClient, etag string) (string, error) { + res, err := w.Settings.DeleteDefaultWorkspaceNamespace(ctx, settings.DeleteDefaultWorkspaceNamespaceRequest{ + Etag: etag, + }) + if err != nil { + return "", err + } + return res.Etag, err + }, } diff --git a/settings/resource_default_namespace_setting_test.go b/settings/resource_default_namespace_setting_test.go deleted file mode 100644 index a99a4937c4..0000000000 --- a/settings/resource_default_namespace_setting_test.go +++ /dev/null @@ -1,319 +0,0 @@ -package settings - -import ( - "testing" - - "github.com/databricks/databricks-sdk-go/apierr" - "github.com/databricks/databricks-sdk-go/service/settings" - "github.com/databricks/terraform-provider-databricks/qa" - "github.com/stretchr/testify/assert" -) - -func TestQueryCreateDefaultNameSetting(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "PATCH", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default", - Status: 404, - ExpectedRequest: settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - FieldMask: "namespace.value", - Setting: &settings.DefaultNamespaceSetting{ - Etag: "", - Namespace: settings.StringMessage{ - Value: "namespace_value", - }, - SettingName: "default", - }, - }, - Response: apierr.APIErrorBody{ - ErrorCode: "NOT_FOUND", - Message: "SomeMessage", - Details: []apierr.ErrorDetail{{ - Type: "type.googleapis.com/google.rpc.ErrorInfo", - Metadata: map[string]string{ - "etag": "etag1", - }, - }}, - }, - }, - { - Method: "PATCH", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default", - Status: 200, - ExpectedRequest: settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - FieldMask: "namespace.value", - Setting: &settings.DefaultNamespaceSetting{ - Etag: "etag1", - Namespace: settings.StringMessage{ - Value: "namespace_value", - }, - SettingName: "default", - }, - }, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "namespace_value", - }, - SettingName: "default", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag2", - Status: 200, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "namespace_value", - }, - SettingName: "default", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Create: true, - HCL: ` - namespace { - value = "namespace_value" - } - `, - }.Apply(t) - - assert.NoError(t, err) - - assert.Equal(t, "etag2", d.Id()) - assert.Equal(t, "namespace_value", d.Get("namespace.0.value")) -} - -func TestQueryReadDefaultNameSetting(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "GET", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag1", - Status: 200, - Response: &settings.DefaultNamespaceSetting{ - // This simulates that the Setting has been changed externally. Thus the different etag. - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "namespace_value", - }, - SettingName: "default", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Read: true, - HCL: ` - namespace { - value = "namespace_value" - } - `, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - - assert.Equal(t, "etag2", d.Id()) - res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) - assert.Equal(t, "namespace_value", res["value"]) -} - -func TestQueryUpdateDefaultNameSetting(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "PATCH", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default", - Status: 200, - ExpectedRequest: settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - FieldMask: "namespace.value", - Setting: &settings.DefaultNamespaceSetting{ - Etag: "etag1", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag2", - Status: 200, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Update: true, - HCL: ` - namespace { - value = "new_namespace_value" - } - `, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - - assert.Equal(t, "etag2", d.Id()) - res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) - assert.Equal(t, "new_namespace_value", res["value"]) -} - -func TestQueryUpdateDefaultNameSettingWithConflict(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "PATCH", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default", - Status: 409, - ExpectedRequest: settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - FieldMask: "namespace.value", - Setting: &settings.DefaultNamespaceSetting{ - Etag: "etag1", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - Response: apierr.APIErrorBody{ - ErrorCode: "RESOURCE_CONFLICT", - Message: "SomeMessage", - Details: []apierr.ErrorDetail{{ - Type: "type.googleapis.com/google.rpc.ErrorInfo", - Metadata: map[string]string{ - "etag": "etag2", - }, - }}, - }, - }, - { - Method: "PATCH", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default", - Status: 200, - ExpectedRequest: settings.UpdateDefaultWorkspaceNamespaceRequest{ - AllowMissing: true, - FieldMask: "namespace.value", - Setting: &settings.DefaultNamespaceSetting{ - Etag: "etag2", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag3", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - { - Method: "GET", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag3", - Status: 200, - Response: &settings.DefaultNamespaceSetting{ - Etag: "etag3", - Namespace: settings.StringMessage{ - Value: "new_namespace_value", - }, - SettingName: "default", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Update: true, - HCL: ` - namespace { - value = "new_namespace_value" - } - `, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - - assert.Equal(t, "etag3", d.Id()) - res := d.Get("namespace").([]interface{})[0].(map[string]interface{}) - assert.Equal(t, "new_namespace_value", res["value"]) -} - -func TestQueryDeleteDefaultNameSetting(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "DELETE", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag1", - Status: 200, - Response: &settings.DeleteDefaultWorkspaceNamespaceResponse{ - Etag: "etag2", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Delete: true, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - assert.Equal(t, "etag2", d.Id()) -} - -func TestQueryDeleteDefaultNameSettingWithConflict(t *testing.T) { - d, err := qa.ResourceFixture{ - Fixtures: []qa.HTTPFixture{ - { - Method: "DELETE", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag1", - Status: 409, - Response: apierr.APIErrorBody{ - ErrorCode: "RESOURCE_CONFLICT", - Message: "SomeMessage", - Details: []apierr.ErrorDetail{{ - Type: "type.googleapis.com/google.rpc.ErrorInfo", - Metadata: map[string]string{ - "etag": "etag2", - }, - }}, - }, - }, - { - Method: "DELETE", - Resource: "/api/2.0/settings/types/default_namespace_ws/names/default?etag=etag2", - Status: 200, - Response: &settings.DeleteDefaultWorkspaceNamespaceResponse{ - Etag: "etag3", - }, - }, - }, - Resource: ResourceDefaultNamespaceSetting(), - Delete: true, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - assert.Equal(t, "etag3", d.Id()) -}