-
Notifications
You must be signed in to change notification settings - Fork 398
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
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
287ae33
WIP: Generic Settings Resource
mgyucht 30f87f6
retries as before
mgyucht 98476c8
fix
mgyucht f9a3327
fix update
mgyucht 2ee62c1
Merge branch 'master' into generic-settings-resource
mgyucht 798736f
Generic settings resource revamped
mgyucht 157bab3
Do not retry on reads, and only retry on resource conflict
mgyucht 4ed36ff
fix
mgyucht File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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. | ||
// 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{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you referring to
settings.DefaultNamespaceSetting
as this struct to be created ortype defaultNamespaceSetting struct{}
?There was a problem hiding this comment.
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.