github.com/kevinklinger/open_terraform@v1.3.6/noninternal/cloud/backend_context.go (about)

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