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