github.com/opentofu/opentofu@v1.7.1/internal/command/jsonformat/jsondiff/diff.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package jsondiff
     7  
     8  import (
     9  	"reflect"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/opentofu/opentofu/internal/command/jsonformat/collections"
    14  	"github.com/opentofu/opentofu/internal/command/jsonformat/computed"
    15  	"github.com/opentofu/opentofu/internal/command/jsonformat/structured"
    16  	"github.com/opentofu/opentofu/internal/plans"
    17  )
    18  
    19  type TransformPrimitiveJson func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff
    20  type TransformObjectJson func(map[string]computed.Diff, plans.Action) computed.Diff
    21  type TransformArrayJson func([]computed.Diff, plans.Action) computed.Diff
    22  type TransformUnknownJson func(computed.Diff, plans.Action) computed.Diff
    23  type TransformSensitiveJson func(computed.Diff, bool, bool, plans.Action) computed.Diff
    24  type TransformTypeChangeJson func(before, after computed.Diff, action plans.Action) computed.Diff
    25  
    26  // JsonOpts defines the external callback functions that callers should
    27  // implement to process the supplied diffs.
    28  type JsonOpts struct {
    29  	Primitive  TransformPrimitiveJson
    30  	Object     TransformObjectJson
    31  	Array      TransformArrayJson
    32  	Unknown    TransformUnknownJson
    33  	Sensitive  TransformSensitiveJson
    34  	TypeChange TransformTypeChangeJson
    35  }
    36  
    37  // Transform accepts a generic before and after value that is assumed to be JSON
    38  // formatted and transforms it into a computed.Diff, using the callbacks
    39  // supplied in the JsonOpts class.
    40  func (opts JsonOpts) Transform(change structured.Change) computed.Diff {
    41  	if sensitive, ok := opts.processSensitive(change); ok {
    42  		return sensitive
    43  	}
    44  
    45  	if unknown, ok := opts.processUnknown(change); ok {
    46  		return unknown
    47  	}
    48  
    49  	beforeType := GetType(change.Before)
    50  	afterType := GetType(change.After)
    51  
    52  	deleted := afterType == Null && !change.AfterExplicit
    53  	created := beforeType == Null && !change.BeforeExplicit
    54  
    55  	if beforeType == afterType || (created || deleted) {
    56  		targetType := beforeType
    57  		if targetType == Null {
    58  			targetType = afterType
    59  		}
    60  		return opts.processUpdate(change, targetType)
    61  	}
    62  
    63  	b := opts.processUpdate(change.AsDelete(), beforeType)
    64  	a := opts.processUpdate(change.AsCreate(), afterType)
    65  	return opts.TypeChange(b, a, plans.Update)
    66  }
    67  
    68  func (opts JsonOpts) processUpdate(change structured.Change, jtype Type) computed.Diff {
    69  	switch jtype {
    70  	case Null:
    71  		return opts.processPrimitive(change, cty.NilType)
    72  	case Bool:
    73  		return opts.processPrimitive(change, cty.Bool)
    74  	case String:
    75  		return opts.processPrimitive(change, cty.String)
    76  	case Number:
    77  		return opts.processPrimitive(change, cty.Number)
    78  	case Object:
    79  		return opts.processObject(change.AsMap())
    80  	case Array:
    81  		return opts.processArray(change.AsSlice())
    82  	default:
    83  		panic("unrecognized json type: " + jtype)
    84  	}
    85  }
    86  
    87  func (opts JsonOpts) processPrimitive(change structured.Change, ctype cty.Type) computed.Diff {
    88  	beforeMissing := change.Before == nil && !change.BeforeExplicit
    89  	afterMissing := change.After == nil && !change.AfterExplicit
    90  
    91  	var action plans.Action
    92  	switch {
    93  	case beforeMissing && !afterMissing:
    94  		action = plans.Create
    95  	case !beforeMissing && afterMissing:
    96  		action = plans.Delete
    97  	case reflect.DeepEqual(change.Before, change.After):
    98  		action = plans.NoOp
    99  	default:
   100  		action = plans.Update
   101  	}
   102  
   103  	return opts.Primitive(change.Before, change.After, ctype, action)
   104  }
   105  
   106  func (opts JsonOpts) processArray(change structured.ChangeSlice) computed.Diff {
   107  	processIndices := func(beforeIx, afterIx int) computed.Diff {
   108  		// It's actually really difficult to render the diffs when some indices
   109  		// within a list are relevant and others aren't. To make this simpler
   110  		// we just treat all children of a relevant list as also relevant, so we
   111  		// ignore the relevant attributes field.
   112  		//
   113  		// Interestingly the tofu plan builder also agrees with this, and
   114  		// never sets relevant attributes beneath lists or sets. We're just
   115  		// going to enforce this logic here as well. If the list is relevant
   116  		// (decided elsewhere), then every element in the list is also relevant.
   117  		return opts.Transform(change.GetChild(beforeIx, afterIx))
   118  	}
   119  
   120  	isObjType := func(value interface{}) bool {
   121  		return GetType(value) == Object
   122  	}
   123  
   124  	return opts.Array(collections.TransformSlice(change.Before, change.After, processIndices, isObjType))
   125  }
   126  
   127  func (opts JsonOpts) processObject(change structured.ChangeMap) computed.Diff {
   128  	return opts.Object(collections.TransformMap(change.Before, change.After, change.AllKeys(), func(key string) computed.Diff {
   129  		child := change.GetChild(key)
   130  		if !child.RelevantAttributes.MatchesPartial() {
   131  			child = child.AsNoOp()
   132  		}
   133  
   134  		return opts.Transform(child)
   135  	}))
   136  }
   137  
   138  func (opts JsonOpts) processUnknown(change structured.Change) (computed.Diff, bool) {
   139  	return change.CheckForUnknown(
   140  		false,
   141  		func(current structured.Change) computed.Diff {
   142  			return opts.Unknown(computed.Diff{}, plans.Create)
   143  		}, func(current structured.Change, before structured.Change) computed.Diff {
   144  			return opts.Unknown(opts.Transform(before), plans.Update)
   145  		},
   146  	)
   147  }
   148  
   149  func (opts JsonOpts) processSensitive(change structured.Change) (computed.Diff, bool) {
   150  	return change.CheckForSensitive(opts.Transform, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff {
   151  		return opts.Sensitive(inner, beforeSensitive, afterSensitive, action)
   152  	})
   153  }