Skip to content

Commit d14bbe8

Browse files
authored
Exporter: improve exporting of databricks_cluster_policy resource (#2680)
This includes: - Listing of all cluster policies, not only that are referenced in the jobs/clusters. - Emit secret scopes when they are referenced in Spark Conf or environment variables Also, fixes generation of non-existent secret scopes that happens when we emit non-existent secret scope (for example, it's a dangling reference in cluster policy or old job configuration). This fixes #2664
1 parent b2e48b0 commit d14bbe8

File tree

5 files changed

+144
-13
lines changed

5 files changed

+144
-13
lines changed

docs/guides/experimental-exporter.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,26 +55,27 @@ All arguments are optional and they tune what code is being generated.
5555
Services are just logical groups of resources used for filtering and organization in files written in `-directory`. All resources are globally sorted by their resource name, which technically allows you to use generated files for compliance purposes. Nevertheless, managing the entire Databricks workspace with Terraform is the preferred way. With the exception of notebooks and possibly libraries, which may have their own CI/CD processes.
5656

5757
* `access` - [databricks_permissions](../resources/permissions.md), [databricks_instance_profile](../resources/instance_profile.md) and [databricks_ip_access_list](../resources/ip_access_list.md).
58-
* `compute` - **listing** [databricks_cluster](../resources/cluster.md). Includes [cluster policies](../resources/cluster_policy.md).
59-
* `directories` - **listing** [databricks_directory](../resources/directory.md)
60-
* `dlt` - **listing** [databricks_pipeline](../resources/pipeline.md)
58+
* `compute` - **listing** [databricks_cluster](../resources/cluster.md).
59+
* `directories` - **listing** [databricks_directory](../resources/directory.md).
60+
* `dlt` - **listing** [databricks_pipeline](../resources/pipeline.md).
6161
* `groups` - [databricks_group](../data-sources/group.md) with [membership](../resources/group_member.md) and [data access](../resources/group_instance_profile.md).
6262
* `jobs` - **listing** [databricks_job](../resources/job.md). Usually, there are more automated jobs than interactive clusters, so they get their own file in this tool's output.
6363
* `mlflow-webhooks` - **listing** [databricks_mlflow_webhook](../resources/mlflow_webhook.md).
6464
* `model-serving` - **listing** [databricks_model_serving](../resources/model_serving.md).
6565
* `mounts` - **listing** works only in combination with `-mounts` command-line option.
66-
* `notebooks` - **listing** [databricks_notebook](../resources/notebook.md) and [databricks_workspace_file](../resources/workspace_file.md)
66+
* `notebooks` - **listing** [databricks_notebook](../resources/notebook.md) and [databricks_workspace_file](../resources/workspace_file.md).
67+
* `policies` - **listing** [databricks_cluster_policy](../resources/cluster_policy).
6768
* `pools` - **listing** [instance pools](../resources/instance_pool.md).
68-
* `repos` - **listing** [databricks_repo](../resources/repo.md)
69+
* `repos` - **listing** [databricks_repo](../resources/repo.md).
6970
* `secrets` - **listing** [databricks_secret_scope](../resources/secret_scope.md) along with [keys](../resources/secret.md) and [ACLs](../resources/secret_acl.md).
7071
* `sql-alerts` - **listing** [databricks_sql_alert](../resources/sql_alert.md).
71-
* `sql-dashboards` - **listing** [databricks_sql_dashboard](../resources/sql_dashboard.md) along with associated [databricks_sql_widget](../resources/sql_widget.md) and [databricks_sql_visualization](../resources/sql_visualization.md)
7272
* `sql-dashboards` - **listing** [databricks_sql_dashboard](../resources/sql_dashboard.md) along with associated [databricks_sql_widget](../resources/sql_widget.md) and [databricks_sql_visualization](../resources/sql_visualization.md).
73-
* `sql-endpoints` - **listing** [databricks_sql_endpoint](../resources/sql_endpoint.md) along with [databricks_sql_global_config](../resources/sql_global_config.md)
74-
* `sql-queries` - **listing** [databricks_sql_query](../resources/sql_query.md)
73+
* `sql-dashboards` - **listing** [databricks_sql_dashboard](../resources/sql_dashboard.md) along with associated [databricks_sql_widget](../resources/sql_widget.md) and [databricks_sql_visualization](../resources/sql_visualization.md).
74+
* `sql-endpoints` - **listing** [databricks_sql_endpoint](../resources/sql_endpoint.md) along with [databricks_sql_global_config](../resources/sql_global_config.md).
75+
* `sql-queries` - **listing** [databricks_sql_query](../resources/sql_query.md).
7576
* `storage` - any referenced [databricks_dbfs_file](../resources/dbfs_file.md) will be downloaded locally and properly arranged into terraform state.
76-
* `users` - [databricks_user](../resources/user.md) and [databricks_service_principal](../resources/service_principal.md) are written to their own file, simply because of their amount. If you use SCIM provisioning, the only use case for importing `users` service is to migrate workspaces.
77-
* `workspace` - [databricks_workspace_conf](../resources/workspace_conf.md) and [databricks_global_init_script](../resources/global_init_script.md)
77+
* `users` - [databricks_user](../resources/user.md) and [databricks_service_principal](../resources/service_principal.md) are written to their own file, simply because of their amount. If you use SCIM provisioning, the only use-case for importing `users` service is to migrate workspaces.
78+
* `workspace` - [databricks_workspace_conf](../resources/workspace_conf.md) and [databricks_global_init_script](../resources/global_init_script.md).
7879

7980
## Secrets
8081

exporter/exporter_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ var emptyPipelines = qa.HTTPFixture{
233233
Response: pipelines.PipelineListResponse{},
234234
}
235235

236+
var emptyClusterPolicies = qa.HTTPFixture{
237+
Method: "GET",
238+
ReuseRequest: true,
239+
Resource: "/api/2.0/policies/clusters/list?",
240+
Response: compute.ListPoliciesResponse{},
241+
}
242+
236243
var emptyMlflowWebhooks = qa.HTTPFixture{
237244
Method: "GET",
238245
ReuseRequest: true,
@@ -354,6 +361,7 @@ func TestImportingUsersGroupsSecretScopes(t *testing.T) {
354361
emptySqlQueries,
355362
emptySqlAlerts,
356363
emptyPipelines,
364+
emptyClusterPolicies,
357365
emptyWorkspaceConf,
358366
allKnownWorkspaceConfs,
359367
dummyWorkspaceConf,
@@ -555,6 +563,7 @@ func TestImportingNoResourcesError(t *testing.T) {
555563
emptyMlflowWebhooks,
556564
emptyWorkspaceConf,
557565
emptyInstancePools,
566+
emptyClusterPolicies,
558567
dummyWorkspaceConf,
559568
{
560569
Method: "GET",
@@ -751,6 +760,30 @@ func TestImportingClusters(t *testing.T) {
751760
ReuseRequest: true,
752761
Response: getJSONObject("test-data/get-job-14-permissions.json"),
753762
},
763+
{
764+
Method: "GET",
765+
Resource: "/api/2.0/secrets/list?scope=some-kv-scope",
766+
ReuseRequest: true,
767+
Response: getJSONObject("test-data/secret-scopes-list-scope-response.json"),
768+
},
769+
{
770+
Method: "GET",
771+
Resource: "/api/2.0/secrets/acls/list?scope=some-kv-scope",
772+
ReuseRequest: true,
773+
Response: getJSONObject("test-data/secret-scopes-list-scope-acls-response.json"),
774+
},
775+
{
776+
Method: "GET",
777+
Resource: "/api/2.0/secrets/acls/get?principal=test%40test.com&scope=some-kv-scope",
778+
ReuseRequest: true,
779+
Response: getJSONObject("test-data/secret-scopes-get-principal-response.json"),
780+
},
781+
{
782+
Method: "GET",
783+
Resource: "/api/2.0/secrets/scopes/list",
784+
ReuseRequest: true,
785+
Response: getJSONObject("test-data/secret-scopes-response.json"),
786+
},
754787
},
755788
func(ctx context.Context, client *common.DatabricksClient) {
756789
os.Setenv("EXPORTER_PARALLELISM_databricks_cluster", "1")

exporter/importables.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"golang.org/x/exp/slices"
1515

16+
"github.com/databricks/databricks-sdk-go/service/compute"
1617
"github.com/databricks/databricks-sdk-go/service/ml"
1718
"github.com/databricks/databricks-sdk-go/service/settings"
1819
"github.com/databricks/terraform-provider-databricks/clusters"
@@ -551,10 +552,38 @@ var resourcesMap map[string]importable = map[string]importable{
551552
},
552553
},
553554
"databricks_cluster_policy": {
554-
Service: "compute",
555+
Service: "policies",
555556
Name: func(ic *importContext, d *schema.ResourceData) string {
556557
return d.Get("name").(string)
557558
},
559+
List: func(ic *importContext) error {
560+
w, err := ic.Client.WorkspaceClient()
561+
if err != nil {
562+
return err
563+
}
564+
policies, err := w.ClusterPolicies.ListAll(ic.Context, compute.ListClusterPoliciesRequest{})
565+
if err != nil {
566+
return err
567+
}
568+
for offset, policy := range policies {
569+
log.Printf("[INFO] Scanning %d: %v", offset+1, policy)
570+
if slices.Contains(predefinedClusterPolicies, policy.Name) {
571+
continue
572+
}
573+
if !ic.MatchesName(policy.Name) {
574+
log.Printf("[DEBUG] Policy %s doesn't match %s filter", policy.Name, ic.match)
575+
continue
576+
}
577+
ic.Emit(&resource{
578+
Resource: "databricks_cluster_policy",
579+
ID: policy.PolicyId,
580+
})
581+
if offset%10 == 0 {
582+
log.Printf("[INFO] Scanned %d of %d cluster policies", offset+1, len(policies))
583+
}
584+
}
585+
return nil
586+
},
558587
Import: func(ic *importContext, r *resource) error {
559588
if ic.meAdmin {
560589
ic.Emit(&resource{
@@ -573,7 +602,8 @@ var resourcesMap map[string]importable = map[string]importable{
573602
defaultValue, dok := policy["defaultValue"]
574603
typ := policy["type"]
575604
if !vok && !dok {
576-
log.Printf("[INFO] Skipping policy element as it doesn't have both value and defaultValue")
605+
log.Printf("[DEBUG] Skipping policy element as it doesn't have both value and defaultValue. k='%v', policy='%v'",
606+
k, policy)
577607
continue
578608
}
579609
if k == "aws_attributes.instance_profile_arn" {
@@ -599,6 +629,15 @@ var resourcesMap map[string]importable = map[string]importable{
599629
ID: eitherString(value, defaultValue),
600630
})
601631
}
632+
if typ == "fixed" && (strings.HasPrefix(k, "spark_conf.") || strings.HasPrefix(k, "spark_env_vars.")) {
633+
either := eitherString(value, defaultValue)
634+
if res := secretPathRegex.FindStringSubmatch(either); res != nil {
635+
ic.Emit(&resource{
636+
Resource: "databricks_secret_scope",
637+
ID: res[1],
638+
})
639+
}
640+
}
602641
}
603642
policyName := r.Data.Get("name").(string)
604643
if slices.Contains(predefinedClusterPolicies, policyName) {
@@ -946,6 +985,9 @@ var resourcesMap map[string]importable = map[string]importable{
946985
}
947986
return nil
948987
},
988+
Ignore: func(ic *importContext, r *resource) bool {
989+
return r.Data.Get("name").(string) == ""
990+
},
949991
},
950992
"databricks_secret": {
951993
Service: "secrets",

exporter/importables_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,61 @@ func TestSecretScopeListNoNameMatch(t *testing.T) {
626626
})
627627
}
628628

629+
func TestPoliciesListing(t *testing.T) {
630+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
631+
{
632+
Method: "GET",
633+
Resource: "/api/2.0/policies/clusters/list?",
634+
Response: compute.ListPoliciesResponse{
635+
Policies: []compute.Policy{
636+
{
637+
Name: "Personal Compute",
638+
PolicyId: "123",
639+
},
640+
{
641+
Name: "abcd",
642+
PolicyId: "456",
643+
},
644+
},
645+
},
646+
},
647+
}, func(ctx context.Context, client *common.DatabricksClient) {
648+
ic := importContextForTest()
649+
ic.Client = client
650+
ic.Context = ctx
651+
err := resourcesMap["databricks_cluster_policy"].List(ic)
652+
assert.NoError(t, err)
653+
assert.Equal(t, 1, len(ic.testEmits))
654+
})
655+
}
656+
657+
func TestPoliciesListNoNameMatch(t *testing.T) {
658+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
659+
{
660+
Method: "GET",
661+
Resource: "/api/2.0/policies/clusters/list?",
662+
Response: compute.ListPoliciesResponse{
663+
Policies: []compute.Policy{
664+
{
665+
Name: "Personal Compute",
666+
},
667+
{
668+
Name: "abcd",
669+
},
670+
},
671+
},
672+
},
673+
}, func(ctx context.Context, client *common.DatabricksClient) {
674+
ic := importContextForTest()
675+
ic.Client = client
676+
ic.Context = ctx
677+
ic.match = "bcd"
678+
err := resourcesMap["databricks_cluster_policy"].List(ic)
679+
assert.NoError(t, err)
680+
assert.Equal(t, 0, len(ic.testEmits))
681+
})
682+
}
683+
629684
func TestAwsS3MountProfile(t *testing.T) {
630685
ic := importContextForTest()
631686
ic.mounts = true
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"created_at_timestamp": 1606308550000,
3-
"definition": "{\"aws_attributes.instance_profile_arn\":{\"hidden\":true,\"type\":\"fixed\",\"value\":\"arn:aws:iam::12345:instance-profile/shard-s3-access\"},\"instance_pool_id\":{\"hidden\":true,\"type\":\"fixed\",\"value\":\"pool1\"},\"autoscale.max_workers\":{\"defaultValue\":2,\"maxValue\":5,\"type\":\"range\"}}",
3+
"definition": "{\"aws_attributes.instance_profile_arn\":{\"hidden\":true,\"type\":\"fixed\",\"value\":\"arn:aws:iam::12345:instance-profile/shard-s3-access\"},\"instance_pool_id\":{\"hidden\":true,\"type\":\"fixed\",\"value\":\"pool1\"},\"spark_conf.abc\":{\"hidden\":true,\"type\":\"fixed\",\"value\":\"{{secrets/some-kv-scope/secret}}\"},\"autoscale.max_workers\":{\"defaultValue\":2,\"maxValue\":5,\"type\":\"range\"}}",
44
"name": "users cluster policy",
55
"policy_id": "123"
66
}

0 commit comments

Comments
 (0)