Skip to content

Commit 2b03da5

Browse files
authored
Add AZURE_KEYVAULT backend for databricks_secret_scope for Azure CLI authenticated environments (#381)
* Add AKV backend for databricks_secret_scope * Implements issue #369 * minor changes Co-authored-by: Serge Smertin <[email protected]>
1 parent c2e7a12 commit 2b03da5

File tree

20 files changed

+423
-135
lines changed

20 files changed

+423
-135
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 0.2.8
44

5+
* Added [Azure Key Vault support](https://github.com/databrickslabs/terraform-provider-databricks/pull/381) for databricks_secret_scope for Azure CLI authenticated users
56
* Added support for pinning clusters (issue #113)
67
* Internal: API for retrieval of the cluster events
78

access/acceptance/secret_scope_test.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,57 @@ import (
1111

1212
"github.com/databrickslabs/databricks-terraform/common"
1313
"github.com/databrickslabs/databricks-terraform/internal/acceptance"
14+
"github.com/databrickslabs/databricks-terraform/internal/qa"
1415
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1617
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1718
"github.com/stretchr/testify/assert"
1819
"github.com/stretchr/testify/require"
1920
)
2021

22+
func TestAccRemoveScopes(t *testing.T) {
23+
if _, ok := os.LookupEnv("VSCODE_PID"); !ok {
24+
t.Skip("Cleaning up tests only from IDE")
25+
}
26+
client := common.CommonEnvironmentClient()
27+
scopesAPI := NewSecretScopesAPI(client)
28+
scopeList, err := scopesAPI.List()
29+
require.NoError(t, err)
30+
for _, scope := range scopeList {
31+
assert.NoError(t, scopesAPI.Delete(scope.Name))
32+
}
33+
}
34+
35+
func TestAzureAccKeyVaultSimple(t *testing.T) {
36+
resourceID := qa.GetEnvOrSkipTest(t, "TEST_KEY_VAULT_RESOURCE_ID")
37+
DNSName := qa.GetEnvOrSkipTest(t, "TEST_KEY_VAULT_DNS_NAME")
38+
39+
client := common.CommonEnvironmentClient()
40+
if client.AzureAuth.IsClientSecretSet() {
41+
t.Skip("AKV scopes don't work for SP auth yet")
42+
}
43+
scopesAPI := NewSecretScopesAPI(client)
44+
name := qa.RandomName("tf-scope-")
45+
46+
err := scopesAPI.Create(SecretScope{
47+
Name: name,
48+
KeyvaultMetadata: &KeyvaultMetadata{
49+
ResourceID: resourceID,
50+
DNSName: DNSName,
51+
},
52+
})
53+
require.NoError(t, err)
54+
defer func() {
55+
assert.NoError(t, scopesAPI.Delete(name))
56+
}()
57+
58+
scope, err := scopesAPI.Read(name)
59+
require.NoError(t, err)
60+
require.Equal(t, "AZURE_KEYVAULT", scope.BackendType)
61+
assert.Equal(t, resourceID, scope.KeyvaultMetadata.ResourceID)
62+
assert.Equal(t, DNSName, scope.KeyvaultMetadata.DNSName)
63+
}
64+
2165
func TestAccInitialManagePrincipals(t *testing.T) {
2266
if _, ok := os.LookupEnv("CLOUD_ENV"); !ok {
2367
t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set")
@@ -26,7 +70,7 @@ func TestAccInitialManagePrincipals(t *testing.T) {
2670
scopesAPI := NewSecretScopesAPI(client)
2771

2872
scope := fmt.Sprintf("tf-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
29-
err := scopesAPI.Create(scope, "")
73+
err := scopesAPI.Create(SecretScope{Name: scope})
3074
require.NoError(t, err)
3175
defer func() {
3276
assert.NoError(t, scopesAPI.Delete(scope))
@@ -51,7 +95,10 @@ func TestAccInitialManagePrincipalsGroup(t *testing.T) {
5195
scopesAPI := NewSecretScopesAPI(client)
5296

5397
scope := fmt.Sprintf("tf-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
54-
err := scopesAPI.Create(scope, "users")
98+
err := scopesAPI.Create(SecretScope{
99+
Name: scope,
100+
InitialManagePrincipal: "users",
101+
})
55102
require.NoError(t, err)
56103
defer func() {
57104
assert.NoError(t, scopesAPI.Delete(scope))
@@ -69,9 +116,7 @@ func TestAccSecretScopeResource(t *testing.T) {
69116
t.Skip("Acceptance tests skipped unless env 'CLOUD_ENV' is set")
70117
}
71118
var secretScope SecretScope
72-
73-
scope := fmt.Sprintf("tf-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
74-
119+
scope := qa.RandomName("tf-")
75120
acceptance.AccTest(t, resource.TestCase{
76121
CheckDestroy: testSecretScopeResourceDestroy,
77122
Steps: []resource.TestStep{
@@ -86,7 +131,7 @@ func TestAccSecretScopeResource(t *testing.T) {
86131
testSecretScopeValues(t, &secretScope, scope),
87132
// verify local values
88133
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "name", scope),
89-
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "backend_type", string(ScopeBackendTypeDatabricks)),
134+
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "backend_type", "DATABRICKS"),
90135
acceptance.ResourceCheck("databricks_secret_scope.my_scope",
91136
func(client *common.DatabricksClient, id string) error {
92137
secretACLAPI := NewSecretAclsAPI(client)
@@ -118,7 +163,7 @@ func TestAccSecretScopeResource(t *testing.T) {
118163
testSecretScopeValues(t, &secretScope, scope),
119164
// verify local values
120165
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "name", scope),
121-
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "backend_type", string(ScopeBackendTypeDatabricks)),
166+
resource.TestCheckResourceAttr("databricks_secret_scope.my_scope", "backend_type", "DATABRICKS"),
122167
),
123168
},
124169
},
@@ -143,7 +188,7 @@ func testSecretScopeResourceDestroy(s *terraform.State) error {
143188
func testSecretScopeValues(t *testing.T, secretScope *SecretScope, scope string) resource.TestCheckFunc {
144189
return func(s *terraform.State) error {
145190
assert.True(t, secretScope.Name == scope)
146-
assert.True(t, secretScope.BackendType == ScopeBackendTypeDatabricks)
191+
assert.True(t, secretScope.BackendType == "DATABRICKS")
147192
return nil
148193
}
149194
}
@@ -168,7 +213,6 @@ func testSecretScopeResourceExists(n string, secretScope *SecretScope, t *testin
168213
// If no error, assign the response Widget attribute to the widget pointer
169214
*secretScope = resp
170215
return nil
171-
//return fmt.Errorf("Token (%s) not found", rs.Primary.ID)
172216
}
173217
}
174218

access/resource_secret.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,6 @@ import (
1010
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1111
)
1212

13-
// ScopeBackendType is a custom type for the backend type for secret scopes
14-
type ScopeBackendType string
15-
16-
// List of constants of ScopeBackendType
17-
const (
18-
ScopeBackendTypeDatabricks ScopeBackendType = "DATABRICKS"
19-
)
20-
21-
// SecretScopeList holds list of secret scopes
22-
type SecretScopeList struct {
23-
Scopes []SecretScope `json:"scopes,omitempty"`
24-
}
25-
26-
// SecretScope is a struct that encapsulates the secret scope
27-
type SecretScope struct {
28-
Name string `json:"name,omitempty"`
29-
BackendType ScopeBackendType `json:"backend_type,omitempty"`
30-
}
31-
3213
// SecretsRequest ...
3314
type SecretsRequest struct {
3415
StringValue string `json:"string_value,omitempty" mask:"true"`

access/resource_secret_acl_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ func TestSecretsScopesAclsIntegration(t *testing.T) {
2525
// TODO: on random group
2626
testPrincipal := "users"
2727

28-
err := NewSecretScopesAPI(client).Create(testScope, initialManagePrincipal)
28+
err := NewSecretScopesAPI(client).Create(SecretScope{
29+
Name: testScope,
30+
InitialManagePrincipal: initialManagePrincipal,
31+
})
2932
assert.NoError(t, err, err)
3033

3134
defer func() {

access/resource_secret_scope.go

Lines changed: 95 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,85 @@
11
package access
22

33
import (
4+
"context"
45
"fmt"
5-
"log"
66
"net/http"
77

88
"github.com/databrickslabs/databricks-terraform/common"
9+
"github.com/databrickslabs/databricks-terraform/internal"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
911
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1012
)
1113

1214
// NewSecretScopesAPI creates SecretScopesAPI instance from provider meta
1315
func NewSecretScopesAPI(m interface{}) SecretScopesAPI {
14-
return SecretScopesAPI{C: m.(*common.DatabricksClient)}
16+
return SecretScopesAPI{client: m.(*common.DatabricksClient)}
1517
}
1618

1719
// SecretScopesAPI exposes the Secret Scopes API
1820
type SecretScopesAPI struct {
19-
C *common.DatabricksClient
21+
client *common.DatabricksClient
22+
}
23+
24+
// SecretScopeList holds list of secret scopes
25+
type SecretScopeList struct {
26+
Scopes []SecretScope `json:"scopes,omitempty"`
27+
}
28+
29+
// SecretScope is a struct that encapsulates the secret scope
30+
type SecretScope struct {
31+
Name string `json:"name"`
32+
BackendType string `json:"backend_type,omitempty" tf:"computed"`
33+
InitialManagePrincipal string `json:"initial_manage_principal,omitempty"`
34+
KeyvaultMetadata *KeyvaultMetadata `json:"keyvault_metadata,omitempty"`
35+
}
36+
37+
// KeyvaultMetadata Azure Key Vault metadata wrapper
38+
type KeyvaultMetadata struct {
39+
// /subscriptions/.../resourceGroups/.../providers/Microsoft.KeyVault/vaults/my-azure-kv
40+
ResourceID string `json:"resource_id"`
41+
// https://my-azure-kv.vault.azure.net/
42+
DNSName string `json:"dns_name"`
43+
}
44+
45+
type secretScopeRequest struct {
46+
Scope string `json:"scope,omitempty"`
47+
BackendType string `json:"scope_backend_type,omitempty"`
48+
InitialManagePrincipal string `json:"initial_manage_principal,omitempty"`
49+
BackendAzureKeyvault *KeyvaultMetadata `json:"backend_azure_keyvault,omitempty"`
2050
}
2151

2252
// Create creates a new secret scope
23-
func (a SecretScopesAPI) Create(scope string, initialManagePrincipal string) error {
24-
req := map[string]string{"scope": scope}
25-
if initialManagePrincipal != "" {
26-
req["initial_manage_principal"] = initialManagePrincipal
53+
func (a SecretScopesAPI) Create(s SecretScope) error {
54+
req := secretScopeRequest{
55+
Scope: s.Name,
56+
InitialManagePrincipal: s.InitialManagePrincipal,
57+
BackendType: "DATABRICKS",
2758
}
28-
return a.C.Post("/secrets/scopes/create", req, nil)
59+
if s.KeyvaultMetadata != nil {
60+
if !a.client.IsAzure() {
61+
return fmt.Errorf("Azure KeyVault is not available")
62+
}
63+
if a.client.AzureAuth.IsClientSecretSet() {
64+
return fmt.Errorf("Azure KeyVault cannot yet be configured for Service Principal authorization")
65+
}
66+
req.BackendType = "AZURE_KEYVAULT"
67+
req.BackendAzureKeyvault = s.KeyvaultMetadata
68+
}
69+
return a.client.Post("/secrets/scopes/create", req, nil)
2970
}
3071

3172
// Delete deletes a secret scope
3273
func (a SecretScopesAPI) Delete(scope string) error {
33-
return a.C.Post("/secrets/scopes/delete", map[string]string{
74+
return a.client.Post("/secrets/scopes/delete", map[string]string{
3475
"scope": scope,
3576
}, nil)
3677
}
3778

3879
// List lists all secret scopes available in the workspace
3980
func (a SecretScopesAPI) List() ([]SecretScope, error) {
4081
var listSecretScopesResponse SecretScopeList
41-
err := a.C.Get("/secrets/scopes/list", nil, &listSecretScopesResponse)
82+
err := a.client.Get("/secrets/scopes/list", nil, &listSecretScopesResponse)
4283
return listSecretScopesResponse.Scopes, err
4384
}
4485

@@ -62,69 +103,56 @@ func (a SecretScopesAPI) Read(scopeName string) (SecretScope, error) {
62103
}
63104
}
64105

106+
// ResourceSecretScope manages secret scopes
65107
func ResourceSecretScope() *schema.Resource {
108+
s := internal.StructToSchema(SecretScope{}, func(s map[string]*schema.Schema) map[string]*schema.Schema {
109+
// TODO: CustomDiffFunc for initial_manage_principal & importing
110+
s["name"].ForceNew = true
111+
s["initial_manage_principal"].ForceNew = true
112+
s["keyvault_metadata"].ForceNew = true
113+
return s
114+
})
115+
readContext := func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
116+
scope, err := NewSecretScopesAPI(m).Read(d.Id())
117+
if e, ok := err.(common.APIError); ok && e.IsMissing() {
118+
d.SetId("")
119+
return nil
120+
}
121+
if err != nil {
122+
return diag.FromErr(err)
123+
}
124+
err = internal.StructToData(scope, s, d)
125+
if err != nil {
126+
return diag.FromErr(err)
127+
}
128+
return nil
129+
}
66130
return &schema.Resource{
67-
Create: resourceSecretScopeCreate,
68-
Read: resourceSecretScopeRead,
69-
Delete: resourceSecretScopeDelete,
70131
Importer: &schema.ResourceImporter{
71132
StateContext: schema.ImportStatePassthroughContext,
72133
},
73-
Schema: map[string]*schema.Schema{
74-
"name": {
75-
Type: schema.TypeString,
76-
Required: true,
77-
ForceNew: true,
78-
},
79-
"initial_manage_principal": {
80-
Type: schema.TypeString,
81-
Optional: true,
82-
ForceNew: true,
83-
},
84-
"backend_type": {
85-
Type: schema.TypeString,
86-
Computed: true,
87-
},
134+
CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
135+
var scope SecretScope
136+
err := internal.DataToStructPointer(d, s, &scope)
137+
if err != nil {
138+
return diag.FromErr(err)
139+
}
140+
err = NewSecretScopesAPI(m).Create(scope)
141+
if err != nil {
142+
return diag.FromErr(err)
143+
}
144+
d.SetId(scope.Name)
145+
return readContext(ctx, d, m)
88146
},
89-
}
90-
}
91-
92-
func resourceSecretScopeCreate(d *schema.ResourceData, m interface{}) error {
93-
client := m.(*common.DatabricksClient)
94-
scopeName := d.Get("name").(string)
95-
initialManagePrincipal := d.Get("initial_manage_principal").(string)
96-
err := NewSecretScopesAPI(client).Create(scopeName, initialManagePrincipal)
97-
if err != nil {
98-
return err
99-
}
100-
d.SetId(scopeName)
101-
return resourceSecretScopeRead(d, m)
102-
}
103-
104-
func resourceSecretScopeRead(d *schema.ResourceData, m interface{}) error {
105-
client := m.(*common.DatabricksClient)
106-
id := d.Id()
107-
scope, err := NewSecretScopesAPI(client).Read(id)
108-
if err != nil {
109-
if e, ok := err.(common.APIError); ok && e.IsMissing() {
110-
log.Printf("[INFO] missing resource due to error: %v\n", e)
111-
d.SetId("")
147+
DeleteContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
148+
err := NewSecretScopesAPI(m).Delete(d.Id())
149+
if err != nil {
150+
return diag.FromErr(err)
151+
}
112152
return nil
113-
}
114-
return err
115-
}
116-
d.SetId(scope.Name)
117-
err = d.Set("name", scope.Name)
118-
if err != nil {
119-
return err
153+
},
154+
ReadContext: readContext,
155+
SchemaVersion: 2,
156+
Schema: s,
120157
}
121-
err = d.Set("backend_type", scope.BackendType)
122-
return err
123-
}
124-
125-
func resourceSecretScopeDelete(d *schema.ResourceData, m interface{}) error {
126-
client := m.(*common.DatabricksClient)
127-
id := d.Id()
128-
err := NewSecretScopesAPI(client).Delete(id)
129-
return err
130158
}

0 commit comments

Comments
 (0)