diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index de8eed7432..91ecf23662 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -7,6 +7,7 @@ ### New Features and Improvements * Add resource and data sources for `databricks_environments_workspace_base_environment`. * Add resource and data source for `databricks_environments_default_workspace_base_environment`. +* Add `string_value_wo` and `string_value_wo_version` attributes to `databricks_secret` resource ([#5480](https://github.com/databricks/terraform-provider-databricks/pull/5480)) * Added `api` field to dual account/workspace resources (`databricks_user`, `databricks_service_principal`, `databricks_group`, `databricks_group_role`, `databricks_group_member`, `databricks_user_role`, `databricks_service_principal_role`, `databricks_user_instance_profile`, `databricks_group_instance_profile`, `databricks_metastore`, `databricks_metastore_assignment`, `databricks_metastore_data_access`, `databricks_storage_credential`, `databricks_service_principal_secret`, `databricks_access_control_rule_set`) to explicitly control whether account-level or workspace-level APIs are used. This enables support for unified hosts like `api.databricks.com` where the API level cannot be inferred from the host ([#5483](https://github.com/databricks/terraform-provider-databricks/pull/5483)). diff --git a/common/resource.go b/common/resource.go index 8230b3ab80..8c41b0856b 100644 --- a/common/resource.go +++ b/common/resource.go @@ -116,6 +116,9 @@ func (r Resource) ToResource() *schema.Resource { if v.Computed { continue } + if v.WriteOnly { + continue + } if nested, ok := v.Elem.(*schema.Resource); ok { queue = append(queue, nested) } diff --git a/docs/resources/secret.md b/docs/resources/secret.md index c6cd8520b9..23a4fca1f6 100644 --- a/docs/resources/secret.md +++ b/docs/resources/secret.md @@ -7,6 +7,8 @@ With this resource you can insert a secret under the provided scope with the giv -> This resource can only be used with a workspace-level provider! +-> **Note** Write-Only argument string_value_wo is available to use in place of string_value. Write-Only argumentss are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/ephemeral#write-only-arguments). + ## Example Usage ```hcl @@ -33,7 +35,9 @@ resource "databricks_cluster" "this" { The following arguments are required: -* `string_value` - (Required) (String) super secret sensitive value. +* `string_value` - (Optional) (String) Specifies text data that you want to encrypt and store in this version of the secret. This is required if `string_value_wo` is not set. +* `string_value_wo` (Optional) (String) Specifies text data that you want to encrypt and store in this version of the secret. This is required if string_value is not set. +* `string_value_wo_version` (Optional) (Integer) Use together with string_value_wo to trigger an update. Increment this value when an update to string_value_wo is required. * `scope` - (Required) (String) name of databricks secret scope. Must consist of alphanumeric characters, dashes, underscores, and periods, and may not exceed 128 characters. * `key` - (Required) (String) key within secret scope. Must consist of alphanumeric characters, dashes, underscores, and periods, and may not exceed 128 characters. * `provider_config` - (Optional) Configure the provider for management through account provider. This block consists of the following fields: diff --git a/qa/cty.go b/qa/cty.go new file mode 100644 index 0000000000..aa680eb39d --- /dev/null +++ b/qa/cty.go @@ -0,0 +1,49 @@ +package qa + +import ( + "github.com/hashicorp/go-cty/cty" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +// https://github.com/hashicorp/terraform-plugin-sdk/blob/866d0b19a878fe2241fa8e008bee8c6cb8b2c32b/internal/configs/hcl2shim/values.go#L130-L194 +func hcl2ValueFromConfigValue(v interface{}) cty.Value { + if v == nil { + return cty.NullVal(cty.DynamicPseudoType) + } + + switch tv := v.(type) { + case bool: + return cty.BoolVal(tv) + case string: + return cty.StringVal(tv) + case int: + return cty.NumberIntVal(int64(tv)) + case float64: + return cty.NumberFloatVal(tv) + case []interface{}: + vals := make([]cty.Value, len(tv)) + for i, ev := range tv { + vals[i] = hcl2ValueFromConfigValue(ev) + } + return cty.TupleVal(vals) + case map[string]interface{}: + vals := map[string]cty.Value{} + for k, ev := range tv { + vals[k] = hcl2ValueFromConfigValue(ev) + } + return cty.ObjectVal(vals) + default: + return cty.NullVal(cty.DynamicPseudoType) + } +} + +func makeResourceRawConfig(config *terraform.ResourceConfig, resource *schema.Resource) cty.Value { + original := hcl2ValueFromConfigValue(config.Raw) + coerced, err := resource.CoreConfigSchema().CoerceValue(original) + if err != nil { + return cty.NullVal(cty.DynamicPseudoType) + } + return coerced +} diff --git a/qa/testing.go b/qa/testing.go index 6f4d3a42be..1895457511 100644 --- a/qa/testing.go +++ b/qa/testing.go @@ -324,6 +324,9 @@ func (f ResourceFixture) Apply(t *testing.T) (*schema.ResourceData, error) { if err != nil { return nil, err } + if diff != nil { + diff.RawConfig = makeResourceRawConfig(resourceConfig, resource) + } if f.Update { err = f.requiresNew(diff) if err != nil { diff --git a/secrets/resource_secret.go b/secrets/resource_secret.go index 0e6a1f66a8..4d91105636 100644 --- a/secrets/resource_secret.go +++ b/secrets/resource_secret.go @@ -8,7 +8,7 @@ import ( "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/databricks/terraform-provider-databricks/common" - + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -35,6 +35,17 @@ func readSecret(ctx context.Context, w *databricks.WorkspaceClient, scope string } } +func getStringValue(d *schema.ResourceData) (string, error) { + woValue, diags := d.GetRawConfigAt(cty.GetAttrPath("string_value_wo")) + if !diags.HasError() && woValue.Type().Equals(cty.String) && !woValue.IsNull() { + return woValue.AsString(), nil + } + if v, ok := d.GetOk("string_value"); ok { + return v.(string), nil + } + return "", fmt.Errorf("failed to get one of attributes `string_value_wo` or `string_value`") +} + // ResourceSecret manages secrets func ResourceSecret() common.Resource { p := common.NewPairSeparatedID("scope", "key", "|||") @@ -42,9 +53,25 @@ func ResourceSecret() common.Resource { "string_value": { Type: schema.TypeString, ValidateFunc: validation.StringIsNotEmpty, - Required: true, + Optional: true, ForceNew: true, Sensitive: true, + ExactlyOneOf: []string{"string_value", "string_value_wo"}, + }, + "string_value_wo": { + Type: schema.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + Optional: true, + WriteOnly: true, + Sensitive: true, + RequiredWith: []string{"string_value_wo_version"}, + ExactlyOneOf: []string{"string_value", "string_value_wo"}, + }, + "string_value_wo_version": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + RequiredWith: []string{"string_value_wo"}, }, "scope": { Type: schema.TypeString, @@ -81,6 +108,11 @@ func ResourceSecret() common.Resource { } var putSecretReq workspace.PutSecret common.DataToStructPointer(d, s, &putSecretReq) + stringValue, err := getStringValue(d) + if err != nil { + return err + } + putSecretReq.StringValue = stringValue err = w.Secrets.PutSecret(ctx, putSecretReq) if err != nil { return err diff --git a/secrets/resource_secret_test.go b/secrets/resource_secret_test.go index 083d72bf16..7b36eadbd1 100644 --- a/secrets/resource_secret_test.go +++ b/secrets/resource_secret_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/databricks/databricks-sdk-go/apierr" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/databricks/databricks-sdk-go/service/workspace" @@ -121,6 +122,95 @@ func TestResourceSecretCreate(t *testing.T) { assert.Equal(t, "foo|||bar", d.Id()) } +func TestResourceSecretCreate_WriteOnlyValue(t *testing.T) { + d, err := qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/secrets/put", + ExpectedRequest: workspace.PutSecret{ + StringValue: "SparkIsTh3Be$t", + Scope: "foo", + Key: "bar", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/secrets/list?scope=foo", + Response: workspace.ListSecretsResponse{ + Secrets: []workspace.SecretMetadata{ + { + Key: "bar", + LastUpdatedTimestamp: 12345678, + }, + }, + }, + }, + }, + Resource: ResourceSecret(), + State: map[string]any{ + "scope": "foo", + "key": "bar", + "string_value_wo": "SparkIsTh3Be$t", + "string_value_wo_version": 1, + }, + Create: true, + }.Apply(t) + assert.NoError(t, err) + assert.Equal(t, "foo|||bar", d.Id()) + assert.Equal(t, "", d.Get("string_value")) +} + +func TestResourceSecretUpdate_WriteOnlyValueVersionChange(t *testing.T) { + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: "POST", + Resource: "/api/2.0/secrets/put", + ExpectedRequest: workspace.PutSecret{ + StringValue: "SparkIsTh3Be$t-v2", + Scope: "foo", + Key: "bar", + }, + }, + { + Method: "GET", + Resource: "/api/2.0/secrets/list?scope=foo", + Response: workspace.ListSecretsResponse{ + Secrets: []workspace.SecretMetadata{ + { + Key: "bar", + LastUpdatedTimestamp: 12345679, + }, + }, + }, + }, + }, + Resource: ResourceSecret(), + InstanceState: map[string]string{ + "scope": "foo", + "key": "bar", + "string_value_wo_version": "1", + }, + State: map[string]any{ + "scope": "foo", + "key": "bar", + "string_value_wo": "SparkIsTh3Be$t-v2", + "string_value_wo_version": 2, + }, + ExpectedDiff: map[string]*terraform.ResourceAttrDiff{ + "config_reference": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "key": {Old: "bar", New: "bar", NewComputed: false, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "last_updated_timestamp": {Old: "", New: "", NewComputed: true, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "scope": {Old: "foo", New: "foo", NewComputed: false, NewRemoved: false, RequiresNew: false, Sensitive: false}, + "string_value_wo": {Old: "", New: "SparkIsTh3Be$t-v2", NewComputed: false, NewRemoved: false, RequiresNew: false, Sensitive: true}, + "string_value_wo_version": {Old: "1", New: "2", NewComputed: false, NewRemoved: false, RequiresNew: true, Sensitive: false}, + }, + RequiresNew: true, + ID: "foo|||bar", + }.Apply(t) +} + func TestResourceSecretCreate_Error(t *testing.T) { d, err := qa.ResourceFixture{ Fixtures: []qa.HTTPFixture{