github.com/pgray/terraform@v0.5.4-0.20170822184730-b6a464c5214d/backend/local/backend.go (about) 1 package local 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "sort" 11 "strings" 12 "sync" 13 14 "github.com/hashicorp/terraform/backend" 15 "github.com/hashicorp/terraform/helper/schema" 16 "github.com/hashicorp/terraform/state" 17 "github.com/hashicorp/terraform/terraform" 18 "github.com/mitchellh/cli" 19 "github.com/mitchellh/colorstring" 20 ) 21 22 const ( 23 DefaultWorkspaceDir = "terraform.tfstate.d" 24 DefaultWorkspaceFile = "environment" 25 DefaultStateFilename = "terraform.tfstate" 26 DefaultDataDir = ".terraform" 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 schema *schema.Backend 83 opLock sync.Mutex 84 once sync.Once 85 } 86 87 func (b *Local) Input( 88 ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { 89 b.once.Do(b.init) 90 91 f := b.schema.Input 92 if b.Backend != nil { 93 f = b.Backend.Input 94 } 95 96 return f(ui, c) 97 } 98 99 func (b *Local) Validate(c *terraform.ResourceConfig) ([]string, []error) { 100 b.once.Do(b.init) 101 102 f := b.schema.Validate 103 if b.Backend != nil { 104 f = b.Backend.Validate 105 } 106 107 return f(c) 108 } 109 110 func (b *Local) Configure(c *terraform.ResourceConfig) error { 111 b.once.Do(b.init) 112 113 f := b.schema.Configure 114 if b.Backend != nil { 115 f = b.Backend.Configure 116 } 117 118 return f(c) 119 } 120 121 func (b *Local) States() ([]string, error) { 122 // If we have a backend handling state, defer to that. 123 if b.Backend != nil { 124 return b.Backend.States() 125 } 126 127 // the listing always start with "default" 128 envs := []string{backend.DefaultStateName} 129 130 entries, err := ioutil.ReadDir(b.stateWorkspaceDir()) 131 // no error if there's no envs configured 132 if os.IsNotExist(err) { 133 return envs, nil 134 } 135 if err != nil { 136 return nil, err 137 } 138 139 var listed []string 140 for _, entry := range entries { 141 if entry.IsDir() { 142 listed = append(listed, filepath.Base(entry.Name())) 143 } 144 } 145 146 sort.Strings(listed) 147 envs = append(envs, listed...) 148 149 return envs, nil 150 } 151 152 // DeleteState removes a named state. 153 // The "default" state cannot be removed. 154 func (b *Local) DeleteState(name string) error { 155 // If we have a backend handling state, defer to that. 156 if b.Backend != nil { 157 return b.Backend.DeleteState(name) 158 } 159 160 if name == "" { 161 return errors.New("empty state name") 162 } 163 164 if name == backend.DefaultStateName { 165 return errors.New("cannot delete default state") 166 } 167 168 delete(b.states, name) 169 return os.RemoveAll(filepath.Join(b.stateWorkspaceDir(), name)) 170 } 171 172 func (b *Local) State(name string) (state.State, error) { 173 statePath, stateOutPath, backupPath := b.StatePaths(name) 174 175 // If we have a backend handling state, defer to that. 176 if b.Backend != nil { 177 s, err := b.Backend.State(name) 178 if err != nil { 179 return nil, err 180 } 181 182 // make sure we always have a backup state, unless it disabled 183 if backupPath == "" { 184 return s, nil 185 } 186 187 // see if the delegated backend returned a BackupState of its own 188 if s, ok := s.(*state.BackupState); ok { 189 return s, nil 190 } 191 192 s = &state.BackupState{ 193 Real: s, 194 Path: backupPath, 195 } 196 return s, nil 197 } 198 199 if s, ok := b.states[name]; ok { 200 return s, nil 201 } 202 203 if err := b.createState(name); err != nil { 204 return nil, err 205 } 206 207 // Otherwise, we need to load the state. 208 var s state.State = &state.LocalState{ 209 Path: statePath, 210 PathOut: stateOutPath, 211 } 212 213 // If we are backing up the state, wrap it 214 if backupPath != "" { 215 s = &state.BackupState{ 216 Real: s, 217 Path: backupPath, 218 } 219 } 220 221 if b.states == nil { 222 b.states = map[string]state.State{} 223 } 224 b.states[name] = s 225 return s, nil 226 } 227 228 // Operation implements backend.Enhanced 229 // 230 // This will initialize an in-memory terraform.Context to perform the 231 // operation within this process. 232 // 233 // The given operation parameter will be merged with the ContextOpts on 234 // the structure with the following rules. If a rule isn't specified and the 235 // name conflicts, assume that the field is overwritten if set. 236 func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { 237 // Determine the function to call for our operation 238 var f func(context.Context, *backend.Operation, *backend.RunningOperation) 239 switch op.Type { 240 case backend.OperationTypeRefresh: 241 f = b.opRefresh 242 case backend.OperationTypePlan: 243 f = b.opPlan 244 case backend.OperationTypeApply: 245 f = b.opApply 246 default: 247 return nil, fmt.Errorf( 248 "Unsupported operation type: %s\n\n"+ 249 "This is a bug in Terraform and should be reported. The local backend\n"+ 250 "is built-in to Terraform and should always support all operations.", 251 op.Type) 252 } 253 254 // Lock 255 b.opLock.Lock() 256 257 // Build our running operation 258 runningCtx, runningCtxCancel := context.WithCancel(context.Background()) 259 runningOp := &backend.RunningOperation{Context: runningCtx} 260 261 // Do it 262 go func() { 263 defer b.opLock.Unlock() 264 defer runningCtxCancel() 265 f(ctx, op, runningOp) 266 }() 267 268 // Return 269 return runningOp, nil 270 } 271 272 // Colorize returns the Colorize structure that can be used for colorizing 273 // output. This is gauranteed to always return a non-nil value and so is useful 274 // as a helper to wrap any potentially colored strings. 275 func (b *Local) Colorize() *colorstring.Colorize { 276 if b.CLIColor != nil { 277 return b.CLIColor 278 } 279 280 return &colorstring.Colorize{ 281 Colors: colorstring.DefaultColors, 282 Disable: true, 283 } 284 } 285 286 func (b *Local) init() { 287 b.schema = &schema.Backend{ 288 Schema: map[string]*schema.Schema{ 289 "path": &schema.Schema{ 290 Type: schema.TypeString, 291 Optional: true, 292 Default: "", 293 }, 294 295 "workspace_dir": &schema.Schema{ 296 Type: schema.TypeString, 297 Optional: true, 298 Default: "", 299 }, 300 301 "environment_dir": &schema.Schema{ 302 Type: schema.TypeString, 303 Optional: true, 304 Default: "", 305 ConflictsWith: []string{"workspace_dir"}, 306 307 Deprecated: "workspace_dir should be used instead, with the same meaning", 308 }, 309 }, 310 311 ConfigureFunc: b.schemaConfigure, 312 } 313 } 314 315 func (b *Local) schemaConfigure(ctx context.Context) error { 316 d := schema.FromContextBackendConfig(ctx) 317 318 // Set the path if it is set 319 pathRaw, ok := d.GetOk("path") 320 if ok { 321 path := pathRaw.(string) 322 if path == "" { 323 return fmt.Errorf("configured path is empty") 324 } 325 326 b.StatePath = path 327 b.StateOutPath = path 328 } 329 330 if raw, ok := d.GetOk("workspace_dir"); ok { 331 path := raw.(string) 332 if path != "" { 333 b.StateWorkspaceDir = path 334 } 335 } 336 337 // Legacy name, which ConflictsWith workspace_dir 338 if raw, ok := d.GetOk("environment_dir"); ok { 339 path := raw.(string) 340 if path != "" { 341 b.StateWorkspaceDir = path 342 } 343 } 344 345 return nil 346 } 347 348 // StatePaths returns the StatePath, StateOutPath, and StateBackupPath as 349 // configured from the CLI. 350 func (b *Local) StatePaths(name string) (string, string, string) { 351 statePath := b.StatePath 352 stateOutPath := b.StateOutPath 353 backupPath := b.StateBackupPath 354 355 if name == "" { 356 name = backend.DefaultStateName 357 } 358 359 if name == backend.DefaultStateName { 360 if statePath == "" { 361 statePath = DefaultStateFilename 362 } 363 } else { 364 statePath = filepath.Join(b.stateWorkspaceDir(), name, DefaultStateFilename) 365 } 366 367 if stateOutPath == "" { 368 stateOutPath = statePath 369 } 370 371 switch backupPath { 372 case "-": 373 backupPath = "" 374 case "": 375 backupPath = stateOutPath + DefaultBackupExtension 376 } 377 378 return statePath, stateOutPath, backupPath 379 } 380 381 // this only ensures that the named directory exists 382 func (b *Local) createState(name string) error { 383 if name == backend.DefaultStateName { 384 return nil 385 } 386 387 stateDir := filepath.Join(b.stateWorkspaceDir(), name) 388 s, err := os.Stat(stateDir) 389 if err == nil && s.IsDir() { 390 // no need to check for os.IsNotExist, since that is covered by os.MkdirAll 391 // which will catch the other possible errors as well. 392 return nil 393 } 394 395 err = os.MkdirAll(stateDir, 0755) 396 if err != nil { 397 return err 398 } 399 400 return nil 401 } 402 403 // stateWorkspaceDir returns the directory where state environments are stored. 404 func (b *Local) stateWorkspaceDir() string { 405 if b.StateWorkspaceDir != "" { 406 return b.StateWorkspaceDir 407 } 408 409 return DefaultWorkspaceDir 410 } 411 412 func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) { 413 b.CLI.Output(b.Colorize().Color(fmt.Sprintf( 414 strings.TrimSpace(errPluginInit)+"\n", 415 providerErr))) 416 } 417 418 // this relies on multierror to format the plugin errors below the copy 419 const errPluginInit = ` 420 [reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset] 421 [yellow]Reason: Could not satisfy plugin requirements. 422 423 Plugins are external binaries that Terraform uses to access and manipulate 424 resources. The configuration provided requires plugins which can't be located, 425 don't satisfy the version constraints, or are otherwise incompatible. 426 427 [reset][red]%s 428 429 [reset][yellow]Terraform automatically discovers provider requirements from your 430 configuration, including providers used in child modules. To see the 431 requirements and constraints from each module, run "terraform providers". 432 `