github.com/hugorut/terraform@v1.1.3/src/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/hugorut/terraform/src/backend"
    13  	"github.com/hugorut/terraform/src/configs"
    14  	"github.com/hugorut/terraform/src/states/statemgr"
    15  	"github.com/hugorut/terraform/src/terraform"
    16  	"github.com/hugorut/terraform/src/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  	// The underlying API expects us to use the opaque workspace id to request
    86  	// variables, so we'll need to look that up using our organization name
    87  	// and workspace name.
    88  	remoteWorkspaceID, err := b.getRemoteWorkspaceID(context.Background(), op.Workspace)
    89  	if err != nil {
    90  		diags = diags.Append(fmt.Errorf("error finding remote workspace: %w", err))
    91  		return nil, nil, diags
    92  	}
    93  
    94  	log.Printf("[TRACE] backend/remote: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID)
    95  	tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, tfe.VariableListOptions{})
    96  	if err != nil && err != tfe.ErrResourceNotFound {
    97  		diags = diags.Append(fmt.Errorf("error loading variables: %w", err))
    98  		return nil, nil, diags
    99  	}
   100  
   101  	if op.AllowUnsetVariables {
   102  		// If we're not going to use the variables in an operation we'll be
   103  		// more lax about them, stubbing out any unset ones as unknown.
   104  		// This gives us enough information to produce a consistent context,
   105  		// but not enough information to run a real operation (plan, apply, etc)
   106  		ret.PlanOpts.SetVariables = stubAllVariables(op.Variables, config.Module.Variables)
   107  	} else {
   108  		if tfeVariables != nil {
   109  			if op.Variables == nil {
   110  				op.Variables = make(map[string]backend.UnparsedVariableValue)
   111  			}
   112  			for _, v := range tfeVariables.Items {
   113  				if v.Category == tfe.CategoryTerraform {
   114  					op.Variables[v.Key] = &remoteStoredVariableValue{
   115  						definition: v,
   116  					}
   117  				}
   118  			}
   119  		}
   120  
   121  		if op.Variables != nil {
   122  			variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables)
   123  			diags = diags.Append(varDiags)
   124  			if diags.HasErrors() {
   125  				return nil, nil, diags
   126  			}
   127  			ret.PlanOpts.SetVariables = variables
   128  		}
   129  	}
   130  
   131  	tfCtx, ctxDiags := terraform.NewContext(&opts)
   132  	diags = diags.Append(ctxDiags)
   133  	ret.Core = tfCtx
   134  
   135  	log.Printf("[TRACE] backend/remote: finished building terraform.Context")
   136  
   137  	return ret, stateMgr, diags
   138  }
   139  
   140  func (b *Remote) getRemoteWorkspaceName(localWorkspaceName string) string {
   141  	switch {
   142  	case localWorkspaceName == backend.DefaultStateName:
   143  		// The default workspace name is a special case, for when the backend
   144  		// is configured to with to an exact remote workspace rather than with
   145  		// a remote workspace _prefix_.
   146  		return b.workspace
   147  	case b.prefix != "" && !strings.HasPrefix(localWorkspaceName, b.prefix):
   148  		return b.prefix + localWorkspaceName
   149  	default:
   150  		return localWorkspaceName
   151  	}
   152  }
   153  
   154  func (b *Remote) getRemoteWorkspace(ctx context.Context, localWorkspaceName string) (*tfe.Workspace, error) {
   155  	remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName)
   156  
   157  	log.Printf("[TRACE] backend/remote: looking up workspace for %s/%s", b.organization, remoteWorkspaceName)
   158  	remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	return remoteWorkspace, nil
   164  }
   165  
   166  func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) {
   167  	remoteWorkspace, err := b.getRemoteWorkspace(ctx, localWorkspaceName)
   168  	if err != nil {
   169  		return "", err
   170  	}
   171  
   172  	return remoteWorkspace.ID, nil
   173  }
   174  
   175  func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues {
   176  	ret := make(terraform.InputValues, len(decls))
   177  
   178  	for name, cfg := range decls {
   179  		raw, exists := vv[name]
   180  		if !exists {
   181  			ret[name] = &terraform.InputValue{
   182  				Value:      cty.UnknownVal(cfg.Type),
   183  				SourceType: terraform.ValueFromConfig,
   184  			}
   185  			continue
   186  		}
   187  
   188  		val, diags := raw.ParseVariableValue(cfg.ParsingMode)
   189  		if diags.HasErrors() {
   190  			ret[name] = &terraform.InputValue{
   191  				Value:      cty.UnknownVal(cfg.Type),
   192  				SourceType: terraform.ValueFromConfig,
   193  			}
   194  			continue
   195  		}
   196  		ret[name] = val
   197  	}
   198  
   199  	return ret
   200  }
   201  
   202  // remoteStoredVariableValue is a backend.UnparsedVariableValue implementation
   203  // that translates from the go-tfe representation of stored variables into
   204  // the Terraform Core backend representation of variables.
   205  type remoteStoredVariableValue struct {
   206  	definition *tfe.Variable
   207  }
   208  
   209  var _ backend.UnparsedVariableValue = (*remoteStoredVariableValue)(nil)
   210  
   211  func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) {
   212  	var diags tfdiags.Diagnostics
   213  	var val cty.Value
   214  
   215  	switch {
   216  	case v.definition.Sensitive:
   217  		// If it's marked as sensitive then it's not available for use in
   218  		// local operations. We'll use an unknown value as a placeholder for
   219  		// it so that operations that don't need it might still work, but
   220  		// we'll also produce a warning about it to add context for any
   221  		// errors that might result here.
   222  		val = cty.DynamicVal
   223  		if !v.definition.HCL {
   224  			// If it's not marked as HCL then we at least know that the
   225  			// value must be a string, so we'll set that in case it allows
   226  			// us to do some more precise type checking.
   227  			val = cty.UnknownVal(cty.String)
   228  		}
   229  
   230  		diags = diags.Append(tfdiags.Sourceless(
   231  			tfdiags.Warning,
   232  			fmt.Sprintf("Value for var.%s unavailable", v.definition.Key),
   233  			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),
   234  		))
   235  
   236  	case v.definition.HCL:
   237  		// If the variable value is marked as being in HCL syntax, we need to
   238  		// parse it the same way as it would be interpreted in a .tfvars
   239  		// file because that is how it would get passed to Terraform CLI for
   240  		// a remote operation and we want to mimic that result as closely as
   241  		// possible.
   242  		var exprDiags hcl.Diagnostics
   243  		expr, exprDiags := hclsyntax.ParseExpression([]byte(v.definition.Value), "<remote workspace>", hcl.Pos{Line: 1, Column: 1})
   244  		if expr != nil {
   245  			var moreDiags hcl.Diagnostics
   246  			val, moreDiags = expr.Value(nil)
   247  			exprDiags = append(exprDiags, moreDiags...)
   248  		} else {
   249  			// We'll have already put some errors in exprDiags above, so we'll
   250  			// just stub out the value here.
   251  			val = cty.DynamicVal
   252  		}
   253  
   254  		// We don't have sufficient context to return decent error messages
   255  		// for syntax errors in the remote values, so we'll just return a
   256  		// generic message instead for now.
   257  		// (More complete error messages will still result from true remote
   258  		// operations, because they'll run on the remote system where we've
   259  		// materialized the values into a tfvars file we can report from.)
   260  		if exprDiags.HasErrors() {
   261  			diags = diags.Append(tfdiags.Sourceless(
   262  				tfdiags.Error,
   263  				fmt.Sprintf("Invalid expression for var.%s", v.definition.Key),
   264  				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),
   265  			))
   266  		}
   267  
   268  	default:
   269  		// A variable value _not_ marked as HCL is always be a string, given
   270  		// literally.
   271  		val = cty.StringVal(v.definition.Value)
   272  	}
   273  
   274  	return &terraform.InputValue{
   275  		Value: val,
   276  
   277  		// We mark these as "from input" with the rationale that entering
   278  		// variable values into the Terraform Cloud or Enterprise UI is,
   279  		// roughly speaking, a similar idea to entering variable values at
   280  		// the interactive CLI prompts. It's not a perfect correspondance,
   281  		// but it's closer than the other options.
   282  		SourceType: terraform.ValueFromInput,
   283  	}, diags
   284  }