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

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