Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit 4d3a3c4

Browse files
kaufersDavid Chung
authored and
David Chung
committed
Check for recent file deltas before running tf apply (#760)
Signed-off-by: Steven Kaufer <kaufer@us.ibm.com>
1 parent 9f0bdd5 commit 4d3a3c4

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed

pkg/provider/terraform/instance/apply.go

+74
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package instance
22

33
import (
44
"bufio"
5+
"fmt"
56
"io"
67
"io/ioutil"
78
"os"
@@ -60,6 +61,21 @@ func (p *plugin) terraformApply() error {
6061
// that multiple .tf.json.new files have time to be created
6162
if initial {
6263
time.Sleep(time.Second * 5)
64+
// And only run if there have been no file deltas in the last few seconds, the delta
65+
// processing ignores files that are more then 30 seconds in the future so this
66+
// should never wait indefinately but, to be safe, only wait for no deltas for at most
67+
// 30 seconds
68+
for i := 0; i < 30; i++ {
69+
hasDelta, err := p.hasRecentDeltas(3)
70+
if hasDelta {
71+
time.Sleep(time.Second * 1)
72+
continue
73+
}
74+
if err != nil {
75+
log.Errorf("Failed to determine file deltas: %v", err)
76+
}
77+
break
78+
}
6379
}
6480
if err := p.handleFiles(fns); err == nil {
6581
if err = p.doTerraformApply(); err == nil {
@@ -136,6 +152,64 @@ type tfFuncs struct {
136152
tfStateList func() (map[TResourceType]map[TResourceName]struct{}, error)
137153
}
138154

155+
// hasRecentDeltas returns true if any tf.json[.new] files have been changed in
156+
// in the last "window" seconds
157+
func (p *plugin) hasRecentDeltas(window int) (bool, error) {
158+
p.fsLock.Lock()
159+
defer p.fsLock.Unlock()
160+
161+
now := time.Now()
162+
modTime := time.Time{}
163+
fs := &afero.Afero{Fs: p.fs}
164+
err := fs.Walk(p.Dir,
165+
func(path string, info os.FileInfo, err error) error {
166+
if err != nil {
167+
log.Debugf("Ignoring file %s due to error: %s", path, err)
168+
return nil
169+
}
170+
if m := tfFileRegex.FindStringSubmatch(info.Name()); len(m) == 3 {
171+
if info.ModTime().After(now) {
172+
// The file timestamp is in the future, this is fine if we are within 30 seconds but
173+
// it should be ignored if it's further out (if there is a file with a timestamp
174+
// that's a full day ahead we'd never process terraform until the local time catches
175+
// up -- this should never happen but we should handle it)
176+
if info.ModTime().After(now.Add(time.Duration(30) * time.Second)) {
177+
log.Errorf(fmt.Sprintf(
178+
"Terraform file %v has been updated in the future, ignoring timestamp in delta check (delta=%v)",
179+
info.Name(),
180+
now.Sub(info.ModTime())),
181+
)
182+
return nil
183+
}
184+
}
185+
if modTime.Before(info.ModTime()) {
186+
modTime = info.ModTime()
187+
}
188+
}
189+
return nil
190+
},
191+
)
192+
if err != nil {
193+
return false, err
194+
}
195+
if !modTime.IsZero() {
196+
if modTime.After(now.Add(-(time.Duration(window) * time.Second))) {
197+
log.Infof(fmt.Sprintf(
198+
"Terraform file updates are within %v seconds (delta=%v)",
199+
window,
200+
now.Sub(modTime)),
201+
)
202+
return true, nil
203+
}
204+
log.Infof(fmt.Sprintf(
205+
"Terraform file updates are outside of %v seconds (delta=%v)",
206+
window,
207+
now.Sub(modTime)),
208+
)
209+
}
210+
return false, nil
211+
}
212+
139213
// handleFiles handles resource pruning and new resources via:
140214
// 1. Acquire file system lock
141215
// 2. Execute "terraform refresh" to refresh state

pkg/provider/terraform/instance/apply_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,96 @@ func TestHandleFilesNoFiles(t *testing.T) {
180180
require.NoError(t, err)
181181
}
182182

183+
func TestHasRecentDelta(t *testing.T) {
184+
tf, dir := getPlugin(t)
185+
defer os.RemoveAll(dir)
186+
187+
// No files (so no deltas)
188+
hasDelta, err := tf.hasRecentDeltas(60)
189+
require.NoError(t, err)
190+
require.False(t, hasDelta)
191+
192+
// Non tf.json[.new] file (should be ignored)
193+
err = afero.WriteFile(tf.fs, filepath.Join(tf.Dir, "foo.txt"), []byte("some-text"), 0644)
194+
require.NoError(t, err)
195+
hasDelta, err = tf.hasRecentDeltas(60)
196+
require.NoError(t, err)
197+
require.False(t, hasDelta)
198+
199+
// Write out a tf.json file
200+
info := fileInfo{
201+
ResInfo: []resInfo{
202+
{
203+
ResType: VMIBMCloud,
204+
ResName: TResourceName("instance-12345"),
205+
},
206+
},
207+
NewFile: false,
208+
Plugin: tf,
209+
}
210+
writeFile(info, t)
211+
212+
// File delta in the last 5 seconds
213+
hasDelta, err = tf.hasRecentDeltas(5)
214+
require.NoError(t, err)
215+
require.True(t, hasDelta)
216+
217+
// Wait 2 seconds, now the file will not a delta in a 1 second window
218+
time.Sleep(2 * time.Second)
219+
hasDelta, err = tf.hasRecentDeltas(1)
220+
require.NoError(t, err)
221+
require.False(t, hasDelta)
222+
223+
// But not if we ask for deltas in a longer window
224+
hasDelta, err = tf.hasRecentDeltas(60)
225+
require.NoError(t, err)
226+
require.True(t, hasDelta)
227+
228+
// Add another, should be a delta
229+
info.ResInfo[0].ResName = TResourceName("instance-12346")
230+
writeFile(info, t)
231+
hasDelta, err = tf.hasRecentDeltas(1)
232+
require.NoError(t, err)
233+
require.True(t, hasDelta)
234+
}
235+
236+
func TestHasRecentDeltaInFuture(t *testing.T) {
237+
tf, dir := getPlugin(t)
238+
defer os.RemoveAll(dir)
239+
240+
// Write out a tf.json file
241+
info := fileInfo{
242+
ResInfo: []resInfo{
243+
{
244+
ResType: VMIBMCloud,
245+
ResName: TResourceName("instance-12345"),
246+
},
247+
},
248+
NewFile: false,
249+
Plugin: tf,
250+
}
251+
writeFile(info, t)
252+
253+
// Update the timestamp to 29 seconds in the future
254+
path := filepath.Join(tf.Dir, "instance-12345.tf.json")
255+
newTime := time.Now().Add(time.Duration(29) * time.Second)
256+
err := tf.fs.Chtimes(path, newTime, newTime)
257+
require.NoError(t, err)
258+
259+
// Since it's less than 30 seconds it will be a delta
260+
hasDelta, err := tf.hasRecentDeltas(1)
261+
require.NoError(t, err)
262+
require.True(t, hasDelta)
263+
264+
// More than 30 seconds will be ignored
265+
newTime = time.Now().Add(time.Duration(35) * time.Second)
266+
err = tf.fs.Chtimes(path, newTime, newTime)
267+
require.NoError(t, err)
268+
hasDelta, err = tf.hasRecentDeltas(1)
269+
require.NoError(t, err)
270+
require.False(t, hasDelta)
271+
}
272+
183273
func TestHandleFilesNoPruneNoNewFiles(t *testing.T) {
184274
tf, dir := getPlugin(t)
185275
defer os.RemoveAll(dir)

0 commit comments

Comments
 (0)