Skip to content

Commit a7cb18a

Browse files
committed
[Feature] Add databricks_secret write-only attributes
1 parent 9f554cc commit a7cb18a

File tree

6 files changed

+236
-18
lines changed

6 files changed

+236
-18
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### New Features and Improvements
88
* Add resource and data sources for `databricks_environments_workspace_base_environment`.
99
* Add resource and data source for `databricks_environments_default_workspace_base_environment`.
10+
* Add `string_value_wo` and `string_value_wo_version` attributes to `databricks_secret` resource ([#5480](https://github.com/databricks/terraform-provider-databricks/pull/5480))
1011

1112
* Added optional `cloud` argument to `databricks_current_config` data source to explicitly set the cloud type (`aws`, `azure`, `gcp`) instead of relying on host-based detection.
1213

docs/resources/secret.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ With this resource you can insert a secret under the provided scope with the giv
77

88
-> This resource can only be used with a workspace-level provider!
99

10+
-> **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).
11+
1012
## Example Usage
1113

1214
```hcl
@@ -33,7 +35,9 @@ resource "databricks_cluster" "this" {
3335

3436
The following arguments are required:
3537

36-
* `string_value` - (Required) (String) super secret sensitive value.
38+
* `string_value` - (Optional) (String) Specifies text data that you want to encrypt and store in the secret. This is required if `string_value_wo` is not set.
39+
* `string_value_wo` (Optional) (String) Specifies text data that you want to encrypt and store in the secret. This is required if `string_value` is not set.
40+
* `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.
3741
* `scope` - (Required) (String) name of databricks secret scope. Must consist of alphanumeric characters, dashes, underscores, and periods, and may not exceed 128 characters.
3842
* `key` - (Required) (String) key within secret scope. Must consist of alphanumeric characters, dashes, underscores, and periods, and may not exceed 128 characters.
3943
* `provider_config` - (Optional) Configure the provider for management through account provider. This block consists of the following fields:

qa/cty.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package qa
2+
3+
import (
4+
"github.com/hashicorp/go-cty/cty"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
8+
)
9+
10+
// https://github.com/hashicorp/terraform-plugin-sdk/blob/866d0b19a878fe2241fa8e008bee8c6cb8b2c32b/internal/configs/hcl2shim/values.go#L130-L194
11+
func hcl2ValueFromConfigValue(v interface{}) cty.Value {
12+
if v == nil {
13+
return cty.NullVal(cty.DynamicPseudoType)
14+
}
15+
16+
switch tv := v.(type) {
17+
case bool:
18+
return cty.BoolVal(tv)
19+
case string:
20+
return cty.StringVal(tv)
21+
case int:
22+
return cty.NumberIntVal(int64(tv))
23+
case float64:
24+
return cty.NumberFloatVal(tv)
25+
case []interface{}:
26+
vals := make([]cty.Value, len(tv))
27+
for i, ev := range tv {
28+
vals[i] = hcl2ValueFromConfigValue(ev)
29+
}
30+
return cty.TupleVal(vals)
31+
case map[string]interface{}:
32+
vals := map[string]cty.Value{}
33+
for k, ev := range tv {
34+
vals[k] = hcl2ValueFromConfigValue(ev)
35+
}
36+
return cty.ObjectVal(vals)
37+
default:
38+
return cty.NullVal(cty.DynamicPseudoType)
39+
}
40+
}
41+
42+
func makeResourceRawConfig(config *terraform.ResourceConfig, resource *schema.Resource) cty.Value {
43+
original := hcl2ValueFromConfigValue(config.Raw)
44+
coerced, err := resource.CoreConfigSchema().CoerceValue(original)
45+
if err != nil {
46+
return cty.NullVal(cty.DynamicPseudoType)
47+
}
48+
return coerced
49+
}

qa/testing.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ func (f ResourceFixture) Apply(t *testing.T) (*schema.ResourceData, error) {
324324
if err != nil {
325325
return nil, err
326326
}
327+
// Populate diff.RawConfig to allow d.GetRawConfigAt() works in tests
328+
// In actual runs terraform's gRPC pipeline will populate this
329+
if diff != nil {
330+
diff.RawConfig = makeResourceRawConfig(resourceConfig, resource)
331+
}
327332
if f.Update {
328333
err = f.requiresNew(diff)
329334
if err != nil {

secrets/resource_secret.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/databricks/databricks-sdk-go/apierr"
99
"github.com/databricks/databricks-sdk-go/service/workspace"
1010
"github.com/databricks/terraform-provider-databricks/common"
11-
11+
"github.com/hashicorp/go-cty/cty"
1212
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1414
)
@@ -35,16 +35,41 @@ func readSecret(ctx context.Context, w *databricks.WorkspaceClient, scope string
3535
}
3636
}
3737

38+
func getStringValue(d *schema.ResourceData) (string, error) {
39+
woValue, diags := d.GetRawConfigAt(cty.GetAttrPath("string_value_wo"))
40+
if !diags.HasError() && woValue.Type().Equals(cty.String) && !woValue.IsNull() {
41+
return woValue.AsString(), nil
42+
}
43+
if v, ok := d.GetOk("string_value"); ok {
44+
return v.(string), nil
45+
}
46+
return "", fmt.Errorf("failed to get one of attributes `string_value_wo` or `string_value`")
47+
}
48+
3849
// ResourceSecret manages secrets
3950
func ResourceSecret() common.Resource {
4051
p := common.NewPairSeparatedID("scope", "key", "|||")
4152
s := map[string]*schema.Schema{
4253
"string_value": {
4354
Type: schema.TypeString,
4455
ValidateFunc: validation.StringIsNotEmpty,
45-
Required: true,
46-
ForceNew: true,
56+
Optional: true,
4757
Sensitive: true,
58+
ExactlyOneOf: []string{"string_value", "string_value_wo"},
59+
},
60+
"string_value_wo": {
61+
Type: schema.TypeString,
62+
ValidateFunc: validation.StringIsNotEmpty,
63+
Optional: true,
64+
WriteOnly: true,
65+
Sensitive: true,
66+
RequiredWith: []string{"string_value_wo_version"},
67+
ExactlyOneOf: []string{"string_value", "string_value_wo"},
68+
},
69+
"string_value_wo_version": {
70+
Type: schema.TypeInt,
71+
Optional: true,
72+
RequiredWith: []string{"string_value_wo"},
4873
},
4974
"scope": {
5075
Type: schema.TypeString,
@@ -69,25 +94,32 @@ func ResourceSecret() common.Resource {
6994
}
7095
common.AddNamespaceInSchema(s)
7196
common.NamespaceCustomizeSchemaMap(s)
97+
upsertSecret := func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
98+
w, err := c.WorkspaceClientUnifiedProvider(ctx, d)
99+
if err != nil {
100+
return err
101+
}
102+
var putSecretReq workspace.PutSecret
103+
common.DataToStructPointer(d, s, &putSecretReq)
104+
stringValue, err := getStringValue(d)
105+
if err != nil {
106+
return err
107+
}
108+
putSecretReq.StringValue = stringValue
109+
err = w.Secrets.PutSecret(ctx, putSecretReq)
110+
if err != nil {
111+
return err
112+
}
113+
p.Pack(d)
114+
return nil
115+
}
72116
return common.Resource{
73117
Schema: s,
74118
CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, c *common.DatabricksClient) error {
75119
return common.NamespaceCustomizeDiff(ctx, d, c)
76120
},
77-
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
78-
w, err := c.WorkspaceClientUnifiedProvider(ctx, d)
79-
if err != nil {
80-
return err
81-
}
82-
var putSecretReq workspace.PutSecret
83-
common.DataToStructPointer(d, s, &putSecretReq)
84-
err = w.Secrets.PutSecret(ctx, putSecretReq)
85-
if err != nil {
86-
return err
87-
}
88-
p.Pack(d)
89-
return nil
90-
},
121+
Create: upsertSecret,
122+
Update: upsertSecret,
91123
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
92124
scope, key, err := p.Unpack(d)
93125
if err != nil {

secrets/resource_secret_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,133 @@ func TestResourceSecretCreate(t *testing.T) {
121121
assert.Equal(t, "foo|||bar", d.Id())
122122
}
123123

124+
func TestResourceSecretUpdate(t *testing.T) {
125+
d, err := qa.ResourceFixture{
126+
Fixtures: []qa.HTTPFixture{
127+
{
128+
Method: "POST",
129+
Resource: "/api/2.0/secrets/put",
130+
ExpectedRequest: workspace.PutSecret{
131+
StringValue: "SparkIsTh3Be$t-v2",
132+
Scope: "foo",
133+
Key: "bar",
134+
},
135+
},
136+
{
137+
Method: "GET",
138+
Resource: "/api/2.0/secrets/list?scope=foo",
139+
Response: workspace.ListSecretsResponse{
140+
Secrets: []workspace.SecretMetadata{
141+
{
142+
Key: "bar",
143+
LastUpdatedTimestamp: 12345678,
144+
},
145+
},
146+
},
147+
},
148+
},
149+
Resource: ResourceSecret(),
150+
InstanceState: map[string]string{
151+
"scope": "foo",
152+
"key": "bar",
153+
"string_value_wo_version": "1",
154+
},
155+
State: map[string]any{
156+
"scope": "foo",
157+
"key": "bar",
158+
"string_value": "SparkIsTh3Be$t-v2",
159+
},
160+
Update: true,
161+
ID: "foo|||bar",
162+
}.Apply(t)
163+
assert.NoError(t, err)
164+
assert.Equal(t, "foo|||bar", d.Id())
165+
}
166+
167+
func TestResourceSecretCreate_WriteOnlyValue(t *testing.T) {
168+
d, err := qa.ResourceFixture{
169+
Fixtures: []qa.HTTPFixture{
170+
{
171+
Method: "POST",
172+
Resource: "/api/2.0/secrets/put",
173+
ExpectedRequest: workspace.PutSecret{
174+
StringValue: "SparkIsTh3Be$t",
175+
Scope: "foo",
176+
Key: "bar",
177+
},
178+
},
179+
{
180+
Method: "GET",
181+
Resource: "/api/2.0/secrets/list?scope=foo",
182+
Response: workspace.ListSecretsResponse{
183+
Secrets: []workspace.SecretMetadata{
184+
{
185+
Key: "bar",
186+
LastUpdatedTimestamp: 12345678,
187+
},
188+
},
189+
},
190+
},
191+
},
192+
Resource: ResourceSecret(),
193+
State: map[string]any{
194+
"scope": "foo",
195+
"key": "bar",
196+
"string_value_wo": "SparkIsTh3Be$t",
197+
"string_value_wo_version": 1,
198+
},
199+
Create: true,
200+
}.Apply(t)
201+
assert.NoError(t, err)
202+
assert.Equal(t, "foo|||bar", d.Id())
203+
assert.Equal(t, "", d.Get("string_value"))
204+
}
205+
206+
func TestResourceSecretUpdate_WriteOnlyValueVersionChange(t *testing.T) {
207+
d, err := qa.ResourceFixture{
208+
Fixtures: []qa.HTTPFixture{
209+
{
210+
Method: "POST",
211+
Resource: "/api/2.0/secrets/put",
212+
ExpectedRequest: workspace.PutSecret{
213+
StringValue: "SparkIsTh3Be$t-v2",
214+
Scope: "foo",
215+
Key: "bar",
216+
},
217+
},
218+
{
219+
Method: "GET",
220+
Resource: "/api/2.0/secrets/list?scope=foo",
221+
Response: workspace.ListSecretsResponse{
222+
Secrets: []workspace.SecretMetadata{
223+
{
224+
Key: "bar",
225+
LastUpdatedTimestamp: 12345679,
226+
},
227+
},
228+
},
229+
},
230+
},
231+
Resource: ResourceSecret(),
232+
InstanceState: map[string]string{
233+
"scope": "foo",
234+
"key": "bar",
235+
"string_value_wo_version": "1",
236+
},
237+
State: map[string]any{
238+
"scope": "foo",
239+
"key": "bar",
240+
"string_value_wo": "SparkIsTh3Be$t-v2",
241+
"string_value_wo_version": 2,
242+
},
243+
Update: true,
244+
ID: "foo|||bar",
245+
}.Apply(t)
246+
assert.NoError(t, err)
247+
assert.Equal(t, "foo|||bar", d.Id())
248+
assert.Equal(t, 2, d.Get("string_value_wo_version"))
249+
}
250+
124251
func TestResourceSecretCreate_Error(t *testing.T) {
125252
d, err := qa.ResourceFixture{
126253
Fixtures: []qa.HTTPFixture{

0 commit comments

Comments
 (0)