github.com/arvindram03/terraform@v0.3.7-0.20150212015210-408f838db36d/remote/remote.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 12 "github.com/hashicorp/terraform/terraform" 13 ) 14 15 const ( 16 // LocalDirectory is the directory created in the working 17 // dir to hold the remote state file. 18 LocalDirectory = ".terraform" 19 20 // HiddenStateFile is the name of the state file in the 21 // LocalDirectory 22 HiddenStateFile = "terraform.tfstate" 23 24 // BackupHiddenStateFile is the path we backup the state 25 // file to before modifications are made 26 BackupHiddenStateFile = "terraform.tfstate.backup" 27 ) 28 29 // StateChangeResult is used to communicate to a caller 30 // what actions have been taken when updating a state file 31 type StateChangeResult int 32 33 const ( 34 // StateChangeNoop indicates nothing has happened, 35 // but that does not indicate an error. Everything is 36 // just up to date. (Push/Pull) 37 StateChangeNoop StateChangeResult = iota 38 39 // StateChangeInit indicates that there is no local or 40 // remote state, and that the state was initialized 41 StateChangeInit 42 43 // StateChangeUpdateLocal indicates the local state 44 // was updated. (Pull) 45 StateChangeUpdateLocal 46 47 // StateChangeUpdateRemote indicates the remote state 48 // was updated. (Push) 49 StateChangeUpdateRemote 50 51 // StateChangeLocalNewer means the pull was a no-op 52 // because the local state is newer than that of the 53 // server. This means a Push should take place. (Pull) 54 StateChangeLocalNewer 55 56 // StateChangeRemoteNewer means the push was a no-op 57 // because the remote state is newer than that of the 58 // local state. This means a Pull should take place. 59 // (Push) 60 StateChangeRemoteNewer 61 62 // StateChangeConflict means that the push or pull 63 // was a no-op because there is a conflict. This means 64 // there are multiple state definitions at the same 65 // serial number with different contents. This requires 66 // an operator to intervene and resolve the conflict. 67 // Shame on the user for doing concurrent apply. 68 // (Push/Pull) 69 StateChangeConflict 70 ) 71 72 func (sc StateChangeResult) String() string { 73 switch sc { 74 case StateChangeNoop: 75 return "Local and remote state in sync" 76 case StateChangeInit: 77 return "Local state initialized" 78 case StateChangeUpdateLocal: 79 return "Local state updated" 80 case StateChangeUpdateRemote: 81 return "Remote state updated" 82 case StateChangeLocalNewer: 83 return "Local state is newer than remote state, push required" 84 case StateChangeRemoteNewer: 85 return "Remote state is newer than local state, pull required" 86 case StateChangeConflict: 87 return "Local and remote state conflict, manual resolution required" 88 default: 89 return fmt.Sprintf("Unknown state change type: %d", sc) 90 } 91 } 92 93 // SuccessfulPull is used to clasify the StateChangeResult for 94 // a pull operation. This is different by operation, but can be used 95 // to determine a proper exit code. 96 func (sc StateChangeResult) SuccessfulPull() bool { 97 switch sc { 98 case StateChangeNoop: 99 return true 100 case StateChangeInit: 101 return true 102 case StateChangeUpdateLocal: 103 return true 104 case StateChangeLocalNewer: 105 return false 106 case StateChangeConflict: 107 return false 108 default: 109 return false 110 } 111 } 112 113 // SuccessfulPush is used to clasify the StateChangeResult for 114 // a push operation. This is different by operation, but can be used 115 // to determine a proper exit code 116 func (sc StateChangeResult) SuccessfulPush() bool { 117 switch sc { 118 case StateChangeNoop: 119 return true 120 case StateChangeUpdateRemote: 121 return true 122 case StateChangeRemoteNewer: 123 return false 124 case StateChangeConflict: 125 return false 126 default: 127 return false 128 } 129 } 130 131 // EnsureDirectory is used to make sure the local storage 132 // directory exists 133 func EnsureDirectory() error { 134 cwd, err := os.Getwd() 135 if err != nil { 136 return fmt.Errorf("Failed to get current directory: %v", err) 137 } 138 path := filepath.Join(cwd, LocalDirectory) 139 if err := os.Mkdir(path, 0770); err != nil { 140 if os.IsExist(err) { 141 return nil 142 } 143 return fmt.Errorf("Failed to make directory '%s': %v", path, err) 144 } 145 return nil 146 } 147 148 // HiddenStatePath is used to return the path to the hidden state file, 149 // should there be one. 150 // TODO: Rename to LocalStatePath 151 func HiddenStatePath() (string, error) { 152 cwd, err := os.Getwd() 153 if err != nil { 154 return "", fmt.Errorf("Failed to get current directory: %v", err) 155 } 156 path := filepath.Join(cwd, LocalDirectory, HiddenStateFile) 157 return path, nil 158 } 159 160 // HaveLocalState is used to check if we have a local state file 161 func HaveLocalState() (bool, error) { 162 path, err := HiddenStatePath() 163 if err != nil { 164 return false, err 165 } 166 return ExistsFile(path) 167 } 168 169 // ExistsFile is used to check if a given file exists 170 func ExistsFile(path string) (bool, error) { 171 _, err := os.Stat(path) 172 if err == nil { 173 return true, nil 174 } 175 if os.IsNotExist(err) { 176 return false, nil 177 } 178 return false, err 179 } 180 181 // ValidConfig does a purely logical validation of the remote config 182 func ValidConfig(conf *terraform.RemoteState) error { 183 // Default the type to Atlas 184 if conf.Type == "" { 185 conf.Type = "atlas" 186 } 187 _, err := NewClientByState(conf) 188 if err != nil { 189 return err 190 } 191 return nil 192 } 193 194 // ReadLocalState is used to read and parse the local state file 195 func ReadLocalState() (*terraform.State, []byte, error) { 196 path, err := HiddenStatePath() 197 if err != nil { 198 return nil, nil, err 199 } 200 201 // Open the existing file 202 raw, err := ioutil.ReadFile(path) 203 if err != nil { 204 if os.IsNotExist(err) { 205 return nil, nil, nil 206 } 207 return nil, nil, fmt.Errorf("Failed to open state file '%s': %s", path, err) 208 } 209 210 // Decode the state 211 state, err := terraform.ReadState(bytes.NewReader(raw)) 212 if err != nil { 213 return nil, nil, fmt.Errorf("Failed to read state file '%s': %v", path, err) 214 } 215 return state, raw, nil 216 } 217 218 // RefreshState is used to read the remote state given 219 // the configuration for the remote endpoint, and update 220 // the local state if necessary. 221 func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) { 222 if conf == nil { 223 return StateChangeNoop, fmt.Errorf("Missing remote server configuration") 224 } 225 226 // Read the state from the server 227 client, err := NewClientByState(conf) 228 if err != nil { 229 return StateChangeNoop, 230 fmt.Errorf("Failed to create remote client: %v", err) 231 } 232 payload, err := client.GetState() 233 if err != nil { 234 return StateChangeNoop, 235 fmt.Errorf("Failed to read remote state: %v", err) 236 } 237 238 // Parse the remote state 239 var remoteState *terraform.State 240 if payload != nil { 241 remoteState, err = terraform.ReadState(bytes.NewReader(payload.State)) 242 if err != nil { 243 return StateChangeNoop, 244 fmt.Errorf("Failed to parse remote state: %v", err) 245 } 246 247 // Ensure we understand the remote version! 248 if remoteState.Version > terraform.StateVersion { 249 return StateChangeNoop, fmt.Errorf( 250 `Remote state is version %d, this version of Terraform only understands up to %d`, remoteState.Version, terraform.StateVersion) 251 } 252 } 253 254 // Decode the state 255 localState, raw, err := ReadLocalState() 256 if err != nil { 257 return StateChangeNoop, err 258 } 259 260 // We need to handle the matrix of cases in reconciling 261 // the local and remote state. Primarily the concern is 262 // around the Serial number which should grow monotonically. 263 // Additionally, we use the MD5 to detect a conflict for 264 // a given Serial. 265 switch { 266 case remoteState == nil && localState == nil: 267 // Initialize a blank state 268 out, _ := blankState(conf) 269 if err := Persist(bytes.NewReader(out)); err != nil { 270 return StateChangeNoop, 271 fmt.Errorf("Failed to persist state: %v", err) 272 } 273 return StateChangeInit, nil 274 275 case remoteState == nil && localState != nil: 276 // User should probably do a push, nothing to do 277 return StateChangeLocalNewer, nil 278 279 case remoteState != nil && localState == nil: 280 goto PERSIST 281 282 case remoteState.Serial < localState.Serial: 283 // User should probably do a push, nothing to do 284 return StateChangeLocalNewer, nil 285 286 case remoteState.Serial > localState.Serial: 287 goto PERSIST 288 289 case remoteState.Serial == localState.Serial: 290 // Check for a hash collision on the local/remote state 291 localMD5 := md5.Sum(raw) 292 if bytes.Equal(localMD5[:md5.Size], payload.MD5) { 293 // Hash collision, everything is up-to-date 294 return StateChangeNoop, nil 295 } else { 296 // This is very bad. This means we have 2 state files 297 // with the same Serial but a different hash. Most probably 298 // explaination is two parallel apply operations. This 299 // requires a manual reconciliation. 300 return StateChangeConflict, nil 301 } 302 default: 303 // We should not reach this point 304 panic("Unhandled remote update case") 305 } 306 307 PERSIST: 308 // Update the local state from the remote state 309 if err := Persist(bytes.NewReader(payload.State)); err != nil { 310 return StateChangeNoop, 311 fmt.Errorf("Failed to persist state: %v", err) 312 } 313 return StateChangeUpdateLocal, nil 314 } 315 316 // PushState is used to read the local state and 317 // update the remote state if necessary. The state push 318 // can be 'forced' to override any conflict detection 319 // on the server-side. 320 func PushState(conf *terraform.RemoteState, force bool) (StateChangeResult, error) { 321 // Read the local state 322 _, raw, err := ReadLocalState() 323 if err != nil { 324 return StateChangeNoop, err 325 } 326 327 // Check if there is no local state 328 if raw == nil { 329 return StateChangeNoop, fmt.Errorf("No local state to push") 330 } 331 332 // Push the state to the server 333 client, err := NewClientByState(conf) 334 if err != nil { 335 return StateChangeNoop, 336 fmt.Errorf("Failed to create remote client: %v", err) 337 } 338 err = client.PutState(raw, force) 339 340 // Handle the various edge cases 341 switch err { 342 case nil: 343 return StateChangeUpdateRemote, nil 344 case ErrServerNewer: 345 return StateChangeRemoteNewer, nil 346 case ErrConflict: 347 return StateChangeConflict, nil 348 default: 349 return StateChangeNoop, err 350 } 351 } 352 353 // DeleteState is used to delete the remote state given 354 // the configuration for the remote endpoint. 355 func DeleteState(conf *terraform.RemoteState) error { 356 if conf == nil { 357 return fmt.Errorf("Missing remote server configuration") 358 } 359 360 // Setup the client 361 client, err := NewClientByState(conf) 362 if err != nil { 363 return fmt.Errorf("Failed to create remote client: %v", err) 364 } 365 366 // Destroy the state 367 err = client.DeleteState() 368 if err != nil { 369 return fmt.Errorf("Failed to delete remote state: %v", err) 370 } 371 return nil 372 } 373 374 // blankState is used to return a serialized form of a blank state 375 // with only the remote info. 376 func blankState(conf *terraform.RemoteState) ([]byte, error) { 377 blank := terraform.NewState() 378 blank.Remote = conf 379 buf := bytes.NewBuffer(nil) 380 err := terraform.WriteState(blank, buf) 381 return buf.Bytes(), err 382 } 383 384 // PersistState is used to persist out the given terraform state 385 // in our local state cache location. 386 func PersistState(s *terraform.State) error { 387 buf := bytes.NewBuffer(nil) 388 if err := terraform.WriteState(s, buf); err != nil { 389 return fmt.Errorf("Failed to encode state: %v", err) 390 } 391 if err := Persist(buf); err != nil { 392 return err 393 } 394 return nil 395 } 396 397 // Persist is used to write out the state given by a reader (likely 398 // being streamed from a remote server) to the local storage. 399 func Persist(r io.Reader) error { 400 cwd, err := os.Getwd() 401 if err != nil { 402 return fmt.Errorf("Failed to get current directory: %v", err) 403 } 404 statePath := filepath.Join(cwd, LocalDirectory, HiddenStateFile) 405 backupPath := filepath.Join(cwd, LocalDirectory, BackupHiddenStateFile) 406 407 // Backup the old file if it exists 408 if err := CopyFile(statePath, backupPath); err != nil { 409 return fmt.Errorf("Failed to backup state file '%s' to '%s': %v", statePath, backupPath, err) 410 } 411 412 // Open the state path 413 fh, err := os.Create(statePath) 414 if err != nil { 415 return fmt.Errorf("Failed to open state file '%s': %v", statePath, err) 416 } 417 418 // Copy the new state 419 _, err = io.Copy(fh, r) 420 fh.Close() 421 if err != nil { 422 os.Remove(statePath) 423 return fmt.Errorf("Failed to persist state file: %v", err) 424 } 425 return nil 426 } 427 428 // CopyFile is used to copy from a source file if it exists to a destination. 429 // This is used to create a backup of the state file. 430 func CopyFile(src, dst string) error { 431 srcFH, err := os.Open(src) 432 if err != nil { 433 if os.IsNotExist(err) { 434 return nil 435 } 436 return err 437 } 438 defer srcFH.Close() 439 440 dstFH, err := os.Create(dst) 441 if err != nil { 442 return err 443 } 444 defer dstFH.Close() 445 446 _, err = io.Copy(dstFH, srcFH) 447 return err 448 }