github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/jsonformat/jsondiff/diff.go (about)

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