terraform/internal/command/jsonformat/differ/block.go
James Bardin bdd5a2cd33 try to render some empty string -> null changes
Changes between empty strings and `null` were hidden in the CLI output,
because the SDK could not reliably detect the difference and may return
either value depending on the situation.

This legacy behavior can be confusing for authors of new provider which
can correctly handle `null`, and it would be preferable to be able to
render those changes in the CLI.

While we don't have enough information to detect when the legacy
behavior is required, we can detect a number of cases where it's
certain that we are not dealing with a legacy schema and should output
the full diff.
2024-02-27 16:59:06 -05:00

153 lines
5.4 KiB
Go

// 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 {
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.
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),
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()
switch NestingMode(blockType.NestingMode) {
case nestingModeSet:
diffs, action := computeBlockDiffsAsSet(childValue, blockType.Block)
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
blocks.AddAllSetBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeList:
diffs, action := computeBlockDiffsAsList(childValue, blockType.Block)
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
blocks.AddAllListBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeMap:
diffs, action := computeBlockDiffsAsMap(childValue, blockType.Block)
if action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
blocks.AddAllMapBlocks(key, diffs, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, action)
case nestingModeSingle, nestingModeGroup:
diff := ComputeDiffForBlock(childValue, blockType.Block)
if diff.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil {
// Don't record nil values in blocks.
continue
}
blocks.AddSingleBlock(key, diff, forcesReplacement, beforeSensitive, afterSensitive)
current = collections.CompareActions(current, diff.Action)
default:
panic("unrecognized nesting mode: " + blockType.NestingMode)
}
}
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
}