Skip to content

Commit 8b42042

Browse files
committed
feat: extend custom field support to text, multiline, secret, url, and email types
Previously, custom fields only supported "text" type. This commit extends support to include multiline, secret, url, and email types - all of which share the same structure ([]string value) in the Go SDK. Changes: - Updated Create functions across all 22 resources to handle 5 field types - Updated Update functions to properly sync all 5 types to vault - Added convertFieldToMap helper to record_fields.go for Update operations - Added TestAccResourceLogin_customFieldTypes to validate all types - All existing custom field tests continue to pass All 5 types work correctly for Create, Read, Update operations.
1 parent e235a08 commit 8b42042

28 files changed

+3004
-479
lines changed

secretsmanager/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ var mapSchemaToRecordFieldName map[string]string = map[string]string{
189189
"activation_date": "date", // softwareLicense
190190
"passphrase": "password", // sshKeys
191191
"identity_number": "accountNumber", // ssnCard
192+
"custom": "custom", // custom fields
192193
}
193194

194195
/*

secretsmanager/record_fields.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,3 +1460,26 @@ func schemaCustomField() *schema.Schema {
14601460
},
14611461
}
14621462
}
1463+
1464+
// convertFieldToMap converts a custom field to a map for RecordDict storage
1465+
func convertFieldToMap(fieldType, label string, required, privacyScreen bool, value []string) map[string]interface{} {
1466+
fieldMap := map[string]interface{}{"type": fieldType}
1467+
if label != "" {
1468+
fieldMap["label"] = label
1469+
}
1470+
if required {
1471+
fieldMap["required"] = required
1472+
}
1473+
if privacyScreen {
1474+
fieldMap["privacyScreen"] = privacyScreen
1475+
}
1476+
if len(value) > 0 {
1477+
// Convert []string to []interface{}
1478+
values := make([]interface{}, len(value))
1479+
for i, v := range value {
1480+
values[i] = v
1481+
}
1482+
fieldMap["value"] = values
1483+
}
1484+
return fieldMap
1485+
}

secretsmanager/resource_address.go

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -116,25 +116,52 @@ func resourceAddressCreate(ctx context.Context, d *schema.ResourceData, m interf
116116
fieldType = ft
117117
}
118118

119-
// For now, support text fields as most common custom field type
120-
if fieldType == "text" {
121-
field := &core.Text{
122-
KeeperRecordField: core.KeeperRecordField{
123-
Type: "text",
124-
},
119+
// Support text, multiline, secret, url, and email field types
120+
switch fieldType {
121+
case "text", "multiline", "secret", "url", "email":
122+
var field interface{}
123+
switch fieldType {
124+
case "text":
125+
field = &core.Text{KeeperRecordField: core.KeeperRecordField{Type: "text"}}
126+
case "multiline":
127+
field = &core.Multiline{KeeperRecordField: core.KeeperRecordField{Type: "multiline"}}
128+
case "secret":
129+
field = &core.Secret{KeeperRecordField: core.KeeperRecordField{Type: "secret"}}
130+
case "url":
131+
field = &core.Url{KeeperRecordField: core.KeeperRecordField{Type: "url"}}
132+
case "email":
133+
field = &core.Email{KeeperRecordField: core.KeeperRecordField{Type: "email"}}
125134
}
126-
if label, ok := customMap["label"].(string); ok {
127-
field.Label = label
128-
}
129-
if required, ok := customMap["required"].(bool); ok {
130-
field.Required = required
131-
}
132-
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok {
133-
field.PrivacyScreen = privacyScreen
134-
}
135-
if value, ok := customMap["value"].(string); ok && value != "" {
136-
field.Value = []string{value}
135+
136+
// Set common properties using type assertion
137+
switch f := field.(type) {
138+
case *core.Text:
139+
if label, ok := customMap["label"].(string); ok { f.Label = label }
140+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
141+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
142+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
143+
case *core.Multiline:
144+
if label, ok := customMap["label"].(string); ok { f.Label = label }
145+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
146+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
147+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
148+
case *core.Secret:
149+
if label, ok := customMap["label"].(string); ok { f.Label = label }
150+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
151+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
152+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
153+
case *core.Url:
154+
if label, ok := customMap["label"].(string); ok { f.Label = label }
155+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
156+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
157+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
158+
case *core.Email:
159+
if label, ok := customMap["label"].(string); ok { f.Label = label }
160+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
161+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
162+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
137163
}
164+
138165
nrc.Custom = append(nrc.Custom, field)
139166
}
140167
}
@@ -284,9 +311,73 @@ func resourceAddressUpdate(ctx context.Context, d *schema.ResourceData, m interf
284311
}
285312

286313
if d.HasChange("custom") {
287-
if _, err := ApplyFieldChange("custom", "custom", d, secret); err != nil {
288-
return diag.FromErr(err)
314+
// Clear existing custom fields
315+
customFields := []interface{}{}
316+
// Add updated custom fields
317+
if customData := d.Get("custom"); customData != nil && len(customData.([]interface{})) > 0 {
318+
for _, customItem := range customData.([]interface{}) {
319+
if customMap, ok := customItem.(map[string]interface{}); ok {
320+
fieldType := "text"
321+
if ft, ok := customMap["type"].(string); ok && ft != "" {
322+
fieldType = ft
323+
}
324+
switch fieldType {
325+
case "text", "multiline", "secret", "url", "email":
326+
var field interface{}
327+
switch fieldType {
328+
case "text":
329+
field = &core.Text{KeeperRecordField: core.KeeperRecordField{Type: "text"}}
330+
case "multiline":
331+
field = &core.Multiline{KeeperRecordField: core.KeeperRecordField{Type: "multiline"}}
332+
case "secret":
333+
field = &core.Secret{KeeperRecordField: core.KeeperRecordField{Type: "secret"}}
334+
case "url":
335+
field = &core.Url{KeeperRecordField: core.KeeperRecordField{Type: "url"}}
336+
case "email":
337+
field = &core.Email{KeeperRecordField: core.KeeperRecordField{Type: "email"}}
338+
}
339+
340+
// Set common properties using type assertion
341+
var fieldMap map[string]interface{}
342+
switch f := field.(type) {
343+
case *core.Text:
344+
if label, ok := customMap["label"].(string); ok { f.Label = label }
345+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
346+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
347+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
348+
fieldMap = convertFieldToMap(f.Type, f.Label, f.Required, f.PrivacyScreen, f.Value)
349+
case *core.Multiline:
350+
if label, ok := customMap["label"].(string); ok { f.Label = label }
351+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
352+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
353+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
354+
fieldMap = convertFieldToMap(f.Type, f.Label, f.Required, f.PrivacyScreen, f.Value)
355+
case *core.Secret:
356+
if label, ok := customMap["label"].(string); ok { f.Label = label }
357+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
358+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
359+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
360+
fieldMap = convertFieldToMap(f.Type, f.Label, f.Required, f.PrivacyScreen, f.Value)
361+
case *core.Url:
362+
if label, ok := customMap["label"].(string); ok { f.Label = label }
363+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
364+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
365+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
366+
fieldMap = convertFieldToMap(f.Type, f.Label, f.Required, f.PrivacyScreen, f.Value)
367+
case *core.Email:
368+
if label, ok := customMap["label"].(string); ok { f.Label = label }
369+
if required, ok := customMap["required"].(bool); ok { f.Required = required }
370+
if privacyScreen, ok := customMap["privacy_screen"].(bool); ok { f.PrivacyScreen = privacyScreen }
371+
if value, ok := customMap["value"].(string); ok && value != "" { f.Value = []string{value} }
372+
fieldMap = convertFieldToMap(f.Type, f.Label, f.Required, f.PrivacyScreen, f.Value)
373+
}
374+
375+
customFields = append(customFields, fieldMap)
376+
}
377+
}
378+
}
289379
}
380+
secret.RecordDict["custom"] = customFields
290381
}
291382

292383
secret.RawJson = core.DictToJson(secret.RecordDict)

secretsmanager/resource_address_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,115 @@ func TestAccResourceAddress_import(t *testing.T) {
200200
},
201201
})
202202
}
203+
204+
// TestAccResourceAddress_customFields tests custom field support for address resources.
205+
// This test is representative of all resources with complex address/name field structures, including:
206+
// - address (this resource)
207+
//
208+
// Custom field functionality is identical across all resource types - this test validates
209+
// the pattern works correctly with address-specific nested field structures.
210+
func TestAccResourceAddress_customFields(t *testing.T) {
211+
secretType := "address"
212+
secretFolderUid := testAcc.getTestFolder()
213+
secretUid := core.GenerateUid()
214+
_, secretTitle := testAcc.getRecordInfo(secretType)
215+
if secretUid == "" || secretTitle == "" {
216+
t.Fatal("Failed to access test data - missing secret UID and/or Title")
217+
}
218+
secretTitle += "_resource_custom_fields"
219+
220+
configCreate := fmt.Sprintf(`
221+
resource "secretsmanager_address" "%v" {
222+
folder_uid = "%v"
223+
uid = "%v"
224+
title = "%v"
225+
notes = "Test custom fields"
226+
address {
227+
value {
228+
street1 = "123 Main St"
229+
city = "San Francisco"
230+
state = "CA"
231+
zip = "94105"
232+
country = "US"
233+
}
234+
}
235+
custom {
236+
type = "text"
237+
label = "Building"
238+
value = "North Tower"
239+
}
240+
custom {
241+
type = "text"
242+
label = "Floor"
243+
value = "15"
244+
}
245+
}
246+
`, secretTitle, secretFolderUid, secretUid, secretTitle)
247+
248+
configUpdate := fmt.Sprintf(`
249+
resource "secretsmanager_address" "%v" {
250+
folder_uid = "%v"
251+
uid = "%v"
252+
title = "%v"
253+
notes = "Test custom fields updated"
254+
address {
255+
value {
256+
street1 = "123 Main St"
257+
city = "San Francisco"
258+
state = "CA"
259+
zip = "94105"
260+
country = "US"
261+
}
262+
}
263+
custom {
264+
type = "text"
265+
label = "Building"
266+
value = "South Tower"
267+
}
268+
custom {
269+
type = "text"
270+
label = "Floor"
271+
value = "20"
272+
}
273+
custom {
274+
type = "text"
275+
label = "Access Code"
276+
value = "1234"
277+
}
278+
}
279+
`, secretTitle, secretFolderUid, secretUid, secretTitle)
280+
281+
resourceName := fmt.Sprintf("secretsmanager_address.%v", secretTitle)
282+
resource.Test(t, resource.TestCase{
283+
Providers: testAccProviders,
284+
PreCheck: testAccPreCheck(t),
285+
Steps: []resource.TestStep{
286+
{
287+
Config: configCreate,
288+
Check: resource.ComposeTestCheckFunc(
289+
checkSecretExistsRemotely(secretUid),
290+
resource.TestCheckResourceAttr(resourceName, "type", secretType),
291+
resource.TestCheckResourceAttr(resourceName, "title", secretTitle),
292+
resource.TestCheckResourceAttr(resourceName, "custom.#", "2"),
293+
resource.TestCheckResourceAttr(resourceName, "custom.0.label", "Building"),
294+
resource.TestCheckResourceAttr(resourceName, "custom.0.value", "North Tower"),
295+
resource.TestCheckResourceAttr(resourceName, "custom.1.label", "Floor"),
296+
resource.TestCheckResourceAttr(resourceName, "custom.1.value", "15"),
297+
),
298+
},
299+
{
300+
Config: configUpdate,
301+
Check: resource.ComposeTestCheckFunc(
302+
checkSecretExistsRemotely(secretUid),
303+
resource.TestCheckResourceAttr(resourceName, "custom.#", "3"),
304+
resource.TestCheckResourceAttr(resourceName, "custom.0.label", "Building"),
305+
resource.TestCheckResourceAttr(resourceName, "custom.0.value", "South Tower"),
306+
resource.TestCheckResourceAttr(resourceName, "custom.1.label", "Floor"),
307+
resource.TestCheckResourceAttr(resourceName, "custom.1.value", "20"),
308+
resource.TestCheckResourceAttr(resourceName, "custom.2.label", "Access Code"),
309+
resource.TestCheckResourceAttr(resourceName, "custom.2.value", "1234"),
310+
),
311+
},
312+
},
313+
})
314+
}

0 commit comments

Comments
 (0)