// 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 ( "context" "log" "sort" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/zclconf/go-cty/cty" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/tfdiags" ) // Input asks for input to fill unset required arguments in provider // configurations. // // Unlike the other better-behaved operation methods, this one actually // modifies some internal state inside the receiving context so that the // captured values will be implicitly available to a subsequent call to Plan, // or to some other operation entry point. Hopefully a future iteration of // this will change design to make that data flow more explicit. // // Because Input saves the results inside the Context object, asking for // input twice on the same Context is invalid and will lead to undefined // behavior. // // Once you've called Input with a particular config, it's invalid to call // any other Context method with a different config, because the aforementioned // modified internal state won't match. Again, this is an architectural wart // that we'll hopefully resolve in future. func (c *Context) Input(config *configs.Config, mode InputMode) tfdiags.Diagnostics { // This function used to be responsible for more than it is now, so its // interface is more general than its current functionality requires. // It now exists only to handle interactive prompts for provider // configurations, with other prompts the responsibility of the CLI // layer prior to calling in to this package. // // (Hopefully in future the remaining functionality here can move to the // CLI layer too in order to avoid this odd situation where core code // produces UI input prompts.) var diags tfdiags.Diagnostics defer c.acquireRun("input")() schemas, moreDiags := c.Schemas(config, nil) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return diags } if c.uiInput == nil { log.Printf("[TRACE] Context.Input: uiInput is nil, so skipping") return diags } ctx := context.Background() 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. Provider configurations in other modules are a // legacy thing we no longer recommend, and even if they weren't we // can't practically prompt for their inputs here because we've not // yet done "expansion" and so we don't know whether the modules are // using count or for_each. pcs := make(map[string]*configs.Provider) pas := make(map[string]addrs.LocalProviderConfig) for _, pc := range 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 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 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, } providerFqn := config.Module.ProviderForLocalConfig(pa) schema := schemas.ProviderConfig(providerFqn) 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.LocalName) continue } // For our purposes here we just want to detect if attributes 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) } absConfigAddr := addrs.AbsProviderConfig{ Provider: providerFqn, Alias: pa.Alias, Module: config.Path, } c.providerInputConfig[absConfigAddr.String()] = 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 }