github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_provider_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  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/opentofu/opentofu/internal/addrs"
    15  	"github.com/opentofu/opentofu/internal/configs"
    16  	"github.com/opentofu/opentofu/internal/configs/configschema"
    17  	"github.com/opentofu/opentofu/internal/lang/marks"
    18  	"github.com/opentofu/opentofu/internal/providers"
    19  	"github.com/opentofu/opentofu/internal/tfdiags"
    20  	"github.com/zclconf/go-cty/cty"
    21  )
    22  
    23  func TestNodeApplyableProviderExecute(t *testing.T) {
    24  	config := &configs.Provider{
    25  		Name: "foo",
    26  		Config: configs.SynthBody("", map[string]cty.Value{
    27  			"user": cty.StringVal("hello"),
    28  		}),
    29  	}
    30  
    31  	schema := &configschema.Block{
    32  		Attributes: map[string]*configschema.Attribute{
    33  			"user": {
    34  				Type:     cty.String,
    35  				Required: true,
    36  			},
    37  			"pw": {
    38  				Type:     cty.String,
    39  				Required: true,
    40  			},
    41  		},
    42  	}
    43  	provider := mockProviderWithConfigSchema(schema)
    44  	providerAddr := addrs.AbsProviderConfig{
    45  		Module:   addrs.RootModule,
    46  		Provider: addrs.NewDefaultProvider("foo"),
    47  	}
    48  
    49  	n := &NodeApplyableProvider{&NodeAbstractProvider{
    50  		Addr:   providerAddr,
    51  		Config: config,
    52  	}}
    53  
    54  	ctx := &MockEvalContext{ProviderProvider: provider}
    55  	ctx.installSimpleEval()
    56  	ctx.ProviderInputValues = map[string]cty.Value{
    57  		"pw": cty.StringVal("so secret"),
    58  	}
    59  
    60  	if diags := n.Execute(ctx, walkApply); diags.HasErrors() {
    61  		t.Fatalf("err: %s", diags.Err())
    62  	}
    63  
    64  	if !ctx.ConfigureProviderCalled {
    65  		t.Fatal("should be called")
    66  	}
    67  
    68  	gotObj := ctx.ConfigureProviderConfig
    69  	if !gotObj.Type().HasAttribute("user") {
    70  		t.Fatal("configuration object does not have \"user\" attribute")
    71  	}
    72  	if got, want := gotObj.GetAttr("user"), cty.StringVal("hello"); !got.RawEquals(want) {
    73  		t.Errorf("wrong configuration value\ngot:  %#v\nwant: %#v", got, want)
    74  	}
    75  
    76  	if !gotObj.Type().HasAttribute("pw") {
    77  		t.Fatal("configuration object does not have \"pw\" attribute")
    78  	}
    79  	if got, want := gotObj.GetAttr("pw"), cty.StringVal("so secret"); !got.RawEquals(want) {
    80  		t.Errorf("wrong configuration value\ngot:  %#v\nwant: %#v", got, want)
    81  	}
    82  }
    83  
    84  func TestNodeApplyableProviderExecute_unknownImport(t *testing.T) {
    85  	config := &configs.Provider{
    86  		Name: "foo",
    87  		Config: configs.SynthBody("", map[string]cty.Value{
    88  			"test_string": cty.UnknownVal(cty.String),
    89  		}),
    90  	}
    91  	provider := mockProviderWithConfigSchema(simpleTestSchema())
    92  	providerAddr := addrs.AbsProviderConfig{
    93  		Module:   addrs.RootModule,
    94  		Provider: addrs.NewDefaultProvider("foo"),
    95  	}
    96  	n := &NodeApplyableProvider{&NodeAbstractProvider{
    97  		Addr:   providerAddr,
    98  		Config: config,
    99  	}}
   100  
   101  	ctx := &MockEvalContext{ProviderProvider: provider}
   102  	ctx.installSimpleEval()
   103  
   104  	diags := n.Execute(ctx, walkImport)
   105  	if !diags.HasErrors() {
   106  		t.Fatal("expected error, got success")
   107  	}
   108  
   109  	detail := `Invalid provider configuration: The configuration for provider["registry.opentofu.org/hashicorp/foo"] depends on values that cannot be determined until apply.`
   110  	if got, want := diags.Err().Error(), detail; got != want {
   111  		t.Errorf("wrong diagnostic detail\n got: %q\nwant: %q", got, want)
   112  	}
   113  
   114  	if ctx.ConfigureProviderCalled {
   115  		t.Fatal("should not be called")
   116  	}
   117  }
   118  
   119  func TestNodeApplyableProviderExecute_unknownApply(t *testing.T) {
   120  	config := &configs.Provider{
   121  		Name: "foo",
   122  		Config: configs.SynthBody("", map[string]cty.Value{
   123  			"test_string": cty.UnknownVal(cty.String),
   124  		}),
   125  	}
   126  	provider := mockProviderWithConfigSchema(simpleTestSchema())
   127  	providerAddr := addrs.AbsProviderConfig{
   128  		Module:   addrs.RootModule,
   129  		Provider: addrs.NewDefaultProvider("foo"),
   130  	}
   131  	n := &NodeApplyableProvider{&NodeAbstractProvider{
   132  		Addr:   providerAddr,
   133  		Config: config,
   134  	}}
   135  	ctx := &MockEvalContext{ProviderProvider: provider}
   136  	ctx.installSimpleEval()
   137  
   138  	if err := n.Execute(ctx, walkApply); err != nil {
   139  		t.Fatalf("err: %s", err)
   140  	}
   141  
   142  	if !ctx.ConfigureProviderCalled {
   143  		t.Fatal("should be called")
   144  	}
   145  
   146  	gotObj := ctx.ConfigureProviderConfig
   147  	if !gotObj.Type().HasAttribute("test_string") {
   148  		t.Fatal("configuration object does not have \"test_string\" attribute")
   149  	}
   150  	if got, want := gotObj.GetAttr("test_string"), cty.UnknownVal(cty.String); !got.RawEquals(want) {
   151  		t.Errorf("wrong configuration value\ngot:  %#v\nwant: %#v", got, want)
   152  	}
   153  }
   154  
   155  func TestNodeApplyableProviderExecute_sensitive(t *testing.T) {
   156  	config := &configs.Provider{
   157  		Name: "foo",
   158  		Config: configs.SynthBody("", map[string]cty.Value{
   159  			"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
   160  		}),
   161  	}
   162  	provider := mockProviderWithConfigSchema(simpleTestSchema())
   163  	providerAddr := addrs.AbsProviderConfig{
   164  		Module:   addrs.RootModule,
   165  		Provider: addrs.NewDefaultProvider("foo"),
   166  	}
   167  
   168  	n := &NodeApplyableProvider{&NodeAbstractProvider{
   169  		Addr:   providerAddr,
   170  		Config: config,
   171  	}}
   172  
   173  	ctx := &MockEvalContext{ProviderProvider: provider}
   174  	ctx.installSimpleEval()
   175  	if err := n.Execute(ctx, walkApply); err != nil {
   176  		t.Fatalf("err: %s", err)
   177  	}
   178  
   179  	if !ctx.ConfigureProviderCalled {
   180  		t.Fatal("should be called")
   181  	}
   182  
   183  	gotObj := ctx.ConfigureProviderConfig
   184  	if !gotObj.Type().HasAttribute("test_string") {
   185  		t.Fatal("configuration object does not have \"test_string\" attribute")
   186  	}
   187  	if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) {
   188  		t.Errorf("wrong configuration value\ngot:  %#v\nwant: %#v", got, want)
   189  	}
   190  }
   191  
   192  func TestNodeApplyableProviderExecute_sensitiveValidate(t *testing.T) {
   193  	config := &configs.Provider{
   194  		Name: "foo",
   195  		Config: configs.SynthBody("", map[string]cty.Value{
   196  			"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
   197  		}),
   198  	}
   199  	provider := mockProviderWithConfigSchema(simpleTestSchema())
   200  	providerAddr := addrs.AbsProviderConfig{
   201  		Module:   addrs.RootModule,
   202  		Provider: addrs.NewDefaultProvider("foo"),
   203  	}
   204  
   205  	n := &NodeApplyableProvider{&NodeAbstractProvider{
   206  		Addr:   providerAddr,
   207  		Config: config,
   208  	}}
   209  
   210  	ctx := &MockEvalContext{ProviderProvider: provider}
   211  	ctx.installSimpleEval()
   212  	if err := n.Execute(ctx, walkValidate); err != nil {
   213  		t.Fatalf("err: %s", err)
   214  	}
   215  
   216  	if !provider.ValidateProviderConfigCalled {
   217  		t.Fatal("should be called")
   218  	}
   219  
   220  	gotObj := provider.ValidateProviderConfigRequest.Config
   221  	if !gotObj.Type().HasAttribute("test_string") {
   222  		t.Fatal("configuration object does not have \"test_string\" attribute")
   223  	}
   224  	if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) {
   225  		t.Errorf("wrong configuration value\ngot:  %#v\nwant: %#v", got, want)
   226  	}
   227  }
   228  
   229  func TestNodeApplyableProviderExecute_emptyValidate(t *testing.T) {
   230  	config := &configs.Provider{
   231  		Name:   "foo",
   232  		Config: configs.SynthBody("", map[string]cty.Value{}),
   233  	}
   234  	provider := mockProviderWithConfigSchema(&configschema.Block{
   235  		Attributes: map[string]*configschema.Attribute{
   236  			"test_string": {
   237  				Type:     cty.String,
   238  				Required: true,
   239  			},
   240  		},
   241  	})
   242  	providerAddr := addrs.AbsProviderConfig{
   243  		Module:   addrs.RootModule,
   244  		Provider: addrs.NewDefaultProvider("foo"),
   245  	}
   246  
   247  	n := &NodeApplyableProvider{&NodeAbstractProvider{
   248  		Addr:   providerAddr,
   249  		Config: config,
   250  	}}
   251  
   252  	ctx := &MockEvalContext{ProviderProvider: provider}
   253  	ctx.installSimpleEval()
   254  	if err := n.Execute(ctx, walkValidate); err != nil {
   255  		t.Fatalf("err: %s", err)
   256  	}
   257  
   258  	if ctx.ConfigureProviderCalled {
   259  		t.Fatal("should not be called")
   260  	}
   261  }
   262  
   263  func TestNodeApplyableProvider_Validate(t *testing.T) {
   264  	provider := mockProviderWithConfigSchema(&configschema.Block{
   265  		Attributes: map[string]*configschema.Attribute{
   266  			"region": {
   267  				Type:     cty.String,
   268  				Required: true,
   269  			},
   270  		},
   271  	})
   272  	ctx := &MockEvalContext{ProviderProvider: provider}
   273  	ctx.installSimpleEval()
   274  
   275  	t.Run("valid", func(t *testing.T) {
   276  		config := &configs.Provider{
   277  			Name: "test",
   278  			Config: configs.SynthBody("", map[string]cty.Value{
   279  				"region": cty.StringVal("mars"),
   280  			}),
   281  		}
   282  
   283  		node := NodeApplyableProvider{
   284  			NodeAbstractProvider: &NodeAbstractProvider{
   285  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   286  				Config: config,
   287  			},
   288  		}
   289  
   290  		diags := node.ValidateProvider(ctx, provider)
   291  		if diags.HasErrors() {
   292  			t.Errorf("unexpected error with valid config: %s", diags.Err())
   293  		}
   294  	})
   295  
   296  	t.Run("invalid", func(t *testing.T) {
   297  		config := &configs.Provider{
   298  			Name: "test",
   299  			Config: configs.SynthBody("", map[string]cty.Value{
   300  				"region": cty.MapValEmpty(cty.String),
   301  			}),
   302  		}
   303  
   304  		node := NodeApplyableProvider{
   305  			NodeAbstractProvider: &NodeAbstractProvider{
   306  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   307  				Config: config,
   308  			},
   309  		}
   310  
   311  		diags := node.ValidateProvider(ctx, provider)
   312  		if !diags.HasErrors() {
   313  			t.Error("missing expected error with invalid config")
   314  		}
   315  	})
   316  
   317  	t.Run("empty config", func(t *testing.T) {
   318  		node := NodeApplyableProvider{
   319  			NodeAbstractProvider: &NodeAbstractProvider{
   320  				Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   321  			},
   322  		}
   323  
   324  		diags := node.ValidateProvider(ctx, provider)
   325  		if diags.HasErrors() {
   326  			t.Errorf("unexpected error with empty config: %s", diags.Err())
   327  		}
   328  	})
   329  }
   330  
   331  // This test specifically tests responses from the
   332  // providers.ValidateProviderConfigFn. See
   333  // TestNodeApplyableProvider_ConfigProvider_config_fn_err for
   334  // providers.ConfigureProviderRequest responses.
   335  func TestNodeApplyableProvider_ConfigProvider(t *testing.T) {
   336  	provider := mockProviderWithConfigSchema(&configschema.Block{
   337  		Attributes: map[string]*configschema.Attribute{
   338  			"region": {
   339  				Type:     cty.String,
   340  				Optional: true,
   341  			},
   342  		},
   343  	})
   344  	// For this test, we're returning an error for an optional argument. This
   345  	// can happen for example if an argument is only conditionally required.
   346  	provider.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
   347  		region := req.Config.GetAttr("region")
   348  		if region.IsNull() {
   349  			resp.Diagnostics = resp.Diagnostics.Append(
   350  				tfdiags.WholeContainingBody(tfdiags.Error, "value is not found", "you did not supply a required value"))
   351  		}
   352  		return
   353  	}
   354  	ctx := &MockEvalContext{ProviderProvider: provider}
   355  	ctx.installSimpleEval()
   356  
   357  	t.Run("valid", func(t *testing.T) {
   358  		config := &configs.Provider{
   359  			Name: "test",
   360  			Config: configs.SynthBody("", map[string]cty.Value{
   361  				"region": cty.StringVal("mars"),
   362  			}),
   363  		}
   364  
   365  		node := NodeApplyableProvider{
   366  			NodeAbstractProvider: &NodeAbstractProvider{
   367  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   368  				Config: config,
   369  			},
   370  		}
   371  
   372  		diags := node.ConfigureProvider(ctx, provider, false)
   373  		if diags.HasErrors() {
   374  			t.Errorf("unexpected error with valid config: %s", diags.Err())
   375  		}
   376  	})
   377  
   378  	t.Run("missing required config (no config at all)", func(t *testing.T) {
   379  		node := NodeApplyableProvider{
   380  			NodeAbstractProvider: &NodeAbstractProvider{
   381  				Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   382  			},
   383  		}
   384  
   385  		diags := node.ConfigureProvider(ctx, provider, false)
   386  		if !diags.HasErrors() {
   387  			t.Fatal("missing expected error with nil config")
   388  		}
   389  		if !strings.Contains(diags.Err().Error(), "requires explicit configuration") {
   390  			t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err())
   391  		}
   392  	})
   393  
   394  	t.Run("missing required config", func(t *testing.T) {
   395  		config := &configs.Provider{
   396  			Name:   "test",
   397  			Config: hcl.EmptyBody(),
   398  		}
   399  		node := NodeApplyableProvider{
   400  			NodeAbstractProvider: &NodeAbstractProvider{
   401  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   402  				Config: config,
   403  			},
   404  		}
   405  
   406  		diags := node.ConfigureProvider(ctx, provider, false)
   407  		if !diags.HasErrors() {
   408  			t.Fatal("missing expected error with invalid config")
   409  		}
   410  		if !strings.Contains(diags.Err().Error(), "value is not found") {
   411  			t.Errorf("wrong diagnostic: %s", diags.Err())
   412  		}
   413  	})
   414  
   415  }
   416  
   417  // This test is similar to TestNodeApplyableProvider_ConfigProvider, but tests responses from the providers.ConfigureProviderRequest
   418  func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) {
   419  	provider := mockProviderWithConfigSchema(&configschema.Block{
   420  		Attributes: map[string]*configschema.Attribute{
   421  			"region": {
   422  				Type:     cty.String,
   423  				Optional: true,
   424  			},
   425  		},
   426  	})
   427  	ctx := &MockEvalContext{ProviderProvider: provider}
   428  	ctx.installSimpleEval()
   429  	// For this test, provider.PrepareConfigFn will succeed every time but the
   430  	// ctx.ConfigureProviderFn will return an error if a value is not found.
   431  	//
   432  	// This is an unlikely but real situation that occurs:
   433  	// https://github.com/hashicorp/terraform/issues/23087
   434  	ctx.ConfigureProviderFn = func(addr addrs.AbsProviderConfig, cfg cty.Value) (diags tfdiags.Diagnostics) {
   435  		if cfg.IsNull() {
   436  			diags = diags.Append(fmt.Errorf("no config provided"))
   437  		} else {
   438  			region := cfg.GetAttr("region")
   439  			if region.IsNull() {
   440  				diags = diags.Append(fmt.Errorf("value is not found"))
   441  			}
   442  		}
   443  		return
   444  	}
   445  
   446  	t.Run("valid", func(t *testing.T) {
   447  		config := &configs.Provider{
   448  			Name: "test",
   449  			Config: configs.SynthBody("", map[string]cty.Value{
   450  				"region": cty.StringVal("mars"),
   451  			}),
   452  		}
   453  
   454  		node := NodeApplyableProvider{
   455  			NodeAbstractProvider: &NodeAbstractProvider{
   456  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   457  				Config: config,
   458  			},
   459  		}
   460  
   461  		diags := node.ConfigureProvider(ctx, provider, false)
   462  		if diags.HasErrors() {
   463  			t.Errorf("unexpected error with valid config: %s", diags.Err())
   464  		}
   465  	})
   466  
   467  	t.Run("missing required config (no config at all)", func(t *testing.T) {
   468  		node := NodeApplyableProvider{
   469  			NodeAbstractProvider: &NodeAbstractProvider{
   470  				Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   471  			},
   472  		}
   473  
   474  		diags := node.ConfigureProvider(ctx, provider, false)
   475  		if !diags.HasErrors() {
   476  			t.Fatal("missing expected error with nil config")
   477  		}
   478  		if !strings.Contains(diags.Err().Error(), "requires explicit configuration") {
   479  			t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err())
   480  		}
   481  	})
   482  
   483  	t.Run("missing required config", func(t *testing.T) {
   484  		config := &configs.Provider{
   485  			Name:   "test",
   486  			Config: hcl.EmptyBody(),
   487  		}
   488  		node := NodeApplyableProvider{
   489  			NodeAbstractProvider: &NodeAbstractProvider{
   490  				Addr:   mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   491  				Config: config,
   492  			},
   493  		}
   494  
   495  		diags := node.ConfigureProvider(ctx, provider, false)
   496  		if !diags.HasErrors() {
   497  			t.Fatal("missing expected error with invalid config")
   498  		}
   499  		if diags.Err().Error() != "value is not found" {
   500  			t.Errorf("wrong diagnostic: %s", diags.Err())
   501  		}
   502  	})
   503  }
   504  
   505  func TestGetSchemaError(t *testing.T) {
   506  	provider := &MockProvider{
   507  		GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
   508  			Diagnostics: tfdiags.Diagnostics.Append(nil, tfdiags.WholeContainingBody(tfdiags.Error, "oops", "error")),
   509  		},
   510  	}
   511  
   512  	providerAddr := mustProviderConfig(`provider["terraform.io/some/provider"]`)
   513  	ctx := &MockEvalContext{ProviderProvider: provider}
   514  	ctx.installSimpleEval()
   515  	node := NodeApplyableProvider{
   516  		NodeAbstractProvider: &NodeAbstractProvider{
   517  			Addr: providerAddr,
   518  		},
   519  	}
   520  
   521  	diags := node.ConfigureProvider(ctx, provider, false)
   522  	for _, d := range diags {
   523  		desc := d.Description()
   524  		if desc.Address != providerAddr.String() {
   525  			t.Fatalf("missing provider address from diagnostics: %#v", desc)
   526  		}
   527  	}
   528  
   529  }