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