Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Generic Settings Resource #2997

Merged
merged 8 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading