| package objchange |
| |
| import ( |
| "github.com/hashicorp/terraform/internal/configs/configschema" |
| "github.com/zclconf/go-cty/cty" |
| ) |
| |
| // NormalizeObjectFromLegacySDK takes an object that may have been generated |
| // by the legacy Terraform SDK (i.e. returned from a provider with the |
| // LegacyTypeSystem opt-out set) and does its best to normalize it for the |
| // assumptions we would normally enforce if the provider had not opted out. |
| // |
| // In particular, this function guarantees that a value representing a nested |
| // block will never itself be unknown or null, instead representing that as |
| // a non-null value that may contain null/unknown values. |
| // |
| // The input value must still conform to the implied type of the given schema, |
| // or else this function may produce garbage results or panic. This is usually |
| // okay because type consistency is enforced when deserializing the value |
| // returned from the provider over the RPC wire protocol anyway. |
| func NormalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { |
| val, valMarks := val.UnmarkDeepWithPaths() |
| val = normalizeObjectFromLegacySDK(val, schema) |
| return val.MarkWithPaths(valMarks) |
| } |
| |
| func normalizeObjectFromLegacySDK(val cty.Value, schema *configschema.Block) cty.Value { |
| if val == cty.NilVal || val.IsNull() { |
| // This should never happen in reasonable use, but we'll allow it |
| // and normalize to a null of the expected type rather than panicking |
| // below. |
| return cty.NullVal(schema.ImpliedType()) |
| } |
| |
| vals := make(map[string]cty.Value) |
| for name := range schema.Attributes { |
| // No normalization for attributes, since them being type-conformant |
| // is all that we require. |
| vals[name] = val.GetAttr(name) |
| } |
| for name, blockS := range schema.BlockTypes { |
| lv := val.GetAttr(name) |
| |
| // Legacy SDK never generates dynamically-typed attributes and so our |
| // normalization code doesn't deal with them, but we need to make sure |
| // we still pass them through properly so that we don't interfere with |
| // objects generated by other SDKs. |
| if ty := blockS.Block.ImpliedType(); ty.HasDynamicTypes() { |
| vals[name] = lv |
| continue |
| } |
| |
| switch blockS.Nesting { |
| case configschema.NestingSingle, configschema.NestingGroup: |
| if lv.IsKnown() { |
| if lv.IsNull() && blockS.Nesting == configschema.NestingGroup { |
| vals[name] = blockS.EmptyValue() |
| } else { |
| vals[name] = normalizeObjectFromLegacySDK(lv, &blockS.Block) |
| } |
| } else { |
| vals[name] = unknownBlockStub(&blockS.Block) |
| } |
| case configschema.NestingList: |
| switch { |
| case !lv.IsKnown(): |
| vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) |
| case lv.IsNull() || lv.LengthInt() == 0: |
| vals[name] = cty.ListValEmpty(blockS.Block.ImpliedType()) |
| default: |
| subVals := make([]cty.Value, 0, lv.LengthInt()) |
| for it := lv.ElementIterator(); it.Next(); { |
| _, subVal := it.Element() |
| subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) |
| } |
| vals[name] = cty.ListVal(subVals) |
| } |
| case configschema.NestingSet: |
| switch { |
| case !lv.IsKnown(): |
| vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) |
| case lv.IsNull() || lv.LengthInt() == 0: |
| vals[name] = cty.SetValEmpty(blockS.Block.ImpliedType()) |
| default: |
| subVals := make([]cty.Value, 0, lv.LengthInt()) |
| for it := lv.ElementIterator(); it.Next(); { |
| _, subVal := it.Element() |
| subVals = append(subVals, normalizeObjectFromLegacySDK(subVal, &blockS.Block)) |
| } |
| vals[name] = cty.SetVal(subVals) |
| } |
| default: |
| // The legacy SDK doesn't support NestingMap, so we just assume |
| // maps are always okay. (If not, we would've detected and returned |
| // an error to the user before we got here.) |
| vals[name] = lv |
| } |
| } |
| return cty.ObjectVal(vals) |
| } |
| |
| // unknownBlockStub constructs an object value that approximates an unknown |
| // block by producing a known block object with all of its leaf attribute |
| // values set to unknown. |
| // |
| // Blocks themselves cannot be unknown, so if the legacy SDK tries to return |
| // such a thing, we'll use this result instead. This convention mimics how |
| // the dynamic block feature deals with being asked to iterate over an unknown |
| // value, because our value-checking functions already accept this convention |
| // as a special case. |
| func unknownBlockStub(schema *configschema.Block) cty.Value { |
| vals := make(map[string]cty.Value) |
| for name, attrS := range schema.Attributes { |
| vals[name] = cty.UnknownVal(attrS.Type) |
| } |
| for name, blockS := range schema.BlockTypes { |
| switch blockS.Nesting { |
| case configschema.NestingSingle, configschema.NestingGroup: |
| vals[name] = unknownBlockStub(&blockS.Block) |
| case configschema.NestingList: |
| // In principle we may be expected to produce a tuple value here, |
| // if there are any dynamically-typed attributes in our nested block, |
| // but the legacy SDK doesn't support that, so we just assume it'll |
| // never be necessary to normalize those. (Incorrect usage in any |
| // other SDK would be caught and returned as an error before we |
| // get here.) |
| vals[name] = cty.ListVal([]cty.Value{unknownBlockStub(&blockS.Block)}) |
| case configschema.NestingSet: |
| vals[name] = cty.SetVal([]cty.Value{unknownBlockStub(&blockS.Block)}) |
| case configschema.NestingMap: |
| // A nesting map can never be unknown since we then wouldn't know |
| // what the keys are. (Legacy SDK doesn't support NestingMap anyway, |
| // so this should never arise.) |
| vals[name] = cty.MapValEmpty(blockS.Block.ImpliedType()) |
| } |
| } |
| return cty.ObjectVal(vals) |
| } |