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