-
Notifications
You must be signed in to change notification settings - Fork 400
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feature] Add
databricks_credential
resource (#4219)
## Changes - Add `databricks_credential` resource that represents [service credential](https://docs.databricks.com/en/connect/unity-catalog/cloud-services/service-credentials.html) Resolves #4214 ## 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 - [x] covered with integration tests in `internal/acceptance` - [x] relevant acceptance tests are passing - [x] using Go SDK --------- Co-authored-by: Farruco Sanjurjo <farruco.sanjurjo@databricks.com> Co-authored-by: Alex Ott <alexey.ott@databricks.com> Co-authored-by: Farruco Sanjurjo <madtrick@users.noreply.github.com>
- Loading branch information
1 parent
9ea68e9
commit f119680
Showing
10 changed files
with
513 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package catalog | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/databricks/databricks-sdk-go/service/catalog" | ||
"github.com/databricks/terraform-provider-databricks/catalog/bindings" | ||
"github.com/databricks/terraform-provider-databricks/common" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
) | ||
|
||
var credentialSchema = common.StructToSchema(catalog.CredentialInfo{}, | ||
func(m map[string]*schema.Schema) map[string]*schema.Schema { | ||
var alofServiceCreds = []string{"aws_iam_role", "azure_managed_identity", "azure_service_principal"} | ||
for _, cred := range alofServiceCreds { | ||
common.CustomizeSchemaPath(m, cred).SetExactlyOneOf(alofServiceCreds) | ||
} | ||
|
||
for _, required := range []string{"name", "purpose"} { | ||
common.CustomizeSchemaPath(m, required).SetRequired() | ||
} | ||
|
||
for _, computed := range []string{"id", "created_at", "created_by", "full_name", "isolation_mode", | ||
"metastore_id", "owner", "updated_at", "updated_by", "used_for_managed_storage"} { | ||
common.CustomizeSchemaPath(m, computed).SetComputed() | ||
} | ||
|
||
common.MustSchemaPath(m, "aws_iam_role", "external_id").Computed = true | ||
common.MustSchemaPath(m, "aws_iam_role", "unity_catalog_iam_arn").Computed = true | ||
common.MustSchemaPath(m, "azure_managed_identity", "credential_id").Computed = true | ||
common.MustSchemaPath(m, "azure_service_principal", "client_secret").Sensitive = true | ||
|
||
m["force_destroy"] = &schema.Schema{ | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
} | ||
m["force_update"] = &schema.Schema{ | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
} | ||
m["skip_validation"] = &schema.Schema{ | ||
Type: schema.TypeBool, | ||
Optional: true, | ||
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { | ||
return old == "false" && new == "true" | ||
}, | ||
} | ||
m["credential_id"] = &schema.Schema{ | ||
Type: schema.TypeString, | ||
Computed: true, | ||
} | ||
m["name"].DiffSuppressFunc = common.EqualFoldDiffSuppress | ||
return m | ||
}) | ||
|
||
func ResourceCredential() common.Resource { | ||
return common.Resource{ | ||
Schema: credentialSchema, | ||
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { | ||
w, err := c.WorkspaceClient() | ||
if err != nil { | ||
return err | ||
} | ||
var create catalog.CreateCredentialRequest | ||
common.DataToStructPointer(d, credentialSchema, &create) | ||
cred, err := w.Credentials.CreateCredential(ctx, create) | ||
if err != nil { | ||
return err | ||
} | ||
d.SetId(cred.Name) | ||
|
||
// Update owner or isolation mode if it is provided | ||
if !updateRequired(d, []string{"owner", "isolation_mode"}) { | ||
return nil | ||
} | ||
|
||
var update catalog.UpdateCredentialRequest | ||
common.DataToStructPointer(d, credentialSchema, &update) | ||
update.NameArg = d.Id() | ||
_, err = w.Credentials.UpdateCredential(ctx, update) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Bind the current workspace if the credential is isolated, otherwise the read will fail | ||
return bindings.AddCurrentWorkspaceBindings(ctx, d, w, cred.Name, catalog.UpdateBindingsSecurableTypeServiceCredential) | ||
}, | ||
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { | ||
w, err := c.WorkspaceClient() | ||
if err != nil { | ||
return err | ||
} | ||
cred, err := w.Credentials.GetCredentialByNameArg(ctx, d.Id()) | ||
if err != nil { | ||
return err | ||
} | ||
d.Set("credential_id", cred.Id) | ||
return common.StructToData(cred, credentialSchema, d) | ||
}, | ||
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { | ||
force := d.Get("force_update").(bool) | ||
w, err := c.WorkspaceClient() | ||
if err != nil { | ||
return err | ||
} | ||
var updateCredRequest catalog.UpdateCredentialRequest | ||
common.DataToStructPointer(d, credentialSchema, &updateCredRequest) | ||
updateCredRequest.NameArg = d.Id() | ||
updateCredRequest.Force = force | ||
|
||
if d.HasChange("owner") { | ||
_, err = w.Credentials.UpdateCredential(ctx, catalog.UpdateCredentialRequest{ | ||
NameArg: updateCredRequest.NameArg, | ||
Owner: updateCredRequest.Owner, | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if !d.HasChangeExcept("owner") { | ||
return nil | ||
} | ||
if d.HasChange("read_only") { | ||
updateCredRequest.ForceSendFields = append(updateCredRequest.ForceSendFields, "ReadOnly") | ||
} | ||
|
||
updateCredRequest.Owner = "" | ||
_, err = w.Credentials.UpdateCredential(ctx, updateCredRequest) | ||
if err != nil { | ||
if d.HasChange("owner") { | ||
// Rollback | ||
old, new := d.GetChange("owner") | ||
_, rollbackErr := w.Credentials.UpdateCredential(ctx, catalog.UpdateCredentialRequest{ | ||
NameArg: updateCredRequest.NameArg, | ||
Owner: old.(string), | ||
}) | ||
if rollbackErr != nil { | ||
return common.OwnerRollbackError(err, rollbackErr, old.(string), new.(string)) | ||
} | ||
} | ||
return err | ||
} | ||
// Bind the current workspace if the credential is isolated, otherwise the read will fail | ||
return bindings.AddCurrentWorkspaceBindings(ctx, d, w, updateCredRequest.NameArg, catalog.UpdateBindingsSecurableTypeServiceCredential) | ||
}, | ||
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error { | ||
force := d.Get("force_destroy").(bool) | ||
w, err := c.WorkspaceClient() | ||
if err != nil { | ||
return err | ||
} | ||
return w.Credentials.DeleteCredential(ctx, catalog.DeleteCredentialRequest{ | ||
NameArg: d.Id(), | ||
Force: force, | ||
}) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package catalog | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/databricks/databricks-sdk-go/experimental/mocks" | ||
"github.com/databricks/databricks-sdk-go/service/catalog" | ||
"github.com/databricks/terraform-provider-databricks/qa" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
func TestCredentialsCornerCases(t *testing.T) { | ||
qa.ResourceCornerCases(t, ResourceCredential()) | ||
} | ||
|
||
func TestCreateCredential(t *testing.T) { | ||
qa.ResourceFixture{ | ||
MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { | ||
e := w.GetMockCredentialsAPI().EXPECT() | ||
e.CreateCredential(mock.Anything, catalog.CreateCredentialRequest{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
}, | ||
Comment: "c", | ||
Purpose: "SERVICE", | ||
}).Return(&catalog.CredentialInfo{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
}, | ||
Purpose: "SERVICE", | ||
Comment: "c", | ||
}, nil) | ||
e.GetCredentialByNameArg(mock.Anything, "a").Return(&catalog.CredentialInfo{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
ExternalId: "123", | ||
}, | ||
Purpose: "SERVICE", | ||
MetastoreId: "d", | ||
Id: "1234-5678", | ||
Owner: "f", | ||
IsolationMode: "ISOLATION_MODE_ISOLATED", | ||
}, nil) | ||
}, | ||
Resource: ResourceCredential(), | ||
Create: true, | ||
HCL: ` | ||
name = "a" | ||
aws_iam_role { | ||
role_arn = "def" | ||
} | ||
purpose = "SERVICE" | ||
comment = "c" | ||
`, | ||
}.ApplyAndExpectData(t, map[string]any{ | ||
"aws_iam_role.0.external_id": "123", | ||
"aws_iam_role.0.role_arn": "def", | ||
"name": "a", | ||
"purpose": "SERVICE", | ||
}) | ||
} | ||
|
||
func TestCreateIsolatedCredential(t *testing.T) { | ||
qa.ResourceFixture{ | ||
MockWorkspaceClientFunc: func(w *mocks.MockWorkspaceClient) { | ||
e := w.GetMockCredentialsAPI().EXPECT() | ||
e.CreateCredential(mock.Anything, catalog.CreateCredentialRequest{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
}, | ||
Comment: "c", | ||
Purpose: "SERVICE", | ||
}).Return(&catalog.CredentialInfo{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
ExternalId: "123", | ||
}, | ||
Purpose: "SERVICE", | ||
MetastoreId: "d", | ||
Id: "1234-5678", | ||
Owner: "f", | ||
}, nil) | ||
e.UpdateCredential(mock.Anything, catalog.UpdateCredentialRequest{ | ||
NameArg: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
}, | ||
Comment: "c", | ||
IsolationMode: "ISOLATION_MODE_ISOLATED", | ||
}).Return(&catalog.CredentialInfo{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
ExternalId: "123", | ||
}, | ||
Purpose: "SERVICE", | ||
MetastoreId: "d", | ||
Id: "1234-5678", | ||
Owner: "f", | ||
IsolationMode: "ISOLATION_MODE_ISOLATED", | ||
}, nil) | ||
w.GetMockMetastoresAPI().EXPECT().Current(mock.Anything).Return(&catalog.MetastoreAssignment{ | ||
MetastoreId: "e", | ||
WorkspaceId: 123456789101112, | ||
}, nil) | ||
w.GetMockWorkspaceBindingsAPI().EXPECT().UpdateBindings(mock.Anything, catalog.UpdateWorkspaceBindingsParameters{ | ||
SecurableName: "a", | ||
SecurableType: catalog.UpdateBindingsSecurableTypeServiceCredential, | ||
Add: []catalog.WorkspaceBinding{ | ||
{ | ||
WorkspaceId: int64(123456789101112), | ||
BindingType: catalog.WorkspaceBindingBindingTypeBindingTypeReadWrite, | ||
}, | ||
}, | ||
}).Return(&catalog.WorkspaceBindingsResponse{ | ||
Bindings: []catalog.WorkspaceBinding{ | ||
{ | ||
WorkspaceId: int64(123456789101112), | ||
BindingType: catalog.WorkspaceBindingBindingTypeBindingTypeReadWrite, | ||
}, | ||
}, | ||
}, nil) | ||
e.GetCredentialByNameArg(mock.Anything, "a").Return(&catalog.CredentialInfo{ | ||
Name: "a", | ||
AwsIamRole: &catalog.AwsIamRole{ | ||
RoleArn: "def", | ||
ExternalId: "123", | ||
}, | ||
Purpose: "SERVICE", | ||
MetastoreId: "d", | ||
Id: "1234-5678", | ||
Owner: "f", | ||
IsolationMode: "ISOLATION_MODE_ISOLATED", | ||
}, nil) | ||
}, | ||
Resource: ResourceCredential(), | ||
Create: true, | ||
HCL: ` | ||
name = "a" | ||
aws_iam_role { | ||
role_arn = "def" | ||
} | ||
comment = "c" | ||
purpose = "SERVICE" | ||
isolation_mode = "ISOLATION_MODE_ISOLATED" | ||
`, | ||
}.ApplyAndExpectData(t, map[string]any{ | ||
"aws_iam_role.0.external_id": "123", | ||
"aws_iam_role.0.role_arn": "def", | ||
"name": "a", | ||
"isolation_mode": "ISOLATION_MODE_ISOLATED", | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.