github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/backend_context.go (about)

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