Skip to content

Commit 1eb0c51

Browse files
authored
[Fix] Tolerate invalid keys in databricks_workspace_conf (#4102)
## Changes Occasionally, support for configuration keys is removed from the GetStatus/SetStatus APIs used to manage workspace configuration. When this happens, users who depended on those keys are in a bad state. The provider queries for the values for each configuration by key in the Terraform state, but those keys no longer exist. To work around this, this PR makes `databricks_workspace_conf` resilient to keys being removed. On the read path, the provider queries for the status of all keys in state. If any key is no longer valid, it is removed from the request and the request is retried. Setting the value for invalid configuration keys is not supported. When removing an unsupported configuration key, the provider resets the key to an original state (`false` or `""`). Failures to reset invalid keys are ignored, both on the update path (removing an unsupported key from the conf) and on the delete path (removing the `databricks_workspace_conf` resource altogether). ## Tests Integration tests verify that the new SafeGetStatus and SafeSetStatus methods work with invalid keys as expected. On the read side, they are ignored, and on the write side, they are ignored if marked for removal. - [ ] `make test` run locally - [ ] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] relevant acceptance tests are passing - [ ] using Go SDK
1 parent e24c780 commit 1eb0c51

File tree

2 files changed

+130
-7
lines changed

2 files changed

+130
-7
lines changed

internal/acceptance/workspace_conf_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/databricks/databricks-sdk-go"
99
"github.com/databricks/databricks-sdk-go/service/settings"
10+
"github.com/databricks/terraform-provider-databricks/workspace"
1011
"github.com/hashicorp/terraform-plugin-testing/terraform"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
@@ -58,7 +59,7 @@ func TestAccWorkspaceConfFullLifecycle(t *testing.T) {
5859
}
5960
}`,
6061
// Assert on server side error returned
61-
ExpectError: regexp.MustCompile(`cannot update workspace conf: Invalid keys`),
62+
ExpectError: regexp.MustCompile(`cannot update workspace conf: failed to set workspace conf because some new keys are invalid: enableIpAccessLissss`),
6263
}, Step{
6364
// Set enableIpAccessLists to true with strange case and maxTokenLifetimeDays to verify
6465
// failed deletion case
@@ -79,3 +80,49 @@ func TestAccWorkspaceConfFullLifecycle(t *testing.T) {
7980
},
8081
})
8182
}
83+
84+
func TestAccWorkspaceConf_GetValidKey(t *testing.T) {
85+
loadWorkspaceEnv(t)
86+
ctx := context.Background()
87+
w := databricks.Must(databricks.NewWorkspaceClient())
88+
conf, err := workspace.SafeGetStatus(ctx, w, []string{"enableIpAccessLists"})
89+
assert.NoError(t, err)
90+
assert.Contains(t, conf, "enableIpAccessLists")
91+
}
92+
93+
func TestAccWorkspaceConf_GetInvalidKey(t *testing.T) {
94+
loadWorkspaceEnv(t)
95+
ctx := context.Background()
96+
w := databricks.Must(databricks.NewWorkspaceClient())
97+
conf, err := workspace.SafeGetStatus(ctx, w, []string{"invalidKey", "enableIpAccessLists"})
98+
assert.NoError(t, err)
99+
assert.Contains(t, conf, "enableIpAccessLists")
100+
}
101+
102+
func TestAccWorkspaceConf_GetOnlyInvalidKeys(t *testing.T) {
103+
loadWorkspaceEnv(t)
104+
ctx := context.Background()
105+
w := databricks.Must(databricks.NewWorkspaceClient())
106+
_, err := workspace.SafeGetStatus(ctx, w, []string{"invalidKey"})
107+
assert.ErrorContains(t, err, "failed to get workspace conf because all keys are invalid: invalidKey")
108+
}
109+
110+
func TestAccWorkspaceConf_SetInvalidKey(t *testing.T) {
111+
loadWorkspaceEnv(t)
112+
ctx := context.Background()
113+
w := databricks.Must(databricks.NewWorkspaceClient())
114+
err := workspace.SafeSetStatus(ctx, w, map[string]struct{}{}, map[string]string{
115+
"invalidKey": "invalidValue",
116+
})
117+
assert.ErrorContains(t, err, "failed to set workspace conf because some new keys are invalid: invalidKey")
118+
}
119+
120+
func TestAccWorkspaceConf_DeleteInvalidKey(t *testing.T) {
121+
loadWorkspaceEnv(t)
122+
ctx := context.Background()
123+
w := databricks.Must(databricks.NewWorkspaceClient())
124+
err := workspace.SafeSetStatus(ctx, w, map[string]struct{}{"invalidKey": {}}, map[string]string{
125+
"invalidKey": "",
126+
})
127+
assert.NoError(t, err)
128+
}

workspace/resource_workspace_conf.go

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ package workspace
55

66
import (
77
"context"
8+
"encoding/json"
89
"errors"
910
"fmt"
1011
"log"
12+
"slices"
1113
"sort"
1214
"strconv"
1315
"strings"
1416

17+
"github.com/databricks/databricks-sdk-go"
1518
"github.com/databricks/terraform-provider-databricks/common"
1619
"github.com/databricks/terraform-provider-databricks/internal/docs"
1720

@@ -30,6 +33,7 @@ func applyWorkspaceConf(ctx context.Context, d *schema.ResourceData, c *common.D
3033
return fmt.Errorf("internal type casting error")
3134
}
3235
log.Printf("[DEBUG] Old workspace config: %v, new: %v", old, new)
36+
removed := map[string]struct{}{}
3337
patch := settings.WorkspaceConf{}
3438

3539
// Add new configuration keys
@@ -44,6 +48,7 @@ func applyWorkspaceConf(ctx context.Context, d *schema.ResourceData, c *common.D
4448
continue
4549
}
4650
log.Printf("[DEBUG] Erasing configuration of %s", k)
51+
removed[k] = struct{}{}
4752
switch r := v.(type) {
4853
default:
4954
patch[k] = ""
@@ -63,7 +68,7 @@ func applyWorkspaceConf(ctx context.Context, d *schema.ResourceData, c *common.D
6368
if err != nil {
6469
return err
6570
}
66-
err = w.WorkspaceConf.SetStatus(ctx, patch)
71+
err = SafeSetStatus(ctx, w, removed, patch)
6772
if err != nil {
6873
return err
6974
}
@@ -121,7 +126,7 @@ func deleteWorkspaceConf(ctx context.Context, d *schema.ResourceData, c *common.
121126
case bool:
122127
patch[k] = "false"
123128
}
124-
err = w.WorkspaceConf.SetStatus(ctx, patch)
129+
err = SafeSetStatus(ctx, w, map[string]struct{}{k: {}}, patch)
125130
// Tolerate errors like the following on deletion:
126131
// cannot delete workspace conf: Some values are not allowed: {"enableGp3":"","enableIpAccessLists":""}
127132
// The API for workspace conf is quite limited and doesn't support a generic "reset to original state"
@@ -136,6 +141,79 @@ func deleteWorkspaceConf(ctx context.Context, d *schema.ResourceData, c *common.
136141
return nil
137142
}
138143

144+
func parseInvalidKeysFromError(err error) ([]string, error) {
145+
var apiErr *apierr.APIError
146+
// The workspace conf API returns an error with a message like "Invalid keys: [key1, key2, ...]"
147+
// when some keys are invalid. We parse this message to get the list of invalid keys.
148+
if errors.As(err, &apiErr) && strings.HasPrefix(apiErr.Message, "Invalid keys: ") {
149+
invalidKeysStr := strings.TrimPrefix(apiErr.Message, "Invalid keys: ")
150+
var invalidKeys []string
151+
err = json.Unmarshal([]byte(invalidKeysStr), &invalidKeys)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to parse missing keys: %w", err)
154+
}
155+
return invalidKeys, nil
156+
}
157+
return nil, nil
158+
}
159+
160+
// SafeGetStatus is a wrapper around the GetStatus API that tolerates invalid keys.
161+
// If any of the provided keys are not valid, the GetStatus API is called again with only the valid keys.
162+
// If all keys are invalid, an error is returned.
163+
func SafeGetStatus(ctx context.Context, w *databricks.WorkspaceClient, keys []string) (map[string]string, error) {
164+
conf, err := w.WorkspaceConf.GetStatus(ctx, settings.GetStatusRequest{
165+
Keys: strings.Join(keys, ","),
166+
})
167+
invalidKeys, parseErr := parseInvalidKeysFromError(err)
168+
if parseErr != nil {
169+
return nil, parseErr
170+
} else if invalidKeys != nil {
171+
tflog.Warn(ctx, fmt.Sprintf("the following keys are not supported by the api: %s. Remove these keys from the configuration to avoid this warning.", strings.Join(invalidKeys, ", ")))
172+
// Request again but remove invalid keys
173+
validKeys := make([]string, 0, len(keys))
174+
for _, k := range keys {
175+
if !slices.Contains(invalidKeys, k) {
176+
validKeys = append(validKeys, k)
177+
}
178+
}
179+
if len(validKeys) == 0 {
180+
return nil, fmt.Errorf("failed to get workspace conf because all keys are invalid: %s", strings.Join(keys, ", "))
181+
}
182+
conf, err = w.WorkspaceConf.GetStatus(context.Background(), settings.GetStatusRequest{
183+
Keys: strings.Join(validKeys, ","),
184+
})
185+
}
186+
if err != nil {
187+
return nil, err
188+
}
189+
return *conf, nil
190+
}
191+
192+
// SafeSetStatus is a wrapper around the SetStatus API that tolerates invalid keys.
193+
// If any of the provided keys are not valid, the removed map is checked to see if those keys are being removed.
194+
// If all keys are being removed, the error is ignored. Otherwise, an error is returned.
195+
func SafeSetStatus(ctx context.Context, w *databricks.WorkspaceClient, removed map[string]struct{}, newConf map[string]string) error {
196+
err := w.WorkspaceConf.SetStatus(ctx, settings.WorkspaceConf(newConf))
197+
invalidKeys, parseErr := parseInvalidKeysFromError(err)
198+
if parseErr != nil {
199+
return parseErr
200+
} else if invalidKeys != nil {
201+
// Tolerate the request if all invalid keys are present in the old map, indicating that they are being removed.
202+
newInvalidKeys := make([]string, 0, len(invalidKeys))
203+
for _, k := range invalidKeys {
204+
if _, ok := removed[k]; !ok {
205+
newInvalidKeys = append(newInvalidKeys, k)
206+
}
207+
}
208+
if len(newInvalidKeys) > 0 {
209+
return fmt.Errorf("failed to set workspace conf because some new keys are invalid: %s", strings.Join(newInvalidKeys, ", "))
210+
}
211+
tflog.Info(ctx, fmt.Sprintf("ignored the following invalid keys because they are being removed: %s", strings.Join(invalidKeys, ", ")))
212+
return nil
213+
}
214+
return err
215+
}
216+
139217
// ResourceWorkspaceConf maintains workspace configuration for specified keys
140218
func ResourceWorkspaceConf() common.Resource {
141219
return common.Resource{
@@ -155,13 +233,11 @@ func ResourceWorkspaceConf() common.Resource {
155233
if len(keys) == 0 {
156234
return nil
157235
}
158-
remote, err := w.WorkspaceConf.GetStatus(ctx, settings.GetStatusRequest{
159-
Keys: strings.Join(keys, ","),
160-
})
236+
remote, err := SafeGetStatus(ctx, w, keys)
161237
if err != nil {
162238
return err
163239
}
164-
for k, v := range *remote {
240+
for k, v := range remote {
165241
config[k] = v
166242
}
167243
log.Printf("[DEBUG] Setting new config to state: %v", config)

0 commit comments

Comments
 (0)