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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package structured
     5  
     6  import (
     7  	"encoding/json"
     8  	"reflect"
     9  
    10  	"github.com/terramate-io/tf/command/jsonformat/structured/attribute_path"
    11  	"github.com/terramate-io/tf/command/jsonplan"
    12  	"github.com/terramate-io/tf/command/jsonstate"
    13  	viewsjson "github.com/terramate-io/tf/command/views/json"
    14  	"github.com/terramate-io/tf/plans"
    15  )
    16  
    17  // Change contains the unmarshalled generic interface{} types that are output by
    18  // the JSON functions in the various json packages (such as jsonplan and
    19  // jsonprovider).
    20  //
    21  // A Change can be converted into a computed.Diff, ready for rendering, with the
    22  // ComputeDiffForAttribute, ComputeDiffForOutput, and ComputeDiffForBlock
    23  // functions.
    24  //
    25  // The Before and After fields are actually go-cty values, but we cannot convert
    26  // them directly because of the Terraform Cloud redacted endpoint. The redacted
    27  // endpoint turns sensitive values into strings regardless of their types.
    28  // Because of this, we cannot just do a direct conversion using the ctyjson
    29  // package. We would have to iterate through the schema first, find the
    30  // sensitive values and their mapped types, update the types inside the schema
    31  // to strings, and then go back and do the overall conversion. This isn't
    32  // including any of the more complicated parts around what happens if something
    33  // was sensitive before and isn't sensitive after or vice versa. This would mean
    34  // the type would need to change between the before and after value. It is in
    35  // fact just easier to iterate through the values as generic JSON interfaces.
    36  type Change struct {
    37  
    38  	// BeforeExplicit matches AfterExplicit except references the Before value.
    39  	BeforeExplicit bool
    40  
    41  	// AfterExplicit refers to whether the After value is explicit or
    42  	// implicit. It is explicit if it has been specified by the user, and
    43  	// implicit if it has been set as a consequence of other changes.
    44  	//
    45  	// For example, explicitly setting a value to null in a list should result
    46  	// in After being null and AfterExplicit being true. In comparison,
    47  	// removing an element from a list should also result in After being null
    48  	// and AfterExplicit being false. Without the explicit information our
    49  	// functions would not be able to tell the difference between these two
    50  	// cases.
    51  	AfterExplicit bool
    52  
    53  	// Before contains the value before the proposed change.
    54  	//
    55  	// The type of the value should be informed by the schema and cast
    56  	// appropriately when needed.
    57  	Before interface{}
    58  
    59  	// After contains the value after the proposed change.
    60  	//
    61  	// The type of the value should be informed by the schema and cast
    62  	// appropriately when needed.
    63  	After interface{}
    64  
    65  	// Unknown describes whether the After value is known or unknown at the time
    66  	// of the plan. In practice, this means the after value should be rendered
    67  	// simply as `(known after apply)`.
    68  	//
    69  	// The concrete value could be a boolean describing whether the entirety of
    70  	// the After value is unknown, or it could be a list or a map depending on
    71  	// the schema describing whether specific elements or attributes within the
    72  	// value are unknown.
    73  	Unknown interface{}
    74  
    75  	// BeforeSensitive matches Unknown, but references whether the Before value
    76  	// is sensitive.
    77  	BeforeSensitive interface{}
    78  
    79  	// AfterSensitive matches Unknown, but references whether the After value is
    80  	// sensitive.
    81  	AfterSensitive interface{}
    82  
    83  	// ReplacePaths contains a set of paths that point to attributes/elements
    84  	// that are causing the overall resource to be replaced rather than simply
    85  	// updated.
    86  	ReplacePaths attribute_path.Matcher
    87  
    88  	// RelevantAttributes contains a set of paths that point attributes/elements
    89  	// that we should display. Any element/attribute not matched by this Matcher
    90  	// should be skipped.
    91  	RelevantAttributes attribute_path.Matcher
    92  }
    93  
    94  // FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
    95  // structs into generic interface{} types that can be reasoned about.
    96  func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change {
    97  	return Change{
    98  		Before:             unmarshalGeneric(change.Before),
    99  		After:              unmarshalGeneric(change.After),
   100  		Unknown:            unmarshalGeneric(change.AfterUnknown),
   101  		BeforeSensitive:    unmarshalGeneric(change.BeforeSensitive),
   102  		AfterSensitive:     unmarshalGeneric(change.AfterSensitive),
   103  		ReplacePaths:       attribute_path.Parse(change.ReplacePaths, false),
   104  		RelevantAttributes: relevantAttributes,
   105  	}
   106  }
   107  
   108  // FromJsonResource unmarshals the raw values in the jsonstate.Resource structs
   109  // into generic interface{} types that can be reasoned about.
   110  func FromJsonResource(resource jsonstate.Resource) Change {
   111  	return Change{
   112  		// We model resource formatting as NoOps.
   113  		Before: unwrapAttributeValues(resource.AttributeValues),
   114  		After:  unwrapAttributeValues(resource.AttributeValues),
   115  
   116  		// We have some sensitive values, but we don't have any unknown values.
   117  		Unknown:         false,
   118  		BeforeSensitive: unmarshalGeneric(resource.SensitiveValues),
   119  		AfterSensitive:  unmarshalGeneric(resource.SensitiveValues),
   120  
   121  		// We don't display replacement data for resources, and all attributes
   122  		// are relevant.
   123  		ReplacePaths:       attribute_path.Empty(false),
   124  		RelevantAttributes: attribute_path.AlwaysMatcher(),
   125  	}
   126  }
   127  
   128  // FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into
   129  // generic interface{} types that can be reasoned about.
   130  func FromJsonOutput(output jsonstate.Output) Change {
   131  	return Change{
   132  		// We model resource formatting as NoOps.
   133  		Before: unmarshalGeneric(output.Value),
   134  		After:  unmarshalGeneric(output.Value),
   135  
   136  		// We have some sensitive values, but we don't have any unknown values.
   137  		Unknown:         false,
   138  		BeforeSensitive: output.Sensitive,
   139  		AfterSensitive:  output.Sensitive,
   140  
   141  		// We don't display replacement data for resources, and all attributes
   142  		// are relevant.
   143  		ReplacePaths:       attribute_path.Empty(false),
   144  		RelevantAttributes: attribute_path.AlwaysMatcher(),
   145  	}
   146  }
   147  
   148  // FromJsonViewsOutput unmarshals the raw values in the viewsjson.Output structs into
   149  // generic interface{} types that can be reasoned about.
   150  func FromJsonViewsOutput(output viewsjson.Output) Change {
   151  	return Change{
   152  		// We model resource formatting as NoOps.
   153  		Before: unmarshalGeneric(output.Value),
   154  		After:  unmarshalGeneric(output.Value),
   155  
   156  		// We have some sensitive values, but we don't have any unknown values.
   157  		Unknown:         false,
   158  		BeforeSensitive: output.Sensitive,
   159  		AfterSensitive:  output.Sensitive,
   160  
   161  		// We don't display replacement data for resources, and all attributes
   162  		// are relevant.
   163  		ReplacePaths:       attribute_path.Empty(false),
   164  		RelevantAttributes: attribute_path.AlwaysMatcher(),
   165  	}
   166  }
   167  
   168  // CalculateAction does a very simple analysis to make the best guess at the
   169  // action this change describes. For complex types such as objects, maps, lists,
   170  // or sets it is likely more efficient to work out the action directly instead
   171  // of relying on this function.
   172  func (change Change) CalculateAction() plans.Action {
   173  	if (change.Before == nil && !change.BeforeExplicit) && (change.After != nil || change.AfterExplicit) {
   174  		return plans.Create
   175  	}
   176  	if (change.After == nil && !change.AfterExplicit) && (change.Before != nil || change.BeforeExplicit) {
   177  		return plans.Delete
   178  	}
   179  
   180  	if reflect.DeepEqual(change.Before, change.After) && change.AfterExplicit == change.BeforeExplicit && change.IsAfterSensitive() == change.IsBeforeSensitive() {
   181  		return plans.NoOp
   182  	}
   183  
   184  	return plans.Update
   185  }
   186  
   187  // GetDefaultActionForIteration is used to guess what the change could be for
   188  // complex attributes (collections and objects) and blocks.
   189  //
   190  // You can't really tell the difference between a NoOp and an Update just by
   191  // looking at the attribute itself as you need to inspect the children.
   192  //
   193  // This function returns a Delete or a Create action if the before or after
   194  // values were null, and returns a NoOp for all other cases. It should be used
   195  // in conjunction with compareActions to calculate the actual action based on
   196  // the actions of the children.
   197  func (change Change) GetDefaultActionForIteration() plans.Action {
   198  	if change.Before == nil && change.After == nil {
   199  		return plans.NoOp
   200  	}
   201  
   202  	if change.Before == nil {
   203  		return plans.Create
   204  	}
   205  	if change.After == nil {
   206  		return plans.Delete
   207  	}
   208  	return plans.NoOp
   209  }
   210  
   211  // AsNoOp returns the current change as if it is a NoOp operation.
   212  //
   213  // Basically it replaces all the after values with the before values.
   214  func (change Change) AsNoOp() Change {
   215  	return Change{
   216  		BeforeExplicit:     change.BeforeExplicit,
   217  		AfterExplicit:      change.BeforeExplicit,
   218  		Before:             change.Before,
   219  		After:              change.Before,
   220  		Unknown:            false,
   221  		BeforeSensitive:    change.BeforeSensitive,
   222  		AfterSensitive:     change.BeforeSensitive,
   223  		ReplacePaths:       change.ReplacePaths,
   224  		RelevantAttributes: change.RelevantAttributes,
   225  	}
   226  }
   227  
   228  // AsDelete returns the current change as if it is a Delete operation.
   229  //
   230  // Basically it replaces all the after values with nil or false.
   231  func (change Change) AsDelete() Change {
   232  	return Change{
   233  		BeforeExplicit:     change.BeforeExplicit,
   234  		AfterExplicit:      false,
   235  		Before:             change.Before,
   236  		After:              nil,
   237  		Unknown:            nil,
   238  		BeforeSensitive:    change.BeforeSensitive,
   239  		AfterSensitive:     nil,
   240  		ReplacePaths:       change.ReplacePaths,
   241  		RelevantAttributes: change.RelevantAttributes,
   242  	}
   243  }
   244  
   245  // AsCreate returns the current change as if it is a Create operation.
   246  //
   247  // Basically it replaces all the before values with nil or false.
   248  func (change Change) AsCreate() Change {
   249  	return Change{
   250  		BeforeExplicit:     false,
   251  		AfterExplicit:      change.AfterExplicit,
   252  		Before:             nil,
   253  		After:              change.After,
   254  		Unknown:            change.Unknown,
   255  		BeforeSensitive:    nil,
   256  		AfterSensitive:     change.AfterSensitive,
   257  		ReplacePaths:       change.ReplacePaths,
   258  		RelevantAttributes: change.RelevantAttributes,
   259  	}
   260  }
   261  
   262  func unmarshalGeneric(raw json.RawMessage) interface{} {
   263  	if raw == nil {
   264  		return nil
   265  	}
   266  
   267  	var out interface{}
   268  	if err := json.Unmarshal(raw, &out); err != nil {
   269  		panic("unrecognized json type: " + err.Error())
   270  	}
   271  	return out
   272  }
   273  
   274  func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} {
   275  	out := make(map[string]interface{})
   276  	for key, value := range values {
   277  		out[key] = unmarshalGeneric(value)
   278  	}
   279  	return out
   280  }