Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform: use hcl.MergeBodies instead of configs.MergeBodies for pro… #29000

Merged
merged 2 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions internal/command/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,98 @@ func TestPlan_providerArgumentUnset(t *testing.T) {
}
}

// Test that terraform properly merges provider configuration that's split
// between config files and interactive input variables.
// https://github.com/hashicorp/terraform/issues/28956
func TestPlan_providerConfigMerge(t *testing.T) {
td := tempDir(t)
testCopyDir(t, testFixturePath("plan-provider-input"), td)
defer os.RemoveAll(td)
defer testChdir(t, td)()

// Disable test mode so input would be asked
test = false
defer func() { test = true }()

// The plan command will prompt for interactive input of provider.test.region
defaultInputReader = bytes.NewBufferString("us-east-1\n")

p := planFixtureProvider()
// override the planFixtureProvider schema to include a required provider argument and a nested block
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"region": {Type: cty.String, Required: true},
"url": {Type: cty.String, Required: true},
},
BlockTypes: map[string]*configschema.NestedBlock{
"auth": {
Nesting: configschema.NestingList,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"user": {Type: cty.String, Required: true},
"password": {Type: cty.String, Required: true},
},
},
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
},
},
},
},
}

view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}

args := []string{}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("bad: %d\n\n%s", code, output.Stderr())
}

if !p.ConfigureProviderCalled {
t.Fatal("configure provider not called")
}

// For this test, we want to confirm that we've sent the expected config
// value *to* the provider.
got := p.ConfigureProviderRequest.Config
want := cty.ObjectVal(map[string]cty.Value{
"auth": cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"user": cty.StringVal("one"),
"password": cty.StringVal("onepw"),
}),
cty.ObjectVal(map[string]cty.Value{
"user": cty.StringVal("two"),
"password": cty.StringVal("twopw"),
}),
}),
"region": cty.StringVal("us-east-1"),
"url": cty.StringVal("example.com"),
})

if !got.RawEquals(want) {
t.Fatal("wrong provider config")
}

}

func TestPlan_varFile(t *testing.T) {
// Create a temporary working directory that is empty
td := tempDir(t)
Expand Down
20 changes: 20 additions & 0 deletions internal/command/testdata/plan-provider-input/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
variable "users" {
default = {
one = "onepw"
two = "twopw"
}
}

provider "test" {
url = "example.com"

dynamic "auth" {
for_each = var.users
content {
user = auth.key
password = auth.value
}
}
}

resource "test_instance" "test" {}
8 changes: 1 addition & 7 deletions internal/terraform/eval_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@ func buildProviderConfig(ctx EvalContext, addr addrs.AbsProviderConfig, config *
switch {
case configBody != nil && inputBody != nil:
log.Printf("[TRACE] buildProviderConfig for %s: merging explicit config and input", addr)
// Note that the inputBody is the _base_ here, because configs.MergeBodies
// expects the base have all of the required fields, while these are
// forced to be optional for the override. The input process should
// guarantee that we have a value for each of the required arguments and
// that in practice the sets of attributes in each body will be
// disjoint.
return configs.MergeBodies(inputBody, configBody)
return hcl.MergeBodies([]hcl.Body{inputBody, configBody})
case configBody != nil:
log.Printf("[TRACE] buildProviderConfig for %s: using explicit config only", addr)
return configBody
Expand Down
38 changes: 31 additions & 7 deletions internal/terraform/node_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,23 @@ func TestNodeApplyableProviderExecute(t *testing.T) {
config := &configs.Provider{
Name: "foo",
Config: configs.SynthBody("", map[string]cty.Value{
"test_string": cty.StringVal("hello"),
"user": cty.StringVal("hello"),
}),
}
provider := mockProviderWithConfigSchema(simpleTestSchema())

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"),
Expand All @@ -34,19 +47,30 @@ func TestNodeApplyableProviderExecute(t *testing.T) {

ctx := &MockEvalContext{ProviderProvider: provider}
ctx.installSimpleEval()
if err := n.Execute(ctx, walkApply); err != nil {
t.Fatalf("err: %s", err)
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("test_string") {
t.Fatal("configuration object does not have \"test_string\" attribute")
if !gotObj.Type().HasAttribute("user") {
t.Fatal("configuration object does not have \"user\" attribute")
}
if got, want := gotObj.GetAttr("test_string"), cty.StringVal("hello"); !got.RawEquals(want) {
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)
}
}
Expand Down