github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/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/iaas-resource-provision/iaas-rpc/internal/backend" 14 "github.com/iaas-resource-provision/iaas-rpc/internal/configs" 15 "github.com/iaas-resource-provision/iaas-rpc/internal/states/statemgr" 16 "github.com/iaas-resource-provision/iaas-rpc/internal/terraform" 17 "github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags" 18 "github.com/zclconf/go-cty/cty" 19 ) 20 21 // Context implements backend.Enhanced. 22 func (b *Remote) Context(op *backend.Operation) (*terraform.Context, statemgr.Full, tfdiags.Diagnostics) { 23 var diags tfdiags.Diagnostics 24 25 op.StateLocker = op.StateLocker.WithContext(context.Background()) 26 27 // Get the remote workspace name. 28 remoteWorkspaceName := b.getRemoteWorkspaceName(op.Workspace) 29 30 // Get the latest state. 31 log.Printf("[TRACE] backend/remote: requesting state manager for workspace %q", remoteWorkspaceName) 32 stateMgr, err := b.StateMgr(op.Workspace) 33 if err != nil { 34 diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) 35 return nil, nil, diags 36 } 37 38 log.Printf("[TRACE] backend/remote: requesting state lock for workspace %q", remoteWorkspaceName) 39 if diags := op.StateLocker.Lock(stateMgr, op.Type.String()); diags.HasErrors() { 40 return nil, nil, diags 41 } 42 43 defer func() { 44 // If we're returning with errors, and thus not producing a valid 45 // context, we'll want to avoid leaving the remote workspace locked. 46 if diags.HasErrors() { 47 diags = diags.Append(op.StateLocker.Unlock()) 48 } 49 }() 50 51 log.Printf("[TRACE] backend/remote: reading remote state for workspace %q", remoteWorkspaceName) 52 if err := stateMgr.RefreshState(); err != nil { 53 diags = diags.Append(errwrap.Wrapf("Error loading state: {{err}}", err)) 54 return nil, nil, diags 55 } 56 57 // Initialize our context options 58 var opts terraform.ContextOpts 59 if v := b.ContextOpts; v != nil { 60 opts = *v 61 } 62 63 // Copy set options from the operation 64 opts.PlanMode = op.PlanMode 65 opts.Targets = op.Targets 66 opts.UIInput = op.UIIn 67 68 // Load the latest state. If we enter contextFromPlanFile below then the 69 // state snapshot in the plan file must match this, or else it'll return 70 // error diagnostics. 71 log.Printf("[TRACE] backend/remote: retrieving remote state snapshot for workspace %q", remoteWorkspaceName) 72 opts.State = stateMgr.State() 73 74 log.Printf("[TRACE] backend/remote: loading configuration for the current working directory") 75 config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir) 76 diags = diags.Append(configDiags) 77 if configDiags.HasErrors() { 78 return nil, nil, diags 79 } 80 opts.Config = config 81 82 // The underlying API expects us to use the opaque workspace id to request 83 // variables, so we'll need to look that up using our organization name 84 // and workspace name. 85 remoteWorkspaceID, err := b.getRemoteWorkspaceID(context.Background(), op.Workspace) 86 if err != nil { 87 diags = diags.Append(errwrap.Wrapf("Error finding remote workspace: {{err}}", err)) 88 return nil, nil, diags 89 } 90 91 log.Printf("[TRACE] backend/remote: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID) 92 tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, tfe.VariableListOptions{}) 93 if err != nil && err != tfe.ErrResourceNotFound { 94 diags = diags.Append(errwrap.Wrapf("Error loading variables: {{err}}", err)) 95 return nil, nil, diags 96 } 97 98 if op.AllowUnsetVariables { 99 // If we're not going to use the variables in an operation we'll be 100 // more lax about them, stubbing out any unset ones as unknown. 101 // This gives us enough information to produce a consistent context, 102 // but not enough information to run a real operation (plan, apply, etc) 103 opts.Variables = stubAllVariables(op.Variables, config.Module.Variables) 104 } else { 105 if tfeVariables != nil { 106 if op.Variables == nil { 107 op.Variables = make(map[string]backend.UnparsedVariableValue) 108 } 109 for _, v := range tfeVariables.Items { 110 if v.Category == tfe.CategoryTerraform { 111 op.Variables[v.Key] = &remoteStoredVariableValue{ 112 definition: v, 113 } 114 } 115 } 116 } 117 118 if op.Variables != nil { 119 variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables) 120 diags = diags.Append(varDiags) 121 if diags.HasErrors() { 122 return nil, nil, diags 123 } 124 opts.Variables = variables 125 } 126 } 127 128 tfCtx, ctxDiags := terraform.NewContext(&opts) 129 diags = diags.Append(ctxDiags) 130 131 log.Printf("[TRACE] backend/remote: finished building terraform.Context") 132 133 return tfCtx, stateMgr, diags 134 } 135 136 func (b *Remote) getRemoteWorkspaceName(localWorkspaceName string) string { 137 switch { 138 case localWorkspaceName == backend.DefaultStateName: 139 // The default workspace name is a special case, for when the backend 140 // is configured to with to an exact remote workspace rather than with 141 // a remote workspace _prefix_. 142 return b.workspace 143 case b.prefix != "" && !strings.HasPrefix(localWorkspaceName, b.prefix): 144 return b.prefix + localWorkspaceName 145 default: 146 return localWorkspaceName 147 } 148 } 149 150 func (b *Remote) getRemoteWorkspace(ctx context.Context, localWorkspaceName string) (*tfe.Workspace, error) { 151 remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName) 152 153 log.Printf("[TRACE] backend/remote: looking up workspace for %s/%s", b.organization, remoteWorkspaceName) 154 remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName) 155 if err != nil { 156 return nil, err 157 } 158 159 return remoteWorkspace, nil 160 } 161 162 func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName string) (string, error) { 163 remoteWorkspace, err := b.getRemoteWorkspace(ctx, localWorkspaceName) 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 }