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