github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/terraform/node_provider_test.go (about)

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