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