Skip to content

Commit

Permalink
Introduce Generic Settings Resource (#2997)
Browse files Browse the repository at this point in the history
* WIP: Generic Settings Resource

* retries as before

* fix

* fix update

* Generic settings resource revamped

* Do not retry on reads, and only retry on resource conflict

* fix
  • Loading branch information
mgyucht authored Jan 29, 2024
1 parent 277537a commit 4acd04d
Show file tree
Hide file tree
Showing 6 changed files with 641 additions and 488 deletions.
4 changes: 3 additions & 1 deletion provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions settings/all_settings.go
Original file line number Diff line number Diff line change
@@ -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_<SETTING_NAME>.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_NAME>_setting".
func AllSettingsResources() map[string]*schema.Resource {
return map[string]*schema.Resource{
"default_namespace": makeSettingResource[settings.DefaultNamespaceSetting, *databricks.WorkspaceClient](defaultNamespaceSetting),
}
}
311 changes: 311 additions & 0 deletions settings/generic_setting.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit 4acd04d

Please sign in to comment.