mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-23 23:50:12 -06:00
cb2e9119aa
Signed-off-by: namgyalangmo <75657887+namgyalangmo@users.noreply.github.com>
530 lines
15 KiB
Go
530 lines
15 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tofu
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/configs/configschema"
|
|
"github.com/opentofu/opentofu/internal/lang/marks"
|
|
"github.com/opentofu/opentofu/internal/providers"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
)
|
|
|
|
func TestNodeApplyableProviderExecute(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"user": cty.StringVal("hello"),
|
|
}),
|
|
}
|
|
|
|
schema := &configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"user": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
"pw": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
}
|
|
provider := mockProviderWithConfigSchema(schema)
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
ctx.ProviderInputValues = map[string]cty.Value{
|
|
"pw": cty.StringVal("so secret"),
|
|
}
|
|
|
|
if diags := n.Execute(ctx, walkApply); diags.HasErrors() {
|
|
t.Fatalf("err: %s", diags.Err())
|
|
}
|
|
|
|
if !ctx.ConfigureProviderCalled {
|
|
t.Fatal("should be called")
|
|
}
|
|
|
|
gotObj := ctx.ConfigureProviderConfig
|
|
if !gotObj.Type().HasAttribute("user") {
|
|
t.Fatal("configuration object does not have \"user\" attribute")
|
|
}
|
|
if got, want := gotObj.GetAttr("user"), cty.StringVal("hello"); !got.RawEquals(want) {
|
|
t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
|
|
if !gotObj.Type().HasAttribute("pw") {
|
|
t.Fatal("configuration object does not have \"pw\" attribute")
|
|
}
|
|
if got, want := gotObj.GetAttr("pw"), cty.StringVal("so secret"); !got.RawEquals(want) {
|
|
t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProviderExecute_unknownImport(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"test_string": cty.UnknownVal(cty.String),
|
|
}),
|
|
}
|
|
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
|
|
diags := n.Execute(ctx, walkImport)
|
|
if !diags.HasErrors() {
|
|
t.Fatal("expected error, got success")
|
|
}
|
|
|
|
detail := `Invalid provider configuration: The configuration for provider["registry.opentofu.org/hashicorp/foo"] depends on values that cannot be determined until apply.`
|
|
if got, want := diags.Err().Error(), detail; got != want {
|
|
t.Errorf("wrong diagnostic detail\n got: %q\nwant: %q", got, want)
|
|
}
|
|
|
|
if ctx.ConfigureProviderCalled {
|
|
t.Fatal("should not be called")
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProviderExecute_unknownApply(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"test_string": cty.UnknownVal(cty.String),
|
|
}),
|
|
}
|
|
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
|
|
if err := n.Execute(ctx, walkApply); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !ctx.ConfigureProviderCalled {
|
|
t.Fatal("should be called")
|
|
}
|
|
|
|
gotObj := ctx.ConfigureProviderConfig
|
|
if !gotObj.Type().HasAttribute("test_string") {
|
|
t.Fatal("configuration object does not have \"test_string\" attribute")
|
|
}
|
|
if got, want := gotObj.GetAttr("test_string"), cty.UnknownVal(cty.String); !got.RawEquals(want) {
|
|
t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProviderExecute_sensitive(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
|
}),
|
|
}
|
|
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
if err := n.Execute(ctx, walkApply); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !ctx.ConfigureProviderCalled {
|
|
t.Fatal("should be called")
|
|
}
|
|
|
|
gotObj := ctx.ConfigureProviderConfig
|
|
if !gotObj.Type().HasAttribute("test_string") {
|
|
t.Fatal("configuration object does not have \"test_string\" attribute")
|
|
}
|
|
if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) {
|
|
t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProviderExecute_sensitiveValidate(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"test_string": cty.StringVal("hello").Mark(marks.Sensitive),
|
|
}),
|
|
}
|
|
provider := mockProviderWithConfigSchema(simpleTestSchema())
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
if err := n.Execute(ctx, walkValidate); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if !provider.ValidateProviderConfigCalled {
|
|
t.Fatal("should be called")
|
|
}
|
|
|
|
gotObj := provider.ValidateProviderConfigRequest.Config
|
|
if !gotObj.Type().HasAttribute("test_string") {
|
|
t.Fatal("configuration object does not have \"test_string\" attribute")
|
|
}
|
|
if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) {
|
|
t.Errorf("wrong configuration value\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProviderExecute_emptyValidate(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "foo",
|
|
Config: configs.SynthBody("", map[string]cty.Value{}),
|
|
}
|
|
provider := mockProviderWithConfigSchema(&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"test_string": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
})
|
|
providerAddr := addrs.AbsProviderConfig{
|
|
Module: addrs.RootModule,
|
|
Provider: addrs.NewDefaultProvider("foo"),
|
|
}
|
|
|
|
n := &NodeApplyableProvider{&NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
Config: config,
|
|
}}
|
|
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
if err := n.Execute(ctx, walkValidate); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
if ctx.ConfigureProviderCalled {
|
|
t.Fatal("should not be called")
|
|
}
|
|
}
|
|
|
|
func TestNodeApplyableProvider_Validate(t *testing.T) {
|
|
provider := mockProviderWithConfigSchema(&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"region": {
|
|
Type: cty.String,
|
|
Required: true,
|
|
},
|
|
},
|
|
})
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
|
|
t.Run("valid", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"region": cty.StringVal("mars"),
|
|
}),
|
|
}
|
|
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ValidateProvider(ctx, provider)
|
|
if diags.HasErrors() {
|
|
t.Errorf("unexpected error with valid config: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("invalid", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"region": cty.MapValEmpty(cty.String),
|
|
}),
|
|
}
|
|
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ValidateProvider(ctx, provider)
|
|
if !diags.HasErrors() {
|
|
t.Error("missing expected error with invalid config")
|
|
}
|
|
})
|
|
|
|
t.Run("empty config", func(t *testing.T) {
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
},
|
|
}
|
|
|
|
diags := node.ValidateProvider(ctx, provider)
|
|
if diags.HasErrors() {
|
|
t.Errorf("unexpected error with empty config: %s", diags.Err())
|
|
}
|
|
})
|
|
}
|
|
|
|
// This test specifically tests responses from the
|
|
// providers.ValidateProviderConfigFn. See
|
|
// TestNodeApplyableProvider_ConfigProvider_config_fn_err for
|
|
// providers.ConfigureProviderRequest responses.
|
|
func TestNodeApplyableProvider_ConfigProvider(t *testing.T) {
|
|
provider := mockProviderWithConfigSchema(&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"region": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
})
|
|
// For this test, we're returning an error for an optional argument. This
|
|
// can happen for example if an argument is only conditionally required.
|
|
provider.ValidateProviderConfigFn = func(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
|
|
region := req.Config.GetAttr("region")
|
|
if region.IsNull() {
|
|
resp.Diagnostics = resp.Diagnostics.Append(
|
|
tfdiags.WholeContainingBody(tfdiags.Error, "value is not found", "you did not supply a required value"))
|
|
}
|
|
return
|
|
}
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
|
|
t.Run("valid", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"region": cty.StringVal("mars"),
|
|
}),
|
|
}
|
|
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if diags.HasErrors() {
|
|
t.Errorf("unexpected error with valid config: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("missing required config (no config at all)", func(t *testing.T) {
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if !diags.HasErrors() {
|
|
t.Fatal("missing expected error with nil config")
|
|
}
|
|
if !strings.Contains(diags.Err().Error(), "requires explicit configuration") {
|
|
t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("missing required config", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: hcl.EmptyBody(),
|
|
}
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if !diags.HasErrors() {
|
|
t.Fatal("missing expected error with invalid config")
|
|
}
|
|
if !strings.Contains(diags.Err().Error(), "value is not found") {
|
|
t.Errorf("wrong diagnostic: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
// This test is similar to TestNodeApplyableProvider_ConfigProvider, but tests responses from the providers.ConfigureProviderRequest
|
|
func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) {
|
|
provider := mockProviderWithConfigSchema(&configschema.Block{
|
|
Attributes: map[string]*configschema.Attribute{
|
|
"region": {
|
|
Type: cty.String,
|
|
Optional: true,
|
|
},
|
|
},
|
|
})
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
// For this test, provider.PrepareConfigFn will succeed every time but the
|
|
// ctx.ConfigureProviderFn will return an error if a value is not found.
|
|
//
|
|
// This is an unlikely but real situation that occurs:
|
|
// https://github.com/hashicorp/terraform/issues/23087
|
|
ctx.ConfigureProviderFn = func(addr addrs.AbsProviderConfig, cfg cty.Value) (diags tfdiags.Diagnostics) {
|
|
if cfg.IsNull() {
|
|
diags = diags.Append(fmt.Errorf("no config provided"))
|
|
} else {
|
|
region := cfg.GetAttr("region")
|
|
if region.IsNull() {
|
|
diags = diags.Append(fmt.Errorf("value is not found"))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
t.Run("valid", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: configs.SynthBody("", map[string]cty.Value{
|
|
"region": cty.StringVal("mars"),
|
|
}),
|
|
}
|
|
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if diags.HasErrors() {
|
|
t.Errorf("unexpected error with valid config: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("missing required config (no config at all)", func(t *testing.T) {
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if !diags.HasErrors() {
|
|
t.Fatal("missing expected error with nil config")
|
|
}
|
|
if !strings.Contains(diags.Err().Error(), "requires explicit configuration") {
|
|
t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err())
|
|
}
|
|
})
|
|
|
|
t.Run("missing required config", func(t *testing.T) {
|
|
config := &configs.Provider{
|
|
Name: "test",
|
|
Config: hcl.EmptyBody(),
|
|
}
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
|
|
Config: config,
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
if !diags.HasErrors() {
|
|
t.Fatal("missing expected error with invalid config")
|
|
}
|
|
if diags.Err().Error() != "value is not found" {
|
|
t.Errorf("wrong diagnostic: %s", diags.Err())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetSchemaError(t *testing.T) {
|
|
provider := &MockProvider{
|
|
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
|
Diagnostics: tfdiags.Diagnostics.Append(nil, tfdiags.WholeContainingBody(tfdiags.Error, "oops", "error")),
|
|
},
|
|
}
|
|
|
|
providerAddr := mustProviderConfig(`provider["terraform.io/some/provider"]`)
|
|
ctx := &MockEvalContext{ProviderProvider: provider}
|
|
ctx.installSimpleEval()
|
|
node := NodeApplyableProvider{
|
|
NodeAbstractProvider: &NodeAbstractProvider{
|
|
Addr: providerAddr,
|
|
},
|
|
}
|
|
|
|
diags := node.ConfigureProvider(ctx, provider, false)
|
|
for _, d := range diags {
|
|
desc := d.Description()
|
|
if desc.Address != providerAddr.String() {
|
|
t.Fatalf("missing provider address from diagnostics: %#v", desc)
|
|
}
|
|
}
|
|
|
|
}
|