github.com/koding/terraform@v0.6.4-0.20170608090606-5d7e0339779d/state/local.go (about) 1 package state 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "sync" 12 "time" 13 14 multierror "github.com/hashicorp/go-multierror" 15 "github.com/hashicorp/terraform/terraform" 16 ) 17 18 // LocalState manages a state storage that is local to the filesystem. 19 type LocalState struct { 20 mu sync.Mutex 21 22 // Path is the path to read the state from. PathOut is the path to 23 // write the state to. If PathOut is not specified, Path will be used. 24 // If PathOut already exists, it will be overwritten. 25 Path string 26 PathOut string 27 28 // the file handle corresponding to PathOut 29 stateFileOut *os.File 30 31 // While the stateFileOut will correspond to the lock directly, 32 // store and check the lock ID to maintain a strict state.Locker 33 // implementation. 34 lockID string 35 36 // created is set to true if stateFileOut didn't exist before we created it. 37 // This is mostly so we can clean up emtpy files during tests, but doesn't 38 // hurt to remove file we never wrote to. 39 created bool 40 41 state *terraform.State 42 readState *terraform.State 43 written bool 44 } 45 46 // SetState will force a specific state in-memory for this local state. 47 func (s *LocalState) SetState(state *terraform.State) { 48 s.mu.Lock() 49 defer s.mu.Unlock() 50 51 s.state = state 52 s.readState = state 53 } 54 55 // StateReader impl. 56 func (s *LocalState) State() *terraform.State { 57 return s.state.DeepCopy() 58 } 59 60 // WriteState for LocalState always persists the state as well. 61 // TODO: this should use a more robust method of writing state, by first 62 // writing to a temp file on the same filesystem, and renaming the file over 63 // the original. 64 // 65 // StateWriter impl. 66 func (s *LocalState) WriteState(state *terraform.State) error { 67 s.mu.Lock() 68 defer s.mu.Unlock() 69 70 if s.stateFileOut == nil { 71 if err := s.createStateFiles(); err != nil { 72 return nil 73 } 74 } 75 defer s.stateFileOut.Sync() 76 77 s.state = state 78 79 if _, err := s.stateFileOut.Seek(0, os.SEEK_SET); err != nil { 80 return err 81 } 82 if err := s.stateFileOut.Truncate(0); err != nil { 83 return err 84 } 85 86 if state == nil { 87 // if we have no state, don't write anything else. 88 return nil 89 } 90 91 s.state.IncrementSerialMaybe(s.readState) 92 s.readState = s.state 93 94 if err := terraform.WriteState(s.state, s.stateFileOut); err != nil { 95 return err 96 } 97 98 s.written = true 99 return nil 100 } 101 102 // PersistState for LocalState is a no-op since WriteState always persists. 103 // 104 // StatePersister impl. 105 func (s *LocalState) PersistState() error { 106 return nil 107 } 108 109 // StateRefresher impl. 110 func (s *LocalState) RefreshState() error { 111 s.mu.Lock() 112 defer s.mu.Unlock() 113 114 var reader io.Reader 115 if !s.written { 116 // we haven't written a state file yet, so load from Path 117 f, err := os.Open(s.Path) 118 if err != nil { 119 // It is okay if the file doesn't exist, we treat that as a nil state 120 if !os.IsNotExist(err) { 121 return err 122 } 123 124 // we need a non-nil reader for ReadState and an empty buffer works 125 // to return EOF immediately 126 reader = bytes.NewBuffer(nil) 127 128 } else { 129 defer f.Close() 130 reader = f 131 } 132 } else { 133 // no state to refresh 134 if s.stateFileOut == nil { 135 return nil 136 } 137 138 // we have a state file, make sure we're at the start 139 s.stateFileOut.Seek(0, os.SEEK_SET) 140 reader = s.stateFileOut 141 } 142 143 state, err := terraform.ReadState(reader) 144 // if there's no state we just assign the nil return value 145 if err != nil && err != terraform.ErrNoState { 146 return err 147 } 148 149 s.state = state 150 s.readState = state 151 return nil 152 } 153 154 // Lock implements a local filesystem state.Locker. 155 func (s *LocalState) Lock(info *LockInfo) (string, error) { 156 s.mu.Lock() 157 defer s.mu.Unlock() 158 159 if s.stateFileOut == nil { 160 if err := s.createStateFiles(); err != nil { 161 return "", err 162 } 163 } 164 165 if s.lockID != "" { 166 return "", fmt.Errorf("state %q already locked", s.stateFileOut.Name()) 167 } 168 169 if err := s.lock(); err != nil { 170 info, infoErr := s.lockInfo() 171 if infoErr != nil { 172 err = multierror.Append(err, infoErr) 173 } 174 175 lockErr := &LockError{ 176 Info: info, 177 Err: err, 178 } 179 180 return "", lockErr 181 } 182 183 s.lockID = info.ID 184 return s.lockID, s.writeLockInfo(info) 185 } 186 187 func (s *LocalState) Unlock(id string) error { 188 s.mu.Lock() 189 defer s.mu.Unlock() 190 191 if s.lockID == "" { 192 return fmt.Errorf("LocalState not locked") 193 } 194 195 if id != s.lockID { 196 idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) 197 info, err := s.lockInfo() 198 if err != nil { 199 err = multierror.Append(idErr, err) 200 } 201 202 return &LockError{ 203 Err: idErr, 204 Info: info, 205 } 206 } 207 208 os.Remove(s.lockInfoPath()) 209 210 fileName := s.stateFileOut.Name() 211 212 unlockErr := s.unlock() 213 214 s.stateFileOut.Close() 215 s.stateFileOut = nil 216 s.lockID = "" 217 218 // clean up the state file if we created it an never wrote to it 219 stat, err := os.Stat(fileName) 220 if err == nil && stat.Size() == 0 && s.created { 221 os.Remove(fileName) 222 } 223 224 return unlockErr 225 } 226 227 // Open the state file, creating the directories and file as needed. 228 func (s *LocalState) createStateFiles() error { 229 if s.PathOut == "" { 230 s.PathOut = s.Path 231 } 232 233 // yes this could race, but we only use it to clean up empty files 234 if _, err := os.Stat(s.PathOut); os.IsNotExist(err) { 235 s.created = true 236 } 237 238 // Create all the directories 239 if err := os.MkdirAll(filepath.Dir(s.PathOut), 0755); err != nil { 240 return err 241 } 242 243 f, err := os.OpenFile(s.PathOut, os.O_RDWR|os.O_CREATE, 0666) 244 if err != nil { 245 return err 246 } 247 248 s.stateFileOut = f 249 return nil 250 } 251 252 // return the path for the lockInfo metadata. 253 func (s *LocalState) lockInfoPath() string { 254 stateDir, stateName := filepath.Split(s.Path) 255 if stateName == "" { 256 panic("empty state file path") 257 } 258 259 if stateName[0] == '.' { 260 stateName = stateName[1:] 261 } 262 263 return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName)) 264 } 265 266 // lockInfo returns the data in a lock info file 267 func (s *LocalState) lockInfo() (*LockInfo, error) { 268 path := s.lockInfoPath() 269 infoData, err := ioutil.ReadFile(path) 270 if err != nil { 271 return nil, err 272 } 273 274 info := LockInfo{} 275 err = json.Unmarshal(infoData, &info) 276 if err != nil { 277 return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.Path, err) 278 } 279 return &info, nil 280 } 281 282 // write a new lock info file 283 func (s *LocalState) writeLockInfo(info *LockInfo) error { 284 path := s.lockInfoPath() 285 info.Path = s.Path 286 info.Created = time.Now().UTC() 287 288 err := ioutil.WriteFile(path, info.Marshal(), 0600) 289 if err != nil { 290 return fmt.Errorf("could not write lock info for %q: %s", s.Path, err) 291 } 292 return nil 293 }