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