github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/normalize_obj.go (about)

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