github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_input.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"context"
    10  	"log"
    11  	"sort"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hcldec"
    15  	"github.com/zclconf/go-cty/cty"
    16  
    17  	"github.com/opentofu/opentofu/internal/addrs"
    18  	"github.com/opentofu/opentofu/internal/configs"
    19  	"github.com/opentofu/opentofu/internal/tfdiags"
    20  )
    21  
    22  // Input asks for input to fill unset required arguments in provider
    23  // configurations.
    24  //
    25  // Unlike the other better-behaved operation methods, this one actually
    26  // modifies some internal state inside the receving context so that the
    27  // captured values will be implicitly available to a subsequent call to Plan,
    28  // or to some other operation entry point. Hopefully a future iteration of
    29  // this will change design to make that data flow more explicit.
    30  //
    31  // Because Input saves the results inside the Context object, asking for
    32  // input twice on the same Context is invalid and will lead to undefined
    33  // behavior.
    34  //
    35  // Once you've called Input with a particular config, it's invalid to call
    36  // any other Context method with a different config, because the aforementioned
    37  // modified internal state won't match. Again, this is an architectural wart
    38  // that we'll hopefully resolve in future.
    39  func (c *Context) Input(config *configs.Config, mode InputMode) tfdiags.Diagnostics {
    40  	// This function used to be responsible for more than it is now, so its
    41  	// interface is more general than its current functionality requires.
    42  	// It now exists only to handle interactive prompts for provider
    43  	// configurations, with other prompts the responsibility of the CLI
    44  	// layer prior to calling in to this package.
    45  	//
    46  	// (Hopefully in future the remaining functionality here can move to the
    47  	// CLI layer too in order to avoid this odd situation where core code
    48  	// produces UI input prompts.)
    49  
    50  	var diags tfdiags.Diagnostics
    51  	defer c.acquireRun("input")()
    52  
    53  	schemas, moreDiags := c.Schemas(config, nil)
    54  	diags = diags.Append(moreDiags)
    55  	if moreDiags.HasErrors() {
    56  		return diags
    57  	}
    58  
    59  	if c.uiInput == nil {
    60  		log.Printf("[TRACE] Context.Input: uiInput is nil, so skipping")
    61  		return diags
    62  	}
    63  
    64  	ctx := context.Background()
    65  
    66  	if mode&InputModeProvider != 0 {
    67  		log.Printf("[TRACE] Context.Input: Prompting for provider arguments")
    68  
    69  		// We prompt for input only for provider configurations defined in
    70  		// the root module. Provider configurations in other modules are a
    71  		// legacy thing we no longer recommend, and even if they weren't we
    72  		// can't practically prompt for their inputs here because we've not
    73  		// yet done "expansion" and so we don't know whether the modules are
    74  		// using count or for_each.
    75  
    76  		pcs := make(map[string]*configs.Provider)
    77  		pas := make(map[string]addrs.LocalProviderConfig)
    78  		for _, pc := range config.Module.ProviderConfigs {
    79  			addr := pc.Addr()
    80  			pcs[addr.String()] = pc
    81  			pas[addr.String()] = addr
    82  			log.Printf("[TRACE] Context.Input: Provider %s declared at %s", addr, pc.DeclRange)
    83  		}
    84  		// We also need to detect _implied_ provider configs from resources.
    85  		// These won't have *configs.Provider objects, but they will still
    86  		// exist in the map and we'll just treat them as empty below.
    87  		for _, rc := range config.Module.ManagedResources {
    88  			pa := rc.ProviderConfigAddr()
    89  			if pa.Alias != "" {
    90  				continue // alias configurations cannot be implied
    91  			}
    92  			if _, exists := pcs[pa.String()]; !exists {
    93  				pcs[pa.String()] = nil
    94  				pas[pa.String()] = pa
    95  				log.Printf("[TRACE] Context.Input: Provider %s implied by resource block at %s", pa, rc.DeclRange)
    96  			}
    97  		}
    98  		for _, rc := range config.Module.DataResources {
    99  			pa := rc.ProviderConfigAddr()
   100  			if pa.Alias != "" {
   101  				continue // alias configurations cannot be implied
   102  			}
   103  			if _, exists := pcs[pa.String()]; !exists {
   104  				pcs[pa.String()] = nil
   105  				pas[pa.String()] = pa
   106  				log.Printf("[TRACE] Context.Input: Provider %s implied by data block at %s", pa, rc.DeclRange)
   107  			}
   108  		}
   109  
   110  		for pk, pa := range pas {
   111  			pc := pcs[pk] // will be nil if this is an implied config
   112  
   113  			// Wrap the input into a namespace
   114  			input := &PrefixUIInput{
   115  				IdPrefix:    pk,
   116  				QueryPrefix: pk + ".",
   117  				UIInput:     c.uiInput,
   118  			}
   119  
   120  			providerFqn := config.Module.ProviderForLocalConfig(pa)
   121  			schema := schemas.ProviderConfig(providerFqn)
   122  			if schema == nil {
   123  				// Could either be an incorrect config or just an incomplete
   124  				// mock in tests. We'll let a later pass decide, and just
   125  				// ignore this for the purposes of gathering input.
   126  				log.Printf("[TRACE] Context.Input: No schema available for provider type %q", pa.LocalName)
   127  				continue
   128  			}
   129  
   130  			// For our purposes here we just want to detect if attrbutes are
   131  			// set in config at all, so rather than doing a full decode
   132  			// (which would require us to prepare an evalcontext, etc) we'll
   133  			// use the low-level HCL API to process only the top-level
   134  			// structure.
   135  			var attrExprs hcl.Attributes // nil if there is no config
   136  			if pc != nil && pc.Config != nil {
   137  				lowLevelSchema := schemaForInputSniffing(hcldec.ImpliedSchema(schema.DecoderSpec()))
   138  				content, _, diags := pc.Config.PartialContent(lowLevelSchema)
   139  				if diags.HasErrors() {
   140  					log.Printf("[TRACE] Context.Input: %s has decode error, so ignoring: %s", pa, diags.Error())
   141  					continue
   142  				}
   143  				attrExprs = content.Attributes
   144  			}
   145  
   146  			keys := make([]string, 0, len(schema.Attributes))
   147  			for key := range schema.Attributes {
   148  				keys = append(keys, key)
   149  			}
   150  			sort.Strings(keys)
   151  
   152  			vals := map[string]cty.Value{}
   153  			for _, key := range keys {
   154  				attrS := schema.Attributes[key]
   155  				if attrS.Optional {
   156  					continue
   157  				}
   158  				if attrExprs != nil {
   159  					if _, exists := attrExprs[key]; exists {
   160  						continue
   161  					}
   162  				}
   163  				if !attrS.Type.Equals(cty.String) {
   164  					continue
   165  				}
   166  
   167  				log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key)
   168  				rawVal, err := input.Input(ctx, &InputOpts{
   169  					Id:          key,
   170  					Query:       key,
   171  					Description: attrS.Description,
   172  				})
   173  				if err != nil {
   174  					log.Printf("[TRACE] Context.Input: Failed to prompt for %s argument %s: %s", pa, key, err)
   175  					continue
   176  				}
   177  
   178  				vals[key] = cty.StringVal(rawVal)
   179  			}
   180  
   181  			absConfigAddr := addrs.AbsProviderConfig{
   182  				Provider: providerFqn,
   183  				Alias:    pa.Alias,
   184  				Module:   config.Path,
   185  			}
   186  			c.providerInputConfig[absConfigAddr.String()] = vals
   187  
   188  			log.Printf("[TRACE] Context.Input: Input for %s: %#v", pk, vals)
   189  		}
   190  	}
   191  
   192  	return diags
   193  }
   194  
   195  // schemaForInputSniffing returns a transformed version of a given schema
   196  // that marks all attributes as optional, which the Context.Input method can
   197  // use to detect whether a required argument is set without missing arguments
   198  // themselves generating errors.
   199  func schemaForInputSniffing(schema *hcl.BodySchema) *hcl.BodySchema {
   200  	ret := &hcl.BodySchema{
   201  		Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)),
   202  		Blocks:     schema.Blocks,
   203  	}
   204  
   205  	for i, attrS := range schema.Attributes {
   206  		ret.Attributes[i] = attrS
   207  		ret.Attributes[i].Required = false
   208  	}
   209  
   210  	return ret
   211  }