Skip to content

Commit 8c096f5

Browse files
committed
Add support for nested schema packing/unpacking
1 parent 0a3fa18 commit 8c096f5

File tree

1 file changed

+109
-27
lines changed

1 file changed

+109
-27
lines changed

internal/resources/grafana/resource_alerting_contact_point.go

Lines changed: 109 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ func valueAsString(value any) any {
442442
// - Flattening the "settings" field created by TF when unpacking the resource schema. This contains any unknown fields
443443
// not present in the resource schema.
444444
func unpackNotifier(tfSettings map[string]any, name string, n notifier) *models.EmbeddedContactPoint {
445-
gfSettings := unpackFields(tfSettings, n.schema().Schema, n.meta().fieldMapper)
445+
gfSettings := unpackFields(tfSettings, "", n.schema().Schema, n.meta().fieldMapper)
446446

447447
// UID, disable_resolve_message, and leftover "settings" are part of the schema so are currently unpacked into gfSettings.
448448
// However, they are not part of the settings schema in Grafana, so we extract them.
@@ -475,34 +475,59 @@ func unpackNotifier(tfSettings map[string]any, name string, n notifier) *models.
475475
}
476476
}
477477

478-
func unpackFields(tfSettings map[string]any, schemas map[string]*schema.Schema, fieldMapping map[string]fieldMapper) map[string]any {
478+
// unpackFields is the recursive counterpart to unpackNotifier.
479+
func unpackFields(tfSettings map[string]any, prefix string, schemas map[string]*schema.Schema, fieldMapping map[string]fieldMapper) map[string]any {
479480
gfSettings := make(map[string]any, len(schemas))
480-
for tfKey := range schemas {
481+
for tfKey, sch := range schemas {
482+
fullTfKey := tfKey
483+
if prefix != "" {
484+
fullTfKey = fmt.Sprintf("%s.%s", prefix, tfKey)
485+
}
486+
481487
val, ok := tfSettings[tfKey]
482488
if !ok {
483489
continue // Skip if the key is not present in the resource map
484490
}
485491

486492
gfKey := tfKey
487-
fMap := fieldMapping[tfKey]
488493
// Apply key mapping to get the grafana-style key if defined.
489-
if fMap.newKey != "" {
494+
if fMap := fieldMapping[fullTfKey]; fMap.newKey != "" {
490495
gfKey = fMap.newKey
491496
}
492497

493-
// Apply the transformation function if provided.
494-
if fMap.unpackValFunc != nil {
495-
val = fMap.unpackValFunc(val)
496-
}
497-
498-
if val != nil {
498+
if unpackedVal := unpackedValue(val, fullTfKey, sch, fieldMapping); unpackedVal != nil {
499499
// Omit nil values, this is usually from a custom transform function or an empty set.
500-
gfSettings[gfKey] = val
500+
gfSettings[gfKey] = unpackedVal
501501
}
502502
}
503503
return gfSettings
504504
}
505505

506+
// unpackedValue recursively returns the appropriate Grafana representation of the TF field value based on the schema.
507+
func unpackedValue(val any, tfKey string, sch *schema.Schema, fieldMapping map[string]fieldMapper) any {
508+
// Apply the transformation function if provided
509+
if fMap := fieldMapping[tfKey]; fMap.unpackValFunc != nil {
510+
val = fMap.unpackValFunc(val)
511+
}
512+
513+
switch sch.Type {
514+
case schema.TypeSet:
515+
// This is a nested schema type, so we coerce the nested value into a map to continue the recursion.
516+
valAsMap, err := extractMapFromSet(val)
517+
if err != nil {
518+
log.Printf("[WARN] cannot extract map from set for key '%s': %v", tfKey, err)
519+
return val
520+
}
521+
if len(valAsMap) == 0 {
522+
return nil // omit empty sets
523+
}
524+
525+
return unpackFields(valAsMap, tfKey, sch.Elem.(*schema.Resource).Schema, fieldMapping)
526+
default:
527+
return val
528+
}
529+
}
530+
506531
// packNotifier takes the grafana-style settings and packs them into the Terraform-style settings. It handles:
507532
// - Applying any transformation functions defined in fieldMapping to the keys and values in gfSettings. This is necessary
508533
// because some field names differ between Terraform and Grafana, and some values need to be transformed (e.g., converting a string to an integer).
@@ -511,7 +536,7 @@ func unpackFields(tfSettings map[string]any, schemas map[string]*schema.Schema,
511536
// - Collecting all remaining fields from the Grafana settings that are not in the resource schema into a "settings" field.
512537
func packNotifier(p *models.EmbeddedContactPoint, data *schema.ResourceData, n notifier) map[string]any {
513538
gfSettings := p.Settings.(map[string]any)
514-
tfSettings := packFields(gfSettings, getNotifierConfigFromStateWithUID(data, n, p.UID), n.schema().Schema, n.meta().fieldMapper)
539+
tfSettings := packFields(gfSettings, getNotifierConfigFromStateWithUID(data, n, p.UID), "", n.schema().Schema, n.meta().fieldMapper)
515540

516541
// Add common fields to the Terraform settings as these aren't available in EmbeddedContactPoint settings.
517542
for k, v := range packCommonNotifierFields(p) {
@@ -528,13 +553,18 @@ func packNotifier(p *models.EmbeddedContactPoint, data *schema.ResourceData, n n
528553
return tfSettings
529554
}
530555

531-
func packFields(gfSettings, state map[string]any, schemas map[string]*schema.Schema, fieldMapping map[string]fieldMapper) map[string]any {
556+
// packFields is the recursive counterpart to packNotifier.
557+
func packFields(gfSettings, state map[string]any, prefix string, schemas map[string]*schema.Schema, fieldMapping map[string]fieldMapper) map[string]any {
532558
settings := make(map[string]any, len(schemas))
533559
for tfKey, sch := range schemas {
560+
fullTfKey := tfKey
561+
if prefix != "" {
562+
fullTfKey = fmt.Sprintf("%s.%s", prefix, tfKey)
563+
}
564+
534565
gfKey := tfKey
535-
fMap := fieldMapping[tfKey]
536566
// Apply key mapping to get the grafana-style key if defined.
537-
if fMap.newKey != "" {
567+
if fMap := fieldMapping[fullTfKey]; fMap.newKey != "" {
538568
gfKey = fMap.newKey
539569
}
540570

@@ -543,23 +573,75 @@ func packFields(gfSettings, state map[string]any, schemas map[string]*schema.Sch
543573
continue // Skip if the key is not present in the grafana settings
544574
}
545575

546-
// Use the state value for sensitive fields as the API returns [REDACTED].
547-
if sch.Sensitive {
548-
val = state[tfKey]
576+
packedVal, remove := packedValue(val, state[tfKey], fullTfKey, sch, fieldMapping)
577+
if packedVal != nil {
578+
// Omit nil values.
579+
settings[tfKey] = packedVal
549580
}
581+
if remove {
582+
delete(gfSettings, gfKey) // Remove the key from the original map to avoid including it in leftover "settings"
583+
}
584+
}
585+
return settings
586+
}
587+
588+
// packedValue recursively returns the appropriate TF representation of the Grafana field value based on the schema.
589+
func packedValue(val any, stateVal any, tfKey string, sch *schema.Schema, fieldMapping map[string]fieldMapper) (any, bool) {
590+
// Use the state value for sensitive fields as the API returns [REDACTED].
591+
if sch.Sensitive {
592+
// Values in state and already correctly packed, so no need to continue the recursion.
593+
return stateVal, true
594+
}
595+
596+
// Apply the transformation function if provided
597+
if fMap := fieldMapping[tfKey]; fMap.packValFunc != nil {
598+
val = fMap.packValFunc(val)
599+
}
550600

551-
// Apply the transformation function if provided
552-
if fMap.packValFunc != nil {
553-
val = fMap.packValFunc(val)
601+
switch sch.Type {
602+
case schema.TypeSet:
603+
// This is a nested schema type, so we coerce the nested value and state into maps to continue the recursion.
604+
valAsMap, ok := val.(map[string]any)
605+
if !ok {
606+
log.Printf("[WARN] Unsupported value type '%s' for key '%s'", sch.Type.String(), tfKey)
607+
return val, true
554608
}
555609

556-
if val != nil {
557-
// Omit nil values.
558-
settings[tfKey] = val
610+
// For nested schemas, the state value should be a schema.Set with MaxItems=1.
611+
stateValAsMap, err := extractMapFromSet(stateVal)
612+
if err != nil {
613+
log.Printf("[WARN] cannot extract map from set for key '%s': %v", tfKey, err)
559614
}
560-
delete(gfSettings, gfKey) // Remove the key from the original map to avoid including it in leftover "settings"
615+
616+
// We use TypeSet with MaxItems=1 to represent nested schemas (map[string]any), but they are technically slices
617+
// and need to be packed as such.
618+
return []any{packFields(valAsMap, stateValAsMap, tfKey, sch.Elem.(*schema.Resource).Schema, fieldMapping)}, len(valAsMap) == 0
619+
default:
620+
return val, true
561621
}
562-
return settings
622+
}
623+
624+
// extractMapFromSet extracts the first item from a schema.Set and returns it as a map[string]any.
625+
// We use TypeSet with MaxItems=1 to represent nested schemas (map[string]any), but they are technically slices in TF.
626+
func extractMapFromSet(val any) (map[string]any, error) {
627+
set, ok := val.(*schema.Set)
628+
if !ok {
629+
return map[string]any{}, fmt.Errorf("unsupported value: %q", val)
630+
}
631+
632+
items := set.List()
633+
if len(items) == 0 {
634+
return map[string]any{}, nil // empty set
635+
}
636+
if len(items) > 1 {
637+
return map[string]any{}, fmt.Errorf("set contains more than one item: %q", items)
638+
}
639+
// Use the first item in the set as the child map
640+
m, ok := items[0].(map[string]any)
641+
if !ok {
642+
return map[string]any{}, fmt.Errorf("unsupported value in set: %q", items[0])
643+
}
644+
return m, nil
563645
}
564646

565647
type notifier interface {

0 commit comments

Comments
 (0)