mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-04-10 21:02:01 -04:00
We're intending to gradually improve all of the existing functions that fail these checks as a separate project from other work, because fixing for these particular lint rules tends to be too invasive to be safe or sensible to combine with other work. Therefore we'll temporarily disable these lints from the main lint run and add a separate .golangci-complexity.yml that we can use to track our progress towards eliminating those lint failures without continuing to litter the code with nolint comments in the meantime. This also removes all of the existing nolint comments for these linters so that we can start fresh and review each one as part of our improvement project. We'll re-enable these linters (and remove .golangci-complexity.yml) once each example has either been rewritten to pass the checks or we've concluded that further decomposition would hurt readability and so added "nolint" comments back in so we can review whether our lint rules are too strict once we've got a bunch of examples to consider together. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
218 lines
7.5 KiB
Go
218 lines
7.5 KiB
Go
// 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(ctx context.Context, 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
|
|
}
|
|
|
|
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)
|
|
// NOTE: Historically we were just passing a context.Background into
|
|
// this, because we didn't have any other context.Context to use.
|
|
// We're now passing in our given context but intentionally ignoring
|
|
// its cancellation because the main UIInput implementation (in
|
|
// package command) already/ had direct SIGINT handling and we want to
|
|
// preserve that behavior.
|
|
// FIXME: Remove the UIInput's own SIGINT handling and rely on the
|
|
// cancellation of ctx instead.
|
|
rawVal, err := input.Input(context.WithoutCancel(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
|
|
}
|