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