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