github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/backend.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 // Package backend provides interfaces that the CLI uses to interact with 5 // Terraform. A backend provides the abstraction that allows the same CLI 6 // to simultaneously support both local and remote operations for seamlessly 7 // using Terraform in a team environment. 8 package backend 9 10 import ( 11 "context" 12 "errors" 13 "io/ioutil" 14 "log" 15 "os" 16 17 svchost "github.com/hashicorp/terraform-svchost" 18 "github.com/mitchellh/go-homedir" 19 "github.com/terramate-io/tf/addrs" 20 "github.com/terramate-io/tf/command/clistate" 21 "github.com/terramate-io/tf/command/views" 22 "github.com/terramate-io/tf/configs" 23 "github.com/terramate-io/tf/configs/configload" 24 "github.com/terramate-io/tf/configs/configschema" 25 "github.com/terramate-io/tf/depsfile" 26 "github.com/terramate-io/tf/plans" 27 "github.com/terramate-io/tf/plans/planfile" 28 "github.com/terramate-io/tf/states" 29 "github.com/terramate-io/tf/states/statemgr" 30 "github.com/terramate-io/tf/terraform" 31 "github.com/terramate-io/tf/tfdiags" 32 "github.com/zclconf/go-cty/cty" 33 ) 34 35 // DefaultStateName is the name of the default, initial state that every 36 // backend must have. This state cannot be deleted. 37 const DefaultStateName = "default" 38 39 var ( 40 // ErrDefaultWorkspaceNotSupported is returned when an operation does not 41 // support using the default workspace, but requires a named workspace to 42 // be selected. 43 ErrDefaultWorkspaceNotSupported = errors.New("default workspace not supported\n" + 44 "You can create a new workspace with the \"workspace new\" command.") 45 46 // ErrWorkspacesNotSupported is an error returned when a caller attempts 47 // to perform an operation on a workspace other than "default" for a 48 // backend that doesn't support multiple workspaces. 49 // 50 // The caller can detect this to do special fallback behavior or produce 51 // a specific, helpful error message. 52 ErrWorkspacesNotSupported = errors.New("workspaces not supported") 53 ) 54 55 // InitFn is used to initialize a new backend. 56 type InitFn func() Backend 57 58 // Backend is the minimal interface that must be implemented to enable Terraform. 59 type Backend interface { 60 // ConfigSchema returns a description of the expected configuration 61 // structure for the receiving backend. 62 // 63 // This method does not have any side-effects for the backend and can 64 // be safely used before configuring. 65 ConfigSchema() *configschema.Block 66 67 // PrepareConfig checks the validity of the values in the given 68 // configuration, and inserts any missing defaults, assuming that its 69 // structure has already been validated per the schema returned by 70 // ConfigSchema. 71 // 72 // This method does not have any side-effects for the backend and can 73 // be safely used before configuring. It also does not consult any 74 // external data such as environment variables, disk files, etc. Validation 75 // that requires such external data should be deferred until the 76 // Configure call. 77 // 78 // If error diagnostics are returned then the configuration is not valid 79 // and must not subsequently be passed to the Configure method. 80 // 81 // This method may return configuration-contextual diagnostics such 82 // as tfdiags.AttributeValue, and so the caller should provide the 83 // necessary context via the diags.InConfigBody method before returning 84 // diagnostics to the user. 85 PrepareConfig(cty.Value) (cty.Value, tfdiags.Diagnostics) 86 87 // Configure uses the provided configuration to set configuration fields 88 // within the backend. 89 // 90 // The given configuration is assumed to have already been validated 91 // against the schema returned by ConfigSchema and passed validation 92 // via PrepareConfig. 93 // 94 // This method may be called only once per backend instance, and must be 95 // called before all other methods except where otherwise stated. 96 // 97 // If error diagnostics are returned, the internal state of the instance 98 // is undefined and no other methods may be called. 99 Configure(cty.Value) tfdiags.Diagnostics 100 101 // StateMgr returns the state manager for the given workspace name. 102 // 103 // If the returned state manager also implements statemgr.Locker then 104 // it's the caller's responsibility to call Lock and Unlock as appropriate. 105 // 106 // If the named workspace doesn't exist, or if it has no state, it will 107 // be created either immediately on this call or the first time 108 // PersistState is called, depending on the state manager implementation. 109 StateMgr(workspace string) (statemgr.Full, error) 110 111 // DeleteWorkspace removes the workspace with the given name if it exists. 112 // 113 // DeleteWorkspace cannot prevent deleting a state that is in use. It is 114 // the responsibility of the caller to hold a Lock for the state manager 115 // belonging to this workspace before calling this method. 116 DeleteWorkspace(name string, force bool) error 117 118 // States returns a list of the names of all of the workspaces that exist 119 // in this backend. 120 Workspaces() ([]string, error) 121 } 122 123 // HostAlias describes a list of aliases that should be used when initializing an 124 // Enhanced Backend 125 type HostAlias struct { 126 From svchost.Hostname 127 To svchost.Hostname 128 } 129 130 // Enhanced implements additional behavior on top of a normal backend. 131 // 132 // 'Enhanced' backends are an implementation detail only, and are no longer reflected as an external 133 // 'feature' of backends. In other words, backends refer to plugins for remote state snapshot 134 // storage only, and the Enhanced interface here is a necessary vestige of the 'local' and 135 // remote/cloud backends only. 136 type Enhanced interface { 137 Backend 138 139 // Operation performs a Terraform operation such as refresh, plan, apply. 140 // It is up to the implementation to determine what "performing" means. 141 // This DOES NOT BLOCK. The context returned as part of RunningOperation 142 // should be used to block for completion. 143 // If the state used in the operation can be locked, it is the 144 // responsibility of the Backend to lock the state for the duration of the 145 // running operation. 146 Operation(context.Context, *Operation) (*RunningOperation, error) 147 148 // ServiceDiscoveryAliases returns a mapping of Alias -> Target hosts to 149 // configure. 150 ServiceDiscoveryAliases() ([]HostAlias, error) 151 } 152 153 // Local implements additional behavior on a Backend that allows local 154 // operations in addition to remote operations. 155 // 156 // This enables more behaviors of Terraform that require more data such 157 // as `console`, `import`, `graph`. These require direct access to 158 // configurations, variables, and more. Not all backends may support this 159 // so we separate it out into its own optional interface. 160 type Local interface { 161 // LocalRun uses information in the Operation to prepare a set of objects 162 // needed to start running that operation. 163 // 164 // The operation doesn't need a Type set, but it needs various other 165 // options set. This is a rather odd API that tries to treat all 166 // operations as the same when they really aren't; see the local and remote 167 // backend's implementations of this to understand what this actually 168 // does, because this operation has no well-defined contract aside from 169 // "whatever it already does". 170 LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics) 171 } 172 173 // LocalRun represents the assortment of objects that we can collect or 174 // calculate from an Operation object, which we can then use for local 175 // operations. 176 // 177 // The operation methods on terraform.Context (Plan, Apply, Import, etc) each 178 // generate new artifacts which supersede parts of the LocalRun object that 179 // started the operation, so callers should be careful to use those subsequent 180 // artifacts instead of the fields of LocalRun where appropriate. The LocalRun 181 // data intentionally doesn't update as a result of calling methods on Context, 182 // in order to make data flow explicit. 183 // 184 // This type is a weird architectural wart resulting from the overly-general 185 // way our backend API models operations, whereby we behave as if all 186 // Terraform operations have the same inputs and outputs even though they 187 // are actually all rather different. The exact meaning of the fields in 188 // this type therefore vary depending on which OperationType was passed to 189 // Local.Context in order to create an object of this type. 190 type LocalRun struct { 191 // Core is an already-initialized Terraform Core context, ready to be 192 // used to run operations such as Plan and Apply. 193 Core *terraform.Context 194 195 // Config is the configuration we're working with, which typically comes 196 // from either config files directly on local disk (when we're creating 197 // a plan, or similar) or from a snapshot embedded in a plan file 198 // (when we're applying a saved plan). 199 Config *configs.Config 200 201 // InputState is the state that should be used for whatever is the first 202 // method call to a context created with CoreOpts. When creating a plan 203 // this will be the previous run state, but when applying a saved plan 204 // this will be the prior state recorded in that plan. 205 InputState *states.State 206 207 // PlanOpts are options to pass to a Plan or Plan-like operation. 208 // 209 // This is nil when we're applying a saved plan, because the plan itself 210 // contains enough information about its options to apply it. 211 PlanOpts *terraform.PlanOpts 212 213 // Plan is a plan loaded from a saved plan file, if our operation is to 214 // apply that saved plan. 215 // 216 // This is nil when we're not applying a saved plan. 217 Plan *plans.Plan 218 } 219 220 // An operation represents an operation for Terraform to execute. 221 // 222 // Note that not all fields are supported by all backends and can result 223 // in an error if set. All backend implementations should show user-friendly 224 // errors explaining any incorrectly set values. For example, the local 225 // backend doesn't support a PlanId being set. 226 // 227 // The operation options are purposely designed to have maximal compatibility 228 // between Terraform and Terraform Servers (a commercial product offered by 229 // HashiCorp). Therefore, it isn't expected that other implementation support 230 // every possible option. The struct here is generalized in order to allow 231 // even partial implementations to exist in the open, without walling off 232 // remote functionality 100% behind a commercial wall. Anyone can implement 233 // against this interface and have Terraform interact with it just as it 234 // would with HashiCorp-provided Terraform Servers. 235 type Operation struct { 236 // Type is the operation to perform. 237 Type OperationType 238 239 // PlanId is an opaque value that backends can use to execute a specific 240 // plan for an apply operation. 241 // 242 // PlanOutBackend is the backend to store with the plan. This is the 243 // backend that will be used when applying the plan. 244 PlanId string 245 PlanRefresh bool // PlanRefresh will do a refresh before a plan 246 PlanOutPath string // PlanOutPath is the path to save the plan 247 PlanOutBackend *plans.Backend 248 249 // ConfigDir is the path to the directory containing the configuration's 250 // root module. 251 ConfigDir string 252 253 // ConfigLoader is a configuration loader that can be used to load 254 // configuration from ConfigDir. 255 ConfigLoader *configload.Loader 256 257 // DependencyLocks represents the locked dependencies associated with 258 // the configuration directory given in ConfigDir. 259 // 260 // Note that if field PlanFile is set then the plan file should contain 261 // its own dependency locks. The backend is responsible for correctly 262 // selecting between these two sets of locks depending on whether it 263 // will be using ConfigDir or PlanFile to get the configuration for 264 // this operation. 265 DependencyLocks *depsfile.Locks 266 267 // Hooks can be used to perform actions triggered by various events during 268 // the operation's lifecycle. 269 Hooks []terraform.Hook 270 271 // Plan is a plan that was passed as an argument. This is valid for 272 // plan and apply arguments but may not work for all backends. 273 PlanFile *planfile.WrappedPlanFile 274 275 // The options below are more self-explanatory and affect the runtime 276 // behavior of the operation. 277 PlanMode plans.Mode 278 AutoApprove bool 279 Targets []addrs.Targetable 280 ForceReplace []addrs.AbsResourceInstance 281 Variables map[string]UnparsedVariableValue 282 283 // Some operations use root module variables only opportunistically or 284 // don't need them at all. If this flag is set, the backend must treat 285 // all variables as optional and provide an unknown value for any required 286 // variables that aren't set in order to allow partial evaluation against 287 // the resulting incomplete context. 288 // 289 // This flag is honored only if PlanFile isn't set. If PlanFile is set then 290 // the variables set in the plan are used instead, and they must be valid. 291 AllowUnsetVariables bool 292 293 // View implements the logic for all UI interactions. 294 View views.Operation 295 296 // Input/output/control options. 297 UIIn terraform.UIInput 298 UIOut terraform.UIOutput 299 300 // StateLocker is used to lock the state while providing UI feedback to the 301 // user. This will be replaced by the Backend to update the context. 302 // 303 // If state locking is not necessary, this should be set to a no-op 304 // implementation of clistate.Locker. 305 StateLocker clistate.Locker 306 307 // Workspace is the name of the workspace that this operation should run 308 // in, which controls which named state is used. 309 Workspace string 310 311 // GenerateConfigOut tells the operation both that it should generate config 312 // for unmatched import targets and where any generated config should be 313 // written to. 314 GenerateConfigOut string 315 } 316 317 // HasConfig returns true if and only if the operation has a ConfigDir value 318 // that refers to a directory containing at least one Terraform configuration 319 // file. 320 func (o *Operation) HasConfig() bool { 321 return o.ConfigLoader.IsConfigDir(o.ConfigDir) 322 } 323 324 // Config loads the configuration that the operation applies to, using the 325 // ConfigDir and ConfigLoader fields within the receiving operation. 326 func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) { 327 var diags tfdiags.Diagnostics 328 config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir) 329 diags = diags.Append(hclDiags) 330 return config, diags 331 } 332 333 // ReportResult is a helper for the common chore of setting the status of 334 // a running operation and showing any diagnostics produced during that 335 // operation. 336 // 337 // If the given diagnostics contains errors then the operation's result 338 // will be set to backend.OperationFailure. It will be set to 339 // backend.OperationSuccess otherwise. It will then use o.View.Diagnostics 340 // to show the given diagnostics before returning. 341 // 342 // Callers should feel free to do each of these operations separately in 343 // more complex cases where e.g. diagnostics are interleaved with other 344 // output, but terminating immediately after reporting error diagnostics is 345 // common and can be expressed concisely via this method. 346 func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) { 347 if diags.HasErrors() { 348 op.Result = OperationFailure 349 } else { 350 op.Result = OperationSuccess 351 } 352 if o.View != nil { 353 o.View.Diagnostics(diags) 354 } else { 355 // Shouldn't generally happen, but if it does then we'll at least 356 // make some noise in the logs to help us spot it. 357 if len(diags) != 0 { 358 log.Printf( 359 "[ERROR] Backend needs to report diagnostics but View is not set:\n%s", 360 diags.ErrWithWarnings(), 361 ) 362 } 363 } 364 } 365 366 // RunningOperation is the result of starting an operation. 367 type RunningOperation struct { 368 // For implementers of a backend, this context should not wrap the 369 // passed in context. Otherwise, cancelling the parent context will 370 // immediately mark this context as "done" but those aren't the semantics 371 // we want: we want this context to be done only when the operation itself 372 // is fully done. 373 context.Context 374 375 // Stop requests the operation to complete early, by calling Stop on all 376 // the plugins. If the process needs to terminate immediately, call Cancel. 377 Stop context.CancelFunc 378 379 // Cancel is the context.CancelFunc associated with the embedded context, 380 // and can be called to terminate the operation early. 381 // Once Cancel is called, the operation should return as soon as possible 382 // to avoid running operations during process exit. 383 Cancel context.CancelFunc 384 385 // Result is the exit status of the operation, populated only after the 386 // operation has completed. 387 Result OperationResult 388 389 // PlanEmpty is populated after a Plan operation completes to note whether 390 // a plan is empty or has changes. This is only used in the CLI to determine 391 // the exit status because the plan value is not available at that point. 392 PlanEmpty bool 393 394 // State is the final state after the operation completed. Persisting 395 // this state is managed by the backend. This should only be read 396 // after the operation completes to avoid read/write races. 397 State *states.State 398 } 399 400 // OperationResult describes the result status of an operation. 401 type OperationResult int 402 403 const ( 404 // OperationSuccess indicates that the operation completed as expected. 405 OperationSuccess OperationResult = 0 406 407 // OperationFailure indicates that the operation encountered some sort 408 // of error, and thus may have been only partially performed or not 409 // performed at all. 410 OperationFailure OperationResult = 1 411 ) 412 413 func (r OperationResult) ExitStatus() int { 414 return int(r) 415 } 416 417 // If the argument is a path, Read loads it and returns the contents, 418 // otherwise the argument is assumed to be the desired contents and is simply 419 // returned. 420 func ReadPathOrContents(poc string) (string, error) { 421 if len(poc) == 0 { 422 return poc, nil 423 } 424 425 path := poc 426 if path[0] == '~' { 427 var err error 428 path, err = homedir.Expand(path) 429 if err != nil { 430 return path, err 431 } 432 } 433 434 if _, err := os.Stat(path); err == nil { 435 contents, err := ioutil.ReadFile(path) 436 if err != nil { 437 return string(contents), err 438 } 439 return string(contents), nil 440 } 441 442 return poc, nil 443 }