github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/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 // 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 }