github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/planfile/tfplan_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package planfile
     5  
     6  import (
     7  	"bytes"
     8  	"testing"
     9  
    10  	"github.com/go-test/deep"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/terramate-io/tf/addrs"
    14  	"github.com/terramate-io/tf/checks"
    15  	"github.com/terramate-io/tf/lang/globalref"
    16  	"github.com/terramate-io/tf/lang/marks"
    17  	"github.com/terramate-io/tf/plans"
    18  	"github.com/terramate-io/tf/states"
    19  )
    20  
    21  func TestTFPlanRoundTrip(t *testing.T) {
    22  	objTy := cty.Object(map[string]cty.Type{
    23  		"id": cty.String,
    24  	})
    25  
    26  	plan := &plans.Plan{
    27  		VariableValues: map[string]plans.DynamicValue{
    28  			"foo": mustNewDynamicValueStr("foo value"),
    29  		},
    30  		Changes: &plans.Changes{
    31  			Outputs: []*plans.OutputChangeSrc{
    32  				{
    33  					Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
    34  					ChangeSrc: plans.ChangeSrc{
    35  						Action: plans.Create,
    36  						After:  mustDynamicOutputValue("bar value"),
    37  					},
    38  					Sensitive: false,
    39  				},
    40  				{
    41  					Addr: addrs.OutputValue{Name: "baz"}.Absolute(addrs.RootModuleInstance),
    42  					ChangeSrc: plans.ChangeSrc{
    43  						Action: plans.NoOp,
    44  						Before: mustDynamicOutputValue("baz value"),
    45  						After:  mustDynamicOutputValue("baz value"),
    46  					},
    47  					Sensitive: false,
    48  				},
    49  				{
    50  					Addr: addrs.OutputValue{Name: "secret"}.Absolute(addrs.RootModuleInstance),
    51  					ChangeSrc: plans.ChangeSrc{
    52  						Action: plans.Update,
    53  						Before: mustDynamicOutputValue("old secret value"),
    54  						After:  mustDynamicOutputValue("new secret value"),
    55  					},
    56  					Sensitive: true,
    57  				},
    58  			},
    59  			Resources: []*plans.ResourceInstanceChangeSrc{
    60  				{
    61  					Addr: addrs.Resource{
    62  						Mode: addrs.ManagedResourceMode,
    63  						Type: "test_thing",
    64  						Name: "woot",
    65  					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
    66  					PrevRunAddr: addrs.Resource{
    67  						Mode: addrs.ManagedResourceMode,
    68  						Type: "test_thing",
    69  						Name: "woot",
    70  					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
    71  					ProviderAddr: addrs.AbsProviderConfig{
    72  						Provider: addrs.NewDefaultProvider("test"),
    73  						Module:   addrs.RootModule,
    74  					},
    75  					ChangeSrc: plans.ChangeSrc{
    76  						Action: plans.DeleteThenCreate,
    77  						Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
    78  							"id": cty.StringVal("foo-bar-baz"),
    79  							"boop": cty.ListVal([]cty.Value{
    80  								cty.StringVal("beep"),
    81  							}),
    82  						}), objTy),
    83  						After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
    84  							"id": cty.UnknownVal(cty.String),
    85  							"boop": cty.ListVal([]cty.Value{
    86  								cty.StringVal("beep"),
    87  								cty.StringVal("honk"),
    88  							}),
    89  						}), objTy),
    90  						AfterValMarks: []cty.PathValueMarks{
    91  							{
    92  								Path:  cty.GetAttrPath("boop").IndexInt(1),
    93  								Marks: cty.NewValueMarks(marks.Sensitive),
    94  							},
    95  						},
    96  					},
    97  					RequiredReplace: cty.NewPathSet(
    98  						cty.GetAttrPath("boop"),
    99  					),
   100  					ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
   101  				},
   102  				{
   103  					Addr: addrs.Resource{
   104  						Mode: addrs.ManagedResourceMode,
   105  						Type: "test_thing",
   106  						Name: "woot",
   107  					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
   108  					PrevRunAddr: addrs.Resource{
   109  						Mode: addrs.ManagedResourceMode,
   110  						Type: "test_thing",
   111  						Name: "woot",
   112  					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
   113  					DeposedKey: "foodface",
   114  					ProviderAddr: addrs.AbsProviderConfig{
   115  						Provider: addrs.NewDefaultProvider("test"),
   116  						Module:   addrs.RootModule,
   117  					},
   118  					ChangeSrc: plans.ChangeSrc{
   119  						Action: plans.Delete,
   120  						Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   121  							"id": cty.StringVal("bar-baz-foo"),
   122  						}), objTy),
   123  					},
   124  				},
   125  				{
   126  					Addr: addrs.Resource{
   127  						Mode: addrs.ManagedResourceMode,
   128  						Type: "test_thing",
   129  						Name: "importing",
   130  					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
   131  					PrevRunAddr: addrs.Resource{
   132  						Mode: addrs.ManagedResourceMode,
   133  						Type: "test_thing",
   134  						Name: "importing",
   135  					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
   136  					ProviderAddr: addrs.AbsProviderConfig{
   137  						Provider: addrs.NewDefaultProvider("test"),
   138  						Module:   addrs.RootModule,
   139  					},
   140  					ChangeSrc: plans.ChangeSrc{
   141  						Action: plans.NoOp,
   142  						Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   143  							"id": cty.StringVal("testing"),
   144  						}), objTy),
   145  						After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   146  							"id": cty.StringVal("testing"),
   147  						}), objTy),
   148  						Importing:       &plans.ImportingSrc{ID: "testing"},
   149  						GeneratedConfig: "resource \\\"test_thing\\\" \\\"importing\\\" {}",
   150  					},
   151  				},
   152  			},
   153  		},
   154  		DriftedResources: []*plans.ResourceInstanceChangeSrc{
   155  			{
   156  				Addr: addrs.Resource{
   157  					Mode: addrs.ManagedResourceMode,
   158  					Type: "test_thing",
   159  					Name: "woot",
   160  				}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   161  				PrevRunAddr: addrs.Resource{
   162  					Mode: addrs.ManagedResourceMode,
   163  					Type: "test_thing",
   164  					Name: "woot",
   165  				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   166  				ProviderAddr: addrs.AbsProviderConfig{
   167  					Provider: addrs.NewDefaultProvider("test"),
   168  					Module:   addrs.RootModule,
   169  				},
   170  				ChangeSrc: plans.ChangeSrc{
   171  					Action: plans.DeleteThenCreate,
   172  					Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   173  						"id": cty.StringVal("foo-bar-baz"),
   174  						"boop": cty.ListVal([]cty.Value{
   175  							cty.StringVal("beep"),
   176  						}),
   177  					}), objTy),
   178  					After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   179  						"id": cty.UnknownVal(cty.String),
   180  						"boop": cty.ListVal([]cty.Value{
   181  							cty.StringVal("beep"),
   182  							cty.StringVal("bonk"),
   183  						}),
   184  					}), objTy),
   185  					AfterValMarks: []cty.PathValueMarks{
   186  						{
   187  							Path:  cty.GetAttrPath("boop").IndexInt(1),
   188  							Marks: cty.NewValueMarks(marks.Sensitive),
   189  						},
   190  					},
   191  				},
   192  			},
   193  		},
   194  		RelevantAttributes: []globalref.ResourceAttr{
   195  			{
   196  				Resource: addrs.Resource{
   197  					Mode: addrs.ManagedResourceMode,
   198  					Type: "test_thing",
   199  					Name: "woot",
   200  				}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   201  				Attr: cty.GetAttrPath("boop").Index(cty.NumberIntVal(1)),
   202  			},
   203  		},
   204  		Checks: &states.CheckResults{
   205  			ConfigResults: addrs.MakeMap(
   206  				addrs.MakeMapElem[addrs.ConfigCheckable](
   207  					addrs.Resource{
   208  						Mode: addrs.ManagedResourceMode,
   209  						Type: "test_thing",
   210  						Name: "woot",
   211  					}.InModule(addrs.RootModule),
   212  					&states.CheckResultAggregate{
   213  						Status: checks.StatusFail,
   214  						ObjectResults: addrs.MakeMap(
   215  							addrs.MakeMapElem[addrs.Checkable](
   216  								addrs.Resource{
   217  									Mode: addrs.ManagedResourceMode,
   218  									Type: "test_thing",
   219  									Name: "woot",
   220  								}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   221  								&states.CheckResultObject{
   222  									Status:          checks.StatusFail,
   223  									FailureMessages: []string{"Oh no!"},
   224  								},
   225  							),
   226  						),
   227  					},
   228  				),
   229  				addrs.MakeMapElem[addrs.ConfigCheckable](
   230  					addrs.Check{
   231  						Name: "check",
   232  					}.InModule(addrs.RootModule),
   233  					&states.CheckResultAggregate{
   234  						Status: checks.StatusFail,
   235  						ObjectResults: addrs.MakeMap(
   236  							addrs.MakeMapElem[addrs.Checkable](
   237  								addrs.Check{
   238  									Name: "check",
   239  								}.Absolute(addrs.RootModuleInstance),
   240  								&states.CheckResultObject{
   241  									Status:          checks.StatusFail,
   242  									FailureMessages: []string{"check failed"},
   243  								},
   244  							),
   245  						),
   246  					},
   247  				),
   248  			),
   249  		},
   250  		TargetAddrs: []addrs.Targetable{
   251  			addrs.Resource{
   252  				Mode: addrs.ManagedResourceMode,
   253  				Type: "test_thing",
   254  				Name: "woot",
   255  			}.Absolute(addrs.RootModuleInstance),
   256  		},
   257  		Backend: plans.Backend{
   258  			Type: "local",
   259  			Config: mustNewDynamicValue(
   260  				cty.ObjectVal(map[string]cty.Value{
   261  					"foo": cty.StringVal("bar"),
   262  				}),
   263  				cty.Object(map[string]cty.Type{
   264  					"foo": cty.String,
   265  				}),
   266  			),
   267  			Workspace: "default",
   268  		},
   269  	}
   270  
   271  	var buf bytes.Buffer
   272  	err := writeTfplan(plan, &buf)
   273  	if err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	newPlan, err := readTfplan(&buf)
   278  	if err != nil {
   279  		t.Fatal(err)
   280  	}
   281  
   282  	{
   283  		oldDepth := deep.MaxDepth
   284  		oldCompare := deep.CompareUnexportedFields
   285  		deep.MaxDepth = 20
   286  		deep.CompareUnexportedFields = true
   287  		defer func() {
   288  			deep.MaxDepth = oldDepth
   289  			deep.CompareUnexportedFields = oldCompare
   290  		}()
   291  	}
   292  	for _, problem := range deep.Equal(newPlan, plan) {
   293  		t.Error(problem)
   294  	}
   295  }
   296  
   297  func mustDynamicOutputValue(val string) plans.DynamicValue {
   298  	ret, err := plans.NewDynamicValue(cty.StringVal(val), cty.DynamicPseudoType)
   299  	if err != nil {
   300  		panic(err)
   301  	}
   302  	return ret
   303  }
   304  
   305  func mustNewDynamicValue(val cty.Value, ty cty.Type) plans.DynamicValue {
   306  	ret, err := plans.NewDynamicValue(val, ty)
   307  	if err != nil {
   308  		panic(err)
   309  	}
   310  	return ret
   311  }
   312  
   313  func mustNewDynamicValueStr(val string) plans.DynamicValue {
   314  	realVal := cty.StringVal(val)
   315  	ret, err := plans.NewDynamicValue(realVal, cty.String)
   316  	if err != nil {
   317  		panic(err)
   318  	}
   319  	return ret
   320  }
   321  
   322  // TestTFPlanRoundTripDestroy ensures that encoding and decoding null values for
   323  // destroy doesn't leave us with any nil values.
   324  func TestTFPlanRoundTripDestroy(t *testing.T) {
   325  	objTy := cty.Object(map[string]cty.Type{
   326  		"id": cty.String,
   327  	})
   328  
   329  	plan := &plans.Plan{
   330  		Changes: &plans.Changes{
   331  			Outputs: []*plans.OutputChangeSrc{
   332  				{
   333  					Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
   334  					ChangeSrc: plans.ChangeSrc{
   335  						Action: plans.Delete,
   336  						Before: mustDynamicOutputValue("output"),
   337  						After:  mustNewDynamicValue(cty.NullVal(cty.String), cty.String),
   338  					},
   339  				},
   340  			},
   341  			Resources: []*plans.ResourceInstanceChangeSrc{
   342  				{
   343  					Addr: addrs.Resource{
   344  						Mode: addrs.ManagedResourceMode,
   345  						Type: "test_thing",
   346  						Name: "woot",
   347  					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   348  					PrevRunAddr: addrs.Resource{
   349  						Mode: addrs.ManagedResourceMode,
   350  						Type: "test_thing",
   351  						Name: "woot",
   352  					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
   353  					ProviderAddr: addrs.AbsProviderConfig{
   354  						Provider: addrs.NewDefaultProvider("test"),
   355  						Module:   addrs.RootModule,
   356  					},
   357  					ChangeSrc: plans.ChangeSrc{
   358  						Action: plans.Delete,
   359  						Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{
   360  							"id": cty.StringVal("foo-bar-baz"),
   361  						}), objTy),
   362  						After: mustNewDynamicValue(cty.NullVal(objTy), objTy),
   363  					},
   364  				},
   365  			},
   366  		},
   367  		DriftedResources: []*plans.ResourceInstanceChangeSrc{},
   368  		TargetAddrs: []addrs.Targetable{
   369  			addrs.Resource{
   370  				Mode: addrs.ManagedResourceMode,
   371  				Type: "test_thing",
   372  				Name: "woot",
   373  			}.Absolute(addrs.RootModuleInstance),
   374  		},
   375  		Backend: plans.Backend{
   376  			Type: "local",
   377  			Config: mustNewDynamicValue(
   378  				cty.ObjectVal(map[string]cty.Value{
   379  					"foo": cty.StringVal("bar"),
   380  				}),
   381  				cty.Object(map[string]cty.Type{
   382  					"foo": cty.String,
   383  				}),
   384  			),
   385  			Workspace: "default",
   386  		},
   387  	}
   388  
   389  	var buf bytes.Buffer
   390  	err := writeTfplan(plan, &buf)
   391  	if err != nil {
   392  		t.Fatal(err)
   393  	}
   394  
   395  	newPlan, err := readTfplan(&buf)
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  
   400  	for _, rics := range newPlan.Changes.Resources {
   401  		ric, err := rics.Decode(objTy)
   402  		if err != nil {
   403  			t.Fatal(err)
   404  		}
   405  
   406  		if ric.After == cty.NilVal {
   407  			t.Fatalf("unexpected nil After value: %#v\n", ric)
   408  		}
   409  	}
   410  	for _, ocs := range newPlan.Changes.Outputs {
   411  		oc, err := ocs.Decode()
   412  		if err != nil {
   413  			t.Fatal(err)
   414  		}
   415  
   416  		if oc.After == cty.NilVal {
   417  			t.Fatalf("unexpected nil After value: %#v\n", ocs)
   418  		}
   419  	}
   420  }