github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/backend/local/backend.go (about) 1 package local 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "os" 10 "path/filepath" 11 "sort" 12 "sync" 13 14 "github.com/hashicorp/terraform/backend" 15 "github.com/hashicorp/terraform/command/clistate" 16 "github.com/hashicorp/terraform/configs/configschema" 17 "github.com/hashicorp/terraform/states/statemgr" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/hashicorp/terraform/tfdiags" 20 "github.com/mitchellh/cli" 21 "github.com/mitchellh/colorstring" 22 "github.com/zclconf/go-cty/cty" 23 ) 24 25 const ( 26 DefaultWorkspaceDir = "terraform.tfstate.d" 27 DefaultWorkspaceFile = "environment" 28 DefaultStateFilename = "terraform.tfstate" 29 DefaultBackupExtension = ".backup" 30 ) 31 32 // Local is an implementation of EnhancedBackend that performs all operations 33 // locally. This is the "default" backend and implements normal Terraform 34 // behavior as it is well known. 35 type Local struct { 36 // CLI and Colorize control the CLI output. If CLI is nil then no CLI 37 // output will be done. If CLIColor is nil then no coloring will be done. 38 CLI cli.Ui 39 CLIColor *colorstring.Colorize 40 41 // ShowDiagnostics prints diagnostic messages to the UI. 42 ShowDiagnostics func(vals ...interface{}) 43 44 // The State* paths are set from the backend config, and may be left blank 45 // to use the defaults. If the actual paths for the local backend state are 46 // needed, use the StatePaths method. 47 // 48 // StatePath is the local path where state is read from. 49 // 50 // StateOutPath is the local path where the state will be written. 51 // If this is empty, it will default to StatePath. 52 // 53 // StateBackupPath is the local path where a backup file will be written. 54 // Set this to "-" to disable state backup. 55 // 56 // StateWorkspaceDir is the path to the folder containing data for 57 // non-default workspaces. This defaults to DefaultWorkspaceDir if not set. 58 StatePath string 59 StateOutPath string 60 StateBackupPath string 61 StateWorkspaceDir string 62 63 // The OverrideState* paths are set based on per-operation CLI arguments 64 // and will override what'd be built from the State* fields if non-empty. 65 // While the interpretation of the State* fields depends on the active 66 // workspace, the OverrideState* fields are always used literally. 67 OverrideStatePath string 68 OverrideStateOutPath string 69 OverrideStateBackupPath string 70 71 // We only want to create a single instance of a local state, so store them 72 // here as they're loaded. 73 states map[string]statemgr.Full 74 75 // Terraform context. Many of these will be overridden or merged by 76 // Operation. See Operation for more details. 77 ContextOpts *terraform.ContextOpts 78 79 // OpInput will ask for necessary input prior to performing any operations. 80 // 81 // OpValidation will perform validation prior to running an operation. The 82 // variable naming doesn't match the style of others since we have a func 83 // Validate. 84 OpInput bool 85 OpValidation bool 86 87 // Backend, if non-nil, will use this backend for non-enhanced behavior. 88 // This allows local behavior with remote state storage. It is a way to 89 // "upgrade" a non-enhanced backend to an enhanced backend with typical 90 // behavior. 91 // 92 // If this is nil, local performs normal state loading and storage. 93 Backend backend.Backend 94 95 // RunningInAutomation indicates that commands are being run by an 96 // automated system rather than directly at a command prompt. 97 // 98 // This is a hint not to produce messages that expect that a user can 99 // run a follow-up command, perhaps because Terraform is running in 100 // some sort of workflow automation tool that abstracts away the 101 // exact commands that are being run. 102 RunningInAutomation bool 103 104 // opLock locks operations 105 opLock sync.Mutex 106 } 107 108 var _ backend.Backend = (*Local)(nil) 109 110 // New returns a new initialized local backend. 111 func New() *Local { 112 return NewWithBackend(nil) 113 } 114 115 // NewWithBackend returns a new local backend initialized with a 116 // dedicated backend for non-enhanced behavior. 117 func NewWithBackend(backend backend.Backend) *Local { 118 return &Local{ 119 Backend: backend, 120 } 121 } 122 123 func (b *Local) ConfigSchema() *configschema.Block { 124 if b.Backend != nil { 125 return b.Backend.ConfigSchema() 126 } 127 return &configschema.Block{ 128 Attributes: map[string]*configschema.Attribute{ 129 "path": { 130 Type: cty.String, 131 Optional: true, 132 }, 133 "workspace_dir": { 134 Type: cty.String, 135 Optional: true, 136 }, 137 }, 138 } 139 } 140 141 func (b *Local) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { 142 if b.Backend != nil { 143 return b.Backend.PrepareConfig(obj) 144 } 145 146 var diags tfdiags.Diagnostics 147 148 if val := obj.GetAttr("path"); !val.IsNull() { 149 p := val.AsString() 150 if p == "" { 151 diags = diags.Append(tfdiags.AttributeValue( 152 tfdiags.Error, 153 "Invalid local state file path", 154 `The "path" attribute value must not be empty.`, 155 cty.Path{cty.GetAttrStep{Name: "path"}}, 156 )) 157 } 158 } 159 160 if val := obj.GetAttr("workspace_dir"); !val.IsNull() { 161 p := val.AsString() 162 if p == "" { 163 diags = diags.Append(tfdiags.AttributeValue( 164 tfdiags.Error, 165 "Invalid local workspace directory path", 166 `The "workspace_dir" attribute value must not be empty.`, 167 cty.Path{cty.GetAttrStep{Name: "workspace_dir"}}, 168 )) 169 } 170 } 171 172 return obj, diags 173 } 174 175 func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics { 176 if b.Backend != nil { 177 return b.Backend.Configure(obj) 178 } 179 180 var diags tfdiags.Diagnostics 181 182 if val := obj.GetAttr("path"); !val.IsNull() { 183 p := val.AsString() 184 b.StatePath = p 185 b.StateOutPath = p 186 } else { 187 b.StatePath = DefaultStateFilename 188 b.StateOutPath = DefaultStateFilename 189 } 190 191 if val := obj.GetAttr("workspace_dir"); !val.IsNull() { 192 p := val.AsString() 193 b.StateWorkspaceDir = p 194 } else { 195 b.StateWorkspaceDir = DefaultWorkspaceDir 196 } 197 198 return diags 199 } 200 201 func (b *Local) Workspaces() ([]string, error) { 202 // If we have a backend handling state, defer to that. 203 if b.Backend != nil { 204 return b.Backend.Workspaces() 205 } 206 207 // the listing always start with "default" 208 envs := []string{backend.DefaultStateName} 209 210 entries, err := ioutil.ReadDir(b.stateWorkspaceDir()) 211 // no error if there's no envs configured 212 if os.IsNotExist(err) { 213 return envs, nil 214 } 215 if err != nil { 216 return nil, err 217 } 218 219 var listed []string 220 for _, entry := range entries { 221 if entry.IsDir() { 222 listed = append(listed, filepath.Base(entry.Name())) 223 } 224 } 225 226 sort.Strings(listed) 227 envs = append(envs, listed...) 228 229 return envs, nil 230 } 231 232 // DeleteWorkspace removes a workspace. 233 // 234 // The "default" workspace cannot be removed. 235 func (b *Local) DeleteWorkspace(name string) error { 236 // If we have a backend handling state, defer to that. 237 if b.Backend != nil { 238 return b.Backend.DeleteWorkspace(name) 239 } 240 241 if name == "" { 242 return errors.New("empty state name") 243 } 244 245 if name == backend.DefaultStateName { 246 return errors.New("cannot delete default state") 247 } 248 249 delete(b.states, name) 250 return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) 251 } 252 253 func (b *Local) StateMgr(name string) (statemgr.Full, error) { 254 // If we have a backend handling state, delegate to that. 255 if b.Backend != nil { 256 return b.Backend.StateMgr(name) 257 } 258 259 if s, ok := b.states[name]; ok { 260 return s, nil 261 } 262 263 if err := b.createState(name); err != nil { 264 return nil, err 265 } 266 267 statePath, stateOutPath, backupPath := b.StatePaths(name) 268 log.Printf("[TRACE] backend/local: state manager for workspace %q will:\n - read initial snapshot from %s\n - write new snapshots to %s\n - create any backup at %s", name, statePath, stateOutPath, backupPath) 269 270 s := statemgr.NewFilesystemBetweenPaths(statePath, stateOutPath) 271 if backupPath != "" { 272 s.SetBackupPath(backupPath) 273 } 274 275 if b.states == nil { 276 b.states = map[string]statemgr.Full{} 277 } 278 b.states[name] = s 279 return s, nil 280 } 281 282 func (b *Local) StateMgrWithoutCheckVersion(name string) (statemgr.Full, error) { 283 return b.StateMgr(name) 284 } 285 286 // Operation implements backend.Enhanced 287 // 288 // This will initialize an in-memory terraform.Context to perform the 289 // operation within this process. 290 // 291 // The given operation parameter will be merged with the ContextOpts on 292 // the structure with the following rules. If a rule isn't specified and the 293 // name conflicts, assume that the field is overwritten if set. 294 func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { 295 // Determine the function to call for our operation 296 var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation) 297 switch op.Type { 298 case backend.OperationTypeRefresh: 299 f = b.opRefresh 300 case backend.OperationTypePlan: 301 f = b.opPlan 302 case backend.OperationTypeApply: 303 f = b.opApply 304 default: 305 return nil, fmt.Errorf( 306 "Unsupported operation type: %s\n\n"+ 307 "This is a bug in Terraform and should be reported. The local backend\n"+ 308 "is built-in to Terraform and should always support all operations.", 309 op.Type) 310 } 311 312 // Lock 313 b.opLock.Lock() 314 315 // Build our running operation 316 // the runninCtx is only used to block until the operation returns. 317 runningCtx, done := context.WithCancel(context.Background()) 318 runningOp := &backend.RunningOperation{ 319 Context: runningCtx, 320 } 321 322 // stopCtx wraps the context passed in, and is used to signal a graceful Stop. 323 stopCtx, stop := context.WithCancel(ctx) 324 runningOp.Stop = stop 325 326 // cancelCtx is used to cancel the operation immediately, usually 327 // indicating that the process is exiting. 328 cancelCtx, cancel := context.WithCancel(context.Background()) 329 runningOp.Cancel = cancel 330 331 if op.LockState { 332 op.StateLocker = clistate.NewLocker(stopCtx, op.StateLockTimeout, b.CLI, b.Colorize()) 333 } else { 334 op.StateLocker = clistate.NewNoopLocker() 335 } 336 337 // Do it 338 go func() { 339 defer done() 340 defer stop() 341 defer cancel() 342 343 // the state was locked during context creation, unlock the state when 344 // the operation completes 345 defer func() { 346 err := op.StateLocker.Unlock(nil) 347 if err != nil { 348 b.ShowDiagnostics(err) 349 runningOp.Result = backend.OperationFailure 350 } 351 }() 352 353 defer b.opLock.Unlock() 354 f(stopCtx, cancelCtx, op, runningOp) 355 }() 356 357 // Return 358 return runningOp, nil 359 } 360 361 // opWait waits for the operation to complete, and a stop signal or a 362 // cancelation signal. 363 func (b *Local) opWait( 364 doneCh <-chan struct{}, 365 stopCtx context.Context, 366 cancelCtx context.Context, 367 tfCtx *terraform.Context, 368 opStateMgr statemgr.Persister) (canceled bool) { 369 // Wait for the operation to finish or for us to be interrupted so 370 // we can handle it properly. 371 select { 372 case <-stopCtx.Done(): 373 if b.CLI != nil { 374 b.CLI.Output("Stopping operation...") 375 } 376 377 // try to force a PersistState just in case the process is terminated 378 // before we can complete. 379 if err := opStateMgr.PersistState(); err != nil { 380 // We can't error out from here, but warn the user if there was an error. 381 // If this isn't transient, we will catch it again below, and 382 // attempt to save the state another way. 383 if b.CLI != nil { 384 b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err)) 385 } 386 } 387 388 // Stop execution 389 log.Println("[TRACE] backend/local: waiting for the running operation to stop") 390 go tfCtx.Stop() 391 392 select { 393 case <-cancelCtx.Done(): 394 log.Println("[WARN] running operation was forcefully canceled") 395 // if the operation was canceled, we need to return immediately 396 canceled = true 397 case <-doneCh: 398 log.Println("[TRACE] backend/local: graceful stop has completed") 399 } 400 case <-cancelCtx.Done(): 401 // this should not be called without first attempting to stop the 402 // operation 403 log.Println("[ERROR] running operation canceled without Stop") 404 canceled = true 405 case <-doneCh: 406 } 407 return 408 } 409 410 // ReportResult is a helper for the common chore of setting the status of 411 // a running operation and showing any diagnostics produced during that 412 // operation. 413 // 414 // If the given diagnostics contains errors then the operation's result 415 // will be set to backend.OperationFailure. It will be set to 416 // backend.OperationSuccess otherwise. It will then use b.ShowDiagnostics 417 // to show the given diagnostics before returning. 418 // 419 // Callers should feel free to do each of these operations separately in 420 // more complex cases where e.g. diagnostics are interleaved with other 421 // output, but terminating immediately after reporting error diagnostics is 422 // common and can be expressed concisely via this method. 423 func (b *Local) ReportResult(op *backend.RunningOperation, diags tfdiags.Diagnostics) { 424 if diags.HasErrors() { 425 op.Result = backend.OperationFailure 426 } else { 427 op.Result = backend.OperationSuccess 428 } 429 if b.ShowDiagnostics != nil { 430 b.ShowDiagnostics(diags) 431 } else { 432 // Shouldn't generally happen, but if it does then we'll at least 433 // make some noise in the logs to help us spot it. 434 if len(diags) != 0 { 435 log.Printf( 436 "[ERROR] Local backend needs to report diagnostics but ShowDiagnostics is not set:\n%s", 437 diags.ErrWithWarnings(), 438 ) 439 } 440 } 441 } 442 443 // Colorize returns the Colorize structure that can be used for colorizing 444 // output. This is guaranteed to always return a non-nil value and so is useful 445 // as a helper to wrap any potentially colored strings. 446 func (b *Local) Colorize() *colorstring.Colorize { 447 if b.CLIColor != nil { 448 return b.CLIColor 449 } 450 451 return &colorstring.Colorize{ 452 Colors: colorstring.DefaultColors, 453 Disable: true, 454 } 455 } 456 457 // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as 458 // configured from the CLI. 459 func (b *Local) StatePaths(name string) (stateIn, stateOut, backupOut string) { 460 statePath := b.OverrideStatePath 461 stateOutPath := b.OverrideStateOutPath 462 backupPath := b.OverrideStateBackupPath 463 464 isDefault := name == backend.DefaultStateName || name == "" 465 466 baseDir := "" 467 if !isDefault { 468 baseDir = filepath.Join(b.stateWorkspaceDir(), name) 469 } 470 471 if statePath == "" { 472 if isDefault { 473 statePath = b.StatePath // s.StatePath applies only to the default workspace, since StateWorkspaceDir is used otherwise 474 } 475 if statePath == "" { 476 statePath = filepath.Join(baseDir, DefaultStateFilename) 477 } 478 } 479 if stateOutPath == "" { 480 stateOutPath = statePath 481 } 482 if backupPath == "" { 483 backupPath = b.StateBackupPath 484 } 485 switch backupPath { 486 case "-": 487 backupPath = "" 488 case "": 489 backupPath = stateOutPath + DefaultBackupExtension 490 } 491 492 return statePath, stateOutPath, backupPath 493 } 494 495 // PathsConflictWith returns true if any state path used by a workspace in 496 // the receiver is the same as any state path used by the other given 497 // local backend instance. 498 // 499 // This should be used when "migrating" from one local backend configuration to 500 // another in order to avoid deleting the "old" state snapshots if they are 501 // in the same files as the "new" state snapshots. 502 func (b *Local) PathsConflictWith(other *Local) bool { 503 otherPaths := map[string]struct{}{} 504 otherWorkspaces, err := other.Workspaces() 505 if err != nil { 506 // If we can't enumerate the workspaces then we'll conservatively 507 // assume that paths _do_ overlap, since we can't be certain. 508 return true 509 } 510 for _, name := range otherWorkspaces { 511 p, _, _ := other.StatePaths(name) 512 otherPaths[p] = struct{}{} 513 } 514 515 ourWorkspaces, err := other.Workspaces() 516 if err != nil { 517 // If we can't enumerate the workspaces then we'll conservatively 518 // assume that paths _do_ overlap, since we can't be certain. 519 return true 520 } 521 522 for _, name := range ourWorkspaces { 523 p, _, _ := b.StatePaths(name) 524 if _, exists := otherPaths[p]; exists { 525 return true 526 } 527 } 528 return false 529 } 530 531 // this only ensures that the named directory exists 532 func (b *Local) createState(name string) error { 533 if name == backend.DefaultStateName { 534 return nil 535 } 536 537 stateDir := filepath.Join(b.stateWorkspaceDir(), name) 538 s, err := os.Stat(stateDir) 539 if err == nil && s.IsDir() { 540 // no need to check for os.IsNotExist, since that is covered by os.MkdirAll 541 // which will catch the other possible errors as well. 542 return nil 543 } 544 545 err = os.MkdirAll(stateDir, 0755) 546 if err != nil { 547 return err 548 } 549 550 return nil 551 } 552 553 // stateWorkspaceDir returns the directory where state environments are stored. 554 func (b *Local) stateWorkspaceDir() string { 555 if b.StateWorkspaceDir != "" { 556 return b.StateWorkspaceDir 557 } 558 559 return DefaultWorkspaceDir 560 }