github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/blocktoattr/fixup_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package blocktoattr 5 6 import ( 7 "testing" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/ext/dynblock" 11 "github.com/hashicorp/hcl/v2/hcldec" 12 "github.com/hashicorp/hcl/v2/hclsyntax" 13 hcljson "github.com/hashicorp/hcl/v2/json" 14 "github.com/terramate-io/tf/configs/configschema" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 func TestFixUpBlockAttrs(t *testing.T) { 19 fooSchema := &configschema.Block{ 20 Attributes: map[string]*configschema.Attribute{ 21 "foo": { 22 Type: cty.List(cty.Object(map[string]cty.Type{ 23 "bar": cty.String, 24 })), 25 Optional: true, 26 }, 27 }, 28 } 29 30 tests := map[string]struct { 31 src string 32 json bool 33 schema *configschema.Block 34 want cty.Value 35 wantErrs bool 36 }{ 37 "empty": { 38 src: ``, 39 schema: &configschema.Block{}, 40 want: cty.EmptyObjectVal, 41 }, 42 "empty JSON": { 43 src: `{}`, 44 json: true, 45 schema: &configschema.Block{}, 46 want: cty.EmptyObjectVal, 47 }, 48 "unset": { 49 src: ``, 50 schema: fooSchema, 51 want: cty.ObjectVal(map[string]cty.Value{ 52 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 53 }), 54 }, 55 "unset JSON": { 56 src: `{}`, 57 json: true, 58 schema: fooSchema, 59 want: cty.ObjectVal(map[string]cty.Value{ 60 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 61 }), 62 }, 63 "no fixup required, with one value": { 64 src: ` 65 foo = [ 66 { 67 bar = "baz" 68 }, 69 ] 70 `, 71 schema: fooSchema, 72 want: cty.ObjectVal(map[string]cty.Value{ 73 "foo": cty.ListVal([]cty.Value{ 74 cty.ObjectVal(map[string]cty.Value{ 75 "bar": cty.StringVal("baz"), 76 }), 77 }), 78 }), 79 }, 80 "no fixup required, with two values": { 81 src: ` 82 foo = [ 83 { 84 bar = "baz" 85 }, 86 { 87 bar = "boop" 88 }, 89 ] 90 `, 91 schema: fooSchema, 92 want: cty.ObjectVal(map[string]cty.Value{ 93 "foo": cty.ListVal([]cty.Value{ 94 cty.ObjectVal(map[string]cty.Value{ 95 "bar": cty.StringVal("baz"), 96 }), 97 cty.ObjectVal(map[string]cty.Value{ 98 "bar": cty.StringVal("boop"), 99 }), 100 }), 101 }), 102 }, 103 "no fixup required, with values, JSON": { 104 src: `{"foo": [{"bar": "baz"}]}`, 105 json: true, 106 schema: fooSchema, 107 want: cty.ObjectVal(map[string]cty.Value{ 108 "foo": cty.ListVal([]cty.Value{ 109 cty.ObjectVal(map[string]cty.Value{ 110 "bar": cty.StringVal("baz"), 111 }), 112 }), 113 }), 114 }, 115 "no fixup required, empty": { 116 src: ` 117 foo = [] 118 `, 119 schema: fooSchema, 120 want: cty.ObjectVal(map[string]cty.Value{ 121 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 122 }), 123 }, 124 "no fixup required, empty, JSON": { 125 src: `{"foo":[]}`, 126 json: true, 127 schema: fooSchema, 128 want: cty.ObjectVal(map[string]cty.Value{ 129 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 130 }), 131 }, 132 "fixup one block": { 133 src: ` 134 foo { 135 bar = "baz" 136 } 137 `, 138 schema: fooSchema, 139 want: cty.ObjectVal(map[string]cty.Value{ 140 "foo": cty.ListVal([]cty.Value{ 141 cty.ObjectVal(map[string]cty.Value{ 142 "bar": cty.StringVal("baz"), 143 }), 144 }), 145 }), 146 }, 147 "fixup one block omitting attribute": { 148 src: ` 149 foo {} 150 `, 151 schema: fooSchema, 152 want: cty.ObjectVal(map[string]cty.Value{ 153 "foo": cty.ListVal([]cty.Value{ 154 cty.ObjectVal(map[string]cty.Value{ 155 "bar": cty.NullVal(cty.String), 156 }), 157 }), 158 }), 159 }, 160 "fixup two blocks": { 161 src: ` 162 foo { 163 bar = baz 164 } 165 foo { 166 bar = "boop" 167 } 168 `, 169 schema: fooSchema, 170 want: cty.ObjectVal(map[string]cty.Value{ 171 "foo": cty.ListVal([]cty.Value{ 172 cty.ObjectVal(map[string]cty.Value{ 173 "bar": cty.StringVal("baz value"), 174 }), 175 cty.ObjectVal(map[string]cty.Value{ 176 "bar": cty.StringVal("boop"), 177 }), 178 }), 179 }), 180 }, 181 "interaction with dynamic block generation": { 182 src: ` 183 dynamic "foo" { 184 for_each = ["baz", beep] 185 content { 186 bar = foo.value 187 } 188 } 189 `, 190 schema: fooSchema, 191 want: cty.ObjectVal(map[string]cty.Value{ 192 "foo": cty.ListVal([]cty.Value{ 193 cty.ObjectVal(map[string]cty.Value{ 194 "bar": cty.StringVal("baz"), 195 }), 196 cty.ObjectVal(map[string]cty.Value{ 197 "bar": cty.StringVal("beep value"), 198 }), 199 }), 200 }), 201 }, 202 "dynamic block with empty iterator": { 203 src: ` 204 dynamic "foo" { 205 for_each = [] 206 content { 207 bar = foo.value 208 } 209 } 210 `, 211 schema: fooSchema, 212 want: cty.ObjectVal(map[string]cty.Value{ 213 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 214 }), 215 }, 216 "both attribute and block syntax": { 217 src: ` 218 foo = [] 219 foo { 220 bar = "baz" 221 } 222 `, 223 schema: fooSchema, 224 wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute) 225 want: cty.ObjectVal(map[string]cty.Value{ 226 "foo": cty.ListVal([]cty.Value{ 227 cty.ObjectVal(map[string]cty.Value{ 228 "bar": cty.StringVal("baz"), 229 }), 230 cty.ObjectVal(map[string]cty.Value{ 231 "bar": cty.StringVal("boop"), 232 }), 233 }), 234 }), 235 }, 236 "fixup inside block": { 237 src: ` 238 container { 239 foo { 240 bar = "baz" 241 } 242 foo { 243 bar = "boop" 244 } 245 } 246 container { 247 foo { 248 bar = beep 249 } 250 } 251 `, 252 schema: &configschema.Block{ 253 BlockTypes: map[string]*configschema.NestedBlock{ 254 "container": { 255 Nesting: configschema.NestingList, 256 Block: *fooSchema, 257 }, 258 }, 259 }, 260 want: cty.ObjectVal(map[string]cty.Value{ 261 "container": cty.ListVal([]cty.Value{ 262 cty.ObjectVal(map[string]cty.Value{ 263 "foo": cty.ListVal([]cty.Value{ 264 cty.ObjectVal(map[string]cty.Value{ 265 "bar": cty.StringVal("baz"), 266 }), 267 cty.ObjectVal(map[string]cty.Value{ 268 "bar": cty.StringVal("boop"), 269 }), 270 }), 271 }), 272 cty.ObjectVal(map[string]cty.Value{ 273 "foo": cty.ListVal([]cty.Value{ 274 cty.ObjectVal(map[string]cty.Value{ 275 "bar": cty.StringVal("beep value"), 276 }), 277 }), 278 }), 279 }), 280 }), 281 }, 282 "fixup inside attribute-as-block": { 283 src: ` 284 container { 285 foo { 286 bar = "baz" 287 } 288 foo { 289 bar = "boop" 290 } 291 } 292 container { 293 foo { 294 bar = beep 295 } 296 } 297 `, 298 schema: &configschema.Block{ 299 Attributes: map[string]*configschema.Attribute{ 300 "container": { 301 Type: cty.List(cty.Object(map[string]cty.Type{ 302 "foo": cty.List(cty.Object(map[string]cty.Type{ 303 "bar": cty.String, 304 })), 305 })), 306 Optional: true, 307 }, 308 }, 309 }, 310 want: cty.ObjectVal(map[string]cty.Value{ 311 "container": cty.ListVal([]cty.Value{ 312 cty.ObjectVal(map[string]cty.Value{ 313 "foo": cty.ListVal([]cty.Value{ 314 cty.ObjectVal(map[string]cty.Value{ 315 "bar": cty.StringVal("baz"), 316 }), 317 cty.ObjectVal(map[string]cty.Value{ 318 "bar": cty.StringVal("boop"), 319 }), 320 }), 321 }), 322 cty.ObjectVal(map[string]cty.Value{ 323 "foo": cty.ListVal([]cty.Value{ 324 cty.ObjectVal(map[string]cty.Value{ 325 "bar": cty.StringVal("beep value"), 326 }), 327 }), 328 }), 329 }), 330 }), 331 }, 332 "nested fixup with dynamic block generation": { 333 src: ` 334 container { 335 dynamic "foo" { 336 for_each = ["baz", beep] 337 content { 338 bar = foo.value 339 } 340 } 341 } 342 `, 343 schema: &configschema.Block{ 344 BlockTypes: map[string]*configschema.NestedBlock{ 345 "container": { 346 Nesting: configschema.NestingList, 347 Block: *fooSchema, 348 }, 349 }, 350 }, 351 want: cty.ObjectVal(map[string]cty.Value{ 352 "container": cty.ListVal([]cty.Value{ 353 cty.ObjectVal(map[string]cty.Value{ 354 "foo": cty.ListVal([]cty.Value{ 355 cty.ObjectVal(map[string]cty.Value{ 356 "bar": cty.StringVal("baz"), 357 }), 358 cty.ObjectVal(map[string]cty.Value{ 359 "bar": cty.StringVal("beep value"), 360 }), 361 }), 362 }), 363 }), 364 }), 365 }, 366 367 "missing nested block items": { 368 src: ` 369 container { 370 foo { 371 bar = "one" 372 } 373 } 374 `, 375 schema: &configschema.Block{ 376 BlockTypes: map[string]*configschema.NestedBlock{ 377 "container": { 378 Nesting: configschema.NestingList, 379 MinItems: 2, 380 Block: configschema.Block{ 381 Attributes: map[string]*configschema.Attribute{ 382 "foo": { 383 Type: cty.List(cty.Object(map[string]cty.Type{ 384 "bar": cty.String, 385 })), 386 Optional: true, 387 }, 388 }, 389 }, 390 }, 391 }, 392 }, 393 want: cty.ObjectVal(map[string]cty.Value{ 394 "container": cty.ListVal([]cty.Value{ 395 cty.ObjectVal(map[string]cty.Value{ 396 "foo": cty.ListVal([]cty.Value{ 397 cty.ObjectVal(map[string]cty.Value{ 398 "bar": cty.StringVal("baz"), 399 }), 400 }), 401 }), 402 }), 403 }), 404 wantErrs: true, 405 }, 406 "no fixup allowed with NestedType": { 407 src: ` 408 container { 409 foo = "one" 410 } 411 `, 412 schema: &configschema.Block{ 413 Attributes: map[string]*configschema.Attribute{ 414 "container": { 415 NestedType: &configschema.Object{ 416 Nesting: configschema.NestingList, 417 Attributes: map[string]*configschema.Attribute{ 418 "foo": { 419 Type: cty.String, 420 }, 421 }, 422 }, 423 }, 424 }, 425 }, 426 want: cty.ObjectVal(map[string]cty.Value{ 427 "container": cty.NullVal(cty.List( 428 cty.Object(map[string]cty.Type{ 429 "foo": cty.String, 430 }), 431 )), 432 }), 433 wantErrs: true, 434 }, 435 "no fixup allowed new types": { 436 src: ` 437 container { 438 foo = "one" 439 } 440 `, 441 schema: &configschema.Block{ 442 Attributes: map[string]*configschema.Attribute{ 443 // This could be a ConfigModeAttr fixup 444 "container": { 445 Type: cty.List(cty.Object(map[string]cty.Type{ 446 "foo": cty.String, 447 })), 448 }, 449 // But the presence of this type means it must have been 450 // declared by a new SDK 451 "new_type": { 452 Type: cty.Object(map[string]cty.Type{ 453 "boo": cty.String, 454 }), 455 }, 456 }, 457 }, 458 want: cty.ObjectVal(map[string]cty.Value{ 459 "container": cty.NullVal(cty.List( 460 cty.Object(map[string]cty.Type{ 461 "foo": cty.String, 462 }), 463 )), 464 }), 465 wantErrs: true, 466 }, 467 } 468 469 ctx := &hcl.EvalContext{ 470 Variables: map[string]cty.Value{ 471 "bar": cty.StringVal("bar value"), 472 "baz": cty.StringVal("baz value"), 473 "beep": cty.StringVal("beep value"), 474 }, 475 } 476 477 for name, test := range tests { 478 t.Run(name, func(t *testing.T) { 479 var f *hcl.File 480 var diags hcl.Diagnostics 481 if test.json { 482 f, diags = hcljson.Parse([]byte(test.src), "test.tf.json") 483 } else { 484 f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1}) 485 } 486 if diags.HasErrors() { 487 for _, diag := range diags { 488 t.Errorf("unexpected diagnostic: %s", diag) 489 } 490 t.FailNow() 491 } 492 493 // We'll expand dynamic blocks in the body first, to mimic how 494 // we process this fixup when using the main "lang" package API. 495 spec := test.schema.DecoderSpec() 496 body := dynblock.Expand(f.Body, ctx) 497 498 body = FixUpBlockAttrs(body, test.schema) 499 got, diags := hcldec.Decode(body, spec, ctx) 500 501 if test.wantErrs { 502 if !diags.HasErrors() { 503 t.Errorf("succeeded, but want error\ngot: %#v", got) 504 } 505 506 // check that our wrapped body returns the correct context by 507 // verifying the Subject is valid. 508 for _, d := range diags { 509 if d.Subject.Filename == "" { 510 t.Errorf("empty diagnostic subject: %#v", d.Subject) 511 } 512 } 513 return 514 } 515 516 if !test.want.RawEquals(got) { 517 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 518 } 519 for _, diag := range diags { 520 t.Errorf("unexpected diagnostic: %s", diag) 521 } 522 }) 523 } 524 }