github.com/opentofu/opentofu@v1.7.1/internal/cloud/backend_context.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package cloud
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"log"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  
    15  	tfe "github.com/hashicorp/go-tfe"
    16  	"github.com/hashicorp/hcl/v2/hclsyntax"
    17  	"github.com/opentofu/opentofu/internal/backend"
    18  	"github.com/opentofu/opentofu/internal/configs"
    19  	"github.com/opentofu/opentofu/internal/states/statemgr"
    20  	"github.com/opentofu/opentofu/internal/tfdiags"
    21  	"github.com/opentofu/opentofu/internal/tofu"
    22  	"github.com/zclconf/go-cty/cty"
    23  )
    24  
    25  // LocalRun implements backend.Local
    26  func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) {
    27  	var diags tfdiags.Diagnostics
    28  	ret := &backend.LocalRun{
    29  		PlanOpts: &tofu.PlanOpts{
    30  			Mode:    op.PlanMode,
    31  			Targets: op.Targets,
    32  		},
    33  	}
    34  
    35  	op.StateLocker = op.StateLocker.WithContext(context.Background())
    36  
    37  	// Get the remote workspace name.
    38  	remoteWorkspaceName := b.getRemoteWorkspaceName(op.Workspace)
    39  
    40  	// Get the latest state.
    41  	log.Printf("[TRACE] cloud: requesting state manager for workspace %q", remoteWorkspaceName)
    42  	stateMgr, err := b.StateMgr(op.Workspace)
    43  	if err != nil {
    44  		diags = diags.Append(fmt.Errorf("error loading state: %w", err))
    45  		return nil, nil, diags
    46  	}
    47  
    48  	log.Printf("[TRACE] cloud: requesting state lock for workspace %q", remoteWorkspaceName)
    49  	if diags := op.StateLocker.Lock(stateMgr, op.Type.String()); diags.HasErrors() {
    50  		return nil, nil, diags
    51  	}
    52  
    53  	defer func() {
    54  		// If we're returning with errors, and thus not producing a valid
    55  		// context, we'll want to avoid leaving the remote workspace locked.
    56  		if diags.HasErrors() {
    57  			diags = diags.Append(op.StateLocker.Unlock())
    58  		}
    59  	}()
    60  
    61  	log.Printf("[TRACE] cloud: reading remote state for workspace %q", remoteWorkspaceName)
    62  	if err := stateMgr.RefreshState(); err != nil {
    63  		diags = diags.Append(fmt.Errorf("error loading state: %w", err))
    64  		return nil, nil, diags
    65  	}
    66  
    67  	// Initialize our context options
    68  	var opts tofu.ContextOpts
    69  	if v := b.ContextOpts; v != nil {
    70  		opts = *v
    71  	}
    72  
    73  	// Copy set options from the operation
    74  	opts.UIInput = op.UIIn
    75  	opts.Encryption = op.Encryption
    76  
    77  	// Load the latest state. If we enter contextFromPlanFile below then the
    78  	// state snapshot in the plan file must match this, or else it'll return
    79  	// error diagnostics.
    80  	log.Printf("[TRACE] cloud: retrieving remote state snapshot for workspace %q", remoteWorkspaceName)
    81  	ret.InputState = stateMgr.State()
    82  
    83  	log.Printf("[TRACE] cloud: loading configuration for the current working directory")
    84  	config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
    85  	diags = diags.Append(configDiags)
    86  	if configDiags.HasErrors() {
    87  		return nil, nil, diags
    88  	}
    89  	ret.Config = config
    90  
    91  	if op.AllowUnsetVariables {
    92  		// If we're not going to use the variables in an operation we'll be
    93  		// more lax about them, stubbing out any unset ones as unknown.
    94  		// This gives us enough information to produce a consistent context,
    95  		// but not enough information to run a real operation (plan, apply, etc)
    96  		ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, config.Module.Variables)
    97  	} else {
    98  		// The underlying API expects us to use the opaque workspace id to request
    99  		// variables, so we'll need to look that up using our organization name
   100  		// and workspace name.
   101  		remoteWorkspaceID, err := b.getRemoteWorkspaceID(context.Background(), op.Workspace)
   102  		if err != nil {
   103  			diags = diags.Append(fmt.Errorf("error finding remote workspace: %w", err))
   104  			return nil, nil, diags
   105  		}
   106  		w, err := b.fetchWorkspace(context.Background(), b.organization, op.Workspace)
   107  		if err != nil {
   108  			diags = diags.Append(fmt.Errorf("error loading workspace: %w", err))
   109  			return nil, nil, diags
   110  		}
   111  
   112  		if isLocalExecutionMode(w.ExecutionMode) {
   113  			log.Printf("[TRACE] skipping retrieving variables from workspace %s/%s (%s), workspace is in Local Execution mode", remoteWorkspaceName, b.organization, remoteWorkspaceID)
   114  		} else {
   115  			log.Printf("[TRACE] cloud: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID)
   116  			tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, nil)
   117  			if err != nil && err != tfe.ErrResourceNotFound {
   118  				diags = diags.Append(fmt.Errorf("error loading variables: %w", err))
   119  				return nil, nil, diags
   120  			}
   121  
   122  			if tfeVariables != nil {
   123  				if op.Variables == nil {
   124  					op.Variables = make(map[string]backend.UnparsedVariableValue)
   125  				}
   126  
   127  				for _, v := range tfeVariables.Items {
   128  					if v.Category == tfe.CategoryTerraform {
   129  						if _, ok := op.Variables[v.Key]; !ok {
   130  							op.Variables[v.Key] = &remoteStoredVariableValue{
   131  								definition: v,
   132  							}
   133  						}
   134  					}
   135  				}
   136  			}
   137  		}
   138  
   139  		if op.Variables != nil {
   140  			variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables)
   141  			diags = diags.Append(varDiags)
   142  			if diags.HasErrors() {
   143  				return nil, nil, diags
   144  			}
   145  			ret.PlanOpts.SetVariables = variables
   146  		}
   147  	}
   148  
   149  	tfCtx, ctxDiags := tofu.NewContext(&opts)
   150  	diags = diags.Append(ctxDiags)
   151  	ret.Core = tfCtx
   152  
   153  	log.Printf("[TRACE] cloud: finished building tofu.Context")
   154  
   155  	return ret, stateMgr, diags
   156  }
   157  
   158  func (b *Cloud) getRemoteWorkspaceName(localWorkspaceName string) string {
   159  	switch {
   160  	case localWorkspaceName == backend.DefaultStateName:
   161  		// The default workspace name is a special case
   162  		return b.WorkspaceMapping.Name
   163  	default:
   164  		return localWorkspaceName
   165  	}
   166  }
   167  
   168  func (b *Cloud) getRemoteWorkspace(ctx context.Context, localWorkspaceName string) (*tfe.Workspace, error) {
   169  	remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName)
   170  
   171  	log.Printf("[TRACE] cloud: looking up workspace for %s/%s", b.organization, remoteWorkspaceName)
   172  	remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	return remoteWorkspace, nil
   178  }
   179  
   180  func (b *Cloud) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) {
   181  	remoteWorkspace, err := b.getRemoteWorkspace(ctx, localWorkspaceName)
   182  	if err != nil {
   183  		return "", err
   184  	}
   185  
   186  	return remoteWorkspace.ID, nil
   187  }
   188  
   189  func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) tofu.InputValues {
   190  	ret := make(tofu.InputValues, len(decls))
   191  
   192  	for name, cfg := range decls {
   193  		raw, exists := vv[name]
   194  		if !exists {
   195  			ret[name] = &tofu.InputValue{
   196  				Value:      cty.UnknownVal(cfg.Type),
   197  				SourceType: tofu.ValueFromConfig,
   198  			}
   199  			continue
   200  		}
   201  
   202  		val, diags := raw.ParseVariableValue(cfg.ParsingMode)
   203  		if diags.HasErrors() {
   204  			ret[name] = &tofu.InputValue{
   205  				Value:      cty.UnknownVal(cfg.Type),
   206  				SourceType: tofu.ValueFromConfig,
   207  			}
   208  			continue
   209  		}
   210  		ret[name] = val
   211  	}
   212  
   213  	return ret
   214  }
   215  
   216  // remoteStoredVariableValue is a backend.UnparsedVariableValue implementation
   217  // that translates from the go-tfe representation of stored variables into
   218  // the Terraform Core backend representation of variables.
   219  type remoteStoredVariableValue struct {
   220  	definition *tfe.Variable
   221  }
   222  
   223  var _ backend.UnparsedVariableValue = (*remoteStoredVariableValue)(nil)
   224  
   225  func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*tofu.InputValue, tfdiags.Diagnostics) {
   226  	var diags tfdiags.Diagnostics
   227  	var val cty.Value
   228  
   229  	switch {
   230  	case v.definition.Sensitive:
   231  		// If it's marked as sensitive then it's not available for use in
   232  		// local operations. We'll use an unknown value as a placeholder for
   233  		// it so that operations that don't need it might still work, but
   234  		// we'll also produce a warning about it to add context for any
   235  		// errors that might result here.
   236  		val = cty.DynamicVal
   237  		if !v.definition.HCL {
   238  			// If it's not marked as HCL then we at least know that the
   239  			// value must be a string, so we'll set that in case it allows
   240  			// us to do some more precise type checking.
   241  			val = cty.UnknownVal(cty.String)
   242  		}
   243  
   244  		diags = diags.Append(tfdiags.Sourceless(
   245  			tfdiags.Warning,
   246  			fmt.Sprintf("Value for var.%s unavailable", v.definition.Key),
   247  			fmt.Sprintf("The value of variable %q is marked as sensitive in the remote workspace. This operation always runs locally, so the value for that variable is not available.", v.definition.Key),
   248  		))
   249  
   250  	case v.definition.HCL:
   251  		// If the variable value is marked as being in HCL syntax, we need to
   252  		// parse it the same way as it would be interpreted in a .tfvars
   253  		// file because that is how it would get passed to Terraform CLI for
   254  		// a remote operation and we want to mimic that result as closely as
   255  		// possible.
   256  		var exprDiags hcl.Diagnostics
   257  		expr, exprDiags := hclsyntax.ParseExpression([]byte(v.definition.Value), "<remote workspace>", hcl.Pos{Line: 1, Column: 1})
   258  		if expr != nil {
   259  			var moreDiags hcl.Diagnostics
   260  			val, moreDiags = expr.Value(nil)
   261  			exprDiags = append(exprDiags, moreDiags...)
   262  		} else {
   263  			// We'll have already put some errors in exprDiags above, so we'll
   264  			// just stub out the value here.
   265  			val = cty.DynamicVal
   266  		}
   267  
   268  		// We don't have sufficient context to return decent error messages
   269  		// for syntax errors in the remote values, so we'll just return a
   270  		// generic message instead for now.
   271  		// (More complete error messages will still result from true remote
   272  		// operations, because they'll run on the remote system where we've
   273  		// materialized the values into a tfvars file we can report from.)
   274  		if exprDiags.HasErrors() {
   275  			diags = diags.Append(tfdiags.Sourceless(
   276  				tfdiags.Error,
   277  				fmt.Sprintf("Invalid expression for var.%s", v.definition.Key),
   278  				fmt.Sprintf("The value of variable %q is marked in the remote workspace as being specified in HCL syntax, but the given value is not valid HCL. Stored variable values must be valid literal expressions and may not contain references to other variables or calls to functions.", v.definition.Key),
   279  			))
   280  		}
   281  
   282  	default:
   283  		// A variable value _not_ marked as HCL is always be a string, given
   284  		// literally.
   285  		val = cty.StringVal(v.definition.Value)
   286  	}
   287  
   288  	return &tofu.InputValue{
   289  		Value: val,
   290  
   291  		// We mark these as "from input" with the rationale that entering
   292  		// variable values into the Terraform Cloud or Enterprise UI is,
   293  		// roughly speaking, a similar idea to entering variable values at
   294  		// the interactive CLI prompts. It's not a perfect correspondance,
   295  		// but it's closer than the other options.
   296  		SourceType: tofu.ValueFromInput,
   297  	}, diags
   298  }