github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/normalize_obj.go (about)

     1  package objchange
     2  
     3  import (
     4  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
     5  	"github.com/zclconf/go-cty/cty"
     6  )
     7  
     8  // NormalizeObjectFromLegacySDK takes an object that may have been generated
     9  // by the legacy Terraform SDK (i.e. returned from a provider with the
    10  // LegacyTypeSystem opt-out set) and does its best to normalize it for the
    11  // assumptions we would normally enforce if the provider had not opted out.
    12  //
    13  // In particular, this function guarantees that a value representing a nested
    14  // block will never itself be unknown or null, instead representing that as
    15  // a non-null value that may contain null/unknown values.
    16  //
    17  // The input value must still conform to the implied type of the given schema,
    18  // or else this function may produce garbage results or panic. This is usually
    19  // okay because type consistency is enforced when deserializing the value
    20  // returned from the provider over the RPC wire protocol anyway.
    21  func NormalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value {
    22  	if val == cty.NilVal || val.IsNull() {
    23  		// This should never happen in reasonable use, but we'll allow it
    24  		// and normalize to a null of the expected type rather than panicking
    25  		// below.
    26  		return cty.NullVal(schema.ImpliedType())
    27  	}
    28  
    29  	vals := make(map[string]cty.Value)
    30  	for name := range schema.Attributes {
    31  		// No normalization for attributes, since them being type-conformant
    32  		// is all that we require.
    33  		vals[name] = val.GetAttr(name)
    34  	}
    35  	for name, blockS := range schema.BlockTypes {
    36  		lv := val.GetAttr(name)
    37  
    38  		// Legacy SDK never generates dynamically-typed attributes and so our
    39  		// normalization code doesn't deal with them, but we need to make sure
    40  		// we still pass them through properly so that we don't interfere with
    41  		// objects generated by other SDKs.
    42  		if ty := blockS.Block.ImpliedType(); ty.HasDynamicTypes() {
    43  			vals[name] = lv
    44  			continue
    45  		}
    46  
    47  		switch blockS.Nesting {
    48  		case configschema.NestingSingle, configschema.NestingGroup:
    49  			if lv.IsKnown() {
    50  				if lv.IsNull() && blockS.Nesting == configschema.NestingGroup {
    51  					vals[name] = blockS.EmptyValue()
    52  				} else {
    53  					vals[name] = NormalizeObjectFromLegacySDK(lv, &blockS.Block)
    54  				}
    55  			} else {
    56  				vals[name] = unknownBlockStub(&blockS.Block)
    57  			}
    58  		case configschema.NestingList:
    59  			switch {
    60  			case !lv.IsKnown():
    61  				vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)})
    62  			case lv.IsNull() || lv.LengthInt() == 0:
    63  				vals[name] = cty.ListValEmpty(blockS.Block.ImpliedType())
    64  			default:
    65  				subVals := make([]cty.Value, 0, lv.LengthInt())
    66  				for it := lv.ElementIterator(); it.Next(); {
    67  					_, subVal := it.Element()
    68  					subVals = append(subVals, NormalizeObjectFromLegacySDK(subVal, &blockS.Block))
    69  				}
    70  				vals[name] = cty.ListVal(subVals)
    71  			}
    72  		case configschema.NestingSet:
    73  			switch {
    74  			case !lv.IsKnown():
    75  				vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)})
    76  			case lv.IsNull() || lv.LengthInt() == 0:
    77  				vals[name] = cty.SetValEmpty(blockS.Block.ImpliedType())
    78  			default:
    79  				subVals := make([]cty.Value, 0, lv.LengthInt())
    80  				for it := lv.ElementIterator(); it.Next(); {
    81  					_, subVal := it.Element()
    82  					subVals = append(subVals, NormalizeObjectFromLegacySDK(subVal, &blockS.Block))
    83  				}
    84  				vals[name] = cty.SetVal(subVals)
    85  			}
    86  		default:
    87  			// The legacy SDK doesn't support NestingMap, so we just assume
    88  			// maps are always okay. (If not, we would've detected and returned
    89  			// an error to the user before we got here.)
    90  			vals[name] = lv
    91  		}
    92  	}
    93  	return cty.ObjectVal(vals)
    94  }
    95  
    96  // unknownBlockStub constructs an object value that approximates an unknown
    97  // block by producing a known block object with all of its leaf attribute
    98  // values set to unknown.
    99  //
   100  // Blocks themselves cannot be unknown, so if the legacy SDK tries to return
   101  // such a thing, we'll use this result instead. This convention mimics how
   102  // the dynamic block feature deals with being asked to iterate over an unknown
   103  // value, because our value-checking functions already accept this convention
   104  // as a special case.
   105  func unknownBlockStub(schema *configschema.Block) cty.Value {
   106  	vals := make(map[string]cty.Value)
   107  	for name, attrS := range schema.Attributes {
   108  		vals[name] = cty.UnknownVal(attrS.Type)
   109  	}
   110  	for name, blockS := range schema.BlockTypes {
   111  		switch blockS.Nesting {
   112  		case configschema.NestingSingle, configschema.NestingGroup:
   113  			vals[name] = unknownBlockStub(&blockS.Block)
   114  		case configschema.NestingList:
   115  			// In principle we may be expected to produce a tuple value here,
   116  			// if there are any dynamically-typed attributes in our nested block,
   117  			// but the legacy SDK doesn't support that, so we just assume it'll
   118  			// never be necessary to normalize those. (Incorrect usage in any
   119  			// other SDK would be caught and returned as an error before we
   120  			// get here.)
   121  			vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)})
   122  		case configschema.NestingSet:
   123  			vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)})
   124  		case configschema.NestingMap:
   125  			// A nesting map can never be unknown since we then wouldn't know
   126  			// what the keys are. (Legacy SDK doesn't support NestingMap anyway,
   127  			// so this should never arise.)
   128  			vals[name] = cty.MapValEmpty(blockS.Block.ImpliedType())
   129  		}
   130  	}
   131  	return cty.ObjectVal(vals)
   132  }