github.com/opentofu/opentofu@v1.7.1/internal/configs/escaping_blocks_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 configs 7 8 import ( 9 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/hashicorp/hcl/v2" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 // "Escaping Blocks" are a special mechanism we have inside our block types 17 // that accept a mixture of meta-arguments and externally-defined arguments, 18 // which allow an author to force particular argument names to be interpreted 19 // as externally-defined even if they have the same name as a meta-argument. 20 // 21 // An escaping block is a block with the special type name "_" (just an 22 // underscore), and is allowed at the top-level of any resource, data, or 23 // module block. It intentionally has a rather "odd" look so that it stands 24 // out as something special and rare. 25 // 26 // This is not something we expect to see used a lot, but it's an important 27 // part of our strategy to evolve the OpenTofu language in future using 28 // editions, so that later editions can define new meta-arguments without 29 // blocking access to externally-defined arguments of the same name. 30 // 31 // We should still define new meta-arguments with care to avoid squatting on 32 // commonly-used names, but we can't see all modules and all providers in 33 // the world and so this is an escape hatch for edge cases. Module migration 34 // tools for future editions that define new meta-arguments should detect 35 // collisions and automatically migrate existing arguments into an escaping 36 // block. 37 38 func TestEscapingBlockResource(t *testing.T) { 39 // (this also tests escaping blocks in provisioner blocks, because 40 // they only appear nested inside resource blocks.) 41 42 parser := NewParser(nil) 43 mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/resource") 44 assertNoDiagnostics(t, diags) 45 if mod == nil { 46 t.Fatal("got nil root module; want non-nil") 47 } 48 49 rc := mod.ManagedResources["foo.bar"] 50 if rc == nil { 51 t.Fatal("no managed resource named foo.bar") 52 } 53 54 t.Run("resource body", func(t *testing.T) { 55 if got := rc.Count; got == nil { 56 t.Errorf("count not set; want count = 2") 57 } else { 58 got, diags := got.Value(nil) 59 assertNoDiagnostics(t, diags) 60 if want := cty.NumberIntVal(2); !want.RawEquals(got) { 61 t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) 62 } 63 } 64 if got, want := rc.ForEach, hcl.Expression(nil); got != want { 65 // Shouldn't have any count because our test fixture only has 66 // for_each in the escaping block. 67 t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) 68 } 69 70 schema := &hcl.BodySchema{ 71 Attributes: []hcl.AttributeSchema{ 72 {Name: "normal", Required: true}, 73 {Name: "count", Required: true}, 74 {Name: "for_each", Required: true}, 75 }, 76 Blocks: []hcl.BlockHeaderSchema{ 77 {Type: "normal_block"}, 78 {Type: "lifecycle"}, 79 {Type: "_"}, 80 }, 81 } 82 content, diags := rc.Config.Content(schema) 83 assertNoDiagnostics(t, diags) 84 85 normalVal, diags := content.Attributes["normal"].Expr.Value(nil) 86 assertNoDiagnostics(t, diags) 87 if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { 88 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 89 } 90 91 countVal, diags := content.Attributes["count"].Expr.Value(nil) 92 assertNoDiagnostics(t, diags) 93 if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { 94 t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) 95 } 96 97 var gotBlockTypes []string 98 for _, block := range content.Blocks { 99 gotBlockTypes = append(gotBlockTypes, block.Type) 100 } 101 wantBlockTypes := []string{"normal_block", "lifecycle", "_"} 102 if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { 103 t.Errorf("wrong block types\n%s", diff) 104 } 105 }) 106 t.Run("provisioner body", func(t *testing.T) { 107 if got, want := len(rc.Managed.Provisioners), 1; got != want { 108 t.Fatalf("wrong number of provisioners %d; want %d", got, want) 109 } 110 pc := rc.Managed.Provisioners[0] 111 112 schema := &hcl.BodySchema{ 113 Attributes: []hcl.AttributeSchema{ 114 {Name: "when", Required: true}, 115 {Name: "normal", Required: true}, 116 }, 117 Blocks: []hcl.BlockHeaderSchema{ 118 {Type: "normal_block"}, 119 {Type: "lifecycle"}, 120 {Type: "_"}, 121 }, 122 } 123 content, diags := pc.Config.Content(schema) 124 assertNoDiagnostics(t, diags) 125 126 normalVal, diags := content.Attributes["normal"].Expr.Value(nil) 127 assertNoDiagnostics(t, diags) 128 if got, want := normalVal, cty.StringVal("yep"); !want.RawEquals(got) { 129 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 130 } 131 whenVal, diags := content.Attributes["when"].Expr.Value(nil) 132 assertNoDiagnostics(t, diags) 133 if got, want := whenVal, cty.StringVal("hell freezes over"); !want.RawEquals(got) { 134 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 135 } 136 }) 137 } 138 139 func TestEscapingBlockData(t *testing.T) { 140 parser := NewParser(nil) 141 mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/data") 142 assertNoDiagnostics(t, diags) 143 if mod == nil { 144 t.Fatal("got nil root module; want non-nil") 145 } 146 147 rc := mod.DataResources["data.foo.bar"] 148 if rc == nil { 149 t.Fatal("no data resource named data.foo.bar") 150 } 151 152 if got := rc.Count; got == nil { 153 t.Errorf("count not set; want count = 2") 154 } else { 155 got, diags := got.Value(nil) 156 assertNoDiagnostics(t, diags) 157 if want := cty.NumberIntVal(2); !want.RawEquals(got) { 158 t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) 159 } 160 } 161 if got, want := rc.ForEach, hcl.Expression(nil); got != want { 162 // Shouldn't have any count because our test fixture only has 163 // for_each in the escaping block. 164 t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) 165 } 166 167 schema := &hcl.BodySchema{ 168 Attributes: []hcl.AttributeSchema{ 169 {Name: "normal", Required: true}, 170 {Name: "count", Required: true}, 171 {Name: "for_each", Required: true}, 172 }, 173 Blocks: []hcl.BlockHeaderSchema{ 174 {Type: "normal_block"}, 175 {Type: "lifecycle"}, 176 {Type: "_"}, 177 }, 178 } 179 content, diags := rc.Config.Content(schema) 180 assertNoDiagnostics(t, diags) 181 182 normalVal, diags := content.Attributes["normal"].Expr.Value(nil) 183 assertNoDiagnostics(t, diags) 184 if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { 185 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 186 } 187 188 countVal, diags := content.Attributes["count"].Expr.Value(nil) 189 assertNoDiagnostics(t, diags) 190 if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { 191 t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) 192 } 193 194 var gotBlockTypes []string 195 for _, block := range content.Blocks { 196 gotBlockTypes = append(gotBlockTypes, block.Type) 197 } 198 wantBlockTypes := []string{"normal_block", "lifecycle", "_"} 199 if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { 200 t.Errorf("wrong block types\n%s", diff) 201 } 202 203 } 204 205 func TestEscapingBlockModule(t *testing.T) { 206 parser := NewParser(nil) 207 mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/module") 208 assertNoDiagnostics(t, diags) 209 if mod == nil { 210 t.Fatal("got nil root module; want non-nil") 211 } 212 213 mc := mod.ModuleCalls["foo"] 214 if mc == nil { 215 t.Fatal("no module call named foo") 216 } 217 218 if got := mc.Count; got == nil { 219 t.Errorf("count not set; want count = 2") 220 } else { 221 got, diags := got.Value(nil) 222 assertNoDiagnostics(t, diags) 223 if want := cty.NumberIntVal(2); !want.RawEquals(got) { 224 t.Errorf("wrong count\ngot: %#v\nwant: %#v", got, want) 225 } 226 } 227 if got, want := mc.ForEach, hcl.Expression(nil); got != want { 228 // Shouldn't have any count because our test fixture only has 229 // for_each in the escaping block. 230 t.Errorf("wrong for_each\ngot: %#v\nwant: %#v", got, want) 231 } 232 233 schema := &hcl.BodySchema{ 234 Attributes: []hcl.AttributeSchema{ 235 {Name: "normal", Required: true}, 236 {Name: "count", Required: true}, 237 {Name: "for_each", Required: true}, 238 }, 239 Blocks: []hcl.BlockHeaderSchema{ 240 {Type: "normal_block"}, 241 {Type: "lifecycle"}, 242 {Type: "_"}, 243 }, 244 } 245 content, diags := mc.Config.Content(schema) 246 assertNoDiagnostics(t, diags) 247 248 normalVal, diags := content.Attributes["normal"].Expr.Value(nil) 249 assertNoDiagnostics(t, diags) 250 if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { 251 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 252 } 253 254 countVal, diags := content.Attributes["count"].Expr.Value(nil) 255 assertNoDiagnostics(t, diags) 256 if got, want := countVal, cty.StringVal("not actually count"); !want.RawEquals(got) { 257 t.Errorf("wrong value for 'count'\ngot: %#v\nwant: %#v", got, want) 258 } 259 260 var gotBlockTypes []string 261 for _, block := range content.Blocks { 262 gotBlockTypes = append(gotBlockTypes, block.Type) 263 } 264 wantBlockTypes := []string{"normal_block", "lifecycle", "_"} 265 if diff := cmp.Diff(gotBlockTypes, wantBlockTypes); diff != "" { 266 t.Errorf("wrong block types\n%s", diff) 267 } 268 269 } 270 271 func TestEscapingBlockProvider(t *testing.T) { 272 parser := NewParser(nil) 273 mod, diags := parser.LoadConfigDir("testdata/escaping-blocks/provider") 274 assertNoDiagnostics(t, diags) 275 if mod == nil { 276 t.Fatal("got nil root module; want non-nil") 277 } 278 279 pc := mod.ProviderConfigs["foo.bar"] 280 if pc == nil { 281 t.Fatal("no provider configuration named foo.bar") 282 } 283 284 if got, want := pc.Alias, "bar"; got != want { 285 t.Errorf("wrong alias\ngot: %#v\nwant: %#v", got, want) 286 } 287 288 schema := &hcl.BodySchema{ 289 Attributes: []hcl.AttributeSchema{ 290 {Name: "normal", Required: true}, 291 {Name: "alias", Required: true}, 292 {Name: "version", Required: true}, 293 }, 294 } 295 content, diags := pc.Config.Content(schema) 296 assertNoDiagnostics(t, diags) 297 298 normalVal, diags := content.Attributes["normal"].Expr.Value(nil) 299 assertNoDiagnostics(t, diags) 300 if got, want := normalVal, cty.StringVal("yes"); !want.RawEquals(got) { 301 t.Errorf("wrong value for 'normal'\ngot: %#v\nwant: %#v", got, want) 302 } 303 aliasVal, diags := content.Attributes["alias"].Expr.Value(nil) 304 assertNoDiagnostics(t, diags) 305 if got, want := aliasVal, cty.StringVal("not actually alias"); !want.RawEquals(got) { 306 t.Errorf("wrong value for 'alias'\ngot: %#v\nwant: %#v", got, want) 307 } 308 versionVal, diags := content.Attributes["version"].Expr.Value(nil) 309 assertNoDiagnostics(t, diags) 310 if got, want := versionVal, cty.StringVal("not actually version"); !want.RawEquals(got) { 311 t.Errorf("wrong value for 'version'\ngot: %#v\nwant: %#v", got, want) 312 } 313 }