@@ -442,7 +442,7 @@ func valueAsString(value any) any {
442
442
// - Flattening the "settings" field created by TF when unpacking the resource schema. This contains any unknown fields
443
443
// not present in the resource schema.
444
444
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 )
446
446
447
447
// UID, disable_resolve_message, and leftover "settings" are part of the schema so are currently unpacked into gfSettings.
448
448
// 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.
475
475
}
476
476
}
477
477
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 {
479
480
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
+
481
487
val , ok := tfSettings [tfKey ]
482
488
if ! ok {
483
489
continue // Skip if the key is not present in the resource map
484
490
}
485
491
486
492
gfKey := tfKey
487
- fMap := fieldMapping [tfKey ]
488
493
// Apply key mapping to get the grafana-style key if defined.
489
- if fMap .newKey != "" {
494
+ if fMap := fieldMapping [ fullTfKey ]; fMap .newKey != "" {
490
495
gfKey = fMap .newKey
491
496
}
492
497
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 {
499
499
// Omit nil values, this is usually from a custom transform function or an empty set.
500
- gfSettings [gfKey ] = val
500
+ gfSettings [gfKey ] = unpackedVal
501
501
}
502
502
}
503
503
return gfSettings
504
504
}
505
505
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
+
506
531
// packNotifier takes the grafana-style settings and packs them into the Terraform-style settings. It handles:
507
532
// - Applying any transformation functions defined in fieldMapping to the keys and values in gfSettings. This is necessary
508
533
// 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,
511
536
// - Collecting all remaining fields from the Grafana settings that are not in the resource schema into a "settings" field.
512
537
func packNotifier (p * models.EmbeddedContactPoint , data * schema.ResourceData , n notifier ) map [string ]any {
513
538
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 )
515
540
516
541
// Add common fields to the Terraform settings as these aren't available in EmbeddedContactPoint settings.
517
542
for k , v := range packCommonNotifierFields (p ) {
@@ -528,13 +553,18 @@ func packNotifier(p *models.EmbeddedContactPoint, data *schema.ResourceData, n n
528
553
return tfSettings
529
554
}
530
555
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 {
532
558
settings := make (map [string ]any , len (schemas ))
533
559
for tfKey , sch := range schemas {
560
+ fullTfKey := tfKey
561
+ if prefix != "" {
562
+ fullTfKey = fmt .Sprintf ("%s.%s" , prefix , tfKey )
563
+ }
564
+
534
565
gfKey := tfKey
535
- fMap := fieldMapping [tfKey ]
536
566
// Apply key mapping to get the grafana-style key if defined.
537
- if fMap .newKey != "" {
567
+ if fMap := fieldMapping [ fullTfKey ]; fMap .newKey != "" {
538
568
gfKey = fMap .newKey
539
569
}
540
570
@@ -543,23 +573,75 @@ func packFields(gfSettings, state map[string]any, schemas map[string]*schema.Sch
543
573
continue // Skip if the key is not present in the grafana settings
544
574
}
545
575
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
549
580
}
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
+ }
550
600
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
554
608
}
555
609
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 )
559
614
}
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
561
621
}
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
563
645
}
564
646
565
647
type notifier interface {
0 commit comments