Skip to content

Commit 9aa654b

Browse files
authored
Deterministic diff for databricks_permissions (#2059)
This PR removes SDK client as the argument to CustomizeDiff callback on the level of `common.Resource` framework. All customize diff validation must be hermetic and take only `schema.ResourceDiff` as the input. Any validation, that may require calls to the platform must be done in scope of Create or Update callbacks. Fix #2052
1 parent 5d55691 commit 9aa654b

File tree

8 files changed

+97
-71
lines changed

8 files changed

+97
-71
lines changed

catalog/resource_grants.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func ResourceGrants() *schema.Resource {
281281
})
282282
return common.Resource{
283283
Schema: s,
284-
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, c any) error {
284+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
285285
if d.Id() == "" {
286286
// unfortunately we cannot do validation before dependent resources exist with tfsdkv2
287287
return nil

catalog/resource_table.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func ResourceTable() *schema.Resource {
8585
"view_definition", "comment", "properties"})
8686
return common.Resource{
8787
Schema: tableSchema,
88-
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, c any) error {
88+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
8989
if d.Get("table_type") != "EXTERNAL" {
9090
return nil
9191
}

common/resource.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Resource struct {
2020
Read func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error
2121
Update func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error
2222
Delete func(ctx context.Context, d *schema.ResourceData, c *DatabricksClient) error
23-
CustomizeDiff func(ctx context.Context, d *schema.ResourceDiff, c any) error
23+
CustomizeDiff func(ctx context.Context, d *schema.ResourceDiff) error
2424
StateUpgraders []schema.StateUpgrader
2525
Schema map[string]*schema.Schema
2626
SchemaVersion int
@@ -54,6 +54,32 @@ func recoverable(cb func(
5454
}
5555
}
5656

57+
func (r Resource) saferCustomizeDiff() schema.CustomizeDiffFunc {
58+
if r.CustomizeDiff == nil {
59+
return nil
60+
}
61+
return func(ctx context.Context, rd *schema.ResourceDiff, _ any) (err error) {
62+
defer func() {
63+
// this is deliberate decision to convert a panic into error,
64+
// so that any unforeseen bug would we visible to end-user
65+
// as an error and not a provider crash, which is way less
66+
// of pleasant experience.
67+
if panic := recover(); panic != nil {
68+
err = nicerError(ctx, fmt.Errorf("panic: %v", panic),
69+
"customize diff for")
70+
}
71+
}()
72+
// we don't propagate instance of SDK client to the diff function, because
73+
// authentication is not deterministic at this stage with the recent Terraform
74+
// versions. Diff customization must be limited to hermetic checks only anyway.
75+
err = r.CustomizeDiff(ctx, rd)
76+
if err != nil {
77+
err = nicerError(ctx, err, "customize diff for")
78+
}
79+
return
80+
}
81+
}
82+
5783
// ToResource converts to Terraform resource definition
5884
func (r Resource) ToResource() *schema.Resource {
5985
var update func(ctx context.Context, d *schema.ResourceData,
@@ -117,7 +143,7 @@ func (r Resource) ToResource() *schema.Resource {
117143
Schema: r.Schema,
118144
SchemaVersion: r.SchemaVersion,
119145
StateUpgraders: r.StateUpgraders,
120-
CustomizeDiff: r.CustomizeDiff,
146+
CustomizeDiff: r.saferCustomizeDiff(),
121147
CreateContext: func(ctx context.Context, d *schema.ResourceData,
122148
m any) diag.Diagnostics {
123149
c := m.(*DatabricksClient)

common/resource_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,26 @@ func TestRecoverableFromPanic(t *testing.T) {
155155
assert.True(t, diags.HasError())
156156
assert.Equal(t, "cannot read sample: panic: what to do?...", diags[0].Summary)
157157
}
158+
159+
func TestCustomizeDiffRobustness(t *testing.T) {
160+
r := Resource{
161+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
162+
return fmt.Errorf("nope")
163+
},
164+
}.ToResource()
165+
166+
ctx := context.Background()
167+
ctx = context.WithValue(ctx, ResourceName, "sample")
168+
169+
err := r.CustomizeDiff(ctx, nil, nil)
170+
assert.EqualError(t, err, "cannot customize diff for sample: nope")
171+
172+
r = Resource{
173+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
174+
panic("oops")
175+
},
176+
}.ToResource()
177+
178+
err = r.CustomizeDiff(ctx, nil, nil)
179+
assert.EqualError(t, err, "cannot customize diff for sample: panic: oops")
180+
}

jobs/resource_job.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,7 @@ func ResourceJob() *schema.Resource {
595595
Create: schema.DefaultTimeout(clusters.DefaultProvisionTimeout),
596596
Update: schema.DefaultTimeout(clusters.DefaultProvisionTimeout),
597597
},
598-
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, m any) error {
598+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
599599
var js JobSettings
600600
common.DiffToStructPointer(d, jobSchema, &js)
601601
alwaysRunning := d.Get("always_running").(bool)

mws/resource_mws_workspaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ func ResourceMwsWorkspaces() *schema.Resource {
570570
}
571571
return NewWorkspacesAPI(ctx, c).Delete(accountID, workspaceID)
572572
},
573-
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, m any) error {
573+
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff) error {
574574
old, new := d.GetChange("private_access_settings_id")
575575
if old != "" && new == "" {
576576
return fmt.Errorf("cannot remove private access setting from workspace")

permissions/resource_permissions.go

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"log"
87
"path"
98
"strconv"
109
"strings"
@@ -381,21 +380,7 @@ func ResourcePermissions() *schema.Resource {
381380
})
382381
return common.Resource{
383382
Schema: s,
384-
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, c any) error {
385-
client := c.(*common.DatabricksClient)
386-
log.Printf("[DEBUG] permissions id=%s, config_present=%v", diff.Id(), client.Config != nil)
387-
if client.Config.Host == "" || client.DatabricksClient.Config.Host == "" {
388-
log.Printf("[WARN] cannot validate permission levels, because host is not known yet")
389-
return nil
390-
}
391-
w, err := client.WorkspaceClient()
392-
if err != nil {
393-
return err
394-
}
395-
me, err := w.CurrentUser.Me(ctx)
396-
if err != nil {
397-
return fmt.Errorf("customize diff: me: %w", err)
398-
}
383+
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff) error {
399384
// Plan time validation for object permission levels
400385
for _, mapping := range permissionsResourceIDFields() {
401386
if _, ok := diff.GetOk(mapping.field); !ok {
@@ -406,10 +391,8 @@ func ResourcePermissions() *schema.Resource {
406391
m := access_control.(map[string]any)
407392
permission_level := m["permission_level"].(string)
408393
if !stringInSlice(permission_level, mapping.allowedPermissionLevels) {
409-
return fmt.Errorf(`permission_level %s is not supported with %s objects`, permission_level, mapping.field)
410-
}
411-
if m["user_name"].(string) == me.UserName {
412-
return fmt.Errorf("it is not possible to decrease administrative permissions for the current user: %s", me.UserName)
394+
return fmt.Errorf(`permission_level %s is not supported with %s objects`,
395+
permission_level, mapping.field)
413396
}
414397
}
415398
}
@@ -443,12 +426,25 @@ func ResourcePermissions() *schema.Resource {
443426
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
444427
var entity PermissionsEntity
445428
common.DataToStructPointer(d, s, &entity)
429+
w, err := c.WorkspaceClient()
430+
if err != nil {
431+
return err
432+
}
433+
me, err := w.CurrentUser.Me(ctx)
434+
if err != nil {
435+
return err
436+
}
437+
// this logic was moved from CustomizeDiff because of undeterministic auth behavior
438+
// in the corner-case scenarios.
439+
// see https://github.com/databricks/terraform-provider-databricks/issues/2052
440+
for _, v := range entity.AccessControlList {
441+
if v.UserName == me.UserName {
442+
format := "it is not possible to decrease administrative permissions for the current user: %s"
443+
return fmt.Errorf(format, me.UserName)
444+
}
445+
}
446446
for _, mapping := range permissionsResourceIDFields() {
447447
if v, ok := d.GetOk(mapping.field); ok {
448-
w, err := c.WorkspaceClient()
449-
if err != nil {
450-
return err
451-
}
452448
id, err := mapping.idRetriever(ctx, w, v.(string))
453449
if err != nil {
454450
return err

permissions/resource_permissions_test.go

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -409,51 +409,32 @@ func TestResourcePermissionsRead_some_error(t *testing.T) {
409409
assert.Error(t, err)
410410
}
411411

412-
func TestResourcePermissionsCustomizeDiff_ErrorOnScimMe(t *testing.T) {
412+
func TestResourcePermissionsCustomizeDiff_ErrorOnCreate(t *testing.T) {
413+
qa.ResourceFixture{
414+
Resource: ResourcePermissions(),
415+
Create: true,
416+
HCL: `
417+
cluster_id = "abc"
418+
access_control {
419+
permission_level = "WHATEVER"
420+
}`,
421+
}.ExpectError(t, "permission_level WHATEVER is not supported with cluster_id objects")
422+
}
423+
424+
func TestResourcePermissionsCustomizeDiff_ErrorOnPermissionsDecreate(t *testing.T) {
413425
qa.ResourceFixture{
414426
Fixtures: []qa.HTTPFixture{
415-
{
416-
Method: http.MethodGet,
417-
Resource: "/api/2.0/permissions/clusters/abc",
418-
Response: ObjectACL{
419-
ObjectID: "/clusters/abc",
420-
ObjectType: "clusters",
421-
AccessControlList: []AccessControl{
422-
{
423-
UserName: TestingUser,
424-
AllPermissions: []Permission{
425-
{
426-
PermissionLevel: "CAN_READ",
427-
Inherited: false,
428-
},
429-
},
430-
},
431-
{
432-
UserName: TestingAdminUser,
433-
AllPermissions: []Permission{
434-
{
435-
PermissionLevel: "CAN_MANAGE",
436-
Inherited: false,
437-
},
438-
},
439-
},
440-
},
441-
},
442-
},
443-
{
444-
Method: http.MethodGet,
445-
Resource: "/api/2.0/preview/scim/v2/Me",
446-
Response: apierr.APIErrorBody{
447-
ErrorCode: "INVALID_REQUEST",
448-
Message: "Internal error happened",
449-
},
450-
Status: 400,
451-
},
427+
me,
452428
},
453429
Resource: ResourcePermissions(),
454-
Read: true,
455-
ID: "/clusters/abc",
456-
}.ExpectError(t, "customize diff: me: Internal error happened")
430+
Create: true,
431+
HCL: `
432+
cluster_id = "abc"
433+
access_control {
434+
permission_level = "CAN_ATTACH_TO"
435+
user_name = "admin"
436+
}`,
437+
}.ExpectError(t, "it is not possible to decrease administrative permissions for the current user: admin")
457438
}
458439

459440
func TestResourcePermissionsRead_ErrorOnScimMe(t *testing.T) {

0 commit comments

Comments
 (0)