github.com/tompao/terraform@v0.6.10-0.20180215233341-e41b29d0961b/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 "strings" 13 "sync" 14 15 "github.com/hashicorp/terraform/backend" 16 "github.com/hashicorp/terraform/helper/schema" 17 "github.com/hashicorp/terraform/state" 18 "github.com/hashicorp/terraform/terraform" 19 "github.com/mitchellh/cli" 20 "github.com/mitchellh/colorstring" 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 // CLI and Colorize control the CLI output. If CLI is nil then no CLI 35 // output will be done. If CLIColor is nil then no coloring will be done. 36 CLI cli.Ui 37 CLIColor *colorstring.Colorize 38 39 // The State* paths are set from the backend config, and may be left blank 40 // to use the defaults. If the actual paths for the local backend state are 41 // needed, use the StatePaths method. 42 // 43 // StatePath is the local path where state is read from. 44 // 45 // StateOutPath is the local path where the state will be written. 46 // If this is empty, it will default to StatePath. 47 // 48 // StateBackupPath is the local path where a backup file will be written. 49 // Set this to "-" to disable state backup. 50 // 51 // StateWorkspaceDir is the path to the folder containing data for 52 // non-default workspaces. This defaults to DefaultWorkspaceDir if not set. 53 StatePath string 54 StateOutPath string 55 StateBackupPath string 56 StateWorkspaceDir string 57 58 // We only want to create a single instance of a local state, so store them 59 // here as they're loaded. 60 states map[string]state.State 61 62 // Terraform context. Many of these will be overridden or merged by 63 // Operation. See Operation for more details. 64 ContextOpts *terraform.ContextOpts 65 66 // OpInput will ask for necessary input prior to performing any operations. 67 // 68 // OpValidation will perform validation prior to running an operation. The 69 // variable naming doesn't match the style of others since we have a func 70 // Validate. 71 OpInput bool 72 OpValidation bool 73 74 // Backend, if non-nil, will use this backend for non-enhanced behavior. 75 // This allows local behavior with remote state storage. It is a way to 76 // "upgrade" a non-enhanced backend to an enhanced backend with typical 77 // behavior. 78 // 79 // If this is nil, local performs normal state loading and storage. 80 Backend backend.Backend 81 82 // RunningInAutomation indicates that commands are being run by an 83 // automated system rather than directly at a command prompt. 84 // 85 // This is a hint not to produce messages that expect that a user can 86 // run a follow-up command, perhaps because Terraform is running in 87 // some sort of workflow automation tool that abstracts away the 88 // exact commands that are being run. 89 RunningInAutomation bool 90 91 schema *schema.Backend 92 opLock sync.Mutex 93 once sync.Once 94 } 95 96 func (b *Local) Input( 97 ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { 98 b.once.Do(b.init) 99 100 f := b.schema.Input 101 if b.Backend != nil { 102 f = b.Backend.Input 103 } 104 105 return f(ui, c) 106 } 107 108 func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) { 109 b.once.Do(b.init) 110 111 f := b.schema.Validate 112 if b.Backend != nil { 113 f = b.Backend.Validate 114 } 115 116 return f(c) 117 } 118 119 func (b *Local) Configure(c *terraform.ResourceConfig) error { 120 b.once.Do(b.init) 121 122 f := b.schema.Configure 123 if b.Backend != nil { 124 f = b.Backend.Configure 125 } 126 127 return f(c) 128 } 129 130 func (b *Local) States() ([]string, error) { 131 // If we have a backend handling state, defer to that. 132 if b.Backend != nil { 133 return b.Backend.States() 134 } 135 136 // the listing always start with "default" 137 envs := []string{backend.DefaultStateName} 138 139 entries, err := ioutil.ReadDir(b.stateWorkspaceDir()) 140 // no error if there's no envs configured 141 if os.IsNotExist(err) { 142 return envs, nil 143 } 144 if err != nil { 145 return nil, err 146 } 147 148 var listed []string 149 for _, entry := range entries { 150 if entry.IsDir() { 151 listed = append(listed, filepath.Base(entry.Name())) 152 } 153 } 154 155 sort.Strings(listed) 156 envs = append(envs, listed...) 157 158 return envs, nil 159 } 160 161 // DeleteState removes a named state. 162 // The "default" state cannot be removed. 163 func (b *Local) DeleteState(name string) error { 164 // If we have a backend handling state, defer to that. 165 if b.Backend != nil { 166 return b.Backend.DeleteState(name) 167 } 168 169 if name == "" { 170 return errors.New("empty state name") 171 } 172 173 if name == backend.DefaultStateName { 174 return errors.New("cannot delete default state") 175 } 176 177 delete(b.states, name) 178 return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) 179 } 180 181 func (b *Local) State(name string) (state.State, error) { 182 statePath, stateOutPath, backupPath := b.StatePaths(name) 183 184 // If we have a backend handling state, delegate to that. 185 if b.Backend != nil { 186 return b.Backend.State(name) 187 } 188 189 if s, ok := b.states[name]; ok { 190 return s, nil 191 } 192 193 if err := b.createState(name); err != nil { 194 return nil, err 195 } 196 197 // Otherwise, we need to load the state. 198 var s state.State = &state.LocalState{ 199 Path: statePath, 200 PathOut: stateOutPath, 201 } 202 203 // If we are backing up the state, wrap it 204 if backupPath != "" { 205 s = &state.BackupState{ 206 Real: s, 207 Path: backupPath, 208 } 209 } 210 211 if b.states == nil { 212 b.states = map[string]state.State{} 213 } 214 b.states[name] = s 215 return s, nil 216 } 217 218 // Operation implements backend.Enhanced 219 // 220 // This will initialize an in-memory terraform.Context to perform the 221 // operation within this process. 222 // 223 // The given operation parameter will be merged with the ContextOpts on 224 // the structure with the following rules. If a rule isn't specified and the 225 // name conflicts, assume that the field is overwritten if set. 226 func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { 227 // Determine the function to call for our operation 228 var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation) 229 switch op.Type { 230 case backend.OperationTypeRefresh: 231 f = b.opRefresh 232 case backend.OperationTypePlan: 233 f = b.opPlan 234 case backend.OperationTypeApply: 235 f = b.opApply 236 default: 237 return nil, fmt.Errorf( 238 "Unsupported operation type: %s\n\n"+ 239 "This is a bug in Terraform and should be reported. The local backend\n"+ 240 "is built-in to Terraform and should always support all operations.", 241 op.Type) 242 } 243 244 // Lock 245 b.opLock.Lock() 246 247 // Build our running operation 248 // the runninCtx is only used to block until the operation returns. 249 runningCtx, done := context.WithCancel(context.Background()) 250 runningOp := &backend.RunningOperation{ 251 Context: runningCtx, 252 } 253 254 // stopCtx wraps the context passed in, and is used to signal a graceful Stop. 255 stopCtx, stop := context.WithCancel(ctx) 256 runningOp.Stop = stop 257 258 // cancelCtx is used to cancel the operation immediately, usually 259 // indicating that the process is exiting. 260 cancelCtx, cancel := context.WithCancel(context.Background()) 261 runningOp.Cancel = cancel 262 263 // Do it 264 go func() { 265 defer done() 266 defer stop() 267 defer cancel() 268 269 defer b.opLock.Unlock() 270 f(stopCtx, cancelCtx, op, runningOp) 271 }() 272 273 // Return 274 return runningOp, nil 275 } 276 277 // opWait wats for the operation to complete, and a stop signal or a 278 // cancelation signal. 279 func (b *Local) opWait( 280 doneCh <-chan struct{}, 281 stopCtx context.Context, 282 cancelCtx context.Context, 283 tfCtx *terraform.Context, 284 opState state.State) (canceled bool) { 285 // Wait for the operation to finish or for us to be interrupted so 286 // we can handle it properly. 287 select { 288 case <-stopCtx.Done(): 289 if b.CLI != nil { 290 b.CLI.Output("stopping operation...") 291 } 292 293 // try to force a PersistState just in case the process is terminated 294 // before we can complete. 295 if err := opState.PersistState(); err != nil { 296 // We can't error out from here, but warn the user if there was an error. 297 // If this isn't transient, we will catch it again below, and 298 // attempt to save the state another way. 299 if b.CLI != nil { 300 b.CLI.Error(fmt.Sprintf(earlyStateWriteErrorFmt, err)) 301 } 302 } 303 304 // Stop execution 305 go tfCtx.Stop() 306 307 select { 308 case <-cancelCtx.Done(): 309 log.Println("[WARN] running operation canceled") 310 // if the operation was canceled, we need to return immediately 311 canceled = true 312 case <-doneCh: 313 } 314 case <-cancelCtx.Done(): 315 // this should not be called without first attempting to stop the 316 // operation 317 log.Println("[ERROR] running operation canceled without Stop") 318 canceled = true 319 case <-doneCh: 320 } 321 return 322 } 323 324 // Colorize returns the Colorize structure that can be used for colorizing 325 // output. This is gauranteed to always return a non-nil value and so is useful 326 // as a helper to wrap any potentially colored strings. 327 func (b *Local) Colorize() *colorstring.Colorize { 328 if b.CLIColor != nil { 329 return b.CLIColor 330 } 331 332 return &colorstring.Colorize{ 333 Colors: colorstring.DefaultColors, 334 Disable: true, 335 } 336 } 337 338 func (b *Local) init() { 339 b.schema = &schema.Backend{ 340 Schema: map[string]*schema.Schema{ 341 "path": &schema.Schema{ 342 Type: schema.TypeString, 343 Optional: true, 344 Default: "", 345 }, 346 347 "workspace_dir": &schema.Schema{ 348 Type: schema.TypeString, 349 Optional: true, 350 Default: "", 351 }, 352 353 "environment_dir": &schema.Schema{ 354 Type: schema.TypeString, 355 Optional: true, 356 Default: "", 357 ConflictsWith: []string{"workspace_dir"}, 358 359 Deprecated: "workspace_dir should be used instead, with the same meaning", 360 }, 361 }, 362 363 ConfigureFunc: b.schemaConfigure, 364 } 365 } 366 367 func (b *Local) schemaConfigure(ctx context.Context) error { 368 d := schema.FromContextBackendConfig(ctx) 369 370 // Set the path if it is set 371 pathRaw, ok := d.GetOk("path") 372 if ok { 373 path := pathRaw.(string) 374 if path == "" { 375 return fmt.Errorf("configured path is empty") 376 } 377 378 b.StatePath = path 379 b.StateOutPath = path 380 } 381 382 if raw, ok := d.GetOk("workspace_dir"); ok { 383 path := raw.(string) 384 if path != "" { 385 b.StateWorkspaceDir = path 386 } 387 } 388 389 // Legacy name, which ConflictsWith workspace_dir 390 if raw, ok := d.GetOk("environment_dir"); ok { 391 path := raw.(string) 392 if path != "" { 393 b.StateWorkspaceDir = path 394 } 395 } 396 397 return nil 398 } 399 400 // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as 401 // configured from the CLI. 402 func (b *Local) StatePaths(name string) (string, string, string) { 403 statePath := b.StatePath 404 stateOutPath := b.StateOutPath 405 backupPath := b.StateBackupPath 406 407 if name == "" { 408 name = backend.DefaultStateName 409 } 410 411 if name == backend.DefaultStateName { 412 if statePath == "" { 413 statePath = DefaultStateFilename 414 } 415 } else { 416 statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename) 417 } 418 419 if stateOutPath == "" { 420 stateOutPath = statePath 421 } 422 423 switch backupPath { 424 case "-": 425 backupPath = "" 426 case "": 427 backupPath = stateOutPath + DefaultBackupExtension 428 } 429 430 return statePath, stateOutPath, backupPath 431 } 432 433 // this only ensures that the named directory exists 434 func (b *Local) createState(name string) error { 435 if name == backend.DefaultStateName { 436 return nil 437 } 438 439 stateDir := filepath.Join(b.stateWorkspaceDir(), name) 440 s, err := os.Stat(stateDir) 441 if err == nil && s.IsDir() { 442 // no need to check for os.IsNotExist, since that is covered by os.MkdirAll 443 // which will catch the other possible errors as well. 444 return nil 445 } 446 447 err = os.MkdirAll(stateDir, 0755) 448 if err != nil { 449 return err 450 } 451 452 return nil 453 } 454 455 // stateWorkspaceDir returns the directory where state environments are stored. 456 func (b *Local) stateWorkspaceDir() string { 457 if b.StateWorkspaceDir != "" { 458 return b.StateWorkspaceDir 459 } 460 461 return DefaultWorkspaceDir 462 } 463 464 func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) { 465 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 466 strings.TrimSpace(errPluginInit)+"\n", 467 providerErr))) 468 } 469 470 // this relies on multierror to format the plugin errors below the copy 471 const errPluginInit = ` 472 [reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset] 473 [yellow]Reason: Could not satisfy plugin requirements. 474 475 Plugins are external binaries that Terraform uses to access and manipulate 476 resources. The configuration provided requires plugins which can't be located, 477 don't satisfy the version constraints, or are otherwise incompatible. 478 479 [reset][red]%s 480 481 [reset][yellow]Terraform automatically discovers provider requirements from your 482 configuration, including providers used in child modules. To see the 483 requirements and constraints from each module, run "terraform providers". 484 `