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 }