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..c271292436 --- /dev/null +++ b/settings/generic_setting.go @@ -0,0 +1,280 @@ +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)) (Resp, error) { + req := firstReq + // Retry once on etag error. + res, err := f(req) + if err == nil { + return res, nil + } + etag, err := getEtagFromError(err) + if err != nil { + return res, err + } + updateReq(&req, etag) + return f(req) +} + +func isEtagVersionError(err error) bool { + return errors.Is(err, databricks.ErrResourceConflict) || errors.Is(err, databricks.ErrNotFound) +} + +func getEtagFromError(err error) (string, error) { + if !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 + } + } + 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 + }) + + 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) + 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) + 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 = retryOnEtagError(func(etag string) (*T, error) { return defn.Read(ctx, w, etag) }, d.Id(), func(req *string, newEtag string) { *req = newEtag }) + if err != nil { + return err + } + case accountSettingDefinition[T]: + a, err := c.AccountClient() + if err != nil { + return err + } + res, err = retryOnEtagError(func(etag string) (*T, error) { return defn.Read(ctx, a, etag) }, d.Id(), func(req *string, newEtag string) { *req = newEtag }) + 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 + 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(), func(req *string, newEtag string) { *req = newEtag }) + 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(), func(req *string, newEtag string) { *req = newEtag }) + 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 e89be619a5..8457b72191 100644 --- a/settings/resource_default_namespace_setting.go +++ b/settings/resource_default_namespace_setting.go @@ -2,239 +2,38 @@ package settings import ( "context" - "errors" - "fmt" "github.com/databricks/databricks-sdk-go" - "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/settings" - "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)) (Resp, error) { - req := firstReq - // Retry once on etag error. - res, err := f(req) - if err == nil { - return res, nil - } - etag, err := getEtagFromError(err) - if err != nil { - return res, err - } - updateReq(&req, etag) - return f(req) -} - -func isEtagVersionError(err error) bool { - return errors.Is(err, databricks.ErrResourceConflict) || errors.Is(err, databricks.ErrNotFound) -} - -func getEtagFromError(err error) (string, error) { - if !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 - } - } - 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) -} - -type workspaceSettingDefinition[T any] genericSettingDefinition[T, *databricks.WorkspaceClient] -type accountSettingDefinition[T any] genericSettingDefinition[T, *databricks.AccountClient] - -// Instructions for adding a new setting: -// -// 1. Add a new setting name constant. The resulting Terraform resource name will be "databricks__setting". -// 2. Create a struct corresponding to your setting, and implement either the workspaceSettingDefinition or accountSettingDefinition interface. -// Add an assertion to ensure that your type implements said interface. If the setting name is user-settable, it will be provided in the -// third argument to the Update 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. -const ( - defaultNamespaceSettingName string = "default_namespace" ) // Default Namespace Setting -type defaultNamespaceSetting struct{} - -func (defaultNamespaceSetting) SettingStruct() settings.DefaultNamespaceSetting { - return settings.DefaultNamespaceSetting{} -} -func (defaultNamespaceSetting) Read(ctx context.Context, w *databricks.WorkspaceClient, etag string) (*settings.DefaultNamespaceSetting, error) { - return w.Settings.ReadDefaultWorkspaceNamespace(ctx, settings.ReadDefaultWorkspaceNamespaceRequest{ - Etag: etag, - }) -} -func (defaultNamespaceSetting) Update(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", - }) - return res.Etag, err -} -func (defaultNamespaceSetting) Delete(ctx context.Context, w *databricks.WorkspaceClient, etag string) (string, error) { - res, err := w.Settings.DeleteDefaultWorkspaceNamespace(ctx, settings.DeleteDefaultWorkspaceNamespaceRequest{ - Etag: etag, - }) - return res.Etag, err -} -func (defaultNamespaceSetting) GetETag(t *settings.DefaultNamespaceSetting) string { - return t.Etag -} -func (defaultNamespaceSetting) SetETag(t *settings.DefaultNamespaceSetting, newEtag string) { - t.Etag = newEtag -} - -var _ workspaceSettingDefinition[settings.DefaultNamespaceSetting] = defaultNamespaceSetting{} - -func AllSettingsResources() map[string]*schema.Resource { - return map[string]*schema.Resource{ - defaultNamespaceSettingName: makeSettingResource[settings.DefaultNamespaceSetting, *databricks.WorkspaceClient](defaultNamespaceSetting{}), - } -} - -// Candidates for code generation: end - -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 +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, }) - - 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) - 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) - if err != nil { - return err - } - default: - return fmt.Errorf("unexpected setting type: %T", defn) + }, + 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 } - 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 = retryOnEtagError(func(etag string) (*T, error) { return defn.Read(ctx, w, etag) }, d.Id(), func(req *string, newEtag string) { *req = newEtag }) - if err != nil { - return err - } - case accountSettingDefinition[T]: - a, err := c.AccountClient() - if err != nil { - return err - } - res, err = retryOnEtagError(func(etag string) (*T, error) { return defn.Read(ctx, a, etag) }, d.Id(), func(req *string, newEtag string) { *req = newEtag }) - 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 - 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(), func(req *string, newEtag string) { *req = newEtag }) - 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(), func(req *string, newEtag string) { *req = newEtag }) - if err != nil { - return err - } - default: - return fmt.Errorf("unexpected setting type: %T", defn) - } - 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 33ce706e5e..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: AllSettingsResources()[defaultNamespaceSettingName], - 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: AllSettingsResources()[defaultNamespaceSettingName], - 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: AllSettingsResources()[defaultNamespaceSettingName], - 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: AllSettingsResources()[defaultNamespaceSettingName], - 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: AllSettingsResources()[defaultNamespaceSettingName], - 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: AllSettingsResources()[defaultNamespaceSettingName], - Delete: true, - ID: "etag1", - }.Apply(t) - - assert.NoError(t, err) - assert.Equal(t, "etag3", d.Id()) -}