| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package differ | |
| import ( | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/collections" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/computed" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" | |
| "github.com/hashicorp/terraform/internal/command/jsonformat/structured" | |
| "github.com/hashicorp/terraform/internal/command/jsonprovider" | |
| "github.com/hashicorp/terraform/internal/plans" | |
| ) | |
| func ComputeDiffForBlock(change structured.Change, block *jsonprovider.Block) computed.Diff { | |
| if sensitive, ok := checkForSensitiveBlock(change, block); ok { | |
| return sensitive | |
| } | |
| if unknown, ok := checkForUnknownBlock(change, block); ok { | |
| return unknown | |
| } | |
| // NonLegacyValue is only ever switched from false to true, since the | |
| // behavior would be for the entire resource. | |
| change.NonLegacySchema = change.NonLegacySchema || containsNonLegacyFeatures(block) | |
| current := change.GetDefaultActionForIteration() | |
| blockValue := change.AsMap() | |
| attributes := make(map[string]computed.Diff) | |
| for key, attr := range block.Attributes { | |
| if attr.WriteOnly { | |
| continue | |
| } | |
| childValue := blockValue.GetChild(key) | |
| if !childValue.RelevantAttributes.MatchesPartial() { | |
| // Mark non-relevant attributes as unchanged. | |
| childValue = childValue.AsNoOp() | |
| } | |
| // Always treat changes to blocks as implicit. | |
| childValue.BeforeExplicit = false | |
| childValue.AfterExplicit = false | |
| childChange := ComputeDiffForAttribute(childValue, attr) | |
| if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil { | |
| // Don't record nil values at all in blocks except if they are write-only. | |
| continue | |
| } | |
| attributes[key] = childChange | |
| current = collections.CompareActions(current, childChange.Action) | |
| } | |
| blocks := renderers.Blocks{ | |
| ReplaceBlocks: make(map[string]bool), | |
| BeforeSensitiveBlocks: make(map[string]bool), | |
| AfterSensitiveBlocks: make(map[string]bool), | |
| UnknownBlocks: make(map[string]bool), | |
| SingleBlocks: make(map[string]computed.Diff), | |
| ListBlocks: make(map[string][]computed.Diff), | |
| SetBlocks: make(map[string][]computed.Diff), | |
| MapBlocks: make(map[string]map[string]computed.Diff), | |
| } | |
| for key, blockType := range block.BlockTypes { | |
| childValue := blockValue.GetChild(key) | |
| if !childValue.RelevantAttributes.MatchesPartial() { | |
| // Mark non-relevant attributes as unchanged. | |
| childValue = childValue.AsNoOp() | |
| } | |
| beforeSensitive := childValue.IsBeforeSensitive() | |
| afterSensitive := childValue.IsAfterSensitive() | |
| forcesReplacement := childValue.ReplacePaths.Matches() | |
| unknown := childValue.IsUnknown() | |
| switch NestingMode(blockType.NestingMode) { | |
| case nestingModeSet: | |
| diffs, action := computeBlockDiffsAsSet(childValue, blockType.Block) | |
| if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { | |
| // Don't record nil values in blocks. | |
| continue | |
| } | |
| blocks.AddAllSetBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) | |
| current = collections.CompareActions(current, action) | |
| case nestingModeList: | |
| diffs, action := computeBlockDiffsAsList(childValue, blockType.Block) | |
| if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { | |
| // Don't record nil values in blocks. | |
| continue | |
| } | |
| blocks.AddAllListBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) | |
| current = collections.CompareActions(current, action) | |
| case nestingModeMap: | |
| diffs, action := computeBlockDiffsAsMap(childValue, blockType.Block) | |
| if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { | |
| // Don't record nil values in blocks. | |
| continue | |
| } | |
| blocks.AddAllMapBlocks(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) | |
| current = collections.CompareActions(current, action) | |
| case nestingModeSingle, nestingModeGroup: | |
| diff := ComputeDiffForBlock(childValue, blockType.Block) | |
| if diff.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { | |
| // Don't record nil values in blocks. | |
| continue | |
| } | |
| blocks.AddSingleBlock(key, diff, forcesReplacement, beforeSensitive, afterSensitive, unknown) | |
| current = collections.CompareActions(current, diff.Action) | |
| default: | |
| panic("unrecognized nesting mode: " + blockType.NestingMode) | |
| } | |
| } | |
| for name, attr := range block.Attributes { | |
| if attr.WriteOnly { | |
| attributes[name] = computeDiffForWriteOnlyAttribute(change, current) | |
| } | |
| } | |
| return computed.NewDiff(renderers.Block(attributes, blocks), current, change.ReplacePaths.Matches()) | |
| } | |
| // containsNonLegacyFeatures checks for features not supported by the legacy | |
| // SDK, so that we can skip the empty string -> null fixup for them. | |
| func containsNonLegacyFeatures(block *jsonprovider.Block) bool { | |
| for _, blockType := range block.BlockTypes { | |
| switch NestingMode(blockType.NestingMode) { | |
| case nestingModeMap, nestingModeGroup: | |
| // these block types were not possible in the SDK | |
| return true | |
| } | |
| } | |
| for _, attribute := range block.Attributes { | |
| //nested object types were not possible in the SDK | |
| if attribute.AttributeNestedType != nil { | |
| return true | |
| } | |
| ty := unmarshalAttribute(attribute) | |
| // these types were not possible in the SDK | |
| switch { | |
| case ty.HasDynamicTypes(): | |
| return true | |
| case ty.IsTupleType() || ty.IsObjectType(): | |
| return true | |
| case ty.IsCollectionType(): | |
| // Nested collections were not really supported, but could be | |
| // generated with string types (though we conservatively limit this | |
| // to primitive types) | |
| ety := ty.ElementType() | |
| if ety.IsCollectionType() && !ety.ElementType().IsPrimitiveType() { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } |