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