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