core: Input walk shouldn't clobber dynamic provider config

During the input walk we stash the values resulting from user input
(if any) in the eval context for use when later walks need to resolve
the provider config.

However, this repository of input results is only able to represent
literal values, since it does not retain the record of which of the keys
have values that are "computed".

Previously we were blindly stashing all of the results, failing to
consider that some of them might be computed. That resulted in the
UnknownValue placeholder being misinterpreted as a literal value when
the data is used later, which ultimately resulted in it clobbering the
actual expression evaluation result and thus causing the provider to
fail to configure itself.

Now we are careful to only retain in this repository the keys whose values
are known statically during the input phase. This eventually gets merged
with the dynamic evaluation results on subsequent walks, with the dynamic
keys left untouched due to their absence from the stored input map.

This fixes #11264.
This commit is contained in:
Martin Atkins 2017-04-03 12:23:50 -07:00
parent c3175a556f
commit dd8af65c82
2 changed files with 106 additions and 7 deletions

View File

@ -30,6 +30,11 @@ func (n *EvalBuildProviderConfig) Eval(ctx EvalContext) (interface{}, error) {
// If we have a configuration set, then merge that in // If we have a configuration set, then merge that in
if input := ctx.ProviderInput(n.Provider); input != nil { if input := ctx.ProviderInput(n.Provider); input != nil {
// "input" is a map of the subset of config values that were known
// during the input walk, set by EvalInputProvider. Note that
// in particular it does *not* include attributes that had
// computed values at input time; those appear *only* in
// "cfg" here.
rc, err := config.NewRawConfig(input) rc, err := config.NewRawConfig(input)
if err != nil { if err != nil {
return nil, err return nil, err
@ -136,7 +141,21 @@ func (n *EvalInputProvider) Eval(ctx EvalContext) (interface{}, error) {
// Set the input that we received so that child modules don't attempt // Set the input that we received so that child modules don't attempt
// to ask for input again. // to ask for input again.
if config != nil && len(config.Config) > 0 { if config != nil && len(config.Config) > 0 {
ctx.SetProviderInput(n.Name, config.Config) // This repository of provider input results on the context doesn't
// retain config.ComputedKeys, so we need to filter those out here
// in order that later users of this data won't try to use the unknown
// value placeholder as if it were a literal value. This map is just
// of known values we've been able to complete so far; dynamic stuff
// will be merged in by EvalBuildProviderConfig on subsequent
// (post-input) walks.
confMap := config.Config
if config.ComputedKeys != nil {
for _, key := range config.ComputedKeys {
delete(confMap, key)
}
}
ctx.SetProviderInput(n.Name, confMap)
} else { } else {
ctx.SetProviderInput(n.Name, map[string]interface{}{}) ctx.SetProviderInput(n.Name, map[string]interface{}{})
} }

View File

@ -3,6 +3,8 @@ package terraform
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/hashicorp/terraform/config"
) )
func TestEvalBuildProviderConfig_impl(t *testing.T) { func TestEvalBuildProviderConfig_impl(t *testing.T) {
@ -10,7 +12,11 @@ func TestEvalBuildProviderConfig_impl(t *testing.T) {
} }
func TestEvalBuildProviderConfig(t *testing.T) { func TestEvalBuildProviderConfig(t *testing.T) {
config := testResourceConfig(t, map[string]interface{}{}) config := testResourceConfig(t, map[string]interface{}{
"set_in_config": "config",
"set_in_config_and_parent": "config",
"computed_in_config": "config",
})
provider := "foo" provider := "foo"
n := &EvalBuildProviderConfig{ n := &EvalBuildProviderConfig{
@ -21,22 +27,33 @@ func TestEvalBuildProviderConfig(t *testing.T) {
ctx := &MockEvalContext{ ctx := &MockEvalContext{
ParentProviderConfigConfig: testResourceConfig(t, map[string]interface{}{ ParentProviderConfigConfig: testResourceConfig(t, map[string]interface{}{
"foo": "bar", "inherited_from_parent": "parent",
"set_in_config_and_parent": "parent",
}), }),
ProviderInputConfig: map[string]interface{}{ ProviderInputConfig: map[string]interface{}{
"bar": "baz", "set_in_config": "input",
"set_by_input": "input",
}, },
} }
if _, err := n.Eval(ctx); err != nil { if _, err := n.Eval(ctx); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
// This is a merger of the following, with later items taking precedence:
// - "config" (the config as written in the current module, with all
// interpolation expressions resolved)
// - ProviderInputConfig (mock of config produced by the input walk, after
// prompting the user interactively for values unspecified in config)
// - ParentProviderConfigConfig (mock of config inherited from a parent module)
expected := map[string]interface{}{ expected := map[string]interface{}{
"foo": "bar", "set_in_config": "input", // in practice, input map contains identical literals from config
"bar": "baz", "set_in_config_and_parent": "parent",
"inherited_from_parent": "parent",
"computed_in_config": "config",
"set_by_input": "input",
} }
if !reflect.DeepEqual(config.Raw, expected) { if !reflect.DeepEqual(config.Raw, expected) {
t.Fatalf("bad: %#v", config.Raw) t.Fatalf("incorrect merged config %#v; want %#v", config.Raw, expected)
} }
} }
@ -151,3 +168,66 @@ func TestEvalGetProvider(t *testing.T) {
t.Fatalf("bad: %#v", ctx.ProviderName) t.Fatalf("bad: %#v", ctx.ProviderName)
} }
} }
func TestEvalInputProvider(t *testing.T) {
var provider ResourceProvider = &MockResourceProvider{
InputFn: func(ui UIInput, c *ResourceConfig) (*ResourceConfig, error) {
if c.Config["mock_config"] != "mock" {
t.Fatalf("original config not passed to provider.Input")
}
rawConfig, err := config.NewRawConfig(map[string]interface{}{
"set_in_config": "input",
"set_by_input": "input",
"computed": "fake_computed",
})
if err != nil {
return nil, err
}
config := NewResourceConfig(rawConfig)
config.ComputedKeys = []string{"computed"} // fake computed key
return config, nil
},
}
ctx := &MockEvalContext{ProviderProvider: provider}
rawConfig, err := config.NewRawConfig(map[string]interface{}{
"mock_config": "mock",
})
if err != nil {
t.Fatalf("NewRawConfig failed: %s", err)
}
config := NewResourceConfig(rawConfig)
n := &EvalInputProvider{
Name: "mock",
Provider: &provider,
Config: &config,
}
result, err := n.Eval(ctx)
if err != nil {
t.Fatalf("Eval failed: %s", err)
}
if result != nil {
t.Fatalf("Eval returned non-nil result %#v", result)
}
if !ctx.SetProviderInputCalled {
t.Fatalf("ctx.SetProviderInput wasn't called")
}
if got, want := ctx.SetProviderInputName, "mock"; got != want {
t.Errorf("wrong provider name %q; want %q", got, want)
}
inputCfg := ctx.SetProviderInputConfig
want := map[string]interface{}{
"set_in_config": "input",
"set_by_input": "input",
// "computed" is omitted because it value isn't known at input time
}
if !reflect.DeepEqual(inputCfg, want) {
t.Errorf("got incorrect input config %#v; want %#v", inputCfg, want)
}
}