Skip to content

Commit d8a27fa

Browse files
authored
[Exporter] add support for databricks_credential (#4292)
## Changes <!-- Summary of your changes that are easy to understand --> ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [x] `make test` run locally - [x] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] relevant acceptance tests are passing - [x] using Go SDK
1 parent 18d13bf commit d8a27fa

File tree

6 files changed

+111
-25
lines changed

6 files changed

+111
-25
lines changed

docs/guides/experimental-exporter.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ Services are just logical groups of resources used for filtering and organizatio
137137
* `uc-artifact-allowlist` - **listing** exports [databricks_artifact_allowlist](../resources/artifact_allowlist.md) resources for Unity Catalog Allow Lists attached to the current metastore.
138138
* `uc-catalogs` - **listing** [databricks_catalog](../resources/catalog.md) and [databricks_workspace_binding](../resources/workspace_binding.md)
139139
* `uc-connections` - **listing** [databricks_connection](../resources/connection.md). *Please note that because API doesn't return sensitive fields, such as, passwords, tokens, ..., the generated `options` block could be incomplete!*
140+
* `uc-credentials` - **listing** exports [databricks_credential](../resources/credential.md) resources on workspace or account level. *Please note that it will skip storage credentials! Use the `uc-storage-credentials` service for them*
140141
* `uc-external-locations` - **listing** exports [databricks_external_location](../resources/external_location.md) resource.
141142
* `uc-grants` - [databricks_grants](../resources/grants.md). *Please note that during export the list of grants is expanded to include the identity that does the export! This is done to allow to creation of objects in case when catalogs/schemas have different owners than the current identity.*.
142143
* `uc-metastores` - **listing** [databricks_metastore](../resources/metastore.md) and [databricks_metastore_assignment](../resource/metastore_assignment.md) (only on account-level). *Please note that when using workspace-level configuration, only the metastores from the workspace's region are listed!*
@@ -179,6 +180,7 @@ Exporter aims to generate HCL code for most of the resources within the Databric
179180
| [databricks_cluster](../resources/cluster.md) | Yes | No | Yes | No |
180181
| [databricks_cluster_policy](../resources/cluster_policy.md) | Yes | No | Yes | No |
181182
| [databricks_connection](../resources/connection.md) | Yes | Yes | Yes | No |
183+
| [databricks_credential](../resources/credential.md) | Yes | Yes | Yes | No |
182184
| [databricks_dashboard](../resources/dashboard.md) | Yes | No | Yes | No |
183185
| [databricks_dbfs_file](../resources/dbfs_file.md) | Yes | No | Yes | No |
184186
| [databricks_external_location](../resources/external_location.md) | Yes | Yes | Yes | No |

exporter/context.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -494,20 +494,20 @@ func (ic *importContext) Run() error {
494494
// nolint
495495
dcfile.WriteString(
496496
`terraform {
497-
required_providers {
498-
databricks = {
499-
source = "databricks/databricks"
500-
version = "` + common.Version() + `"
501-
}
502-
}
503-
}
497+
required_providers {
498+
databricks = {
499+
source = "databricks/databricks"
500+
version = "` + common.Version() + `"
501+
}
502+
}
503+
}
504504
505-
provider "databricks" {
506-
`)
505+
provider "databricks" {
506+
`)
507507
if ic.accountLevel {
508-
dcfile.WriteString(fmt.Sprintf(` host = "%s"
509-
account_id = "%s"
510-
`, ic.Client.Config.Host, ic.Client.Config.AccountID))
508+
dcfile.WriteString(fmt.Sprintf(` host = "%s"
509+
account_id = "%s"
510+
`, ic.Client.Config.Host, ic.Client.Config.AccountID))
511511
}
512512
dcfile.WriteString(`}`)
513513
dcfile.Close()

exporter/exporter_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,13 +273,20 @@ var emptyExternalLocations = qa.HTTPFixture{
273273
Response: &catalog.ListExternalLocationsResponse{},
274274
}
275275

276-
var emptyStorageCrdentials = qa.HTTPFixture{
276+
var emptyStorageCredentials = qa.HTTPFixture{
277277
Method: "GET",
278278
Resource: "/api/2.1/unity-catalog/storage-credentials?",
279279
Status: 200,
280280
Response: &catalog.ListStorageCredentialsResponse{},
281281
}
282282

283+
var emptyUcCredentials = qa.HTTPFixture{
284+
Method: "GET",
285+
Resource: "/api/2.1/unity-catalog/credentials?",
286+
Status: 200,
287+
Response: &catalog.ListCredentialsResponse{},
288+
}
289+
283290
var emptyConnections = qa.HTTPFixture{
284291
Method: "GET",
285292
Resource: "/api/2.1/unity-catalog/connections?",
@@ -495,7 +502,8 @@ func TestImportingUsersGroupsSecretScopes(t *testing.T) {
495502
emptyInstancePools,
496503
emptyModelServing,
497504
emptyExternalLocations,
498-
emptyStorageCrdentials,
505+
emptyStorageCredentials,
506+
emptyUcCredentials,
499507
emptyMlflowWebhooks,
500508
emptySqlDashboards,
501509
emptySqlEndpoints,
@@ -763,7 +771,8 @@ func TestImportingNoResourcesError(t *testing.T) {
763771
emptyMetastoreList,
764772
emptyRepos,
765773
emptyExternalLocations,
766-
emptyStorageCrdentials,
774+
emptyStorageCredentials,
775+
emptyUcCredentials,
767776
emptyShares,
768777
emptyConnections,
769778
emptyRecipients,

exporter/importables.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,12 +2987,47 @@ var resourcesMap map[string]importable = map[string]importable{
29872987
}
29882988
return nil
29892989
},
2990-
ShouldOmitField: func(ic *importContext, pathString string, as *schema.Schema, d *schema.ResourceData) bool {
2991-
if pathString == "isolation_mode" {
2992-
return d.Get(pathString).(string) != "ISOLATION_MODE_ISOLATED"
2990+
ShouldOmitField: shouldOmitWithIsolationMode,
2991+
Depends: []reference{
2992+
{Path: "azure_service_principal.client_secret", Variable: true},
2993+
},
2994+
},
2995+
"databricks_credential": {
2996+
WorkspaceLevel: true,
2997+
Service: "uc-credentials",
2998+
Import: func(ic *importContext, r *resource) error {
2999+
ic.emitUCGrantsWithOwner("credential/"+r.ID, r)
3000+
if r.Data != nil {
3001+
isolationMode := r.Data.Get("isolation_mode").(string)
3002+
if isolationMode == "ISOLATION_MODE_ISOLATED" {
3003+
purpose := r.Data.Get("purpose").(string)
3004+
if purpose == "SERVICE" {
3005+
ic.emitWorkspaceBindings("credential", r.ID)
3006+
} else if purpose == "STORAGE" {
3007+
ic.emitWorkspaceBindings("storage_credential", r.ID)
3008+
}
3009+
}
29933010
}
2994-
return shouldOmitForUnityCatalog(ic, pathString, as, d)
3011+
return nil
3012+
},
3013+
List: func(ic *importContext) error {
3014+
it := ic.workspaceClient.Credentials.ListCredentials(ic.Context, catalog.ListCredentialsRequest{})
3015+
for it.HasNext(ic.Context) {
3016+
v, err := it.Next(ic.Context)
3017+
if err != nil {
3018+
return err
3019+
}
3020+
if v.Purpose == catalog.CredentialPurposeStorage {
3021+
continue // we're handling storage credentials separately
3022+
}
3023+
ic.EmitIfUpdatedAfterMillisAndNameMatches(&resource{
3024+
Resource: "databricks_credential",
3025+
ID: v.Name,
3026+
}, v.Name, v.UpdatedAt, fmt.Sprintf("credential %s", v.Name))
3027+
}
3028+
return nil
29953029
},
3030+
ShouldOmitField: shouldOmitWithIsolationMode,
29963031
Depends: []reference{
29973032
{Path: "azure_service_principal.client_secret", Variable: true},
29983033
},
@@ -3032,12 +3067,7 @@ var resourcesMap map[string]importable = map[string]importable{
30323067
}
30333068
return nil
30343069
},
3035-
ShouldOmitField: func(ic *importContext, pathString string, as *schema.Schema, d *schema.ResourceData) bool {
3036-
if pathString == "isolation_mode" {
3037-
return d.Get(pathString).(string) != "ISOLATION_MODE_ISOLATED"
3038-
}
3039-
return shouldOmitForUnityCatalog(ic, pathString, as, d)
3040-
},
3070+
ShouldOmitField: shouldOmitWithIsolationMode,
30413071
// This external location is automatically created when metastore is created with the `storage_root`
30423072
Ignore: func(ic *importContext, r *resource) bool {
30433073
return r.ID == "metastore_default_location"

exporter/importables_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,44 @@ func TestListExternalLocations(t *testing.T) {
20172017
})
20182018
}
20192019

2020+
func TestServiceCredentials(t *testing.T) {
2021+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
2022+
{
2023+
ReuseRequest: true,
2024+
Method: "GET",
2025+
Resource: "/api/2.1/unity-catalog/credentials?",
2026+
Response: catalog.ListCredentialsResponse{
2027+
Credentials: []catalog.CredentialInfo{
2028+
{
2029+
Name: "test-storage",
2030+
Purpose: catalog.CredentialPurposeStorage,
2031+
},
2032+
{
2033+
Name: "test-service",
2034+
Purpose: catalog.CredentialPurposeService,
2035+
},
2036+
},
2037+
},
2038+
},
2039+
}, func(ctx context.Context, client *common.DatabricksClient) {
2040+
ic := importContextForTestWithClient(ctx, client)
2041+
ic.enableServices("uc-credentials,uc-grants")
2042+
ic.currentMetastore = currentMetastoreResponse
2043+
// Test listing
2044+
err := resourcesMap["databricks_credential"].List(ic)
2045+
assert.NoError(t, err)
2046+
require.Equal(t, 1, len(ic.testEmits))
2047+
assert.True(t, ic.testEmits["databricks_credential[<unknown>] (id: test-service)"])
2048+
// Test import
2049+
err = resourcesMap["databricks_credential"].Import(ic, &resource{
2050+
ID: "1234",
2051+
})
2052+
assert.NoError(t, err)
2053+
require.Equal(t, 2, len(ic.testEmits))
2054+
assert.True(t, ic.testEmits["databricks_grants[<unknown>] (id: credential/1234)"])
2055+
})
2056+
}
2057+
20202058
func TestStorageCredentials(t *testing.T) {
20212059
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
20222060
{

exporter/util.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,13 @@ func shouldOmitForUnityCatalog(ic *importContext, pathString string, as *schema.
436436
return defaultShouldOmitFieldFunc(ic, pathString, as, d)
437437
}
438438

439+
func shouldOmitWithIsolationMode(ic *importContext, pathString string, as *schema.Schema, d *schema.ResourceData) bool {
440+
if pathString == "isolation_mode" {
441+
return d.Get(pathString).(string) != "ISOLATION_MODE_ISOLATED"
442+
}
443+
return shouldOmitForUnityCatalog(ic, pathString, as, d)
444+
}
445+
439446
func appendEndingSlashToDirName(dir string) string {
440447
if dir == "" || dir[len(dir)-1] == '/' {
441448
return dir

0 commit comments

Comments
 (0)