| package terraform |
| |
| import ( |
| "context" |
| "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 |
| } |
| |
| ctx := context.Background() |
| |
| 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(ctx, &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(ctx, &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 |
| } |