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