core: Context.Input as config walk, rather than graph walk
Now that core has access to the provider configuration schema, our input logic can be implemented entirely within Context.Input, removing the need to execute a full graph walk to gather input. This commit replaces the graph walk call with instead just visiting the provider configurations (explicit and implied) in the root module, using the schema to prompt. The code to manage the input graph walk is not yet removed by this commit, and will be cleaned up in a subsequent commit once we've made sure there aren't any other callers/tests depending on parts of it.
This commit is contained in:
parent
1761faa29c
commit
2002fee32e
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
@ -433,108 +432,6 @@ func (c *Context) Interpolater() *Interpolater {
|
|||
return &Interpolater{}
|
||||
}
|
||||
|
||||
// Input asks for input to fill variables and provider configurations.
|
||||
// This modifies the configuration in-place, so asking for Input twice
|
||||
// may result in different UI output showing different current values.
|
||||
func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
defer c.acquireRun("input")()
|
||||
|
||||
if mode&InputModeVar != 0 {
|
||||
// Walk the variables first for the root module. We walk them in
|
||||
// alphabetical order for UX reasons.
|
||||
configs := c.config.Module.Variables
|
||||
names := make([]string, 0, len(configs))
|
||||
for name := range configs {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
Variables:
|
||||
for _, n := range names {
|
||||
v := configs[n]
|
||||
|
||||
// If we only care about unset variables, then we should set any
|
||||
// variable that is already set.
|
||||
if mode&InputModeVarUnset != 0 {
|
||||
if _, isSet := c.variables[n]; isSet {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// this should only happen during tests
|
||||
if c.uiInput == nil {
|
||||
log.Println("[WARN] Context.uiInput is nil during input walk")
|
||||
continue
|
||||
}
|
||||
|
||||
// Ask the user for a value for this variable
|
||||
var rawValue string
|
||||
retry := 0
|
||||
for {
|
||||
var err error
|
||||
rawValue, err = c.uiInput.Input(&InputOpts{
|
||||
Id: fmt.Sprintf("var.%s", n),
|
||||
Query: fmt.Sprintf("var.%s", n),
|
||||
Description: v.Description,
|
||||
})
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to request interactive input",
|
||||
fmt.Sprintf("Terraform attempted to request a value for var.%s interactively, but encountered an error: %s.", n, err),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
if rawValue == "" && v.Default == cty.NilVal {
|
||||
// Redo if it is required, but abort if we keep getting
|
||||
// blank entries
|
||||
if retry > 2 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required variable not assigned",
|
||||
fmt.Sprintf("The variable %q is required, so Terraform cannot proceed without a defined value for it.", n),
|
||||
))
|
||||
continue Variables
|
||||
}
|
||||
retry++
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
val, valDiags := v.ParsingMode.Parse(n, rawValue)
|
||||
diags = diags.Append(valDiags)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
c.variables[n] = &InputValue{
|
||||
Value: val,
|
||||
SourceType: ValueFromInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode&InputModeProvider != 0 {
|
||||
// Build the graph
|
||||
graph, err := c.Graph(GraphTypeInput, nil)
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
return diags
|
||||
}
|
||||
|
||||
// Do the walk
|
||||
if _, err := c.walk(graph, walkInput); err != nil {
|
||||
diags = diags.Append(err)
|
||||
return diags
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// Apply applies the changes represented by this context and returns
|
||||
// the resulting state.
|
||||
//
|
||||
|
|
|
@ -0,0 +1,248 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcldec"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
// Input asks for input to fill variables and provider configurations.
|
||||
// This modifies the configuration in-place, so asking for Input twice
|
||||
// may result in different UI output showing different current values.
|
||||
func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
defer c.acquireRun("input")()
|
||||
|
||||
if c.uiInput == nil {
|
||||
log.Printf("[TRACE] Context.Input: uiInput is nil, so skipping")
|
||||
return diags
|
||||
}
|
||||
|
||||
if mode&InputModeVar != 0 {
|
||||
log.Printf("[TRACE] Context.Input: Prompting for variables")
|
||||
|
||||
// Walk the variables first for the root module. We walk them in
|
||||
// alphabetical order for UX reasons.
|
||||
configs := c.config.Module.Variables
|
||||
names := make([]string, 0, len(configs))
|
||||
for name := range configs {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
Variables:
|
||||
for _, n := range names {
|
||||
v := configs[n]
|
||||
|
||||
// If we only care about unset variables, then we should set any
|
||||
// variable that is already set.
|
||||
if mode&InputModeVarUnset != 0 {
|
||||
if _, isSet := c.variables[n]; isSet {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// this should only happen during tests
|
||||
if c.uiInput == nil {
|
||||
log.Println("[WARN] Context.uiInput is nil during input walk")
|
||||
continue
|
||||
}
|
||||
|
||||
// Ask the user for a value for this variable
|
||||
var rawValue string
|
||||
retry := 0
|
||||
for {
|
||||
var err error
|
||||
rawValue, err = c.uiInput.Input(&InputOpts{
|
||||
Id: fmt.Sprintf("var.%s", n),
|
||||
Query: fmt.Sprintf("var.%s", n),
|
||||
Description: v.Description,
|
||||
})
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Failed to request interactive input",
|
||||
fmt.Sprintf("Terraform attempted to request a value for var.%s interactively, but encountered an error: %s.", n, err),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
if rawValue == "" && v.Default == cty.NilVal {
|
||||
// Redo if it is required, but abort if we keep getting
|
||||
// blank entries
|
||||
if retry > 2 {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required variable not assigned",
|
||||
fmt.Sprintf("The variable %q is required, so Terraform cannot proceed without a defined value for it.", n),
|
||||
))
|
||||
continue Variables
|
||||
}
|
||||
retry++
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
val, valDiags := v.ParsingMode.Parse(n, rawValue)
|
||||
diags = diags.Append(valDiags)
|
||||
if diags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
c.variables[n] = &InputValue{
|
||||
Value: val,
|
||||
SourceType: ValueFromInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode&InputModeProvider != 0 {
|
||||
log.Printf("[TRACE] Context.Input: Prompting for provider arguments")
|
||||
|
||||
// We prompt for input only for provider configurations defined in
|
||||
// the root module. At the time of writing that is an arbitrary
|
||||
// restriction, but we have future plans to support "count" and
|
||||
// "for_each" on modules that will then prevent us from supporting
|
||||
// input for child module configurations anyway (since we'd need to
|
||||
// dynamic-expand first), and provider configurations in child modules
|
||||
// are not recommended since v0.11 anyway, so this restriction allows
|
||||
// us to keep this relatively simple without significant hardship.
|
||||
|
||||
pcs := make(map[string]*configs.Provider)
|
||||
pas := make(map[string]addrs.ProviderConfig)
|
||||
for _, pc := range c.config.Module.ProviderConfigs {
|
||||
addr := pc.Addr()
|
||||
pcs[addr.String()] = pc
|
||||
pas[addr.String()] = addr
|
||||
log.Printf("[TRACE] Context.Input: Provider %s declared at %s", addr, pc.DeclRange)
|
||||
}
|
||||
// We also need to detect _implied_ provider configs from resources.
|
||||
// These won't have *configs.Provider objects, but they will still
|
||||
// exist in the map and we'll just treat them as empty below.
|
||||
for _, rc := range c.config.Module.ManagedResources {
|
||||
pa := rc.ProviderConfigAddr()
|
||||
if pa.Alias != "" {
|
||||
continue // alias configurations cannot be implied
|
||||
}
|
||||
if _, exists := pcs[pa.String()]; !exists {
|
||||
pcs[pa.String()] = nil
|
||||
pas[pa.String()] = pa
|
||||
log.Printf("[TRACE] Context.Input: Provider %s implied by resource block at %s", pa, rc.DeclRange)
|
||||
}
|
||||
}
|
||||
for _, rc := range c.config.Module.DataResources {
|
||||
pa := rc.ProviderConfigAddr()
|
||||
if pa.Alias != "" {
|
||||
continue // alias configurations cannot be implied
|
||||
}
|
||||
if _, exists := pcs[pa.String()]; !exists {
|
||||
pcs[pa.String()] = nil
|
||||
pas[pa.String()] = pa
|
||||
log.Printf("[TRACE] Context.Input: Provider %s implied by data block at %s", pa, rc.DeclRange)
|
||||
}
|
||||
}
|
||||
|
||||
for pk, pa := range pas {
|
||||
pc := pcs[pk] // will be nil if this is an implied config
|
||||
|
||||
// Wrap the input into a namespace
|
||||
input := &PrefixUIInput{
|
||||
IdPrefix: pk,
|
||||
QueryPrefix: pk + ".",
|
||||
UIInput: c.uiInput,
|
||||
}
|
||||
|
||||
schema := c.schemas.ProviderConfig(pa.Type)
|
||||
if schema == nil {
|
||||
// Could either be an incorrect config or just an incomplete
|
||||
// mock in tests. We'll let a later pass decide, and just
|
||||
// ignore this for the purposes of gathering input.
|
||||
log.Printf("[TRACE] Context.Input: No schema available for provider type %q", pa.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
// For our purposes here we just want to detect if attrbutes are
|
||||
// set in config at all, so rather than doing a full decode
|
||||
// (which would require us to prepare an evalcontext, etc) we'll
|
||||
// use the low-level HCL API to process only the top-level
|
||||
// structure.
|
||||
var attrExprs hcl.Attributes // nil if there is no config
|
||||
if pc != nil && pc.Config != nil {
|
||||
lowLevelSchema := schemaForInputSniffing(hcldec.ImpliedSchema(schema.DecoderSpec()))
|
||||
content, _, diags := pc.Config.PartialContent(lowLevelSchema)
|
||||
if diags.HasErrors() {
|
||||
log.Printf("[TRACE] Context.Input: %s has decode error, so ignoring: %s", pa, diags.Error())
|
||||
continue
|
||||
}
|
||||
attrExprs = content.Attributes
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(schema.Attributes))
|
||||
for key := range schema.Attributes {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
vals := map[string]cty.Value{}
|
||||
for _, key := range keys {
|
||||
attrS := schema.Attributes[key]
|
||||
if attrS.Optional {
|
||||
continue
|
||||
}
|
||||
if attrExprs != nil {
|
||||
if _, exists := attrExprs[key]; exists {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !attrS.Type.Equals(cty.String) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key)
|
||||
rawVal, err := input.Input(&InputOpts{
|
||||
Id: key,
|
||||
Query: key,
|
||||
Description: attrS.Description,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] Context.Input: Failed to prompt for %s argument %s: %s", pa, key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
vals[key] = cty.StringVal(rawVal)
|
||||
}
|
||||
|
||||
c.providerInputConfig[pk] = vals
|
||||
log.Printf("[TRACE] Context.Input: Input for %s: %#v", pk, vals)
|
||||
}
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// schemaForInputSniffing returns a transformed version of a given schema
|
||||
// that marks all attributes as optional, which the Context.Input method can
|
||||
// use to detect whether a required argument is set without missing arguments
|
||||
// themselves generating errors.
|
||||
func schemaForInputSniffing(schema *hcl.BodySchema) *hcl.BodySchema {
|
||||
ret := &hcl.BodySchema{
|
||||
Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)),
|
||||
Blocks: schema.Blocks,
|
||||
}
|
||||
|
||||
for i, attrS := range schema.Attributes {
|
||||
ret.Attributes[i] = attrS
|
||||
ret.Attributes[i].Required = false
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
|
@ -112,6 +112,27 @@ func TestContext2Input_provider(t *testing.T) {
|
|||
p := testProvider("aws")
|
||||
p.ApplyFn = testApplyFn
|
||||
p.DiffFn = testDiffFn
|
||||
p.GetSchemaReturn = &ProviderSchema{
|
||||
Provider: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Description: "something something",
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"aws_instance": {},
|
||||
},
|
||||
}
|
||||
|
||||
inp := &MockUIInput{
|
||||
InputReturnMap: map[string]string{
|
||||
"provider.aws.foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
ProviderResolver: ResourceProviderResolverFixed(
|
||||
|
@ -119,13 +140,10 @@ func TestContext2Input_provider(t *testing.T) {
|
|||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
),
|
||||
UIInput: inp,
|
||||
})
|
||||
|
||||
var actual interface{}
|
||||
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
|
||||
c.Config["foo"] = "bar"
|
||||
return c, nil
|
||||
}
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
actual = c.Config["foo"]
|
||||
return nil
|
||||
|
@ -138,6 +156,13 @@ func TestContext2Input_provider(t *testing.T) {
|
|||
t.Fatalf("input errors: %s", diags.Err())
|
||||
}
|
||||
|
||||
if !inp.InputCalled {
|
||||
t.Fatal("no input prompt; want prompt for argument \"foo\"")
|
||||
}
|
||||
if got, want := inp.InputOpts.Description, "something something"; got != want {
|
||||
t.Errorf("wrong description\ngot: %q\nwant: %q", got, want)
|
||||
}
|
||||
|
||||
if _, diags := ctx.Plan(); diags.HasErrors() {
|
||||
t.Fatalf("plan errors: %s", diags.Err())
|
||||
}
|
||||
|
@ -153,9 +178,32 @@ func TestContext2Input_provider(t *testing.T) {
|
|||
|
||||
func TestContext2Input_providerMulti(t *testing.T) {
|
||||
m := testModule(t, "input-provider-multi")
|
||||
|
||||
p := testProvider("aws")
|
||||
p.ApplyFn = testApplyFn
|
||||
p.DiffFn = testDiffFn
|
||||
p.GetSchemaReturn = &ProviderSchema{
|
||||
Provider: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Description: "something something",
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"aws_instance": {},
|
||||
},
|
||||
}
|
||||
|
||||
inp := &MockUIInput{
|
||||
InputReturnMap: map[string]string{
|
||||
"provider.aws.foo": "bar",
|
||||
"provider.aws.east.foo": "bar",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
ProviderResolver: ResourceProviderResolverFixed(
|
||||
|
@ -163,20 +211,11 @@ func TestContext2Input_providerMulti(t *testing.T) {
|
|||
"aws": testProviderFuncFixed(p),
|
||||
},
|
||||
),
|
||||
UIInput: inp,
|
||||
})
|
||||
|
||||
var actual []interface{}
|
||||
var lock sync.Mutex
|
||||
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
|
||||
c.Config["foo"] = "bar"
|
||||
return c, nil
|
||||
}
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
actual = append(actual, c.Config["foo"])
|
||||
return nil
|
||||
}
|
||||
p.ValidateFn = func(c *ResourceConfig) ([]string, []error) {
|
||||
return nil, c.CheckSet([]string{"foo"})
|
||||
}
|
||||
|
@ -189,6 +228,12 @@ func TestContext2Input_providerMulti(t *testing.T) {
|
|||
t.Fatalf("plan errors: %s", diags.Err())
|
||||
}
|
||||
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
actual = append(actual, c.Config["foo"])
|
||||
return nil
|
||||
}
|
||||
if _, diags := ctx.Apply(); diags.HasErrors() {
|
||||
t.Fatalf("apply errors: %s", diags.Err())
|
||||
}
|
||||
|
@ -239,10 +284,27 @@ func TestContext2Input_providerOnce(t *testing.T) {
|
|||
|
||||
func TestContext2Input_providerId(t *testing.T) {
|
||||
input := new(MockUIInput)
|
||||
|
||||
m := testModule(t, "input-provider")
|
||||
|
||||
p := testProvider("aws")
|
||||
p.ApplyFn = testApplyFn
|
||||
p.DiffFn = testDiffFn
|
||||
p.GetSchemaReturn = &ProviderSchema{
|
||||
Provider: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Description: "something something",
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"aws_instance": {},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
ProviderResolver: ResourceProviderResolverFixed(
|
||||
|
@ -254,15 +316,6 @@ func TestContext2Input_providerId(t *testing.T) {
|
|||
})
|
||||
|
||||
var actual interface{}
|
||||
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
|
||||
v, err := i.Input(&InputOpts{Id: "foo"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Config["foo"] = v
|
||||
return c, nil
|
||||
}
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
actual = c.Config["foo"]
|
||||
return nil
|
||||
|
@ -291,10 +344,33 @@ func TestContext2Input_providerId(t *testing.T) {
|
|||
|
||||
func TestContext2Input_providerOnly(t *testing.T) {
|
||||
input := new(MockUIInput)
|
||||
|
||||
m := testModule(t, "input-provider-vars")
|
||||
|
||||
p := testProvider("aws")
|
||||
p.ApplyFn = testApplyFn
|
||||
p.DiffFn = testDiffFn
|
||||
p.GetSchemaReturn = &ProviderSchema{
|
||||
Provider: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"aws_instance": {
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"foo": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Config: m,
|
||||
ProviderResolver: ResourceProviderResolverFixed(
|
||||
|
@ -312,14 +388,10 @@ func TestContext2Input_providerOnly(t *testing.T) {
|
|||
})
|
||||
|
||||
input.InputReturnMap = map[string]string{
|
||||
"var.foo": "us-east-1",
|
||||
"provider.aws.foo": "bar",
|
||||
}
|
||||
|
||||
var actual interface{}
|
||||
p.InputFn = func(i UIInput, c *ResourceConfig) (*ResourceConfig, error) {
|
||||
c.Config["foo"] = "bar"
|
||||
return c, nil
|
||||
}
|
||||
p.ConfigureFn = func(c *ResourceConfig) error {
|
||||
actual = c.Config["foo"]
|
||||
return nil
|
||||
|
|
|
@ -211,17 +211,12 @@ func (ctx *BuiltinEvalContext) ProviderInput(pc addrs.ProviderConfig) map[string
|
|||
ctx.ProviderLock.Lock()
|
||||
defer ctx.ProviderLock.Unlock()
|
||||
|
||||
// Go up the module tree, looking for input results for the given provider
|
||||
// configuration.
|
||||
path := ctx.Path()
|
||||
for i := len(path); i >= 0; i-- {
|
||||
k := pc.Absolute(path[:i]).String()
|
||||
if v, ok := ctx.ProviderInputConfig[k]; ok {
|
||||
return v
|
||||
}
|
||||
if !ctx.Path().IsRoot() {
|
||||
// Only root module provider configurations can have input.
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return ctx.ProviderInputConfig[pc.String()]
|
||||
}
|
||||
|
||||
func (ctx *BuiltinEvalContext) SetProviderInput(pc addrs.ProviderConfig, c map[string]cty.Value) {
|
||||
|
|
|
@ -2,28 +2,47 @@ package terraform
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
func buildProviderConfig(ctx EvalContext, addr addrs.ProviderConfig, body hcl.Body) hcl.Body {
|
||||
// If we have an Input configuration set, then merge that in
|
||||
if input := ctx.ProviderInput(addr); 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.
|
||||
|
||||
inputBody := configs.SynthBody("<input prompt>", input)
|
||||
body = configs.MergeBodies(body, inputBody)
|
||||
func buildProviderConfig(ctx EvalContext, addr addrs.ProviderConfig, config *configs.Provider) hcl.Body {
|
||||
var configBody hcl.Body
|
||||
if config != nil {
|
||||
configBody = config.Config
|
||||
}
|
||||
|
||||
return body
|
||||
var inputBody hcl.Body
|
||||
inputConfig := ctx.ProviderInput(addr)
|
||||
if len(inputConfig) > 0 {
|
||||
inputBody = configs.SynthBody("<input-prompt>", inputConfig)
|
||||
}
|
||||
|
||||
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)
|
||||
case configBody != nil:
|
||||
log.Printf("[TRACE] buildProviderConfig for %s: using explicit config only", addr)
|
||||
return configBody
|
||||
case inputBody != nil:
|
||||
log.Printf("[TRACE] buildProviderConfig for %s: using input only", addr)
|
||||
return inputBody
|
||||
default:
|
||||
log.Printf("[TRACE] buildProviderConfig for %s: no configuration at all", addr)
|
||||
return hcl.EmptyBody()
|
||||
}
|
||||
}
|
||||
|
||||
// EvalConfigProvider is an EvalNode implementation that configures
|
||||
|
@ -43,12 +62,7 @@ func (n *EvalConfigProvider) Eval(ctx EvalContext) (interface{}, error) {
|
|||
provider := *n.Provider
|
||||
config := n.Config
|
||||
|
||||
if config == nil {
|
||||
// If we have no explicit configuration, just write an empty
|
||||
// configuration into the provider.
|
||||
configDiags := ctx.ConfigureProvider(n.Addr, cty.EmptyObjectVal)
|
||||
return nil, configDiags.ErrWithWarnings()
|
||||
}
|
||||
configBody := buildProviderConfig(ctx, n.Addr, config)
|
||||
|
||||
schema, err := provider.GetSchema(&ProviderSchemaRequest{})
|
||||
if err != nil {
|
||||
|
@ -60,7 +74,6 @@ func (n *EvalConfigProvider) Eval(ctx EvalContext) (interface{}, error) {
|
|||
}
|
||||
|
||||
configSchema := schema.Provider
|
||||
configBody := buildProviderConfig(ctx, n.Addr, config.Config)
|
||||
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, addrs.NoKey)
|
||||
diags = diags.Append(evalDiags)
|
||||
if evalDiags.HasErrors() {
|
||||
|
|
|
@ -15,26 +15,29 @@ import (
|
|||
|
||||
func TestBuildProviderConfig(t *testing.T) {
|
||||
configBody := configs.SynthBody("", map[string]cty.Value{
|
||||
"set_in_config": cty.StringVal("config"),
|
||||
"set_in_config_and_input": cty.StringVal("config"),
|
||||
"set_in_config": cty.StringVal("config"),
|
||||
})
|
||||
providerAddr := addrs.ProviderConfig{
|
||||
Type: "foo",
|
||||
}
|
||||
|
||||
ctx := &MockEvalContext{
|
||||
// The input values map is expected to contain only keys that aren't
|
||||
// already present in the config, since we skip prompting for
|
||||
// attributes that are already set.
|
||||
ProviderInputValues: map[string]cty.Value{
|
||||
"set_in_config_and_input": cty.StringVal("input"),
|
||||
"set_by_input": cty.StringVal("input"),
|
||||
"set_by_input": cty.StringVal("input"),
|
||||
},
|
||||
}
|
||||
gotBody := buildProviderConfig(ctx, providerAddr, configBody)
|
||||
gotBody := buildProviderConfig(ctx, providerAddr, &configs.Provider{
|
||||
Name: "foo",
|
||||
Config: configBody,
|
||||
})
|
||||
|
||||
schema := &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"set_in_config": {Type: cty.String, Optional: true},
|
||||
"set_in_config_and_input": {Type: cty.String, Optional: true},
|
||||
"set_by_input": {Type: cty.String, Optional: true},
|
||||
"set_in_config": {Type: cty.String, Optional: true},
|
||||
"set_by_input": {Type: cty.String, Optional: true},
|
||||
},
|
||||
}
|
||||
got, diags := hcldec.Decode(gotBody, schema.DecoderSpec(), nil)
|
||||
|
@ -44,9 +47,8 @@ func TestBuildProviderConfig(t *testing.T) {
|
|||
|
||||
// We expect the provider config with the added input value
|
||||
want := cty.ObjectVal(map[string]cty.Value{
|
||||
"set_in_config": cty.StringVal("config"),
|
||||
"set_in_config_and_input": cty.StringVal("input"),
|
||||
"set_by_input": cty.StringVal("input"),
|
||||
"set_in_config": cty.StringVal("config"),
|
||||
"set_by_input": cty.StringVal("input"),
|
||||
})
|
||||
if !got.RawEquals(want) {
|
||||
t.Fatalf("incorrect merged config\ngot: %#v\nwant: %#v", got, want)
|
||||
|
|
|
@ -73,15 +73,7 @@ func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) {
|
|||
var diags tfdiags.Diagnostics
|
||||
provider := *n.Provider
|
||||
|
||||
var sourceBody hcl.Body
|
||||
if n.Config != nil && n.Config.Config != nil {
|
||||
sourceBody = n.Config.Config
|
||||
} else {
|
||||
// If the provider configuration is implicit (no block in configuration
|
||||
// but referred to by resources) then we'll assume an empty body
|
||||
// as a placeholder.
|
||||
sourceBody = hcl.EmptyBody()
|
||||
}
|
||||
configBody := buildProviderConfig(ctx, n.Addr, n.Config)
|
||||
|
||||
schema, err := provider.GetSchema(&ProviderSchemaRequest{})
|
||||
if err != nil {
|
||||
|
@ -100,7 +92,6 @@ func (n *EvalValidateProvider) Eval(ctx EvalContext) (interface{}, error) {
|
|||
configSchema = &configschema.Block{}
|
||||
}
|
||||
|
||||
configBody := buildProviderConfig(ctx, n.Addr, sourceBody)
|
||||
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, addrs.NoKey)
|
||||
diags = diags.Append(evalDiags)
|
||||
if evalDiags.HasErrors() {
|
||||
|
|
Loading…
Reference in New Issue