github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/core_schema_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 schema 7 8 import ( 9 "fmt" 10 "testing" 11 12 "github.com/google/go-cmp/cmp" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/opentofu/opentofu/internal/configs/configschema" 16 ) 17 18 // add the implicit "id" attribute for test resources 19 func testResource(block *configschema.Block) *configschema.Block { 20 if block.Attributes == nil { 21 block.Attributes = make(map[string]*configschema.Attribute) 22 } 23 24 if block.BlockTypes == nil { 25 block.BlockTypes = make(map[string]*configschema.NestedBlock) 26 } 27 28 if block.Attributes["id"] == nil { 29 block.Attributes["id"] = &configschema.Attribute{ 30 Type: cty.String, 31 Optional: true, 32 Computed: true, 33 } 34 } 35 return block 36 } 37 38 func TestSchemaMapCoreConfigSchema(t *testing.T) { 39 tests := map[string]struct { 40 Schema map[string]*Schema 41 Want *configschema.Block 42 }{ 43 "empty": { 44 map[string]*Schema{}, 45 testResource(&configschema.Block{}), 46 }, 47 "primitives": { 48 map[string]*Schema{ 49 "int": { 50 Type: TypeInt, 51 Required: true, 52 Description: "foo bar baz", 53 }, 54 "float": { 55 Type: TypeFloat, 56 Optional: true, 57 }, 58 "bool": { 59 Type: TypeBool, 60 Computed: true, 61 }, 62 "string": { 63 Type: TypeString, 64 Optional: true, 65 Computed: true, 66 }, 67 }, 68 testResource(&configschema.Block{ 69 Attributes: map[string]*configschema.Attribute{ 70 "int": { 71 Type: cty.Number, 72 Required: true, 73 Description: "foo bar baz", 74 }, 75 "float": { 76 Type: cty.Number, 77 Optional: true, 78 }, 79 "bool": { 80 Type: cty.Bool, 81 Computed: true, 82 }, 83 "string": { 84 Type: cty.String, 85 Optional: true, 86 Computed: true, 87 }, 88 }, 89 BlockTypes: map[string]*configschema.NestedBlock{}, 90 }), 91 }, 92 "simple collections": { 93 map[string]*Schema{ 94 "list": { 95 Type: TypeList, 96 Required: true, 97 Elem: &Schema{ 98 Type: TypeInt, 99 }, 100 }, 101 "set": { 102 Type: TypeSet, 103 Optional: true, 104 Elem: &Schema{ 105 Type: TypeString, 106 }, 107 }, 108 "map": { 109 Type: TypeMap, 110 Optional: true, 111 Elem: &Schema{ 112 Type: TypeBool, 113 }, 114 }, 115 "map_default_type": { 116 Type: TypeMap, 117 Optional: true, 118 // Maps historically don't have elements because we 119 // assumed they would be strings, so this needs to work 120 // for pre-existing schemas. 121 }, 122 }, 123 testResource(&configschema.Block{ 124 Attributes: map[string]*configschema.Attribute{ 125 "list": { 126 Type: cty.List(cty.Number), 127 Required: true, 128 }, 129 "set": { 130 Type: cty.Set(cty.String), 131 Optional: true, 132 }, 133 "map": { 134 Type: cty.Map(cty.Bool), 135 Optional: true, 136 }, 137 "map_default_type": { 138 Type: cty.Map(cty.String), 139 Optional: true, 140 }, 141 }, 142 BlockTypes: map[string]*configschema.NestedBlock{}, 143 }), 144 }, 145 "incorrectly-specified collections": { 146 // Historically we tolerated setting a type directly as the Elem 147 // attribute, rather than a Schema object. This is common enough 148 // in existing provider code that we must support it as an alias 149 // for a schema object with the given type. 150 map[string]*Schema{ 151 "list": { 152 Type: TypeList, 153 Required: true, 154 Elem: TypeInt, 155 }, 156 "set": { 157 Type: TypeSet, 158 Optional: true, 159 Elem: TypeString, 160 }, 161 "map": { 162 Type: TypeMap, 163 Optional: true, 164 Elem: TypeBool, 165 }, 166 }, 167 testResource(&configschema.Block{ 168 Attributes: map[string]*configschema.Attribute{ 169 "list": { 170 Type: cty.List(cty.Number), 171 Required: true, 172 }, 173 "set": { 174 Type: cty.Set(cty.String), 175 Optional: true, 176 }, 177 "map": { 178 Type: cty.Map(cty.Bool), 179 Optional: true, 180 }, 181 }, 182 BlockTypes: map[string]*configschema.NestedBlock{}, 183 }), 184 }, 185 "sub-resource collections": { 186 map[string]*Schema{ 187 "list": { 188 Type: TypeList, 189 Required: true, 190 Elem: &Resource{ 191 Schema: map[string]*Schema{}, 192 }, 193 MinItems: 1, 194 MaxItems: 2, 195 }, 196 "set": { 197 Type: TypeSet, 198 Required: true, 199 Elem: &Resource{ 200 Schema: map[string]*Schema{}, 201 }, 202 }, 203 "map": { 204 Type: TypeMap, 205 Optional: true, 206 Elem: &Resource{ 207 Schema: map[string]*Schema{}, 208 }, 209 }, 210 }, 211 testResource(&configschema.Block{ 212 Attributes: map[string]*configschema.Attribute{ 213 // This one becomes a string attribute because helper/schema 214 // doesn't actually support maps of resource. The given 215 // "Elem" is just ignored entirely here, which is important 216 // because that is also true of the helper/schema logic and 217 // existing providers rely on this being ignored for 218 // correct operation. 219 "map": { 220 Type: cty.Map(cty.String), 221 Optional: true, 222 }, 223 }, 224 BlockTypes: map[string]*configschema.NestedBlock{ 225 "list": { 226 Nesting: configschema.NestingList, 227 Block: configschema.Block{}, 228 MinItems: 1, 229 MaxItems: 2, 230 }, 231 "set": { 232 Nesting: configschema.NestingSet, 233 Block: configschema.Block{}, 234 MinItems: 1, // because schema is Required 235 }, 236 }, 237 }), 238 }, 239 "sub-resource collections minitems+optional": { 240 // This particular case is an odd one where the provider gives 241 // conflicting information about whether a sub-resource is required, 242 // by marking it as optional but also requiring one item. 243 // Historically the optional-ness "won" here, and so we must 244 // honor that for compatibility with providers that relied on this 245 // undocumented interaction. 246 map[string]*Schema{ 247 "list": { 248 Type: TypeList, 249 Optional: true, 250 Elem: &Resource{ 251 Schema: map[string]*Schema{}, 252 }, 253 MinItems: 1, 254 MaxItems: 1, 255 }, 256 "set": { 257 Type: TypeSet, 258 Optional: true, 259 Elem: &Resource{ 260 Schema: map[string]*Schema{}, 261 }, 262 MinItems: 1, 263 MaxItems: 1, 264 }, 265 }, 266 testResource(&configschema.Block{ 267 Attributes: map[string]*configschema.Attribute{}, 268 BlockTypes: map[string]*configschema.NestedBlock{ 269 "list": { 270 Nesting: configschema.NestingList, 271 Block: configschema.Block{}, 272 MinItems: 0, 273 MaxItems: 1, 274 }, 275 "set": { 276 Nesting: configschema.NestingSet, 277 Block: configschema.Block{}, 278 MinItems: 0, 279 MaxItems: 1, 280 }, 281 }, 282 }), 283 }, 284 "sub-resource collections minitems+computed": { 285 map[string]*Schema{ 286 "list": { 287 Type: TypeList, 288 Computed: true, 289 Elem: &Resource{ 290 Schema: map[string]*Schema{}, 291 }, 292 MinItems: 1, 293 MaxItems: 1, 294 }, 295 "set": { 296 Type: TypeSet, 297 Computed: true, 298 Elem: &Resource{ 299 Schema: map[string]*Schema{}, 300 }, 301 MinItems: 1, 302 MaxItems: 1, 303 }, 304 }, 305 testResource(&configschema.Block{ 306 Attributes: map[string]*configschema.Attribute{ 307 "list": { 308 Type: cty.List(cty.EmptyObject), 309 Computed: true, 310 }, 311 "set": { 312 Type: cty.Set(cty.EmptyObject), 313 Computed: true, 314 }, 315 }, 316 }), 317 }, 318 "nested attributes and blocks": { 319 map[string]*Schema{ 320 "foo": { 321 Type: TypeList, 322 Required: true, 323 Elem: &Resource{ 324 Schema: map[string]*Schema{ 325 "bar": { 326 Type: TypeList, 327 Required: true, 328 Elem: &Schema{ 329 Type: TypeList, 330 Elem: &Schema{ 331 Type: TypeString, 332 }, 333 }, 334 }, 335 "baz": { 336 Type: TypeSet, 337 Optional: true, 338 Elem: &Resource{ 339 Schema: map[string]*Schema{}, 340 }, 341 }, 342 }, 343 }, 344 }, 345 }, 346 testResource(&configschema.Block{ 347 Attributes: map[string]*configschema.Attribute{}, 348 BlockTypes: map[string]*configschema.NestedBlock{ 349 "foo": &configschema.NestedBlock{ 350 Nesting: configschema.NestingList, 351 Block: configschema.Block{ 352 Attributes: map[string]*configschema.Attribute{ 353 "bar": { 354 Type: cty.List(cty.List(cty.String)), 355 Required: true, 356 }, 357 }, 358 BlockTypes: map[string]*configschema.NestedBlock{ 359 "baz": { 360 Nesting: configschema.NestingSet, 361 Block: configschema.Block{}, 362 }, 363 }, 364 }, 365 MinItems: 1, // because schema is Required 366 }, 367 }, 368 }), 369 }, 370 "sensitive": { 371 map[string]*Schema{ 372 "string": { 373 Type: TypeString, 374 Optional: true, 375 Sensitive: true, 376 }, 377 }, 378 testResource(&configschema.Block{ 379 Attributes: map[string]*configschema.Attribute{ 380 "string": { 381 Type: cty.String, 382 Optional: true, 383 Sensitive: true, 384 }, 385 }, 386 BlockTypes: map[string]*configschema.NestedBlock{}, 387 }), 388 }, 389 "conditionally required on": { 390 map[string]*Schema{ 391 "string": { 392 Type: TypeString, 393 Required: true, 394 DefaultFunc: func() (interface{}, error) { 395 return nil, nil 396 }, 397 }, 398 }, 399 testResource(&configschema.Block{ 400 Attributes: map[string]*configschema.Attribute{ 401 "string": { 402 Type: cty.String, 403 Required: true, 404 }, 405 }, 406 BlockTypes: map[string]*configschema.NestedBlock{}, 407 }), 408 }, 409 "conditionally required off": { 410 map[string]*Schema{ 411 "string": { 412 Type: TypeString, 413 Required: true, 414 DefaultFunc: func() (interface{}, error) { 415 // If we return a non-nil default then this overrides 416 // the "Required: true" for the purpose of building 417 // the core schema, so that core will ignore it not 418 // being set and let the provider handle it. 419 return "boop", nil 420 }, 421 }, 422 }, 423 testResource(&configschema.Block{ 424 Attributes: map[string]*configschema.Attribute{ 425 "string": { 426 Type: cty.String, 427 Optional: true, 428 }, 429 }, 430 BlockTypes: map[string]*configschema.NestedBlock{}, 431 }), 432 }, 433 "conditionally required error": { 434 map[string]*Schema{ 435 "string": { 436 Type: TypeString, 437 Required: true, 438 DefaultFunc: func() (interface{}, error) { 439 return nil, fmt.Errorf("placeholder error") 440 }, 441 }, 442 }, 443 testResource(&configschema.Block{ 444 Attributes: map[string]*configschema.Attribute{ 445 "string": { 446 Type: cty.String, 447 Optional: true, // Just so we can progress to provider-driven validation and return the error there 448 }, 449 }, 450 BlockTypes: map[string]*configschema.NestedBlock{}, 451 }), 452 }, 453 } 454 455 for name, test := range tests { 456 t.Run(name, func(t *testing.T) { 457 got := (&Resource{Schema: test.Schema}).CoreConfigSchema() 458 if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) { 459 t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer)) 460 } 461 }) 462 } 463 }