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 4 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 @@ -106,7 +106,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
284 changes: 173 additions & 111 deletions settings/resource_default_namespace_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,35 @@ import (
"context"
"errors"
"fmt"
"net/http"

"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"
)

// 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 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 (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 isEtagVersionError(err error) bool {
return errors.Is(err, databricks.ErrResourceConflict) || errors.Is(err, databricks.ErrNotFound)
}

func (a DefaultNamespaceSettingsAPI) getEtagFromError(err error) (string, error) {
if !a.isEtagVersionError(err) {
func getEtagFromError(err error) (string, error) {
if !isEtagVersionError(err) {
return "", err
}
errorInfos := apierr.GetErrorInfo(err)
Expand All @@ -46,130 +45,193 @@ func (a DefaultNamespaceSettingsAPI) getEtagFromError(err error) (string, error)
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)
}
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

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
}
// Read the setting from the server. The etag is provided as the third argument.
Read(ctx context.Context, c U, etag string) (*T, error)

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)
// 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 (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
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_<your settings_name>_setting".
// 2. Create a struct corresponding to your setting, and implement either the workspaceSettingDefinition or accountSettingDefinition interface.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a struct corresponding to your setting,

Are you referring to settings.DefaultNamespaceSetting as this struct to be created or type defaultNamespaceSetting struct{}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter, I can clarify that.

// 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
}

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 _ workspaceSettingDefinition[settings.DefaultNamespaceSetting] = defaultNamespaceSetting{}

func AllSettingsResources() map[string]*schema.Resource {
return map[string]*schema.Resource{
defaultNamespaceSettingName: makeSettingResource[settings.DefaultNamespaceSetting, *databricks.WorkspaceClient](defaultNamespaceSetting{}),
}
}

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
// Candidates for code generation: end

return s
})
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
})

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",
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
}
settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c)
etag, err := settingAPI.UpdateWithRetry(request)
res, err = retryOnEtagError[T, string](func(setting T) (string, error) { return defn.Update(ctx, w, setting) }, setting, defn.SetETag)
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())
case accountSettingDefinition[T]:
a, err := c.AccountClient()
if err != nil {
return err
}
err = common.StructToData(res, resourceSchema, d)
res, err = retryOnEtagError(func(setting T) (string, error) { return defn.Update(ctx, a, setting) }, setting, defn.SetETag)
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
default:
return fmt.Errorf("unexpected setting type: %T", defn)
}
d.SetId(res)
return nil
}

return common.Resource{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The common resource definition and the common methods should move out of this file

Schema: resourceSchema,
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
var setting T
return createOrUpdate(ctx, d, c, setting)
},
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",
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)
}
settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c)
etag, err := settingAPI.UpdateWithRetry(request)
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(etag)
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 {
etag, err := NewDefaultNamespaceSettingsAPI(ctx, c).DeleteWithRetry(d.Id())
if err != nil {
return err
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
Expand Down
Loading
Loading