Skip to content

Commit 6d95fd0

Browse files
authored
Impement replacePermissions for databricks_grant (#1183)
Read existing permissions from platform before updating them, further improving drift detection. Existing permissions are merged onto diffs as removals. Fix #1164
1 parent e67df6a commit 6d95fd0

File tree

3 files changed

+194
-271
lines changed

3 files changed

+194
-271
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Added diff suppression for `min_num_clusters` field in `databricks_sql_endpoint` ([#1172](https://github.com/databrickslabs/terraform-provider-databricks/pull/1172)).
99
* Added special case for handling `Cannot access cluster that was terminated or unpinned more than 30 days ago` error in `databricks_cluster` as an indication of resource removed on the platform side ([#1177](https://github.com/databrickslabs/terraform-provider-databricks/issues/1177)).
1010
* Fixed updating of `databricks_table` resources ([#1175](https://github.com/databrickslabs/terraform-provider-databricks/issues/1175)).
11+
* Fixed configuration drift in `databricks_grant` by reading existing permissions and removing them in subsequent update calls ([#1164](https://github.com/databrickslabs/terraform-provider-databricks/issues/1164)).
1112

1213
Updated dependency versions:
1314

catalog/resource_grants.go

Lines changed: 80 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package catalog
33
import (
44
"context"
55
"fmt"
6+
"sort"
67
"strings"
78

89
"github.com/databrickslabs/terraform-provider-databricks/common"
@@ -14,6 +15,17 @@ type PermissionsAPI struct {
1415
context context.Context
1516
}
1617

18+
// permissionsDiff is the inner structure of updatePermissions RPC
19+
type permissionsDiff struct {
20+
Changes []permissionsChange `json:"changes"`
21+
}
22+
23+
type permissionsChange struct {
24+
Principal string `json:"principal"`
25+
Add []string `json:"add,omitempty"`
26+
Remove []string `json:"remove,omitempty"`
27+
}
28+
1729
// PrivilegeAssignment reflects on `grant` block
1830
type PrivilegeAssignment struct {
1931
Principal string `json:"principal"`
@@ -26,34 +38,59 @@ type PermissionsList struct {
2638
Assignments []PrivilegeAssignment `json:"privilege_assignments" tf:"slice_set,alias:grant"`
2739
}
2840

29-
func (pl PermissionsList) toCreate() (diff PermissionsDiff) {
41+
// diff returns permissionsDiff of this permissions list with `diff` privileges removed
42+
func (pl PermissionsList) diff(existing PermissionsList) (diff permissionsDiff) {
43+
// diffs change sets
44+
configured := map[string]*schema.Set{}
3045
for _, v := range pl.Assignments {
31-
diff.Changes = append(diff.Changes, PermissionsChange{
32-
Principal: v.Principal,
33-
Add: v.Privileges,
46+
configured[v.Principal] = newStringSet(v.Privileges)
47+
}
48+
// existing permissions that needs removal
49+
remote := map[string]*schema.Set{}
50+
for _, v := range existing.Assignments {
51+
remote[v.Principal] = newStringSet(v.Privileges)
52+
}
53+
// STEP 1: detect overlaps
54+
for principal, confPrivs := range configured {
55+
remotePrivs, ok := remote[principal]
56+
if !ok {
57+
remotePrivs = newStringSet([]string{})
58+
}
59+
add := setToStrings(confPrivs.Difference(remotePrivs))
60+
remove := setToStrings(remotePrivs.Difference(confPrivs))
61+
if len(add) == 0 && len(remove) == 0 {
62+
continue
63+
}
64+
diff.Changes = append(diff.Changes, permissionsChange{
65+
Principal: principal,
66+
Add: add,
67+
Remove: remove,
3468
})
3569
}
36-
return
37-
}
38-
39-
func (pl PermissionsList) toDelete() (diff PermissionsDiff) {
40-
for _, v := range pl.Assignments {
41-
diff.Changes = append(diff.Changes, PermissionsChange{
42-
Principal: v.Principal,
43-
Remove: v.Privileges,
70+
// STEP 2: non overlap - simply remove
71+
for principal, remove := range remote {
72+
_, ok := configured[principal]
73+
if ok { // already handled in STEP 1
74+
continue
75+
}
76+
diff.Changes = append(diff.Changes, permissionsChange{
77+
Principal: principal,
78+
Remove: setToStrings(remove),
4479
})
4580
}
46-
return
47-
}
48-
49-
type PermissionsDiff struct {
50-
Changes []PermissionsChange `json:"changes"`
81+
// so that we can deterministic tests
82+
sort.Slice(diff.Changes, func(i, j int) bool {
83+
return diff.Changes[i].Principal < diff.Changes[j].Principal
84+
})
85+
return diff
5186
}
5287

53-
type PermissionsChange struct {
54-
Principal string `json:"principal"`
55-
Add []string `json:"add,omitempty"`
56-
Remove []string `json:"remove,omitempty"`
88+
func newStringSet(in []string) *schema.Set {
89+
var out []interface{}
90+
for _, v := range in {
91+
out = append(out, v)
92+
}
93+
return schema.NewSet(schema.HashString, out)
5794
}
5895

5996
func NewPermissionsAPI(ctx context.Context, m interface{}) PermissionsAPI {
@@ -65,10 +102,19 @@ func (a PermissionsAPI) getPermissions(securable, name string) (list Permissions
65102
return
66103
}
67104

68-
func (a PermissionsAPI) updatePermissions(securable, name string, diff PermissionsDiff) error {
105+
func (a PermissionsAPI) updatePermissions(securable, name string, diff permissionsDiff) error {
69106
return a.client.Patch(a.context, fmt.Sprintf("/unity-catalog/permissions/%s/%s", securable, name), diff)
70107
}
71108

109+
// replacePermissions merges removal diff of existing permissions on the platform
110+
func (a PermissionsAPI) replacePermissions(securable, name string, list PermissionsList) error {
111+
existing, err := a.getPermissions(securable, name)
112+
if err != nil {
113+
return err
114+
}
115+
return a.updatePermissions(securable, name, list.diff(existing))
116+
}
117+
72118
type securableMapping map[string]map[string]bool
73119

74120
// reuse ResourceDiff and ResourceData
@@ -150,61 +196,6 @@ func setToStrings(set *schema.Set) (ss []string) {
150196
return
151197
}
152198

153-
func principalAndPrivsFromRaw(v interface{}) (string, *schema.Set) {
154-
item := v.(map[string]interface{})
155-
principal := item["principal"].(string)
156-
privileges := item["privileges"].(*schema.Set)
157-
return principal, privileges
158-
}
159-
160-
func permissionDiffFromRaw(old, new interface{}) PermissionsDiff {
161-
o := old.(*schema.Set)
162-
n := new.(*schema.Set)
163-
prev := map[string]*schema.Set{}
164-
for _, v := range o.List() {
165-
principal, privileges := principalAndPrivsFromRaw(v)
166-
prev[principal] = privileges
167-
}
168-
diff := PermissionsDiff{Changes: []PermissionsChange{}}
169-
mix := map[string]bool{}
170-
for _, v := range n.List() {
171-
principal, newPrivs := principalAndPrivsFromRaw(v)
172-
oldPrivs, exist := prev[principal]
173-
if !exist {
174-
// add new principal privileges
175-
diff.Changes = append(diff.Changes, PermissionsChange{
176-
Principal: principal,
177-
Add: setToStrings(newPrivs),
178-
})
179-
continue
180-
}
181-
// add or remove principal privileges
182-
change := PermissionsChange{
183-
Principal: principal,
184-
Remove: setToStrings(oldPrivs.Difference(newPrivs)),
185-
Add: setToStrings(newPrivs.Difference(oldPrivs)),
186-
}
187-
if len(change.Add) == 0 && len(change.Remove) == 0 {
188-
continue
189-
}
190-
diff.Changes = append(diff.Changes, change)
191-
mix[principal] = true
192-
}
193-
for _, v := range o.Difference(n).List() {
194-
// remove old principal privs
195-
principal, oldPrivs := principalAndPrivsFromRaw(v)
196-
if mix[principal] {
197-
// skip if mixed removes/adds were detected
198-
continue
199-
}
200-
diff.Changes = append(diff.Changes, PermissionsChange{
201-
Principal: principal,
202-
Remove: setToStrings(oldPrivs),
203-
})
204-
}
205-
return diff
206-
}
207-
208199
func ResourceGrants() *schema.Resource {
209200
s := common.StructToSchema(PermissionsList{},
210201
func(s map[string]*schema.Schema) map[string]*schema.Schema {
@@ -237,7 +228,7 @@ func ResourceGrants() *schema.Resource {
237228
var grants PermissionsList
238229
common.DataToStructPointer(d, s, &grants)
239230
securable, name := mapping.kv(d)
240-
err := NewPermissionsAPI(ctx, c).updatePermissions(securable, name, grants.toCreate())
231+
err := NewPermissionsAPI(ctx, c).replacePermissions(securable, name, grants)
241232
if err != nil {
242233
return err
243234
}
@@ -253,18 +244,23 @@ func ResourceGrants() *schema.Resource {
253244
if err != nil {
254245
return err
255246
}
247+
if len(grants.Assignments) == 0 {
248+
return common.NotFound("got empty permissions list")
249+
}
256250
return common.StructToData(grants, s, d)
257251
},
258252
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
259253
securable, name := mapping.kv(d)
260-
diff := permissionDiffFromRaw(d.GetChange("grant"))
261-
return NewPermissionsAPI(ctx, c).updatePermissions(securable, name, diff)
262-
},
263-
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
264254
var grants PermissionsList
265255
common.DataToStructPointer(d, s, &grants)
266-
securable, name := mapping.kv(d)
267-
return NewPermissionsAPI(ctx, c).updatePermissions(securable, name, grants.toDelete())
256+
return NewPermissionsAPI(ctx, c).replacePermissions(securable, name, grants)
257+
},
258+
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
259+
split := strings.SplitN(d.Id(), "/", 2)
260+
if len(split) != 2 {
261+
return fmt.Errorf("ID must be two elements split by `/`: %s", d.Id())
262+
}
263+
return NewPermissionsAPI(ctx, c).replacePermissions(split[0], split[1], PermissionsList{})
268264
},
269265
}.ToResource()
270266
}

0 commit comments

Comments
 (0)