Skip to content

Commit ee3bafc

Browse files
authored
Allow to override built-in databricks_cluster_policy resources (#2941)
* Allow to override built-in `databricks_cluster_policy` resources Databricks is shipped with a number of built-in cluster policies that are inherited from the cluster policy families. For built-in cluster policies we need to provide an override, not the `definition`. We also need to handle deletes for them differently - just remove the overrides because we can't remove built-in policies. We also need to distinguish cases when we override built-in policy from cases when we create a new policy based on the cluster policy family. This PR makes following changes: * Discovers a list of cluster policies families dynamically instead of relying on hardcoded list of policies * When creating a new `databricks_cluster_policy`, check if it's built-in policy or not, and if it's built-in, then update it with policy override instead of trying to create. * When deleting a created `databricks_cluster_policy`, check if it's built-in policy or not, and if it's built-in, then reset policy overrides to empty string to return back to the original state. This fixes #2935 * Add integration test for policy overrides
1 parent f9d4e38 commit ee3bafc

File tree

10 files changed

+482
-31
lines changed

10 files changed

+482
-31
lines changed

docs/resources/cluster_policy.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,38 @@ module "engineering_compute_policy" {
100100
}
101101
```
102102

103+
### Overriding the built-in cluster policies
104+
105+
You can override built-in cluster policies by creating a `databricks_cluster_policy` resource with following attributes:
106+
107+
* `name` - the name of the built-in cluster policy.
108+
* `policy_family_id` - the ID of the cluster policy family used for built-in cluster policy.
109+
* `policy_family_definition_overrides` - settings to override in the built-in cluster policy.
110+
111+
You can obtain the list of defined cluster policies families using the `databricks policy-families list` command of the new [Databricks CLI](https://docs.databricks.com/en/dev-tools/cli/index.html), or via [list policy families](https://docs.databricks.com/api/workspace/policyfamilies/list) REST API.
112+
113+
```hcl
114+
locals {
115+
personal_vm_override = {
116+
"autotermination_minutes" : {
117+
"type" : "fixed",
118+
"value" : 220,
119+
"hidden" : true
120+
},
121+
"custom_tags.Team" : {
122+
"type" : "fixed",
123+
"value" : var.team
124+
}
125+
}
126+
}
127+
128+
resource "databricks_cluster_policy" "personal_vm" {
129+
policy_family_id = "personal-vm"
130+
policy_family_definition_overrides = jsonencode(personal_vm_override)
131+
name = "Personal Compute"
132+
}
133+
```
134+
103135
## Argument Reference
104136

105137
The following arguments are supported:

docs/resources/default_namespace_settings.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ This setting requires a restart of clusters and SQL warehouses to take effect. A
1414

1515
```hcl
1616
resource "databricks_default_namespace_setting" "this" {
17-
namespace {
18-
value = "namespace_value"
19-
}
17+
namespace {
18+
value = "namespace_value"
19+
}
2020
}
2121
```
2222

exporter/context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"time"
1919

2020
"github.com/databricks/databricks-sdk-go"
21+
"github.com/databricks/databricks-sdk-go/service/compute"
2122

2223
"github.com/databricks/terraform-provider-databricks/commands"
2324
"github.com/databricks/terraform-provider-databricks/common"
@@ -134,6 +135,9 @@ type importContext struct {
134135
allDirectories []workspace.ObjectStatus
135136
allWorkspaceObjects []workspace.ObjectStatus
136137
wsObjectsMutex sync.RWMutex
138+
139+
builtInPolicies map[string]compute.PolicyFamily
140+
builtInPoliciesMutex sync.Mutex
137141
}
138142

139143
type mount struct {

exporter/exporter_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,15 @@ var emptyClusterPolicies = qa.HTTPFixture{
253253
Response: compute.ListPoliciesResponse{},
254254
}
255255

256+
var emptyPolicyFamilies = qa.HTTPFixture{
257+
Method: "GET",
258+
Resource: "/api/2.0/policy-families?",
259+
Response: compute.ListPolicyFamiliesResponse{
260+
PolicyFamilies: []compute.PolicyFamily{},
261+
},
262+
ReuseRequest: true,
263+
}
264+
256265
var emptyMlflowWebhooks = qa.HTTPFixture{
257266
Method: "GET",
258267
ReuseRequest: true,
@@ -392,6 +401,7 @@ func TestImportingUsersGroupsSecretScopes(t *testing.T) {
392401
emptySqlAlerts,
393402
emptyPipelines,
394403
emptyClusterPolicies,
404+
emptyPolicyFamilies,
395405
emptyWorkspaceConf,
396406
allKnownWorkspaceConfs,
397407
dummyWorkspaceConf,
@@ -649,6 +659,7 @@ func TestImportingNoResourcesError(t *testing.T) {
649659
emptySqlDashboards,
650660
emptySqlAlerts,
651661
emptyPipelines,
662+
emptyPolicyFamilies,
652663
{
653664
Method: "GET",
654665
Resource: "/api/2.0/global-init-scripts",

exporter/importables.go

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import (
1212
"strconv"
1313
"strings"
1414

15-
"golang.org/x/exp/slices"
16-
1715
"github.com/databricks/databricks-sdk-go/service/compute"
1816
"github.com/databricks/databricks-sdk-go/service/iam"
1917
sdk_jobs "github.com/databricks/databricks-sdk-go/service/jobs"
@@ -37,18 +35,16 @@ import (
3735
)
3836

3937
var (
40-
adlsGen2Regex = regexp.MustCompile(`^(abfss?)://([^@]+)@([^.]+)\.(?:[^/]+)(/.*)?$`)
41-
adlsGen1Regex = regexp.MustCompile(`^(adls?)://([^.]+)\.(?:[^/]+)(/.*)?$`)
42-
wasbsRegex = regexp.MustCompile(`^(wasbs?)://([^@]+)@([^.]+)\.(?:[^/]+)(/.*)?$`)
43-
s3Regex = regexp.MustCompile(`^(s3a?)://([^/]+)(/.*)?$`)
44-
gsRegex = regexp.MustCompile(`^gs://([^/]+)(/.*)?$`)
45-
globalWorkspaceConfName = "global_workspace_conf"
46-
nameNormalizationRegex = regexp.MustCompile(`\W+`)
47-
fileNameNormalizationRegex = regexp.MustCompile(`[^-_\w/.@]`)
48-
jobClustersRegex = regexp.MustCompile(`^((job_cluster|task)\.[0-9]+\.new_cluster\.[0-9]+\.)`)
49-
dltClusterRegex = regexp.MustCompile(`^(cluster\.[0-9]+\.)`)
50-
predefinedClusterPolicies = []string{"personal-vm", "shared-data-science", "shared-compute",
51-
"power-user", "job-cluster"}
38+
adlsGen2Regex = regexp.MustCompile(`^(abfss?)://([^@]+)@([^.]+)\.(?:[^/]+)(/.*)?$`)
39+
adlsGen1Regex = regexp.MustCompile(`^(adls?)://([^.]+)\.(?:[^/]+)(/.*)?$`)
40+
wasbsRegex = regexp.MustCompile(`^(wasbs?)://([^@]+)@([^.]+)\.(?:[^/]+)(/.*)?$`)
41+
s3Regex = regexp.MustCompile(`^(s3a?)://([^/]+)(/.*)?$`)
42+
gsRegex = regexp.MustCompile(`^gs://([^/]+)(/.*)?$`)
43+
globalWorkspaceConfName = "global_workspace_conf"
44+
nameNormalizationRegex = regexp.MustCompile(`\W+`)
45+
fileNameNormalizationRegex = regexp.MustCompile(`[^-_\w/.@]`)
46+
jobClustersRegex = regexp.MustCompile(`^((job_cluster|task)\.[0-9]+\.new_cluster\.[0-9]+\.)`)
47+
dltClusterRegex = regexp.MustCompile(`^(cluster\.[0-9]+\.)`)
5248
secretPathRegex = regexp.MustCompile(`^\{\{secrets\/([^\/]+)\/([^}]+)\}\}$`)
5349
sqlParentRegexp = regexp.MustCompile(`^folders/(\d+)$`)
5450
dltDefaultStorageRegex = regexp.MustCompile(`^dbfs:/pipelines/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
@@ -631,14 +627,18 @@ var resourcesMap map[string]importable = map[string]importable{
631627
if err != nil {
632628
return err
633629
}
634-
policies, err := w.ClusterPolicies.ListAll(ic.Context, compute.ListClusterPoliciesRequest{})
630+
policiesList, err := w.ClusterPolicies.ListAll(ic.Context, compute.ListClusterPoliciesRequest{})
635631
if err != nil {
636632
return err
637633
}
638-
for offset, policy := range policies {
634+
635+
builtInClusterPolicies := ic.getBuiltinPolicyFamilies()
636+
for offset, policy := range policiesList {
639637
log.Printf("[TRACE] Scanning %d: %v", offset+1, policy)
640-
if policy.PolicyFamilyId != "" && slices.Contains(predefinedClusterPolicies, policy.PolicyFamilyId) &&
638+
family, isBuiltin := builtInClusterPolicies[policy.PolicyFamilyId]
639+
if policy.PolicyFamilyId != "" && isBuiltin && family.Name == policy.Name &&
641640
policy.PolicyFamilyDefinitionOverrides == "" {
641+
log.Printf("[DEBUG] Skipping builtin cluster policy '%s' without overrides", policy.Name)
642642
continue
643643
}
644644
if !ic.MatchesName(policy.Name) {
@@ -650,7 +650,7 @@ var resourcesMap map[string]importable = map[string]importable{
650650
ID: policy.PolicyId,
651651
})
652652
if offset%10 == 0 {
653-
log.Printf("[INFO] Scanned %d of %d cluster policies", offset+1, len(policies))
653+
log.Printf("[INFO] Scanned %d of %d cluster policies", offset+1, len(policiesList))
654654
}
655655
}
656656
return nil
@@ -718,7 +718,9 @@ var resourcesMap map[string]importable = map[string]importable{
718718
// we need to set definition to empty value because otherwise it will be put into
719719
// generated HCL code for data source, and it only supports the `name` attribute
720720
r.Data.Set("definition", "")
721-
if slices.Contains(predefinedClusterPolicies, policyFamilyId) && policyOverrides == "" {
721+
builtInClusterPolicies := ic.getBuiltinPolicyFamilies()
722+
_, isBuiltin := builtInClusterPolicies[policyFamilyId]
723+
if isBuiltin && policyOverrides == "" {
722724
r.Mode = "data"
723725
}
724726
}

exporter/importables_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ func TestPredefinedClusterPolicy(t *testing.T) {
117117
policy, _ := json.Marshal(map[string]map[string]string{})
118118
d.Set("definition", string(policy))
119119
ic := importContextForTest()
120+
ic.builtInPolicies = map[string]compute.PolicyFamily{
121+
"job-cluster": {Name: "Job Compute"},
122+
}
120123
r := resource{ID: "abc", Data: d}
121124
err := ic.Importables["databricks_cluster_policy"].Import(ic, &r)
122125
assert.NoError(t, err)
@@ -683,10 +686,20 @@ func TestPoliciesListing(t *testing.T) {
683686
},
684687
},
685688
},
689+
{
690+
Method: "GET",
691+
Resource: "/api/2.0/policy-families?",
692+
Response: compute.ListPolicyFamiliesResponse{
693+
PolicyFamilies: []compute.PolicyFamily{
694+
{
695+
Name: "Personal Compute",
696+
PolicyFamilyId: "personal-vm",
697+
},
698+
},
699+
},
700+
},
686701
}, func(ctx context.Context, client *common.DatabricksClient) {
687-
ic := importContextForTest()
688-
ic.Client = client
689-
ic.Context = ctx
702+
ic := importContextForTestWithClient(ctx, client)
690703
err := resourcesMap["databricks_cluster_policy"].List(ic)
691704
assert.NoError(t, err)
692705
assert.Equal(t, 1, len(ic.testEmits))
@@ -709,10 +722,9 @@ func TestPoliciesListNoNameMatch(t *testing.T) {
709722
},
710723
},
711724
},
725+
emptyPolicyFamilies,
712726
}, func(ctx context.Context, client *common.DatabricksClient) {
713-
ic := importContextForTest()
714-
ic.Client = client
715-
ic.Context = ctx
727+
ic := importContextForTestWithClient(ctx, client)
716728
ic.match = "bcd"
717729
err := resourcesMap["databricks_cluster_policy"].List(ic)
718730
assert.NoError(t, err)

exporter/util.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/databricks/terraform-provider-databricks/storage"
2424
"github.com/databricks/terraform-provider-databricks/workspace"
2525

26+
"github.com/databricks/databricks-sdk-go/service/compute"
2627
"github.com/databricks/databricks-sdk-go/service/iam"
2728

2829
"golang.org/x/exp/slices"
@@ -411,6 +412,32 @@ func (ic *importContext) getSpsMapping() {
411412
}
412413
}
413414

415+
func (ic *importContext) getBuiltinPolicyFamilies() map[string]compute.PolicyFamily {
416+
ic.builtInPoliciesMutex.Lock()
417+
defer ic.builtInPoliciesMutex.Unlock()
418+
if ic.builtInPolicies == nil {
419+
if !ic.accountLevel {
420+
log.Printf("[DEBUG] Going to initialize ic.builtInPolicies. Getting policy families...")
421+
families, err := ic.workspaceClient.PolicyFamilies.ListAll(ic.Context, compute.ListPolicyFamiliesRequest{})
422+
log.Printf("[DEBUG] Going to initialize ic.builtInPolicies. Getting policy families...")
423+
if err == nil {
424+
ic.builtInPolicies = make(map[string]compute.PolicyFamily, len(families))
425+
for _, f := range families {
426+
f2 := f
427+
ic.builtInPolicies[f2.PolicyFamilyId] = f2
428+
}
429+
} else {
430+
log.Printf("[ERROR] Can't fetch cluster policy families: %v", err)
431+
ic.builtInPolicies = map[string]compute.PolicyFamily{}
432+
}
433+
} else {
434+
log.Print("[WARN] Can't list cluster policy families on account level")
435+
ic.builtInPolicies = map[string]compute.PolicyFamily{}
436+
}
437+
}
438+
return ic.builtInPolicies
439+
}
440+
414441
func (ic *importContext) findSpnByAppID(applicationID string) (u scim.User, err error) {
415442
log.Printf("[DEBUG] Looking for SP %s", applicationID)
416443
ic.spsMutex.RLocker().Lock()

internal/acceptance/cluster_policy_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,29 @@ func TestAccClusterPolicyResourceFullLifecycle(t *testing.T) {
2828
}`,
2929
})
3030
}
31+
32+
func TestAccClusterPolicyResourceOverrideBuiltIn(t *testing.T) {
33+
workspaceLevel(t, step{
34+
Template: `resource "databricks_cluster_policy" "personal_vm" {
35+
name = "Personal Compute"
36+
policy_family_id = "personal-vm"
37+
policy_family_definition_overrides = jsonencode({
38+
"node_type_id": {
39+
"type": "fixed",
40+
"value": "Standard_DS3_v2"
41+
}
42+
})
43+
}
44+
`,
45+
})
46+
}
47+
48+
func TestAccClusterPolicyResourceOverrideNew(t *testing.T) {
49+
workspaceLevel(t, step{
50+
Template: `resource "databricks_cluster_policy" "policyoverrideempty" {
51+
policy_family_id = "personal-vm"
52+
name = "Policy Override {var.RANDOM}"
53+
}
54+
`,
55+
})
56+
}

0 commit comments

Comments
 (0)