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 }