github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend_local.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  
     9  	"github.com/hashicorp/errwrap"
    10  	"github.com/hashicorp/terraform/backend"
    11  	"github.com/hashicorp/terraform/command/clistate"
    12  	"github.com/hashicorp/terraform/configs"
    13  	"github.com/hashicorp/terraform/configs/configload"
    14  	"github.com/hashicorp/terraform/plans/planfile"
    15  	"github.com/hashicorp/terraform/states/statemgr"
    16  	"github.com/hashicorp/terraform/terraform"
    17  	"github.com/hashicorp/terraform/tfdiags"
    18  	"github.com/zclconf/go-cty/cty"
    19  )
    20  
    21  // backend.Local implementation.
    22  func (b *Local) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) {
    23  	// Make sure the type is invalid. We use this as a way to know not
    24  	// to ask for input/validate.
    25  	op.Type = backend.OperationTypeInvalid
    26  
    27  	if op.LockState {
    28  		op.StateLocker = clistate.NewLocker(context.Background(), op.StateLockTimeout, b.CLI, b.Colorize())
    29  	} else {
    30  		op.StateLocker = clistate.NewNoopLocker()
    31  	}
    32  
    33  	ctx, _, stateMgr, diags := b.context(op)
    34  	return ctx, stateMgr, diags
    35  }
    36  
    37  func (b *Local) context(op *backend.Operation) (*terraform.Context, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) {
    38  	var diags tfdiags.Diagnostics
    39  
    40  	// Get the latest state.
    41  	log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace)
    42  	s, err := b.StateMgr(op.Workspace)
    43  	if err != nil {
    44  		diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
    45  		return nil, nil, nil, diags
    46  	}
    47  	log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace)
    48  	if err := op.StateLocker.Lock(s, op.Type.String()); err != nil {
    49  		diags = diags.Append(errwrap.Wrapf("Error locking state: {{err}}", err))
    50  		return nil, nil, nil, diags
    51  	}
    52  	log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace)
    53  	if err := s.RefreshState(); err != nil {
    54  		diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err))
    55  		return nil, nil, nil, diags
    56  	}
    57  
    58  	// Initialize our context options
    59  	var opts terraform.ContextOpts
    60  	if v := b.ContextOpts; v != nil {
    61  		opts = *v
    62  	}
    63  
    64  	// Copy set options from the operation
    65  	opts.Destroy = op.Destroy
    66  	opts.Targets = op.Targets
    67  	opts.UIInput = op.UIIn
    68  
    69  	// Load the latest state. If we enter contextFromPlanFile below then the
    70  	// state snapshot in the plan file must match this, or else it'll return
    71  	// error diagnostics.
    72  	log.Printf("[TRACE] backend/local: retrieving local state snapshot for workspace %q", op.Workspace)
    73  	opts.State = s.State()
    74  
    75  	var tfCtx *terraform.Context
    76  	var ctxDiags tfdiags.Diagnostics
    77  	var configSnap *configload.Snapshot
    78  	if op.PlanFile != nil {
    79  		var stateMeta *statemgr.SnapshotMeta
    80  		// If the statemgr implements our optional PersistentMeta interface then we'll
    81  		// additionally verify that the state snapshot in the plan file has
    82  		// consistent metadata, as an additional safety check.
    83  		if sm, ok := s.(statemgr.PersistentMeta); ok {
    84  			m := sm.StateSnapshotMeta()
    85  			stateMeta = &m
    86  		}
    87  		log.Printf("[TRACE] backend/local: building context from plan file")
    88  		tfCtx, configSnap, ctxDiags = b.contextFromPlanFile(op.PlanFile, opts, stateMeta)
    89  		// Write sources into the cache of the main loader so that they are
    90  		// available if we need to generate diagnostic message snippets.
    91  		op.ConfigLoader.ImportSourcesFromSnapshot(configSnap)
    92  	} else {
    93  		log.Printf("[TRACE] backend/local: building context for current working directory")
    94  		tfCtx, configSnap, ctxDiags = b.contextDirect(op, opts)
    95  	}
    96  	diags = diags.Append(ctxDiags)
    97  	if diags.HasErrors() {
    98  		return nil, nil, nil, diags
    99  	}
   100  	log.Printf("[TRACE] backend/local: finished building terraform.Context")
   101  
   102  	// If we have an operation, then we automatically do the input/validate
   103  	// here since every option requires this.
   104  	if op.Type != backend.OperationTypeInvalid {
   105  		// If input asking is enabled, then do that
   106  		if op.PlanFile == nil && b.OpInput {
   107  			mode := terraform.InputModeProvider
   108  
   109  			log.Printf("[TRACE] backend/local: requesting interactive input, if necessary")
   110  			inputDiags := tfCtx.Input(mode)
   111  			diags = diags.Append(inputDiags)
   112  			if inputDiags.HasErrors() {
   113  				return nil, nil, nil, diags
   114  			}
   115  		}
   116  
   117  		// If validation is enabled, validate
   118  		if b.OpValidation {
   119  			log.Printf("[TRACE] backend/local: running validation operation")
   120  			validateDiags := tfCtx.Validate()
   121  			diags = diags.Append(validateDiags)
   122  		}
   123  	}
   124  
   125  	return tfCtx, configSnap, s, diags
   126  }
   127  
   128  func (b *Local) contextDirect(op *backend.Operation, opts terraform.ContextOpts) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) {
   129  	var diags tfdiags.Diagnostics
   130  
   131  	// Load the configuration using the caller-provided configuration loader.
   132  	config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
   133  	diags = diags.Append(configDiags)
   134  	if configDiags.HasErrors() {
   135  		return nil, nil, diags
   136  	}
   137  	opts.Config = config
   138  
   139  	var rawVariables map[string]backend.UnparsedVariableValue
   140  	if op.AllowUnsetVariables {
   141  		// Rather than prompting for input, we'll just stub out the required
   142  		// but unset variables with unknown values to represent that they are
   143  		// placeholders for values the user would need to provide for other
   144  		// operations.
   145  		rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables)
   146  	} else {
   147  		// If interactive input is enabled, we might gather some more variable
   148  		// values through interactive prompts.
   149  		// TODO: Need to route the operation context through into here, so that
   150  		// the interactive prompts can be sensitive to its timeouts/etc.
   151  		rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, opts.UIInput)
   152  	}
   153  
   154  	variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables)
   155  	diags = diags.Append(varDiags)
   156  	if diags.HasErrors() {
   157  		return nil, nil, diags
   158  	}
   159  	opts.Variables = variables
   160  
   161  	tfCtx, ctxDiags := terraform.NewContext(&opts)
   162  	diags = diags.Append(ctxDiags)
   163  	return tfCtx, configSnap, diags
   164  }
   165  
   166  func (b *Local) contextFromPlanFile(pf *planfile.Reader, opts terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*terraform.Context, *configload.Snapshot, tfdiags.Diagnostics) {
   167  	var diags tfdiags.Diagnostics
   168  
   169  	const errSummary = "Invalid plan file"
   170  
   171  	// A plan file has a snapshot of configuration embedded inside it, which
   172  	// is used instead of whatever configuration might be already present
   173  	// in the filesystem.
   174  	snap, err := pf.ReadConfigSnapshot()
   175  	if err != nil {
   176  		diags = diags.Append(tfdiags.Sourceless(
   177  			tfdiags.Error,
   178  			errSummary,
   179  			fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err),
   180  		))
   181  		return nil, snap, diags
   182  	}
   183  	loader := configload.NewLoaderFromSnapshot(snap)
   184  	config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
   185  	diags = diags.Append(configDiags)
   186  	if configDiags.HasErrors() {
   187  		return nil, snap, diags
   188  	}
   189  	opts.Config = config
   190  
   191  	// A plan file also contains a snapshot of the prior state the changes
   192  	// are intended to apply to.
   193  	priorStateFile, err := pf.ReadStateFile()
   194  	if err != nil {
   195  		diags = diags.Append(tfdiags.Sourceless(
   196  			tfdiags.Error,
   197  			errSummary,
   198  			fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err),
   199  		))
   200  		return nil, snap, diags
   201  	}
   202  	if currentStateMeta != nil {
   203  		// If the caller sets this, we require that the stored prior state
   204  		// has the same metadata, which is an extra safety check that nothing
   205  		// has changed since the plan was created. (All of the "real-world"
   206  		// state manager implementstions support this, but simpler test backends
   207  		// may not.)
   208  		if currentStateMeta.Lineage != "" && priorStateFile.Lineage != "" {
   209  			if priorStateFile.Serial != currentStateMeta.Serial || priorStateFile.Lineage != currentStateMeta.Lineage {
   210  				diags = diags.Append(tfdiags.Sourceless(
   211  					tfdiags.Error,
   212  					"Saved plan is stale",
   213  					"The given plan file can no longer be applied because the state was changed by another operation after the plan was created.",
   214  				))
   215  			}
   216  		}
   217  	}
   218  	// The caller already wrote the "current state" here, but we're overriding
   219  	// it here with the prior state. These two should actually be identical in
   220  	// normal use, particularly if we validated the state meta above, but
   221  	// we do this here anyway to ensure consistent behavior.
   222  	opts.State = priorStateFile.State
   223  
   224  	plan, err := pf.ReadPlan()
   225  	if err != nil {
   226  		diags = diags.Append(tfdiags.Sourceless(
   227  			tfdiags.Error,
   228  			errSummary,
   229  			fmt.Sprintf("Failed to read plan from plan file: %s.", err),
   230  		))
   231  		return nil, snap, diags
   232  	}
   233  
   234  	variables := terraform.InputValues{}
   235  	for name, dyVal := range plan.VariableValues {
   236  		val, err := dyVal.Decode(cty.DynamicPseudoType)
   237  		if err != nil {
   238  			diags = diags.Append(tfdiags.Sourceless(
   239  				tfdiags.Error,
   240  				errSummary,
   241  				fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
   242  			))
   243  			continue
   244  		}
   245  
   246  		variables[name] = &terraform.InputValue{
   247  			Value:      val,
   248  			SourceType: terraform.ValueFromPlan,
   249  		}
   250  	}
   251  	opts.Variables = variables
   252  	opts.Changes = plan.Changes
   253  	opts.Targets = plan.TargetAddrs
   254  	opts.ProviderSHA256s = plan.ProviderSHA256s
   255  
   256  	tfCtx, ctxDiags := terraform.NewContext(&opts)
   257  	diags = diags.Append(ctxDiags)
   258  	return tfCtx, snap, diags
   259  }
   260  
   261  // interactiveCollectVariables attempts to complete the given existing
   262  // map of variables by interactively prompting for any variables that are
   263  // declared as required but not yet present.
   264  //
   265  // If interactive input is disabled for this backend instance then this is
   266  // a no-op. If input is enabled but fails for some reason, the resulting
   267  // map will be incomplete. For these reasons, the caller must still validate
   268  // that the result is complete and valid.
   269  //
   270  // This function does not modify the map given in "existing", but may return
   271  // it unchanged if no modifications are required. If modifications are required,
   272  // the result is a new map with all of the elements from "existing" plus
   273  // additional elements as appropriate.
   274  //
   275  // Interactive prompting is a "best effort" thing for first-time user UX and
   276  // not something we expect folks to be relying on for routine use. Terraform
   277  // is primarily a non-interactive tool and so we prefer to report in error
   278  // messages that variables are not set rather than reporting that input failed:
   279  // the primary resolution to missing variables is to provide them by some other
   280  // means.
   281  func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue {
   282  	var needed []string
   283  	if b.OpInput && uiInput != nil {
   284  		for name, vc := range vcs {
   285  			if !vc.Required() {
   286  				continue // We only prompt for required variables
   287  			}
   288  			if _, exists := existing[name]; !exists {
   289  				needed = append(needed, name)
   290  			}
   291  		}
   292  	} else {
   293  		log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled")
   294  	}
   295  	if len(needed) == 0 {
   296  		return existing
   297  	}
   298  
   299  	log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed)
   300  
   301  	// If we get here then we're planning to prompt for at least one additional
   302  	// variable's value.
   303  	sort.Strings(needed) // prompt in lexical order
   304  	ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
   305  	for k, v := range existing {
   306  		ret[k] = v
   307  	}
   308  	for _, name := range needed {
   309  		vc := vcs[name]
   310  		rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{
   311  			Id:          fmt.Sprintf("var.%s", name),
   312  			Query:       fmt.Sprintf("var.%s", name),
   313  			Description: vc.Description,
   314  		})
   315  		if err != nil {
   316  			// Since interactive prompts are best-effort, we'll just continue
   317  			// here and let subsequent validation report this as a variable
   318  			// not specified.
   319  			log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err)
   320  			continue
   321  		}
   322  		ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue}
   323  	}
   324  	return ret
   325  }
   326  
   327  // stubUnsetVariables ensures that all required variables defined in the
   328  // configuration exist in the resulting map, by adding new elements as necessary.
   329  //
   330  // The stubbed value of any additions will be an unknown variable conforming
   331  // to the variable's configured type constraint, meaning that no particular
   332  // value is known and that one must be provided by the user in order to get
   333  // a complete result.
   334  //
   335  // Unset optional attributes (those with default values) will not be populated
   336  // by this function, under the assumption that a later step will handle those.
   337  // In this sense, stubUnsetRequiredVariables is essentially a non-interactive,
   338  // non-error-producing variant of interactiveCollectVariables that creates
   339  // placeholders for values the user would be prompted for interactively on
   340  // other operations.
   341  //
   342  // This function should be used only in situations where variables values
   343  // will not be directly used and the variables map is being constructed only
   344  // to produce a complete Terraform context for some ancillary functionality
   345  // like "terraform console", "terraform state ...", etc.
   346  //
   347  // This function is guaranteed not to modify the given map, but it may return
   348  // the given map unchanged if no additions are required. If additions are
   349  // required then the result will be a new map containing everything in the
   350  // given map plus additional elements.
   351  func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue {
   352  	var missing bool // Do we need to add anything?
   353  	for name, vc := range vcs {
   354  		if !vc.Required() {
   355  			continue // We only stub required variables
   356  		}
   357  		if _, exists := existing[name]; !exists {
   358  			missing = true
   359  		}
   360  	}
   361  	if !missing {
   362  		return existing
   363  	}
   364  
   365  	// If we get down here then there's at least one variable value to add.
   366  	ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
   367  	for k, v := range existing {
   368  		ret[k] = v
   369  	}
   370  	for name, vc := range vcs {
   371  		if !vc.Required() {
   372  			continue
   373  		}
   374  		if _, exists := existing[name]; !exists {
   375  			ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type}
   376  		}
   377  	}
   378  	return ret
   379  }
   380  
   381  type unparsedInteractiveVariableValue struct {
   382  	Name, RawValue string
   383  }
   384  
   385  var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{}
   386  
   387  func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   388  	var diags tfdiags.Diagnostics
   389  	val, valDiags := mode.Parse(v.Name, v.RawValue)
   390  	diags = diags.Append(valDiags)
   391  	if diags.HasErrors() {
   392  		return nil, diags
   393  	}
   394  	return &terraform.InputValue{
   395  		Value:      val,
   396  		SourceType: terraform.ValueFromInput,
   397  	}, diags
   398  }
   399  
   400  type unparsedUnknownVariableValue struct {
   401  	Name     string
   402  	WantType cty.Type
   403  }
   404  
   405  var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{}
   406  
   407  func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   408  	return &terraform.InputValue{
   409  		Value:      cty.UnknownVal(v.WantType),
   410  		SourceType: terraform.ValueFromInput,
   411  	}, nil
   412  }