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