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