github.com/opentofu/opentofu@v1.7.1/internal/legacy/tofu/resource_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 tofu
     7  
     8  import (
     9  	"fmt"
    10  	"reflect"
    11  	"testing"
    12  
    13  	"github.com/opentofu/opentofu/internal/configs/configschema"
    14  	"github.com/zclconf/go-cty/cty"
    15  
    16  	"github.com/mitchellh/reflectwalk"
    17  	"github.com/opentofu/opentofu/internal/configs/hcl2shim"
    18  )
    19  
    20  func TestResourceConfigGet(t *testing.T) {
    21  	fooStringSchema := &configschema.Block{
    22  		Attributes: map[string]*configschema.Attribute{
    23  			"foo": {Type: cty.String, Optional: true},
    24  		},
    25  	}
    26  	fooListSchema := &configschema.Block{
    27  		Attributes: map[string]*configschema.Attribute{
    28  			"foo": {Type: cty.List(cty.Number), Optional: true},
    29  		},
    30  	}
    31  
    32  	cases := []struct {
    33  		Config cty.Value
    34  		Schema *configschema.Block
    35  		Key    string
    36  		Value  interface{}
    37  	}{
    38  		{
    39  			Config: cty.ObjectVal(map[string]cty.Value{
    40  				"foo": cty.StringVal("bar"),
    41  			}),
    42  			Schema: fooStringSchema,
    43  			Key:    "foo",
    44  			Value:  "bar",
    45  		},
    46  
    47  		{
    48  			Config: cty.ObjectVal(map[string]cty.Value{
    49  				"foo": cty.UnknownVal(cty.String),
    50  			}),
    51  			Schema: fooStringSchema,
    52  			Key:    "foo",
    53  			Value:  hcl2shim.UnknownVariableValue,
    54  		},
    55  
    56  		{
    57  			Config: cty.ObjectVal(map[string]cty.Value{
    58  				"foo": cty.ListVal([]cty.Value{
    59  					cty.NumberIntVal(1),
    60  					cty.NumberIntVal(2),
    61  					cty.NumberIntVal(5),
    62  				}),
    63  			}),
    64  			Schema: fooListSchema,
    65  			Key:    "foo.0",
    66  			Value:  1,
    67  		},
    68  
    69  		{
    70  			Config: cty.ObjectVal(map[string]cty.Value{
    71  				"foo": cty.ListVal([]cty.Value{
    72  					cty.NumberIntVal(1),
    73  					cty.NumberIntVal(2),
    74  					cty.NumberIntVal(5),
    75  				}),
    76  			}),
    77  			Schema: fooListSchema,
    78  			Key:    "foo.5",
    79  			Value:  nil,
    80  		},
    81  
    82  		{
    83  			Config: cty.ObjectVal(map[string]cty.Value{
    84  				"foo": cty.ListVal([]cty.Value{
    85  					cty.NumberIntVal(1),
    86  					cty.NumberIntVal(2),
    87  					cty.NumberIntVal(5),
    88  				}),
    89  			}),
    90  			Schema: fooListSchema,
    91  			Key:    "foo.-1",
    92  			Value:  nil,
    93  		},
    94  
    95  		// get from map
    96  		{
    97  			Config: cty.ObjectVal(map[string]cty.Value{
    98  				"mapname": cty.ListVal([]cty.Value{
    99  					cty.MapVal(map[string]cty.Value{
   100  						"key": cty.NumberIntVal(1),
   101  					}),
   102  				}),
   103  			}),
   104  			Schema: &configschema.Block{
   105  				Attributes: map[string]*configschema.Attribute{
   106  					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
   107  				},
   108  			},
   109  			Key:   "mapname.0.key",
   110  			Value: 1,
   111  		},
   112  
   113  		// get from map with dot in key
   114  		{
   115  			Config: cty.ObjectVal(map[string]cty.Value{
   116  				"mapname": cty.ListVal([]cty.Value{
   117  					cty.MapVal(map[string]cty.Value{
   118  						"key.name": cty.NumberIntVal(1),
   119  					}),
   120  				}),
   121  			}),
   122  			Schema: &configschema.Block{
   123  				Attributes: map[string]*configschema.Attribute{
   124  					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
   125  				},
   126  			},
   127  			Key:   "mapname.0.key.name",
   128  			Value: 1,
   129  		},
   130  
   131  		// get from map with overlapping key names
   132  		{
   133  			Config: cty.ObjectVal(map[string]cty.Value{
   134  				"mapname": cty.ListVal([]cty.Value{
   135  					cty.MapVal(map[string]cty.Value{
   136  						"key.name":   cty.NumberIntVal(1),
   137  						"key.name.2": cty.NumberIntVal(2),
   138  					}),
   139  				}),
   140  			}),
   141  			Schema: &configschema.Block{
   142  				Attributes: map[string]*configschema.Attribute{
   143  					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
   144  				},
   145  			},
   146  			Key:   "mapname.0.key.name.2",
   147  			Value: 2,
   148  		},
   149  		{
   150  			Config: cty.ObjectVal(map[string]cty.Value{
   151  				"mapname": cty.ListVal([]cty.Value{
   152  					cty.MapVal(map[string]cty.Value{
   153  						"key.name":     cty.NumberIntVal(1),
   154  						"key.name.foo": cty.NumberIntVal(2),
   155  					}),
   156  				}),
   157  			}),
   158  			Schema: &configschema.Block{
   159  				Attributes: map[string]*configschema.Attribute{
   160  					"mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true},
   161  				},
   162  			},
   163  			Key:   "mapname.0.key.name",
   164  			Value: 1,
   165  		},
   166  		{
   167  			Config: cty.ObjectVal(map[string]cty.Value{
   168  				"mapname": cty.ListVal([]cty.Value{
   169  					cty.MapVal(map[string]cty.Value{
   170  						"listkey": cty.ListVal([]cty.Value{
   171  							cty.MapVal(map[string]cty.Value{
   172  								"key": cty.NumberIntVal(3),
   173  							}),
   174  						}),
   175  					}),
   176  				}),
   177  			}),
   178  			Schema: &configschema.Block{
   179  				Attributes: map[string]*configschema.Attribute{
   180  					"mapname": {Type: cty.List(cty.Map(cty.List(cty.Map(cty.Number)))), Optional: true},
   181  				},
   182  			},
   183  			Key:   "mapname.0.listkey.0.key",
   184  			Value: 3,
   185  		},
   186  	}
   187  
   188  	for i, tc := range cases {
   189  		rc := NewResourceConfigShimmed(tc.Config, tc.Schema)
   190  
   191  		// Test getting a key
   192  		t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) {
   193  			v, ok := rc.Get(tc.Key)
   194  			if ok && v == nil {
   195  				t.Fatal("(nil, true) returned from Get")
   196  			}
   197  
   198  			if !reflect.DeepEqual(v, tc.Value) {
   199  				t.Fatalf("%d bad: %#v", i, v)
   200  			}
   201  		})
   202  
   203  		// Test copying and equality
   204  		t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) {
   205  			copy := rc.DeepCopy()
   206  			if !reflect.DeepEqual(copy, rc) {
   207  				t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc)
   208  			}
   209  
   210  			if !copy.Equal(rc) {
   211  				t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc)
   212  			}
   213  			if !rc.Equal(copy) {
   214  				t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc)
   215  			}
   216  		})
   217  	}
   218  }
   219  
   220  func TestResourceConfigDeepCopy_nil(t *testing.T) {
   221  	var nilRc *ResourceConfig
   222  	actual := nilRc.DeepCopy()
   223  	if actual != nil {
   224  		t.Fatalf("bad: %#v", actual)
   225  	}
   226  }
   227  
   228  func TestResourceConfigDeepCopy_nilComputed(t *testing.T) {
   229  	rc := &ResourceConfig{}
   230  	actual := rc.DeepCopy()
   231  	if actual.ComputedKeys != nil {
   232  		t.Fatalf("bad: %#v", actual)
   233  	}
   234  }
   235  
   236  func TestResourceConfigEqual_nil(t *testing.T) {
   237  	var nilRc *ResourceConfig
   238  	notNil := NewResourceConfigShimmed(cty.EmptyObjectVal, &configschema.Block{})
   239  
   240  	if nilRc.Equal(notNil) {
   241  		t.Fatal("should not be equal")
   242  	}
   243  
   244  	if notNil.Equal(nilRc) {
   245  		t.Fatal("should not be equal")
   246  	}
   247  }
   248  
   249  func TestResourceConfigEqual_computedKeyOrder(t *testing.T) {
   250  	v := cty.ObjectVal(map[string]cty.Value{
   251  		"foo": cty.UnknownVal(cty.String),
   252  	})
   253  	schema := &configschema.Block{
   254  		Attributes: map[string]*configschema.Attribute{
   255  			"foo": {Type: cty.String, Optional: true},
   256  		},
   257  	}
   258  	rc := NewResourceConfigShimmed(v, schema)
   259  	rc2 := NewResourceConfigShimmed(v, schema)
   260  
   261  	// Set the computed keys manually to force ordering to differ
   262  	rc.ComputedKeys = []string{"foo", "bar"}
   263  	rc2.ComputedKeys = []string{"bar", "foo"}
   264  
   265  	if !rc.Equal(rc2) {
   266  		t.Fatal("should be equal")
   267  	}
   268  }
   269  
   270  func TestUnknownCheckWalker(t *testing.T) {
   271  	cases := []struct {
   272  		Name   string
   273  		Input  interface{}
   274  		Result bool
   275  	}{
   276  		{
   277  			"primitive",
   278  			42,
   279  			false,
   280  		},
   281  
   282  		{
   283  			"primitive computed",
   284  			hcl2shim.UnknownVariableValue,
   285  			true,
   286  		},
   287  
   288  		{
   289  			"list",
   290  			[]interface{}{"foo", hcl2shim.UnknownVariableValue},
   291  			true,
   292  		},
   293  
   294  		{
   295  			"nested list",
   296  			[]interface{}{
   297  				"foo",
   298  				[]interface{}{hcl2shim.UnknownVariableValue},
   299  			},
   300  			true,
   301  		},
   302  	}
   303  
   304  	for i, tc := range cases {
   305  		t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
   306  			var w unknownCheckWalker
   307  			if err := reflectwalk.Walk(tc.Input, &w); err != nil {
   308  				t.Fatalf("err: %s", err)
   309  			}
   310  
   311  			if w.Unknown != tc.Result {
   312  				t.Fatalf("bad: %v", w.Unknown)
   313  			}
   314  		})
   315  	}
   316  }
   317  
   318  func TestNewResourceConfigShimmed(t *testing.T) {
   319  	for _, tc := range []struct {
   320  		Name     string
   321  		Val      cty.Value
   322  		Schema   *configschema.Block
   323  		Expected *ResourceConfig
   324  	}{
   325  		{
   326  			Name: "empty object",
   327  			Val:  cty.NullVal(cty.EmptyObject),
   328  			Schema: &configschema.Block{
   329  				Attributes: map[string]*configschema.Attribute{
   330  					"foo": {
   331  						Type:     cty.String,
   332  						Optional: true,
   333  					},
   334  				},
   335  			},
   336  			Expected: &ResourceConfig{
   337  				Raw:    map[string]interface{}{},
   338  				Config: map[string]interface{}{},
   339  			},
   340  		},
   341  		{
   342  			Name: "basic",
   343  			Val: cty.ObjectVal(map[string]cty.Value{
   344  				"foo": cty.StringVal("bar"),
   345  			}),
   346  			Schema: &configschema.Block{
   347  				Attributes: map[string]*configschema.Attribute{
   348  					"foo": {
   349  						Type:     cty.String,
   350  						Optional: true,
   351  					},
   352  				},
   353  			},
   354  			Expected: &ResourceConfig{
   355  				Raw: map[string]interface{}{
   356  					"foo": "bar",
   357  				},
   358  				Config: map[string]interface{}{
   359  					"foo": "bar",
   360  				},
   361  			},
   362  		},
   363  		{
   364  			Name: "null string",
   365  			Val: cty.ObjectVal(map[string]cty.Value{
   366  				"foo": cty.NullVal(cty.String),
   367  			}),
   368  			Schema: &configschema.Block{
   369  				Attributes: map[string]*configschema.Attribute{
   370  					"foo": {
   371  						Type:     cty.String,
   372  						Optional: true,
   373  					},
   374  				},
   375  			},
   376  			Expected: &ResourceConfig{
   377  				Raw:    map[string]interface{}{},
   378  				Config: map[string]interface{}{},
   379  			},
   380  		},
   381  		{
   382  			Name: "unknown string",
   383  			Val: cty.ObjectVal(map[string]cty.Value{
   384  				"foo": cty.UnknownVal(cty.String),
   385  			}),
   386  			Schema: &configschema.Block{
   387  				Attributes: map[string]*configschema.Attribute{
   388  					"foo": {
   389  						Type:     cty.String,
   390  						Optional: true,
   391  					},
   392  				},
   393  			},
   394  			Expected: &ResourceConfig{
   395  				ComputedKeys: []string{"foo"},
   396  				Raw: map[string]interface{}{
   397  					"foo": hcl2shim.UnknownVariableValue,
   398  				},
   399  				Config: map[string]interface{}{
   400  					"foo": hcl2shim.UnknownVariableValue,
   401  				},
   402  			},
   403  		},
   404  		{
   405  			Name: "unknown collections",
   406  			Val: cty.ObjectVal(map[string]cty.Value{
   407  				"bar": cty.UnknownVal(cty.Map(cty.String)),
   408  				"baz": cty.UnknownVal(cty.List(cty.String)),
   409  			}),
   410  			Schema: &configschema.Block{
   411  				Attributes: map[string]*configschema.Attribute{
   412  					"bar": {
   413  						Type:     cty.Map(cty.String),
   414  						Required: true,
   415  					},
   416  					"baz": {
   417  						Type:     cty.List(cty.String),
   418  						Optional: true,
   419  					},
   420  				},
   421  			},
   422  			Expected: &ResourceConfig{
   423  				ComputedKeys: []string{"bar", "baz"},
   424  				Raw: map[string]interface{}{
   425  					"bar": hcl2shim.UnknownVariableValue,
   426  					"baz": hcl2shim.UnknownVariableValue,
   427  				},
   428  				Config: map[string]interface{}{
   429  					"bar": hcl2shim.UnknownVariableValue,
   430  					"baz": hcl2shim.UnknownVariableValue,
   431  				},
   432  			},
   433  		},
   434  		{
   435  			Name: "null collections",
   436  			Val: cty.ObjectVal(map[string]cty.Value{
   437  				"bar": cty.NullVal(cty.Map(cty.String)),
   438  				"baz": cty.NullVal(cty.List(cty.String)),
   439  			}),
   440  			Schema: &configschema.Block{
   441  				Attributes: map[string]*configschema.Attribute{
   442  					"bar": {
   443  						Type:     cty.Map(cty.String),
   444  						Required: true,
   445  					},
   446  					"baz": {
   447  						Type:     cty.List(cty.String),
   448  						Optional: true,
   449  					},
   450  				},
   451  			},
   452  			Expected: &ResourceConfig{
   453  				Raw:    map[string]interface{}{},
   454  				Config: map[string]interface{}{},
   455  			},
   456  		},
   457  		{
   458  			Name: "unknown blocks",
   459  			Val: cty.ObjectVal(map[string]cty.Value{
   460  				"bar": cty.UnknownVal(cty.Map(cty.String)),
   461  				"baz": cty.UnknownVal(cty.List(cty.String)),
   462  			}),
   463  			Schema: &configschema.Block{
   464  				BlockTypes: map[string]*configschema.NestedBlock{
   465  					"bar": {
   466  						Block:   configschema.Block{},
   467  						Nesting: configschema.NestingList,
   468  					},
   469  					"baz": {
   470  						Block:   configschema.Block{},
   471  						Nesting: configschema.NestingSet,
   472  					},
   473  				},
   474  			},
   475  			Expected: &ResourceConfig{
   476  				ComputedKeys: []string{"bar", "baz"},
   477  				Raw: map[string]interface{}{
   478  					"bar": hcl2shim.UnknownVariableValue,
   479  					"baz": hcl2shim.UnknownVariableValue,
   480  				},
   481  				Config: map[string]interface{}{
   482  					"bar": hcl2shim.UnknownVariableValue,
   483  					"baz": hcl2shim.UnknownVariableValue,
   484  				},
   485  			},
   486  		},
   487  		{
   488  			Name: "unknown in nested blocks",
   489  			Val: cty.ObjectVal(map[string]cty.Value{
   490  				"bar": cty.ListVal([]cty.Value{
   491  					cty.ObjectVal(map[string]cty.Value{
   492  						"baz": cty.ListVal([]cty.Value{
   493  							cty.ObjectVal(map[string]cty.Value{
   494  								"list": cty.UnknownVal(cty.List(cty.String)),
   495  							}),
   496  						}),
   497  					}),
   498  				}),
   499  			}),
   500  			Schema: &configschema.Block{
   501  				BlockTypes: map[string]*configschema.NestedBlock{
   502  					"bar": {
   503  						Block: configschema.Block{
   504  							BlockTypes: map[string]*configschema.NestedBlock{
   505  								"baz": {
   506  									Block: configschema.Block{
   507  										Attributes: map[string]*configschema.Attribute{
   508  											"list": {Type: cty.List(cty.String),
   509  												Optional: true,
   510  											},
   511  										},
   512  									},
   513  									Nesting: configschema.NestingList,
   514  								},
   515  							},
   516  						},
   517  						Nesting: configschema.NestingList,
   518  					},
   519  				},
   520  			},
   521  			Expected: &ResourceConfig{
   522  				ComputedKeys: []string{"bar.0.baz.0.list"},
   523  				Raw: map[string]interface{}{
   524  					"bar": []interface{}{map[string]interface{}{
   525  						"baz": []interface{}{map[string]interface{}{
   526  							"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
   527  						}},
   528  					}},
   529  				},
   530  				Config: map[string]interface{}{
   531  					"bar": []interface{}{map[string]interface{}{
   532  						"baz": []interface{}{map[string]interface{}{
   533  							"list": "74D93920-ED26-11E3-AC10-0800200C9A66",
   534  						}},
   535  					}},
   536  				},
   537  			},
   538  		},
   539  		{
   540  			Name: "unknown in set",
   541  			Val: cty.ObjectVal(map[string]cty.Value{
   542  				"bar": cty.SetVal([]cty.Value{
   543  					cty.ObjectVal(map[string]cty.Value{
   544  						"val": cty.UnknownVal(cty.String),
   545  					}),
   546  				}),
   547  			}),
   548  			Schema: &configschema.Block{
   549  				BlockTypes: map[string]*configschema.NestedBlock{
   550  					"bar": {
   551  						Block: configschema.Block{
   552  							Attributes: map[string]*configschema.Attribute{
   553  								"val": {
   554  									Type:     cty.String,
   555  									Optional: true,
   556  								},
   557  							},
   558  						},
   559  						Nesting: configschema.NestingSet,
   560  					},
   561  				},
   562  			},
   563  			Expected: &ResourceConfig{
   564  				ComputedKeys: []string{"bar.0.val"},
   565  				Raw: map[string]interface{}{
   566  					"bar": []interface{}{map[string]interface{}{
   567  						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
   568  					}},
   569  				},
   570  				Config: map[string]interface{}{
   571  					"bar": []interface{}{map[string]interface{}{
   572  						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
   573  					}},
   574  				},
   575  			},
   576  		},
   577  		{
   578  			Name: "unknown in attribute sets",
   579  			Val: cty.ObjectVal(map[string]cty.Value{
   580  				"bar": cty.SetVal([]cty.Value{
   581  					cty.ObjectVal(map[string]cty.Value{
   582  						"val": cty.UnknownVal(cty.String),
   583  					}),
   584  				}),
   585  				"baz": cty.SetVal([]cty.Value{
   586  					cty.ObjectVal(map[string]cty.Value{
   587  						"obj": cty.UnknownVal(cty.Object(map[string]cty.Type{
   588  							"attr": cty.List(cty.String),
   589  						})),
   590  					}),
   591  					cty.ObjectVal(map[string]cty.Value{
   592  						"obj": cty.ObjectVal(map[string]cty.Value{
   593  							"attr": cty.UnknownVal(cty.List(cty.String)),
   594  						}),
   595  					}),
   596  				}),
   597  			}),
   598  			Schema: &configschema.Block{
   599  				Attributes: map[string]*configschema.Attribute{
   600  					"bar": &configschema.Attribute{
   601  						Type: cty.Set(cty.Object(map[string]cty.Type{
   602  							"val": cty.String,
   603  						})),
   604  					},
   605  					"baz": &configschema.Attribute{
   606  						Type: cty.Set(cty.Object(map[string]cty.Type{
   607  							"obj": cty.Object(map[string]cty.Type{
   608  								"attr": cty.List(cty.String),
   609  							}),
   610  						})),
   611  					},
   612  				},
   613  			},
   614  			Expected: &ResourceConfig{
   615  				ComputedKeys: []string{"bar.0.val", "baz.0.obj.attr", "baz.1.obj"},
   616  				Raw: map[string]interface{}{
   617  					"bar": []interface{}{map[string]interface{}{
   618  						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
   619  					}},
   620  					"baz": []interface{}{
   621  						map[string]interface{}{
   622  							"obj": map[string]interface{}{
   623  								"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
   624  							},
   625  						},
   626  						map[string]interface{}{
   627  							"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
   628  						},
   629  					},
   630  				},
   631  				Config: map[string]interface{}{
   632  					"bar": []interface{}{map[string]interface{}{
   633  						"val": "74D93920-ED26-11E3-AC10-0800200C9A66",
   634  					}},
   635  					"baz": []interface{}{
   636  						map[string]interface{}{
   637  							"obj": map[string]interface{}{
   638  								"attr": "74D93920-ED26-11E3-AC10-0800200C9A66",
   639  							},
   640  						},
   641  						map[string]interface{}{
   642  							"obj": "74D93920-ED26-11E3-AC10-0800200C9A66",
   643  						},
   644  					},
   645  				},
   646  			},
   647  		},
   648  		{
   649  			Name: "null blocks",
   650  			Val: cty.ObjectVal(map[string]cty.Value{
   651  				"bar": cty.NullVal(cty.Map(cty.String)),
   652  				"baz": cty.NullVal(cty.List(cty.String)),
   653  			}),
   654  			Schema: &configschema.Block{
   655  				BlockTypes: map[string]*configschema.NestedBlock{
   656  					"bar": {
   657  						Block:   configschema.Block{},
   658  						Nesting: configschema.NestingMap,
   659  					},
   660  					"baz": {
   661  						Block:   configschema.Block{},
   662  						Nesting: configschema.NestingSingle,
   663  					},
   664  				},
   665  			},
   666  			Expected: &ResourceConfig{
   667  				Raw:    map[string]interface{}{},
   668  				Config: map[string]interface{}{},
   669  			},
   670  		},
   671  	} {
   672  		t.Run(tc.Name, func(*testing.T) {
   673  			cfg := NewResourceConfigShimmed(tc.Val, tc.Schema)
   674  			if !tc.Expected.Equal(cfg) {
   675  				t.Fatalf("expected:\n%#v\ngot:\n%#v", tc.Expected, cfg)
   676  			}
   677  		})
   678  	}
   679  }