// Copyright (c) HashiCorp, Inc. // 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 }