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