Skip to content

Commit ccc895b

Browse files
authored
fixes to extra-record file watcher (#2298)
* Fix excess error message during writes Fixes #2290 Signed-off-by: Kristoffer Dalby <[email protected]> * retry filewatcher on removed files This should handled if files are deleted and added again, and for rename scenarios. Fixes #2289 Signed-off-by: Kristoffer Dalby <[email protected]> * test more write and remove in filewatcher Signed-off-by: Kristoffer Dalby <[email protected]> --------- Signed-off-by: Kristoffer Dalby <[email protected]>
1 parent 5345f19 commit ccc895b

File tree

2 files changed

+120
-11
lines changed

2 files changed

+120
-11
lines changed

hscontrol/dns/extrarecords.go

+44-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"sync"
99

10+
"github.com/cenkalti/backoff/v4"
1011
"github.com/fsnotify/fsnotify"
1112
"github.com/rs/zerolog/log"
1213
"tailscale.com/tailcfg"
@@ -83,12 +84,39 @@ func (e *ExtraRecordsMan) Run() {
8384
log.Error().Caller().Msgf("file watcher event channel closing")
8485
return
8586
}
86-
87-
log.Trace().Caller().Str("path", event.Name).Str("op", event.Op.String()).Msg("extra records received filewatch event")
88-
if event.Name != e.path {
89-
continue
87+
switch event.Op {
88+
case fsnotify.Create, fsnotify.Write, fsnotify.Chmod:
89+
log.Trace().Caller().Str("path", event.Name).Str("op", event.Op.String()).Msg("extra records received filewatch event")
90+
if event.Name != e.path {
91+
continue
92+
}
93+
e.updateRecords()
94+
95+
// If a file is removed or renamed, fsnotify will loose track of it
96+
// and not watch it. We will therefore attempt to re-add it with a backoff.
97+
case fsnotify.Remove, fsnotify.Rename:
98+
err := backoff.Retry(func() error {
99+
if _, err := os.Stat(e.path); err != nil {
100+
return err
101+
}
102+
103+
return nil
104+
}, backoff.NewExponentialBackOff())
105+
106+
if err != nil {
107+
log.Error().Caller().Err(err).Msgf("extra records filewatcher retrying to find file after delete")
108+
continue
109+
}
110+
111+
err = e.watcher.Add(e.path)
112+
if err != nil {
113+
log.Error().Caller().Err(err).Msgf("extra records filewatcher re-adding file after delete failed, giving up.")
114+
return
115+
} else {
116+
log.Trace().Caller().Str("path", e.path).Msg("extra records file re-added after delete")
117+
e.updateRecords()
118+
}
90119
}
91-
e.updateRecords()
92120

93121
case err, ok := <-e.watcher.Errors:
94122
if !ok {
@@ -116,6 +144,11 @@ func (e *ExtraRecordsMan) updateRecords() {
116144
return
117145
}
118146

147+
// If there are no records, ignore the update.
148+
if records == nil {
149+
return
150+
}
151+
119152
e.mu.Lock()
120153
defer e.mu.Unlock()
121154

@@ -143,6 +176,12 @@ func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error
143176
return nil, [32]byte{}, fmt.Errorf("reading path: %s, err: %w", path, err)
144177
}
145178

179+
// If the read was triggered too fast, and the file is not complete, ignore the update
180+
// if the file is empty. A consecutive update will be triggered when the file is complete.
181+
if len(b) == 0 {
182+
return nil, [32]byte{}, nil
183+
}
184+
146185
var records []tailcfg.DNSRecord
147186
err = json.Unmarshal(b, &records)
148187
if err != nil {

integration/dns_test.go

+76-6
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,34 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
146146
assertCommandOutputContains(t, client, []string{"dig", "test.myvpn.example.com"}, "6.6.6.6")
147147
}
148148

149+
hs, err := scenario.Headscale()
150+
assertNoErr(t, err)
151+
152+
// Write the file directly into place from the docker API.
153+
b0, _ := json.Marshal([]tailcfg.DNSRecord{
154+
{
155+
Name: "docker.myvpn.example.com",
156+
Type: "A",
157+
Value: "2.2.2.2",
158+
},
159+
})
160+
161+
err = hs.WriteFile(erPath, b0)
162+
assertNoErr(t, err)
163+
164+
for _, client := range allClients {
165+
assertCommandOutputContains(t, client, []string{"dig", "docker.myvpn.example.com"}, "2.2.2.2")
166+
}
167+
168+
// Write a new file and move it to the path to ensure the reload
169+
// works when a file is moved atomically into place.
149170
extraRecords = append(extraRecords, tailcfg.DNSRecord{
150171
Name: "otherrecord.myvpn.example.com",
151172
Type: "A",
152173
Value: "7.7.7.7",
153174
})
154175
b2, _ := json.Marshal(extraRecords)
155176

156-
hs, err := scenario.Headscale()
157-
assertNoErr(t, err)
158-
159-
// Write it to a separate file to ensure Docker's API doesnt
160-
// do anything unexpected and rather move it into place to trigger
161-
// a reload.
162177
err = hs.WriteFile(erPath+"2", b2)
163178
assertNoErr(t, err)
164179
_, err = hs.Execute([]string{"mv", erPath + "2", erPath})
@@ -168,6 +183,61 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
168183
assertCommandOutputContains(t, client, []string{"dig", "test.myvpn.example.com"}, "6.6.6.6")
169184
assertCommandOutputContains(t, client, []string{"dig", "otherrecord.myvpn.example.com"}, "7.7.7.7")
170185
}
186+
187+
// Write a new file and copy it to the path to ensure the reload
188+
// works when a file is copied into place.
189+
b3, _ := json.Marshal([]tailcfg.DNSRecord{
190+
{
191+
Name: "copy.myvpn.example.com",
192+
Type: "A",
193+
Value: "8.8.8.8",
194+
},
195+
})
196+
197+
err = hs.WriteFile(erPath+"3", b3)
198+
assertNoErr(t, err)
199+
_, err = hs.Execute([]string{"cp", erPath + "3", erPath})
200+
assertNoErr(t, err)
201+
202+
for _, client := range allClients {
203+
assertCommandOutputContains(t, client, []string{"dig", "copy.myvpn.example.com"}, "8.8.8.8")
204+
}
205+
206+
// Write in place to ensure pipe like behaviour works
207+
b4, _ := json.Marshal([]tailcfg.DNSRecord{
208+
{
209+
Name: "docker.myvpn.example.com",
210+
Type: "A",
211+
Value: "9.9.9.9",
212+
},
213+
})
214+
command := []string{"echo", fmt.Sprintf("'%s'", string(b4)), ">", erPath}
215+
_, err = hs.Execute([]string{"bash", "-c", strings.Join(command, " ")})
216+
assertNoErr(t, err)
217+
218+
for _, client := range allClients {
219+
assertCommandOutputContains(t, client, []string{"dig", "docker.myvpn.example.com"}, "9.9.9.9")
220+
}
221+
222+
// Delete the file and create a new one to ensure it is picked up again.
223+
_, err = hs.Execute([]string{"rm", erPath})
224+
assertNoErr(t, err)
225+
226+
time.Sleep(2 * time.Second)
227+
228+
// The same paths should still be available as it is not cleared on delete.
229+
for _, client := range allClients {
230+
assertCommandOutputContains(t, client, []string{"dig", "docker.myvpn.example.com"}, "9.9.9.9")
231+
}
232+
233+
// Write a new file, the backoff mechanism should make the filewatcher pick it up
234+
// again.
235+
err = hs.WriteFile(erPath, b3)
236+
assertNoErr(t, err)
237+
238+
for _, client := range allClients {
239+
assertCommandOutputContains(t, client, []string{"dig", "copy.myvpn.example.com"}, "8.8.8.8")
240+
}
171241
}
172242

173243
// TestValidateResolvConf validates that the resolv.conf file

0 commit comments

Comments
 (0)