github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/local/backend_local.go (about)

     1  package local
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"sort"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/terraform/internal/backend"
    11  	"github.com/hashicorp/terraform/internal/configs"
    12  	"github.com/hashicorp/terraform/internal/configs/configload"
    13  	"github.com/hashicorp/terraform/internal/plans/planfile"
    14  	"github.com/hashicorp/terraform/internal/states/statemgr"
    15  	"github.com/hashicorp/terraform/internal/terraform"
    16  	"github.com/hashicorp/terraform/internal/tfdiags"
    17  	"github.com/zclconf/go-cty/cty"
    18  )
    19  
    20  // backend.Local implementation.
    21  func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) {
    22  	// Make sure the type is invalid. We use this as a way to know not
    23  	// to ask for input/validate. We're modifying this through a pointer,
    24  	// so we're mutating an object that belongs to the caller here, which
    25  	// seems bad but we're preserving it for now until we have time to
    26  	// properly design this API, vs. just preserving whatever it currently
    27  	// happens to do.
    28  	op.Type = backend.OperationTypeInvalid
    29  
    30  	op.StateLocker = op.StateLocker.WithContext(context.Background())
    31  
    32  	lr, _, stateMgr, diags := b.localRun(op)
    33  	return lr, stateMgr, diags
    34  }
    35  
    36  func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) {
    37  	var diags tfdiags.Diagnostics
    38  
    39  	// Get the latest state.
    40  	log.Printf("[TRACE] backend/local: requesting state manager for workspace %q", op.Workspace)
    41  	s, err := b.StateMgr(op.Workspace)
    42  	if err != nil {
    43  		diags = diags.Append(fmt.Errorf("error loading state: %w", err))
    44  		return nil, nil, nil, diags
    45  	}
    46  	log.Printf("[TRACE] backend/local: requesting state lock for workspace %q", op.Workspace)
    47  	if diags := op.StateLocker.Lock(s, op.Type.String()); diags.HasErrors() {
    48  		return nil, nil, nil, diags
    49  	}
    50  
    51  	defer func() {
    52  		// If we're returning with errors, and thus not producing a valid
    53  		// context, we'll want to avoid leaving the workspace locked.
    54  		if diags.HasErrors() {
    55  			diags = diags.Append(op.StateLocker.Unlock())
    56  		}
    57  	}()
    58  
    59  	log.Printf("[TRACE] backend/local: reading remote state for workspace %q", op.Workspace)
    60  	if err := s.RefreshState(); err != nil {
    61  		diags = diags.Append(fmt.Errorf("error loading state: %w", err))
    62  		return nil, nil, nil, diags
    63  	}
    64  
    65  	ret := &backend.LocalRun{}
    66  
    67  	// Initialize our context options
    68  	var coreOpts terraform.ContextOpts
    69  	if v := b.ContextOpts; v != nil {
    70  		coreOpts = *v
    71  	}
    72  	coreOpts.UIInput = op.UIIn
    73  	coreOpts.Hooks = op.Hooks
    74  
    75  	var ctxDiags tfdiags.Diagnostics
    76  	var configSnap *configload.Snapshot
    77  	if op.PlanFile != nil {
    78  		var stateMeta *statemgr.SnapshotMeta
    79  		// If the statemgr implements our optional PersistentMeta interface then we'll
    80  		// additionally verify that the state snapshot in the plan file has
    81  		// consistent metadata, as an additional safety check.
    82  		if sm, ok := s.(statemgr.PersistentMeta); ok {
    83  			m := sm.StateSnapshotMeta()
    84  			stateMeta = &m
    85  		}
    86  		log.Printf("[TRACE] backend/local: populating backend.LocalRun from plan file")
    87  		ret, configSnap, ctxDiags = b.localRunForPlanFile(op, op.PlanFile, ret, &coreOpts, stateMeta)
    88  		if ctxDiags.HasErrors() {
    89  			diags = diags.Append(ctxDiags)
    90  			return nil, nil, nil, diags
    91  		}
    92  
    93  		// Write sources into the cache of the main loader so that they are
    94  		// available if we need to generate diagnostic message snippets.
    95  		op.ConfigLoader.ImportSourcesFromSnapshot(configSnap)
    96  	} else {
    97  		log.Printf("[TRACE] backend/local: populating backend.LocalRun for current working directory")
    98  		ret, configSnap, ctxDiags = b.localRunDirect(op, ret, &coreOpts, s)
    99  	}
   100  	diags = diags.Append(ctxDiags)
   101  	if diags.HasErrors() {
   102  		return nil, nil, nil, diags
   103  	}
   104  
   105  	// If we have an operation, then we automatically do the input/validate
   106  	// here since every option requires this.
   107  	if op.Type != backend.OperationTypeInvalid {
   108  		// If input asking is enabled, then do that
   109  		if op.PlanFile == nil && b.OpInput {
   110  			mode := terraform.InputModeProvider
   111  
   112  			log.Printf("[TRACE] backend/local: requesting interactive input, if necessary")
   113  			inputDiags := ret.Core.Input(ret.Config, mode)
   114  			diags = diags.Append(inputDiags)
   115  			if inputDiags.HasErrors() {
   116  				return nil, nil, nil, diags
   117  			}
   118  		}
   119  
   120  		// If validation is enabled, validate
   121  		if b.OpValidation {
   122  			log.Printf("[TRACE] backend/local: running validation operation")
   123  			validateDiags := ret.Core.Validate(ret.Config)
   124  			diags = diags.Append(validateDiags)
   125  		}
   126  	}
   127  
   128  	return ret, configSnap, s, diags
   129  }
   130  
   131  func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) {
   132  	var diags tfdiags.Diagnostics
   133  
   134  	// Load the configuration using the caller-provided configuration loader.
   135  	config, configSnap, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir)
   136  	diags = diags.Append(configDiags)
   137  	if configDiags.HasErrors() {
   138  		return nil, nil, diags
   139  	}
   140  	run.Config = config
   141  
   142  	if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
   143  		var buf strings.Builder
   144  		for _, err := range errs {
   145  			fmt.Fprintf(&buf, "\n  - %s", err.Error())
   146  		}
   147  		var suggestion string
   148  		switch {
   149  		case op.DependencyLocks == nil:
   150  			// If we get here then it suggests that there's a caller that we
   151  			// didn't yet update to populate DependencyLocks, which is a bug.
   152  			suggestion = "This run has no dependency lock information provided at all, which is a bug in Terraform; please report it!"
   153  		case op.DependencyLocks.Empty():
   154  			suggestion = "To make the initial dependency selections that will initialize the dependency lock file, run:\n  terraform init"
   155  		default:
   156  			suggestion = "To update the locked dependency selections to match a changed configuration, run:\n  terraform init -upgrade"
   157  		}
   158  		diags = diags.Append(tfdiags.Sourceless(
   159  			tfdiags.Error,
   160  			"Inconsistent dependency lock file",
   161  			fmt.Sprintf(
   162  				"The following dependency selections recorded in the lock file are inconsistent with the current configuration:%s\n\n%s",
   163  				buf.String(), suggestion,
   164  			),
   165  		))
   166  	}
   167  
   168  	var rawVariables map[string]backend.UnparsedVariableValue
   169  	if op.AllowUnsetVariables {
   170  		// Rather than prompting for input, we'll just stub out the required
   171  		// but unset variables with unknown values to represent that they are
   172  		// placeholders for values the user would need to provide for other
   173  		// operations.
   174  		rawVariables = b.stubUnsetRequiredVariables(op.Variables, config.Module.Variables)
   175  	} else {
   176  		// If interactive input is enabled, we might gather some more variable
   177  		// values through interactive prompts.
   178  		// TODO: Need to route the operation context through into here, so that
   179  		// the interactive prompts can be sensitive to its timeouts/etc.
   180  		rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn)
   181  	}
   182  
   183  	variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables)
   184  	diags = diags.Append(varDiags)
   185  	if diags.HasErrors() {
   186  		return nil, nil, diags
   187  	}
   188  
   189  	planOpts := &terraform.PlanOpts{
   190  		Mode:         op.PlanMode,
   191  		Targets:      op.Targets,
   192  		ForceReplace: op.ForceReplace,
   193  		SetVariables: variables,
   194  		SkipRefresh:  op.Type != backend.OperationTypeRefresh && !op.PlanRefresh,
   195  	}
   196  	run.PlanOpts = planOpts
   197  
   198  	// For a "direct" local run, the input state is the most recently stored
   199  	// snapshot, from the previous run.
   200  	run.InputState = s.State()
   201  
   202  	tfCtx, moreDiags := terraform.NewContext(coreOpts)
   203  	diags = diags.Append(moreDiags)
   204  	if moreDiags.HasErrors() {
   205  		return nil, nil, diags
   206  	}
   207  	run.Core = tfCtx
   208  	return run, configSnap, diags
   209  }
   210  
   211  func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, run *backend.LocalRun, coreOpts *terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) {
   212  	var diags tfdiags.Diagnostics
   213  
   214  	const errSummary = "Invalid plan file"
   215  
   216  	// A plan file has a snapshot of configuration embedded inside it, which
   217  	// is used instead of whatever configuration might be already present
   218  	// in the filesystem.
   219  	snap, err := pf.ReadConfigSnapshot()
   220  	if err != nil {
   221  		diags = diags.Append(tfdiags.Sourceless(
   222  			tfdiags.Error,
   223  			errSummary,
   224  			fmt.Sprintf("Failed to read configuration snapshot from plan file: %s.", err),
   225  		))
   226  		return nil, snap, diags
   227  	}
   228  	loader := configload.NewLoaderFromSnapshot(snap)
   229  	config, configDiags := loader.LoadConfig(snap.Modules[""].Dir)
   230  	diags = diags.Append(configDiags)
   231  	if configDiags.HasErrors() {
   232  		return nil, snap, diags
   233  	}
   234  	run.Config = config
   235  
   236  	// NOTE: We're intentionally comparing the current locks with the
   237  	// configuration snapshot, rather than the lock snapshot in the plan file,
   238  	// because it's the current locks which dictate our plugin selections
   239  	// in coreOpts below. However, we'll also separately check that the
   240  	// plan file has identical locked plugins below, and thus we're effectively
   241  	// checking consistency with both here.
   242  	if errs := config.VerifyDependencySelections(op.DependencyLocks); len(errs) > 0 {
   243  		var buf strings.Builder
   244  		for _, err := range errs {
   245  			fmt.Fprintf(&buf, "\n  - %s", err.Error())
   246  		}
   247  		diags = diags.Append(tfdiags.Sourceless(
   248  			tfdiags.Error,
   249  			"Inconsistent dependency lock file",
   250  			fmt.Sprintf(
   251  				"The following dependency selections recorded in the lock file are inconsistent with the configuration in the saved plan:%s\n\nA saved plan can be applied only to the same configuration it was created from. Create a new plan from the updated configuration.",
   252  				buf.String(),
   253  			),
   254  		))
   255  	}
   256  
   257  	// This check is an important complement to the check above: the locked
   258  	// dependencies in the configuration must match the configuration, and
   259  	// the locked dependencies in the plan must match the locked dependencies
   260  	// in the configuration, and so transitively we ensure that the locked
   261  	// dependencies in the plan match the configuration too. However, this
   262  	// additionally catches any inconsistency between the two sets of locks
   263  	// even if they both happen to be valid per the current configuration,
   264  	// which is one of several ways we try to catch the mistake of applying
   265  	// a saved plan file in a different place than where we created it.
   266  	depLocksFromPlan, moreDiags := pf.ReadDependencyLocks()
   267  	diags = diags.Append(moreDiags)
   268  	if depLocksFromPlan != nil && !op.DependencyLocks.Equal(depLocksFromPlan) {
   269  		diags = diags.Append(tfdiags.Sourceless(
   270  			tfdiags.Error,
   271  			"Inconsistent dependency lock file",
   272  			"The given plan file was created with a different set of external dependency selections than the current configuration. A saved plan can be applied only to the same configuration it was created from.\n\nCreate a new plan from the updated configuration.",
   273  		))
   274  	}
   275  
   276  	// A plan file also contains a snapshot of the prior state the changes
   277  	// are intended to apply to.
   278  	priorStateFile, err := pf.ReadStateFile()
   279  	if err != nil {
   280  		diags = diags.Append(tfdiags.Sourceless(
   281  			tfdiags.Error,
   282  			errSummary,
   283  			fmt.Sprintf("Failed to read prior state snapshot from plan file: %s.", err),
   284  		))
   285  		return nil, snap, diags
   286  	}
   287  
   288  	if currentStateMeta != nil {
   289  		// If the caller sets this, we require that the stored prior state
   290  		// has the same metadata, which is an extra safety check that nothing
   291  		// has changed since the plan was created. (All of the "real-world"
   292  		// state manager implementations support this, but simpler test backends
   293  		// may not.)
   294  
   295  		// Because the plan always contains a state, even if it is empty, the
   296  		// first plan to be applied will have empty snapshot metadata. In this
   297  		// case we compare only the serial in order to provide a more correct
   298  		// error.
   299  		firstPlan := priorStateFile.Lineage == "" && priorStateFile.Serial == 0
   300  
   301  		switch {
   302  		case !firstPlan && priorStateFile.Lineage != currentStateMeta.Lineage:
   303  			diags = diags.Append(tfdiags.Sourceless(
   304  				tfdiags.Error,
   305  				"Saved plan does not match the given state",
   306  				"The given plan file can not be applied because it was created from a different state lineage.",
   307  			))
   308  
   309  		case priorStateFile.Serial != currentStateMeta.Serial:
   310  			diags = diags.Append(tfdiags.Sourceless(
   311  				tfdiags.Error,
   312  				"Saved plan is stale",
   313  				"The given plan file can no longer be applied because the state was changed by another operation after the plan was created.",
   314  			))
   315  		}
   316  	}
   317  	// When we're applying a saved plan, the input state is the "prior state"
   318  	// recorded in the plan, which incorporates the result of all of the
   319  	// refreshing we did while building the plan.
   320  	run.InputState = priorStateFile.State
   321  
   322  	plan, err := pf.ReadPlan()
   323  	if err != nil {
   324  		diags = diags.Append(tfdiags.Sourceless(
   325  			tfdiags.Error,
   326  			errSummary,
   327  			fmt.Sprintf("Failed to read plan from plan file: %s.", err),
   328  		))
   329  		return nil, snap, diags
   330  	}
   331  	// When we're applying a saved plan, we populate Plan instead of PlanOpts,
   332  	// because a plan object incorporates the subset of data from PlanOps that
   333  	// we need to apply the plan.
   334  	run.Plan = plan
   335  
   336  	tfCtx, moreDiags := terraform.NewContext(coreOpts)
   337  	diags = diags.Append(moreDiags)
   338  	if moreDiags.HasErrors() {
   339  		return nil, nil, diags
   340  	}
   341  	run.Core = tfCtx
   342  	return run, snap, diags
   343  }
   344  
   345  // interactiveCollectVariables attempts to complete the given existing
   346  // map of variables by interactively prompting for any variables that are
   347  // declared as required but not yet present.
   348  //
   349  // If interactive input is disabled for this backend instance then this is
   350  // a no-op. If input is enabled but fails for some reason, the resulting
   351  // map will be incomplete. For these reasons, the caller must still validate
   352  // that the result is complete and valid.
   353  //
   354  // This function does not modify the map given in "existing", but may return
   355  // it unchanged if no modifications are required. If modifications are required,
   356  // the result is a new map with all of the elements from "existing" plus
   357  // additional elements as appropriate.
   358  //
   359  // Interactive prompting is a "best effort" thing for first-time user UX and
   360  // not something we expect folks to be relying on for routine use. Terraform
   361  // is primarily a non-interactive tool and so we prefer to report in error
   362  // messages that variables are not set rather than reporting that input failed:
   363  // the primary resolution to missing variables is to provide them by some other
   364  // means.
   365  func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue {
   366  	var needed []string
   367  	if b.OpInput && uiInput != nil {
   368  		for name, vc := range vcs {
   369  			if !vc.Required() {
   370  				continue // We only prompt for required variables
   371  			}
   372  			if _, exists := existing[name]; !exists {
   373  				needed = append(needed, name)
   374  			}
   375  		}
   376  	} else {
   377  		log.Print("[DEBUG] backend/local: Skipping interactive prompts for variables because input is disabled")
   378  	}
   379  	if len(needed) == 0 {
   380  		return existing
   381  	}
   382  
   383  	log.Printf("[DEBUG] backend/local: will prompt for input of unset required variables %s", needed)
   384  
   385  	// If we get here then we're planning to prompt for at least one additional
   386  	// variable's value.
   387  	sort.Strings(needed) // prompt in lexical order
   388  	ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
   389  	for k, v := range existing {
   390  		ret[k] = v
   391  	}
   392  	for _, name := range needed {
   393  		vc := vcs[name]
   394  		rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{
   395  			Id:          fmt.Sprintf("var.%s", name),
   396  			Query:       fmt.Sprintf("var.%s", name),
   397  			Description: vc.Description,
   398  			Secret:      vc.Sensitive,
   399  		})
   400  		if err != nil {
   401  			// Since interactive prompts are best-effort, we'll just continue
   402  			// here and let subsequent validation report this as a variable
   403  			// not specified.
   404  			log.Printf("[WARN] backend/local: Failed to request user input for variable %q: %s", name, err)
   405  			continue
   406  		}
   407  		ret[name] = unparsedInteractiveVariableValue{Name: name, RawValue: rawValue}
   408  	}
   409  	return ret
   410  }
   411  
   412  // stubUnsetVariables ensures that all required variables defined in the
   413  // configuration exist in the resulting map, by adding new elements as necessary.
   414  //
   415  // The stubbed value of any additions will be an unknown variable conforming
   416  // to the variable's configured type constraint, meaning that no particular
   417  // value is known and that one must be provided by the user in order to get
   418  // a complete result.
   419  //
   420  // Unset optional attributes (those with default values) will not be populated
   421  // by this function, under the assumption that a later step will handle those.
   422  // In this sense, stubUnsetRequiredVariables is essentially a non-interactive,
   423  // non-error-producing variant of interactiveCollectVariables that creates
   424  // placeholders for values the user would be prompted for interactively on
   425  // other operations.
   426  //
   427  // This function should be used only in situations where variables values
   428  // will not be directly used and the variables map is being constructed only
   429  // to produce a complete Terraform context for some ancillary functionality
   430  // like "terraform console", "terraform state ...", etc.
   431  //
   432  // This function is guaranteed not to modify the given map, but it may return
   433  // the given map unchanged if no additions are required. If additions are
   434  // required then the result will be a new map containing everything in the
   435  // given map plus additional elements.
   436  func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue {
   437  	var missing bool // Do we need to add anything?
   438  	for name, vc := range vcs {
   439  		if !vc.Required() {
   440  			continue // We only stub required variables
   441  		}
   442  		if _, exists := existing[name]; !exists {
   443  			missing = true
   444  		}
   445  	}
   446  	if !missing {
   447  		return existing
   448  	}
   449  
   450  	// If we get down here then there's at least one variable value to add.
   451  	ret := make(map[string]backend.UnparsedVariableValue, len(vcs))
   452  	for k, v := range existing {
   453  		ret[k] = v
   454  	}
   455  	for name, vc := range vcs {
   456  		if !vc.Required() {
   457  			continue
   458  		}
   459  		if _, exists := existing[name]; !exists {
   460  			ret[name] = unparsedUnknownVariableValue{Name: name, WantType: vc.Type}
   461  		}
   462  	}
   463  	return ret
   464  }
   465  
   466  type unparsedInteractiveVariableValue struct {
   467  	Name, RawValue string
   468  }
   469  
   470  var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{}
   471  
   472  func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   473  	var diags tfdiags.Diagnostics
   474  	val, valDiags := mode.Parse(v.Name, v.RawValue)
   475  	diags = diags.Append(valDiags)
   476  	if diags.HasErrors() {
   477  		return nil, diags
   478  	}
   479  	return &terraform.InputValue{
   480  		Value:      val,
   481  		SourceType: terraform.ValueFromInput,
   482  	}, diags
   483  }
   484  
   485  type unparsedUnknownVariableValue struct {
   486  	Name     string
   487  	WantType cty.Type
   488  }
   489  
   490  var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{}
   491  
   492  func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   493  	return &terraform.InputValue{
   494  		Value:      cty.UnknownVal(v.WantType),
   495  		SourceType: terraform.ValueFromInput,
   496  	}, nil
   497  }