// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package renderers import ( "bytes" "fmt" "sort" "github.com/opentofu/opentofu/internal/command/jsonformat/computed" "github.com/opentofu/opentofu/internal/plans" ) var ( _ computed.DiffRenderer = (*blockRenderer)(nil) importantAttributes = []string{ "id", "name", "tags", } ) func importantAttribute(attr string) bool { for _, attribute := range importantAttributes { if attribute == attr { return true } } return false } func Block(attributes map[string]computed.Diff, blocks Blocks) computed.DiffRenderer { return &blockRenderer{ attributes: attributes, blocks: blocks, } } type blockRenderer struct { NoWarningsRenderer attributes map[string]computed.Diff blocks Blocks } func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { if len(renderer.attributes) == 0 && len(renderer.blocks.GetAllKeys()) == 0 { return fmt.Sprintf("{}%s", forcesReplacement(diff.Replace, opts)) } unchangedAttributes := 0 unchangedBlocks := 0 maximumAttributeKeyLen := 0 var attributeKeys []string escapedAttributeKeys := make(map[string]string) for key := range renderer.attributes { attributeKeys = append(attributeKeys, key) escapedKey := EnsureValidAttributeName(key) escapedAttributeKeys[key] = escapedKey if maximumAttributeKeyLen < len(escapedKey) { maximumAttributeKeyLen = len(escapedKey) } } sort.Strings(attributeKeys) importantAttributeOpts := opts.Clone() importantAttributeOpts.ShowUnchangedChildren = true attributeOpts := opts.Clone() var buf bytes.Buffer buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts))) for _, key := range attributeKeys { attribute := renderer.attributes[key] if importantAttribute(key) { // Always display the important attributes. for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) { buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) } buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, importantAttributeOpts), maximumAttributeKeyLen, key, attribute.RenderHuman(indent+1, importantAttributeOpts))) continue } if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren { unchangedAttributes++ continue } for _, warning := range attribute.WarningsHuman(indent+1, opts) { buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) } buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts))) } if unchangedAttributes > 0 { buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("attribute", unchangedAttributes, opts))) } blockKeys := renderer.blocks.GetAllKeys() for _, key := range blockKeys { foundChangedBlock := false renderBlock := func(diff computed.Diff, mapKey string, opts computed.RenderHumanOpts) { creatingSensitiveValue := diff.Action == plans.Create && renderer.blocks.AfterSensitiveBlocks[key] deletingSensitiveValue := diff.Action == plans.Delete && renderer.blocks.BeforeSensitiveBlocks[key] modifyingSensitiveValue := (diff.Action == plans.Update || diff.Action == plans.NoOp) && (renderer.blocks.AfterSensitiveBlocks[key] || renderer.blocks.BeforeSensitiveBlocks[key]) if creatingSensitiveValue || deletingSensitiveValue || modifyingSensitiveValue { // Intercept the renderer here if the sensitive data was set // across all the blocks instead of individually. action := diff.Action if diff.Action == plans.NoOp && renderer.blocks.BeforeSensitiveBlocks[key] != renderer.blocks.AfterSensitiveBlocks[key] { action = plans.Update } diff = computed.NewDiff(SensitiveBlock(diff, renderer.blocks.BeforeSensitiveBlocks[key], renderer.blocks.AfterSensitiveBlocks[key]), action, diff.Replace) } if diff.Action == plans.NoOp && !opts.ShowUnchangedChildren { unchangedBlocks++ return } if !foundChangedBlock && len(renderer.attributes) > 0 { // We always want to put an extra new line between the // attributes and blocks, and between groups of blocks. buf.WriteString("\n") foundChangedBlock = true } // If the force replacement metadata was set for every entry in the // block we need to override that here. Our child blocks will only // know about the replace function if it was set on them // specifically, and not if it was set for all the blocks. blockOpts := opts.Clone() blockOpts.OverrideForcesReplacement = renderer.blocks.ReplaceBlocks[key] for _, warning := range diff.WarningsHuman(indent+1, blockOpts) { buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) } buf.WriteString(fmt.Sprintf("%s%s%s%s %s\n", formatIndent(indent+1), writeDiffActionSymbol(diff.Action, blockOpts), EnsureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts))) } switch { case renderer.blocks.IsSingleBlock(key): renderBlock(renderer.blocks.SingleBlocks[key], "", opts) case renderer.blocks.IsMapBlock(key): var keys []string for key := range renderer.blocks.MapBlocks[key] { keys = append(keys, key) } sort.Strings(keys) for _, innerKey := range keys { renderBlock(renderer.blocks.MapBlocks[key][innerKey], fmt.Sprintf(" %q", innerKey), opts) } case renderer.blocks.IsSetBlock(key): setOpts := opts.Clone() setOpts.OverrideForcesReplacement = diff.Replace for _, block := range renderer.blocks.SetBlocks[key] { renderBlock(block, "", setOpts) } case renderer.blocks.IsListBlock(key): for _, block := range renderer.blocks.ListBlocks[key] { renderBlock(block, "", opts) } } } if unchangedBlocks > 0 { buf.WriteString(fmt.Sprintf("\n%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("block", unchangedBlocks, opts))) } buf.WriteString(fmt.Sprintf("%s%s}", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts))) return buf.String() }