github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/lang/eval_test.go (about) 1 package lang 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "testing" 7 8 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 9 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/hclsyntax" 13 14 "github.com/zclconf/go-cty/cty" 15 ctyjson "github.com/zclconf/go-cty/cty/json" 16 ) 17 18 func TestScopeEvalContext(t *testing.T) { 19 data := &dataForTests{ 20 CountAttrs: map[string]cty.Value{ 21 "index": cty.NumberIntVal(0), 22 }, 23 ForEachAttrs: map[string]cty.Value{ 24 "key": cty.StringVal("a"), 25 "value": cty.NumberIntVal(1), 26 }, 27 Resources: map[string]cty.Value{ 28 "null_resource.foo": cty.ObjectVal(map[string]cty.Value{ 29 "attr": cty.StringVal("bar"), 30 }), 31 "data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{ 32 "attr": cty.StringVal("bar"), 33 }), 34 "null_resource.multi": cty.TupleVal([]cty.Value{ 35 cty.ObjectVal(map[string]cty.Value{ 36 "attr": cty.StringVal("multi0"), 37 }), 38 cty.ObjectVal(map[string]cty.Value{ 39 "attr": cty.StringVal("multi1"), 40 }), 41 }), 42 "null_resource.each": cty.ObjectVal(map[string]cty.Value{ 43 "each0": cty.ObjectVal(map[string]cty.Value{ 44 "attr": cty.StringVal("each0"), 45 }), 46 "each1": cty.ObjectVal(map[string]cty.Value{ 47 "attr": cty.StringVal("each1"), 48 }), 49 }), 50 "null_resource.multi[1]": cty.ObjectVal(map[string]cty.Value{ 51 "attr": cty.StringVal("multi1"), 52 }), 53 }, 54 LocalValues: map[string]cty.Value{ 55 "foo": cty.StringVal("bar"), 56 }, 57 Modules: map[string]cty.Value{ 58 "module.foo": cty.ObjectVal(map[string]cty.Value{ 59 "output0": cty.StringVal("bar0"), 60 "output1": cty.StringVal("bar1"), 61 }), 62 }, 63 PathAttrs: map[string]cty.Value{ 64 "module": cty.StringVal("foo/bar"), 65 }, 66 TerraformAttrs: map[string]cty.Value{ 67 "workspace": cty.StringVal("default"), 68 }, 69 InputVariables: map[string]cty.Value{ 70 "baz": cty.StringVal("boop"), 71 }, 72 } 73 74 tests := []struct { 75 Expr string 76 Want map[string]cty.Value 77 }{ 78 { 79 `12`, 80 map[string]cty.Value{}, 81 }, 82 { 83 `count.index`, 84 map[string]cty.Value{ 85 "count": cty.ObjectVal(map[string]cty.Value{ 86 "index": cty.NumberIntVal(0), 87 }), 88 }, 89 }, 90 { 91 `each.key`, 92 map[string]cty.Value{ 93 "each": cty.ObjectVal(map[string]cty.Value{ 94 "key": cty.StringVal("a"), 95 }), 96 }, 97 }, 98 { 99 `each.value`, 100 map[string]cty.Value{ 101 "each": cty.ObjectVal(map[string]cty.Value{ 102 "value": cty.NumberIntVal(1), 103 }), 104 }, 105 }, 106 { 107 `local.foo`, 108 map[string]cty.Value{ 109 "local": cty.ObjectVal(map[string]cty.Value{ 110 "foo": cty.StringVal("bar"), 111 }), 112 }, 113 }, 114 { 115 `null_resource.foo`, 116 map[string]cty.Value{ 117 "null_resource": cty.ObjectVal(map[string]cty.Value{ 118 "foo": cty.ObjectVal(map[string]cty.Value{ 119 "attr": cty.StringVal("bar"), 120 }), 121 }), 122 }, 123 }, 124 { 125 `null_resource.foo.attr`, 126 map[string]cty.Value{ 127 "null_resource": cty.ObjectVal(map[string]cty.Value{ 128 "foo": cty.ObjectVal(map[string]cty.Value{ 129 "attr": cty.StringVal("bar"), 130 }), 131 }), 132 }, 133 }, 134 { 135 `null_resource.multi`, 136 map[string]cty.Value{ 137 "null_resource": cty.ObjectVal(map[string]cty.Value{ 138 "multi": cty.TupleVal([]cty.Value{ 139 cty.ObjectVal(map[string]cty.Value{ 140 "attr": cty.StringVal("multi0"), 141 }), 142 cty.ObjectVal(map[string]cty.Value{ 143 "attr": cty.StringVal("multi1"), 144 }), 145 }), 146 }), 147 }, 148 }, 149 { 150 // at this level, all instance references return the entire resource 151 `null_resource.multi[1]`, 152 map[string]cty.Value{ 153 "null_resource": cty.ObjectVal(map[string]cty.Value{ 154 "multi": cty.TupleVal([]cty.Value{ 155 cty.ObjectVal(map[string]cty.Value{ 156 "attr": cty.StringVal("multi0"), 157 }), 158 cty.ObjectVal(map[string]cty.Value{ 159 "attr": cty.StringVal("multi1"), 160 }), 161 }), 162 }), 163 }, 164 }, 165 { 166 // at this level, all instance references return the entire resource 167 `null_resource.each["each1"]`, 168 map[string]cty.Value{ 169 "null_resource": cty.ObjectVal(map[string]cty.Value{ 170 "each": cty.ObjectVal(map[string]cty.Value{ 171 "each0": cty.ObjectVal(map[string]cty.Value{ 172 "attr": cty.StringVal("each0"), 173 }), 174 "each1": cty.ObjectVal(map[string]cty.Value{ 175 "attr": cty.StringVal("each1"), 176 }), 177 }), 178 }), 179 }, 180 }, 181 { 182 `foo(null_resource.multi, null_resource.multi[1])`, 183 map[string]cty.Value{ 184 "null_resource": cty.ObjectVal(map[string]cty.Value{ 185 "multi": cty.TupleVal([]cty.Value{ 186 cty.ObjectVal(map[string]cty.Value{ 187 "attr": cty.StringVal("multi0"), 188 }), 189 cty.ObjectVal(map[string]cty.Value{ 190 "attr": cty.StringVal("multi1"), 191 }), 192 }), 193 }), 194 }, 195 }, 196 { 197 `data.null_data_source.foo`, 198 map[string]cty.Value{ 199 "data": cty.ObjectVal(map[string]cty.Value{ 200 "null_data_source": cty.ObjectVal(map[string]cty.Value{ 201 "foo": cty.ObjectVal(map[string]cty.Value{ 202 "attr": cty.StringVal("bar"), 203 }), 204 }), 205 }), 206 }, 207 }, 208 { 209 `module.foo`, 210 map[string]cty.Value{ 211 "module": cty.ObjectVal(map[string]cty.Value{ 212 "foo": cty.ObjectVal(map[string]cty.Value{ 213 "output0": cty.StringVal("bar0"), 214 "output1": cty.StringVal("bar1"), 215 }), 216 }), 217 }, 218 }, 219 { 220 `module.foo.output1`, 221 map[string]cty.Value{ 222 "module": cty.ObjectVal(map[string]cty.Value{ 223 "foo": cty.ObjectVal(map[string]cty.Value{ 224 "output1": cty.StringVal("bar1"), 225 }), 226 }), 227 }, 228 }, 229 { 230 `path.module`, 231 map[string]cty.Value{ 232 "path": cty.ObjectVal(map[string]cty.Value{ 233 "module": cty.StringVal("foo/bar"), 234 }), 235 }, 236 }, 237 { 238 `self.baz`, 239 map[string]cty.Value{ 240 "self": cty.ObjectVal(map[string]cty.Value{ 241 "attr": cty.StringVal("multi1"), 242 }), 243 }, 244 }, 245 { 246 `terraform.workspace`, 247 map[string]cty.Value{ 248 "terraform": cty.ObjectVal(map[string]cty.Value{ 249 "workspace": cty.StringVal("default"), 250 }), 251 }, 252 }, 253 { 254 `var.baz`, 255 map[string]cty.Value{ 256 "var": cty.ObjectVal(map[string]cty.Value{ 257 "baz": cty.StringVal("boop"), 258 }), 259 }, 260 }, 261 } 262 263 for _, test := range tests { 264 t.Run(test.Expr, func(t *testing.T) { 265 expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1}) 266 if len(parseDiags) != 0 { 267 t.Errorf("unexpected diagnostics during parse") 268 for _, diag := range parseDiags { 269 t.Errorf("- %s", diag) 270 } 271 return 272 } 273 274 refs, refsDiags := ReferencesInExpr(expr) 275 if refsDiags.HasErrors() { 276 t.Fatal(refsDiags.Err()) 277 } 278 279 scope := &Scope{ 280 Data: data, 281 282 // "self" will just be an arbitrary one of the several resource 283 // instances we have in our test dataset. 284 SelfAddr: addrs.ResourceInstance{ 285 Resource: addrs.Resource{ 286 Mode: addrs.ManagedResourceMode, 287 Type: "null_resource", 288 Name: "multi", 289 }, 290 Key: addrs.IntKey(1), 291 }, 292 } 293 ctx, ctxDiags := scope.EvalContext(refs) 294 if ctxDiags.HasErrors() { 295 t.Fatal(ctxDiags.Err()) 296 } 297 298 // For easier test assertions we'll just remove any top-level 299 // empty objects from our variables map. 300 for k, v := range ctx.Variables { 301 if v.RawEquals(cty.EmptyObjectVal) { 302 delete(ctx.Variables, k) 303 } 304 } 305 306 gotVal := cty.ObjectVal(ctx.Variables) 307 wantVal := cty.ObjectVal(test.Want) 308 309 if !gotVal.RawEquals(wantVal) { 310 // We'll JSON-ize our values here just so it's easier to 311 // read them in the assertion output. 312 gotJSON := formattedJSONValue(gotVal) 313 wantJSON := formattedJSONValue(wantVal) 314 315 t.Errorf( 316 "wrong result\nexpr: %s\ngot: %s\nwant: %s", 317 test.Expr, gotJSON, wantJSON, 318 ) 319 } 320 }) 321 } 322 } 323 324 func TestScopeExpandEvalBlock(t *testing.T) { 325 nestedObjTy := cty.Object(map[string]cty.Type{ 326 "boop": cty.String, 327 }) 328 schema := &configschema.Block{ 329 Attributes: map[string]*configschema.Attribute{ 330 "foo": {Type: cty.String, Optional: true}, 331 "list_of_obj": {Type: cty.List(nestedObjTy), Optional: true}, 332 }, 333 BlockTypes: map[string]*configschema.NestedBlock{ 334 "bar": { 335 Nesting: configschema.NestingMap, 336 Block: configschema.Block{ 337 Attributes: map[string]*configschema.Attribute{ 338 "baz": {Type: cty.String, Optional: true}, 339 }, 340 }, 341 }, 342 }, 343 } 344 data := &dataForTests{ 345 LocalValues: map[string]cty.Value{ 346 "greeting": cty.StringVal("howdy"), 347 "list": cty.ListVal([]cty.Value{ 348 cty.StringVal("elem0"), 349 cty.StringVal("elem1"), 350 }), 351 "map": cty.MapVal(map[string]cty.Value{ 352 "key1": cty.StringVal("val1"), 353 "key2": cty.StringVal("val2"), 354 }), 355 }, 356 } 357 358 tests := map[string]struct { 359 Config string 360 Want cty.Value 361 }{ 362 "empty": { 363 ` 364 `, 365 cty.ObjectVal(map[string]cty.Value{ 366 "foo": cty.NullVal(cty.String), 367 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 368 "bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 369 "baz": cty.String, 370 })), 371 }), 372 }, 373 "literal attribute": { 374 ` 375 foo = "hello" 376 `, 377 cty.ObjectVal(map[string]cty.Value{ 378 "foo": cty.StringVal("hello"), 379 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 380 "bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 381 "baz": cty.String, 382 })), 383 }), 384 }, 385 "variable attribute": { 386 ` 387 foo = local.greeting 388 `, 389 cty.ObjectVal(map[string]cty.Value{ 390 "foo": cty.StringVal("howdy"), 391 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 392 "bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 393 "baz": cty.String, 394 })), 395 }), 396 }, 397 "one static block": { 398 ` 399 bar "static" {} 400 `, 401 cty.ObjectVal(map[string]cty.Value{ 402 "foo": cty.NullVal(cty.String), 403 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 404 "bar": cty.MapVal(map[string]cty.Value{ 405 "static": cty.ObjectVal(map[string]cty.Value{ 406 "baz": cty.NullVal(cty.String), 407 }), 408 }), 409 }), 410 }, 411 "two static blocks": { 412 ` 413 bar "static0" { 414 baz = 0 415 } 416 bar "static1" { 417 baz = 1 418 } 419 `, 420 cty.ObjectVal(map[string]cty.Value{ 421 "foo": cty.NullVal(cty.String), 422 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 423 "bar": cty.MapVal(map[string]cty.Value{ 424 "static0": cty.ObjectVal(map[string]cty.Value{ 425 "baz": cty.StringVal("0"), 426 }), 427 "static1": cty.ObjectVal(map[string]cty.Value{ 428 "baz": cty.StringVal("1"), 429 }), 430 }), 431 }), 432 }, 433 "dynamic blocks from list": { 434 ` 435 dynamic "bar" { 436 for_each = local.list 437 labels = [bar.value] 438 content { 439 baz = bar.key 440 } 441 } 442 `, 443 cty.ObjectVal(map[string]cty.Value{ 444 "foo": cty.NullVal(cty.String), 445 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 446 "bar": cty.MapVal(map[string]cty.Value{ 447 "elem0": cty.ObjectVal(map[string]cty.Value{ 448 "baz": cty.StringVal("0"), 449 }), 450 "elem1": cty.ObjectVal(map[string]cty.Value{ 451 "baz": cty.StringVal("1"), 452 }), 453 }), 454 }), 455 }, 456 "dynamic blocks from map": { 457 ` 458 dynamic "bar" { 459 for_each = local.map 460 labels = [bar.key] 461 content { 462 baz = bar.value 463 } 464 } 465 `, 466 cty.ObjectVal(map[string]cty.Value{ 467 "foo": cty.NullVal(cty.String), 468 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 469 "bar": cty.MapVal(map[string]cty.Value{ 470 "key1": cty.ObjectVal(map[string]cty.Value{ 471 "baz": cty.StringVal("val1"), 472 }), 473 "key2": cty.ObjectVal(map[string]cty.Value{ 474 "baz": cty.StringVal("val2"), 475 }), 476 }), 477 }), 478 }, 479 "list-of-object attribute": { 480 ` 481 list_of_obj = [ 482 { 483 boop = local.greeting 484 }, 485 ] 486 `, 487 cty.ObjectVal(map[string]cty.Value{ 488 "foo": cty.NullVal(cty.String), 489 "list_of_obj": cty.ListVal([]cty.Value{ 490 cty.ObjectVal(map[string]cty.Value{ 491 "boop": cty.StringVal("howdy"), 492 }), 493 }), 494 "bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 495 "baz": cty.String, 496 })), 497 }), 498 }, 499 "list-of-object attribute as blocks": { 500 ` 501 list_of_obj { 502 boop = local.greeting 503 } 504 `, 505 cty.ObjectVal(map[string]cty.Value{ 506 "foo": cty.NullVal(cty.String), 507 "list_of_obj": cty.ListVal([]cty.Value{ 508 cty.ObjectVal(map[string]cty.Value{ 509 "boop": cty.StringVal("howdy"), 510 }), 511 }), 512 "bar": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 513 "baz": cty.String, 514 })), 515 }), 516 }, 517 "lots of things at once": { 518 ` 519 foo = "whoop" 520 bar "static0" { 521 baz = "s0" 522 } 523 dynamic "bar" { 524 for_each = local.list 525 labels = [bar.value] 526 content { 527 baz = bar.key 528 } 529 } 530 bar "static1" { 531 baz = "s1" 532 } 533 dynamic "bar" { 534 for_each = local.map 535 labels = [bar.key] 536 content { 537 baz = bar.value 538 } 539 } 540 bar "static2" { 541 baz = "s2" 542 } 543 `, 544 cty.ObjectVal(map[string]cty.Value{ 545 "foo": cty.StringVal("whoop"), 546 "list_of_obj": cty.NullVal(cty.List(nestedObjTy)), 547 "bar": cty.MapVal(map[string]cty.Value{ 548 "key1": cty.ObjectVal(map[string]cty.Value{ 549 "baz": cty.StringVal("val1"), 550 }), 551 "key2": cty.ObjectVal(map[string]cty.Value{ 552 "baz": cty.StringVal("val2"), 553 }), 554 "elem0": cty.ObjectVal(map[string]cty.Value{ 555 "baz": cty.StringVal("0"), 556 }), 557 "elem1": cty.ObjectVal(map[string]cty.Value{ 558 "baz": cty.StringVal("1"), 559 }), 560 "static0": cty.ObjectVal(map[string]cty.Value{ 561 "baz": cty.StringVal("s0"), 562 }), 563 "static1": cty.ObjectVal(map[string]cty.Value{ 564 "baz": cty.StringVal("s1"), 565 }), 566 "static2": cty.ObjectVal(map[string]cty.Value{ 567 "baz": cty.StringVal("s2"), 568 }), 569 }), 570 }), 571 }, 572 } 573 574 for name, test := range tests { 575 t.Run(name, func(t *testing.T) { 576 file, parseDiags := hclsyntax.ParseConfig([]byte(test.Config), "", hcl.Pos{Line: 1, Column: 1}) 577 if len(parseDiags) != 0 { 578 t.Errorf("unexpected diagnostics during parse") 579 for _, diag := range parseDiags { 580 t.Errorf("- %s", diag) 581 } 582 return 583 } 584 585 body := file.Body 586 scope := &Scope{ 587 Data: data, 588 } 589 590 body, expandDiags := scope.ExpandBlock(body, schema) 591 if expandDiags.HasErrors() { 592 t.Fatal(expandDiags.Err()) 593 } 594 595 got, valDiags := scope.EvalBlock(body, schema) 596 if valDiags.HasErrors() { 597 t.Fatal(valDiags.Err()) 598 } 599 600 if !got.RawEquals(test.Want) { 601 // We'll JSON-ize our values here just so it's easier to 602 // read them in the assertion output. 603 gotJSON := formattedJSONValue(got) 604 wantJSON := formattedJSONValue(test.Want) 605 606 t.Errorf( 607 "wrong result\nconfig: %s\ngot: %s\nwant: %s", 608 test.Config, gotJSON, wantJSON, 609 ) 610 } 611 612 }) 613 } 614 615 } 616 617 func formattedJSONValue(val cty.Value) string { 618 val = cty.UnknownAsNull(val) // since JSON can't represent unknowns 619 j, err := ctyjson.Marshal(val, val.Type()) 620 if err != nil { 621 panic(err) 622 } 623 var buf bytes.Buffer 624 json.Indent(&buf, j, "", " ") 625 return buf.String() 626 }