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 }