github.com/hashicorp/terraform-plugin-sdk@v1.17.2/terraform/context_input.go (about)

     1  package terraform
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hcldec"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    14  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
    15  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    16  )
    17  
    18  // Input asks for input to fill variables and provider configurations.
    19  // This modifies the configuration in-place, so asking for Input twice
    20  // may result in different UI output showing different current values.
    21  func (c *Context) Input(mode InputMode) tfdiags.Diagnostics {
    22  	var diags tfdiags.Diagnostics
    23  	defer c.acquireRun("input")()
    24  
    25  	if c.uiInput == nil {
    26  		log.Printf("[TRACE] Context.Input: uiInput is nil, so skipping")
    27  		return diags
    28  	}
    29  
    30  	ctx := context.Background()
    31  
    32  	if mode&InputModeVar != 0 {
    33  		log.Printf("[TRACE] Context.Input: Prompting for variables")
    34  
    35  		// Walk the variables first for the root module. We walk them in
    36  		// alphabetical order for UX reasons.
    37  		configs := c.config.Module.Variables
    38  		names := make([]string, 0, len(configs))
    39  		for name := range configs {
    40  			names = append(names, name)
    41  		}
    42  		sort.Strings(names)
    43  	Variables:
    44  		for _, n := range names {
    45  			v := configs[n]
    46  
    47  			// If we only care about unset variables, then we should set any
    48  			// variable that is already set.
    49  			if mode&InputModeVarUnset != 0 {
    50  				if _, isSet := c.variables[n]; isSet {
    51  					continue
    52  				}
    53  			}
    54  
    55  			// this should only happen during tests
    56  			if c.uiInput == nil {
    57  				log.Println("[WARN] Context.uiInput is nil during input walk")
    58  				continue
    59  			}
    60  
    61  			// Ask the user for a value for this variable
    62  			var rawValue string
    63  			retry := 0
    64  			for {
    65  				var err error
    66  				rawValue, err = c.uiInput.Input(ctx, &InputOpts{
    67  					Id:          fmt.Sprintf("var.%s", n),
    68  					Query:       fmt.Sprintf("var.%s", n),
    69  					Description: v.Description,
    70  				})
    71  				if err != nil {
    72  					diags = diags.Append(tfdiags.Sourceless(
    73  						tfdiags.Error,
    74  						"Failed to request interactive input",
    75  						fmt.Sprintf("Terraform attempted to request a value for var.%s interactively, but encountered an error: %s.", n, err),
    76  					))
    77  					return diags
    78  				}
    79  
    80  				if rawValue == "" && v.Default == cty.NilVal {
    81  					// Redo if it is required, but abort if we keep getting
    82  					// blank entries
    83  					if retry > 2 {
    84  						diags = diags.Append(tfdiags.Sourceless(
    85  							tfdiags.Error,
    86  							"Required variable not assigned",
    87  							fmt.Sprintf("The variable %q is required, so Terraform cannot proceed without a defined value for it.", n),
    88  						))
    89  						continue Variables
    90  					}
    91  					retry++
    92  					continue
    93  				}
    94  
    95  				break
    96  			}
    97  
    98  			val, valDiags := v.ParsingMode.Parse(n, rawValue)
    99  			diags = diags.Append(valDiags)
   100  			if diags.HasErrors() {
   101  				continue
   102  			}
   103  
   104  			c.variables[n] = &InputValue{
   105  				Value:      val,
   106  				SourceType: ValueFromInput,
   107  			}
   108  		}
   109  	}
   110  
   111  	if mode&InputModeProvider != 0 {
   112  		log.Printf("[TRACE] Context.Input: Prompting for provider arguments")
   113  
   114  		// We prompt for input only for provider configurations defined in
   115  		// the root module. At the time of writing that is an arbitrary
   116  		// restriction, but we have future plans to support "count" and
   117  		// "for_each" on modules that will then prevent us from supporting
   118  		// input for child module configurations anyway (since we'd need to
   119  		// dynamic-expand first), and provider configurations in child modules
   120  		// are not recommended since v0.11 anyway, so this restriction allows
   121  		// us to keep this relatively simple without significant hardship.
   122  
   123  		pcs := make(map[string]*configs.Provider)
   124  		pas := make(map[string]addrs.ProviderConfig)
   125  		for _, pc := range c.config.Module.ProviderConfigs {
   126  			addr := pc.Addr()
   127  			pcs[addr.String()] = pc
   128  			pas[addr.String()] = addr
   129  			log.Printf("[TRACE] Context.Input: Provider %s declared at %s", addr, pc.DeclRange)
   130  		}
   131  		// We also need to detect _implied_ provider configs from resources.
   132  		// These won't have *configs.Provider objects, but they will still
   133  		// exist in the map and we'll just treat them as empty below.
   134  		for _, rc := range c.config.Module.ManagedResources {
   135  			pa := rc.ProviderConfigAddr()
   136  			if pa.Alias != "" {
   137  				continue // alias configurations cannot be implied
   138  			}
   139  			if _, exists := pcs[pa.String()]; !exists {
   140  				pcs[pa.String()] = nil
   141  				pas[pa.String()] = pa
   142  				log.Printf("[TRACE] Context.Input: Provider %s implied by resource block at %s", pa, rc.DeclRange)
   143  			}
   144  		}
   145  		for _, rc := range c.config.Module.DataResources {
   146  			pa := rc.ProviderConfigAddr()
   147  			if pa.Alias != "" {
   148  				continue // alias configurations cannot be implied
   149  			}
   150  			if _, exists := pcs[pa.String()]; !exists {
   151  				pcs[pa.String()] = nil
   152  				pas[pa.String()] = pa
   153  				log.Printf("[TRACE] Context.Input: Provider %s implied by data block at %s", pa, rc.DeclRange)
   154  			}
   155  		}
   156  
   157  		for pk, pa := range pas {
   158  			pc := pcs[pk] // will be nil if this is an implied config
   159  
   160  			// Wrap the input into a namespace
   161  			input := &PrefixUIInput{
   162  				IdPrefix:    pk,
   163  				QueryPrefix: pk + ".",
   164  				UIInput:     c.uiInput,
   165  			}
   166  
   167  			schema := c.schemas.ProviderConfig(pa.Type)
   168  			if schema == nil {
   169  				// Could either be an incorrect config or just an incomplete
   170  				// mock in tests. We'll let a later pass decide, and just
   171  				// ignore this for the purposes of gathering input.
   172  				log.Printf("[TRACE] Context.Input: No schema available for provider type %q", pa.Type)
   173  				continue
   174  			}
   175  
   176  			// For our purposes here we just want to detect if attrbutes are
   177  			// set in config at all, so rather than doing a full decode
   178  			// (which would require us to prepare an evalcontext, etc) we'll
   179  			// use the low-level HCL API to process only the top-level
   180  			// structure.
   181  			var attrExprs hcl.Attributes // nil if there is no config
   182  			if pc != nil && pc.Config != nil {
   183  				lowLevelSchema := schemaForInputSniffing(hcldec.ImpliedSchema(schema.DecoderSpec()))
   184  				content, _, diags := pc.Config.PartialContent(lowLevelSchema)
   185  				if diags.HasErrors() {
   186  					log.Printf("[TRACE] Context.Input: %s has decode error, so ignoring: %s", pa, diags.Error())
   187  					continue
   188  				}
   189  				attrExprs = content.Attributes
   190  			}
   191  
   192  			keys := make([]string, 0, len(schema.Attributes))
   193  			for key := range schema.Attributes {
   194  				keys = append(keys, key)
   195  			}
   196  			sort.Strings(keys)
   197  
   198  			vals := map[string]cty.Value{}
   199  			for _, key := range keys {
   200  				attrS := schema.Attributes[key]
   201  				if attrS.Optional {
   202  					continue
   203  				}
   204  				if attrExprs != nil {
   205  					if _, exists := attrExprs[key]; exists {
   206  						continue
   207  					}
   208  				}
   209  				if !attrS.Type.Equals(cty.String) {
   210  					continue
   211  				}
   212  
   213  				log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key)
   214  				rawVal, err := input.Input(ctx, &InputOpts{
   215  					Id:          key,
   216  					Query:       key,
   217  					Description: attrS.Description,
   218  				})
   219  				if err != nil {
   220  					log.Printf("[TRACE] Context.Input: Failed to prompt for %s argument %s: %s", pa, key, err)
   221  					continue
   222  				}
   223  
   224  				vals[key] = cty.StringVal(rawVal)
   225  			}
   226  
   227  			c.providerInputConfig[pk] = vals
   228  			log.Printf("[TRACE] Context.Input: Input for %s: %#v", pk, vals)
   229  		}
   230  	}
   231  
   232  	return diags
   233  }
   234  
   235  // schemaForInputSniffing returns a transformed version of a given schema
   236  // that marks all attributes as optional, which the Context.Input method can
   237  // use to detect whether a required argument is set without missing arguments
   238  // themselves generating errors.
   239  func schemaForInputSniffing(schema *hcl.BodySchema) *hcl.BodySchema {
   240  	ret := &hcl.BodySchema{
   241  		Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)),
   242  		Blocks:     schema.Blocks,
   243  	}
   244  
   245  	for i, attrS := range schema.Attributes {
   246  		ret.Attributes[i] = attrS
   247  		ret.Attributes[i].Required = false
   248  	}
   249  
   250  	return ret
   251  }