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 }