kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/backend/local/backend_local.go (about)

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