Skip to content

Commit ddff8ea

Browse files
Add Terraform Provider for DefaultNamespaceSettings (#2710)
* Terraform Provider for DefaultNamespaceSettings * Add resource to providers * Initial docuentation * Update Delete method and tests * More fixes * Rename tests * Add acceptance tests * Fix typo * Change retry strategy * Change function name * Update test * Small fixes * Update test * Update comment * Add some comments * Enable test
1 parent 10402f7 commit ddff8ea

File tree

6 files changed

+612
-2
lines changed

6 files changed

+612
-2
lines changed

common/client.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,16 +107,26 @@ func (c *DatabricksClient) Post(ctx context.Context, path string, request any, r
107107
return c.Do(ctx, http.MethodPost, path, nil, request, response, c.addApiPrefix)
108108
}
109109

110-
// Delete on path
110+
// Delete on path. Ignores succesfull responses from the server.
111111
func (c *DatabricksClient) Delete(ctx context.Context, path string, request any) error {
112112
return c.Do(ctx, http.MethodDelete, path, nil, request, nil, c.addApiPrefix)
113113
}
114114

115-
// Patch on path
115+
// Delete on path. Deserializes the response into the response parameter.
116+
func (c *DatabricksClient) DeleteWithResponse(ctx context.Context, path string, request any, response any) error {
117+
return c.Do(ctx, http.MethodDelete, path, nil, request, response, c.addApiPrefix)
118+
}
119+
120+
// Patch on path. Ignores succesfull responses from the server.
116121
func (c *DatabricksClient) Patch(ctx context.Context, path string, request any) error {
117122
return c.Do(ctx, http.MethodPatch, path, nil, request, nil, c.addApiPrefix)
118123
}
119124

125+
// Patch on path. Deserializes the response into the response parameter.
126+
func (c *DatabricksClient) PatchWithResponse(ctx context.Context, path string, request any, response any) error {
127+
return c.Do(ctx, http.MethodPatch, path, nil, request, response, c.addApiPrefix)
128+
}
129+
120130
// Put on path
121131
func (c *DatabricksClient) Put(ctx context.Context, path string, request any) error {
122132
return c.Do(ctx, http.MethodPut, path, nil, request, nil, c.addApiPrefix)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
subcategory: "Settings"
3+
---
4+
5+
# databricks_namespace_settings Resource
6+
7+
The `databricks_default_namespace_settings` resource allows you to operate the setting configuration for the default namespace in the Databricks workspace.
8+
Setting the default catalog for the workspace determines the catalog that is used when queries do not reference
9+
a fully qualified 3 level name. For example, if the default catalog is set to 'retail_prod' then a query
10+
'SELECT * FROM myTable' would reference the object 'retail_prod.default.myTable'
11+
(the schema 'default' is always assumed).
12+
This setting requires a restart of clusters and SQL warehouses to take effect. Additionally, the default namespace only applies when using Unity Catalog-enabled compute.
13+
## Example Usage
14+
15+
```hcl
16+
resource "databricks_default_namespace_settings" "this" {
17+
namespace {
18+
value = "namespace_value"
19+
}
20+
}
21+
```
22+
23+
## Argument Reference
24+
25+
The resource supports the following arguments:
26+
27+
* `namespace` - (Required) The configuration details.
28+
* `value` - (Required) The value for the setting.
29+
30+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package acceptance
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/databricks/databricks-sdk-go/apierr"
9+
"github.com/databricks/databricks-sdk-go/service/settings"
10+
"github.com/databricks/terraform-provider-databricks/common"
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestAccDefaultNamespaceSetting(t *testing.T) {
15+
workspaceLevel(t, step{
16+
Template: `
17+
resource "databricks_default_namespace_settings" "this" {
18+
namespace {
19+
value = "namespace_value"
20+
}
21+
}
22+
`,
23+
Check: resourceCheck("databricks_default_namespace_settings.this", func(ctx context.Context, client *common.DatabricksClient, id string) error {
24+
ctx = context.WithValue(ctx, common.Api, common.API_2_1)
25+
w, err := client.WorkspaceClient()
26+
assert.NoError(t, err)
27+
res, err := w.Settings.ReadDefaultWorkspaceNamespace(ctx, settings.ReadDefaultWorkspaceNamespaceRequest{
28+
Etag: id,
29+
})
30+
assert.NoError(t, err)
31+
// Check that the resource has been created and that it has the correct value.
32+
assert.Equal(t, res.Namespace.Value, "namespace_value")
33+
return nil
34+
}),
35+
},
36+
step{
37+
Destroy: true,
38+
Check: resourceCheck("databricks_default_namespace_settings.this", func(ctx context.Context, client *common.DatabricksClient, id string) error {
39+
ctx = context.WithValue(ctx, common.Api, common.API_2_1)
40+
w, err := client.WorkspaceClient()
41+
assert.NoError(t, err)
42+
// Terraform Check returns the latest resource status before it is destroyed, which has an outdated eTag.
43+
// We are making an update call to get the correct eTag in the response error.
44+
_, err = w.Settings.UpdateDefaultWorkspaceNamespace(ctx, settings.UpdateDefaultWorkspaceNamespaceRequest{
45+
AllowMissing: true,
46+
Setting: &settings.DefaultNamespaceSetting{
47+
Namespace: settings.StringMessage{
48+
Value: "this_call_should_fail",
49+
},
50+
},
51+
FieldMask: "namespace.value",
52+
})
53+
assert.Error(t, err)
54+
var aerr *apierr.APIError
55+
if !errors.As(err, &aerr) {
56+
assert.FailNow(t, "cannot parse error message %v", err)
57+
}
58+
etag := aerr.Details[0].Metadata["etag"]
59+
_, err = w.Settings.ReadDefaultWorkspaceNamespace(ctx, settings.ReadDefaultWorkspaceNamespaceRequest{
60+
Etag: etag,
61+
})
62+
if !errors.As(err, &aerr) {
63+
assert.FailNow(t, "cannot parse error message %v", err)
64+
}
65+
66+
assert.Equal(t, aerr.ErrorCode, "RESOURCE_DOES_NOT_EXIST")
67+
return nil
68+
}),
69+
},
70+
)
71+
}

provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/databricks/terraform-provider-databricks/scim"
3737
"github.com/databricks/terraform-provider-databricks/secrets"
3838
"github.com/databricks/terraform-provider-databricks/serving"
39+
"github.com/databricks/terraform-provider-databricks/settings"
3940
"github.com/databricks/terraform-provider-databricks/sharing"
4041
"github.com/databricks/terraform-provider-databricks/sql"
4142
"github.com/databricks/terraform-provider-databricks/storage"
@@ -103,6 +104,7 @@ func DatabricksProvider() *schema.Provider {
103104
"databricks_cluster": clusters.ResourceCluster(),
104105
"databricks_cluster_policy": policies.ResourceClusterPolicy(),
105106
"databricks_dbfs_file": storage.ResourceDbfsFile(),
107+
"databricks_default_namespace_settings": settings.ResourceDefaultNamespaceSettings(),
106108
"databricks_directory": workspace.ResourceDirectory(),
107109
"databricks_entitlements": scim.ResourceEntitlements(),
108110
"databricks_external_location": catalog.ResourceExternalLocation(),
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package settings
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/databricks/databricks-sdk-go/apierr"
10+
"github.com/databricks/databricks-sdk-go/service/settings"
11+
"github.com/databricks/terraform-provider-databricks/common"
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
)
14+
15+
// NewDefaultNamespaceSettingsAPI creates a DefaultNamespaceSettingsAPI instance
16+
func NewDefaultNamespaceSettingsAPI(ctx context.Context, m any) DefaultNamespaceSettingsAPI {
17+
client := m.(*common.DatabricksClient)
18+
return DefaultNamespaceSettingsAPI{client, ctx}
19+
}
20+
21+
// DefaultNamespaceSettingsAPI exposes the Default Namespace Settings API
22+
type DefaultNamespaceSettingsAPI struct {
23+
client *common.DatabricksClient
24+
context context.Context
25+
}
26+
27+
func (a DefaultNamespaceSettingsAPI) isEtagVersionError(err error) bool {
28+
var aerr *apierr.APIError
29+
if !errors.As(err, &aerr) {
30+
return false
31+
}
32+
return aerr.StatusCode == http.StatusNotFound || (aerr.StatusCode == http.StatusConflict && aerr.ErrorCode == "RESOURCE_CONFLICT")
33+
}
34+
35+
func (a DefaultNamespaceSettingsAPI) getEtagFromError(err error) (string, error) {
36+
if !a.isEtagVersionError(err) {
37+
return "", err
38+
}
39+
errorInfos := apierr.GetErrorInfo(err)
40+
if len(errorInfos) > 0 {
41+
metadata := errorInfos[0].Metadata
42+
if etag, ok := metadata["etag"]; ok {
43+
return etag, nil
44+
}
45+
}
46+
return "", fmt.Errorf("error fetching the default workspace namespace settings: %w", err)
47+
}
48+
49+
func (a DefaultNamespaceSettingsAPI) DeleteWithRetry(etag string) (string, error) {
50+
etag, err := a.executeDelete(etag)
51+
if err == nil {
52+
return etag, nil
53+
}
54+
etag, err = a.getEtagFromError(err)
55+
if err != nil {
56+
return "", err
57+
}
58+
return a.executeDelete(etag)
59+
}
60+
61+
func (a DefaultNamespaceSettingsAPI) executeDelete(etag string) (string, error) {
62+
var response settings.DefaultNamespaceSetting
63+
err := a.client.DeleteWithResponse(a.context, "/settings/types/default_namespace_ws/names/default", map[string]string{
64+
"etag": etag,
65+
}, &response)
66+
if err != nil {
67+
return "", err
68+
}
69+
return response.Etag, nil
70+
}
71+
72+
func (a DefaultNamespaceSettingsAPI) UpdateWithRetry(request settings.UpdateDefaultWorkspaceNamespaceRequest) (string, error) {
73+
etag, err := a.executeUpdate(request)
74+
if err == nil {
75+
return etag, nil
76+
}
77+
etag, err = a.getEtagFromError(err)
78+
if err != nil {
79+
return "", err
80+
}
81+
request.Setting.Etag = etag
82+
return a.executeUpdate(request)
83+
}
84+
85+
func (a DefaultNamespaceSettingsAPI) executeUpdate(request settings.UpdateDefaultWorkspaceNamespaceRequest) (string, error) {
86+
var response settings.DefaultNamespaceSetting
87+
err := a.client.PatchWithResponse(a.context, "/settings/types/default_namespace_ws/names/default", request, &response)
88+
if err != nil {
89+
return "", err
90+
}
91+
return response.Etag, nil
92+
}
93+
94+
func (a DefaultNamespaceSettingsAPI) Read(etag string) (settings.DefaultNamespaceSetting, error) {
95+
var setting settings.DefaultNamespaceSetting
96+
err := a.client.Get(a.context, "/settings/types/default_namespace_ws/names/default", map[string]string{
97+
"etag": etag,
98+
}, &setting)
99+
return setting, err
100+
}
101+
102+
var resourceSchema = common.StructToSchema(settings.DefaultNamespaceSetting{},
103+
func(s map[string]*schema.Schema) map[string]*schema.Schema {
104+
s["etag"].Computed = true
105+
s["setting_name"].Computed = true
106+
107+
return s
108+
})
109+
110+
func ResourceDefaultNamespaceSettings() *schema.Resource {
111+
return common.Resource{
112+
Schema: resourceSchema,
113+
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
114+
var setting settings.DefaultNamespaceSetting
115+
common.DataToStructPointer(d, resourceSchema, &setting)
116+
setting.SettingName = "default"
117+
request := settings.UpdateDefaultWorkspaceNamespaceRequest{
118+
AllowMissing: true,
119+
Setting: &setting,
120+
FieldMask: "namespace.value",
121+
}
122+
settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c)
123+
etag, err := settingAPI.UpdateWithRetry(request)
124+
if err != nil {
125+
return err
126+
}
127+
d.SetId(etag)
128+
return nil
129+
},
130+
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
131+
settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c)
132+
res, err := settingAPI.Read(d.Id())
133+
if err != nil {
134+
return err
135+
}
136+
err = common.StructToData(res, resourceSchema, d)
137+
if err != nil {
138+
return err
139+
}
140+
// Update the etag. The server will accept any etag and respond
141+
// with a response which is at least as recent as the etag.
142+
// Updating, while not always necessary, ensures that the
143+
// server responds with an updated response.
144+
d.SetId(res.Etag)
145+
return nil
146+
},
147+
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
148+
var setting settings.DefaultNamespaceSetting
149+
common.DataToStructPointer(d, resourceSchema, &setting)
150+
setting.SettingName = "default"
151+
setting.Etag = d.Id()
152+
request := settings.UpdateDefaultWorkspaceNamespaceRequest{
153+
AllowMissing: true,
154+
Setting: &setting,
155+
FieldMask: "namespace.value",
156+
}
157+
settingAPI := NewDefaultNamespaceSettingsAPI(ctx, c)
158+
etag, err := settingAPI.UpdateWithRetry(request)
159+
if err != nil {
160+
return err
161+
}
162+
// Update the etag. The server will accept any etag and respond
163+
// with a response which is at least as recent as the etag.
164+
// Updating, while not always necessary, ensures that the
165+
// server responds with an updated response.
166+
d.SetId(etag)
167+
return nil
168+
},
169+
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
170+
etag, err := NewDefaultNamespaceSettingsAPI(ctx, c).DeleteWithRetry(d.Id())
171+
if err != nil {
172+
return err
173+
}
174+
d.SetId(etag)
175+
return nil
176+
},
177+
}.ToResource()
178+
}

0 commit comments

Comments
 (0)