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