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