Skip to content

Commit 36dd263

Browse files
Fix inotify path translation for different mountPoint
This change fixes the issue where inotify events are not properly detected when mount location and mountPoint differ. Fixes #3871 Signed-off-by: Tatsuya Hayashino <tatsuya.hayashino@gmail.com>
1 parent 3cb4858 commit 36dd263

File tree

2 files changed

+163
-7
lines changed

2 files changed

+163
-7
lines changed

pkg/hostagent/inotify.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import (
2020
const CacheSize = 10000
2121

2222
var (
23-
inotifyCache = make(map[string]int64)
24-
mountSymlinks = make(map[string]string)
23+
inotifyCache = make(map[string]int64)
24+
mountSymlinks = make(map[string]string)
25+
mountLocations = make(map[string]string)
2526
)
2627

2728
func (a *HostAgent) startInotify(ctx context.Context) error {
@@ -54,11 +55,8 @@ func (a *HostAgent) startInotify(ctx context.Context) error {
5455
continue
5556
}
5657

57-
for k, v := range mountSymlinks {
58-
if strings.HasPrefix(watchPath, k) {
59-
watchPath = strings.ReplaceAll(watchPath, k, v)
60-
}
61-
}
58+
watchPath = translateToGuestPath(watchPath, mountSymlinks, mountLocations)
59+
6260
utcTimestamp := timestamppb.New(stat.ModTime().UTC())
6361
event := &guestagentapi.Inotify{MountPath: watchPath, Time: utcTimestamp}
6462
err = inotifyClient.Send(event)
@@ -81,6 +79,9 @@ func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {
8179
if m.Location != symlink {
8280
mountSymlinks[symlink] = m.Location
8381
}
82+
if m.MountPoint != nil && m.Location != *m.MountPoint {
83+
mountLocations[m.Location] = *m.MountPoint
84+
}
8485

8586
logrus.Infof("enable inotify for writable mount: %s", m.Location)
8687
err = notify.Watch(path.Join(m.Location, "..."), events, GetNotifyEvent())
@@ -91,6 +92,24 @@ func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error {
9192
return nil
9293
}
9394

95+
func translateToGuestPath(hostPath string, symlinks, locations map[string]string) string {
96+
result := hostPath
97+
98+
for symlink, original := range symlinks {
99+
if strings.HasPrefix(result, symlink) {
100+
result = strings.ReplaceAll(result, symlink, original)
101+
}
102+
}
103+
104+
for location, mountPoint := range locations {
105+
if suffix, ok := strings.CutPrefix(result, location); ok {
106+
return mountPoint + suffix
107+
}
108+
}
109+
110+
return result
111+
}
112+
94113
func filterEvents(event notify.EventInfo, stat os.FileInfo) bool {
95114
currTime := stat.ModTime()
96115
eventPath := event.Path()

pkg/hostagent/inotify_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package hostagent
5+
6+
import (
7+
"testing"
8+
9+
"gotest.tools/v3/assert"
10+
)
11+
12+
func TestTranslateToGuestPath(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
hostPath string
16+
symlinks map[string]string
17+
locations map[string]string
18+
expected string
19+
}{
20+
{
21+
name: "no translation needed - empty maps",
22+
hostPath: "/Users/user/file.txt",
23+
symlinks: map[string]string{},
24+
locations: map[string]string{},
25+
expected: "/Users/user/file.txt",
26+
},
27+
{
28+
name: "no translation needed - location equals mountPoint",
29+
hostPath: "/Users/user/project/file.txt",
30+
symlinks: map[string]string{},
31+
locations: map[string]string{"/Users/user": "/Users/user"},
32+
expected: "/Users/user/project/file.txt",
33+
},
34+
{
35+
name: "translate location to different mountPoint",
36+
hostPath: "/Users/user/source/file.txt",
37+
symlinks: map[string]string{},
38+
locations: map[string]string{"/Users/user/source": "/mnt/dest"},
39+
expected: "/mnt/dest/file.txt",
40+
},
41+
{
42+
name: "translate location to different mountPoint - nested path",
43+
hostPath: "/Users/user/source/subdir/deep/file.txt",
44+
symlinks: map[string]string{},
45+
locations: map[string]string{"/Users/user/source": "/mnt/dest"},
46+
expected: "/mnt/dest/subdir/deep/file.txt",
47+
},
48+
{
49+
name: "translate location to different mountPoint - root file",
50+
hostPath: "/Users/user/source/file.txt",
51+
symlinks: map[string]string{},
52+
locations: map[string]string{"/Users/user/source": "/mnt/dest"},
53+
expected: "/mnt/dest/file.txt",
54+
},
55+
{
56+
name: "symlink resolution only",
57+
hostPath: "/private/tmp/file.txt",
58+
symlinks: map[string]string{"/private/tmp": "/tmp"},
59+
locations: map[string]string{},
60+
expected: "/tmp/file.txt",
61+
},
62+
{
63+
name: "symlink resolution with location translation",
64+
hostPath: "/private/var/folders/source/file.txt",
65+
symlinks: map[string]string{"/private/var": "/var"},
66+
locations: map[string]string{"/var/folders/source": "/mnt/dest"},
67+
expected: "/mnt/dest/file.txt",
68+
},
69+
{
70+
name: "more specific location matches",
71+
hostPath: "/Users/user/source/file.txt",
72+
symlinks: map[string]string{},
73+
locations: map[string]string{"/Users/user/source": "/mnt/source"},
74+
expected: "/mnt/source/file.txt",
75+
},
76+
{
77+
name: "less specific location matches when more specific not present",
78+
hostPath: "/Users/user/other/file.txt",
79+
symlinks: map[string]string{},
80+
locations: map[string]string{"/Users/user": "/mnt/home"},
81+
expected: "/mnt/home/other/file.txt",
82+
},
83+
{
84+
name: "path not matching any location",
85+
hostPath: "/other/path/file.txt",
86+
symlinks: map[string]string{},
87+
locations: map[string]string{"/Users/user/source": "/mnt/dest"},
88+
expected: "/other/path/file.txt",
89+
},
90+
{
91+
name: "exact location match - file at mount root",
92+
hostPath: "/Users/user/source",
93+
symlinks: map[string]string{},
94+
locations: map[string]string{"/Users/user/source": "/mnt/dest"},
95+
expected: "/mnt/dest",
96+
},
97+
{
98+
name: "multiple locations - non-overlapping",
99+
hostPath: "/Users/user/project/file.txt",
100+
symlinks: map[string]string{},
101+
locations: map[string]string{
102+
"/Users/user/project": "/mnt/project",
103+
"/Users/user/other": "/mnt/other",
104+
},
105+
expected: "/mnt/project/file.txt",
106+
},
107+
{
108+
name: "multiple symlinks",
109+
hostPath: "/private/var/tmp/file.txt",
110+
symlinks: map[string]string{
111+
"/private/var": "/var",
112+
"/private/tmp": "/tmp",
113+
},
114+
locations: map[string]string{},
115+
expected: "/var/tmp/file.txt",
116+
},
117+
{
118+
name: "multiple locations and symlinks combined",
119+
hostPath: "/private/var/folders/source/file.txt",
120+
symlinks: map[string]string{
121+
"/private/var": "/var",
122+
},
123+
locations: map[string]string{
124+
"/var/folders/source": "/mnt/dest1",
125+
"/tmp/test": "/mnt/dest2",
126+
},
127+
expected: "/mnt/dest1/file.txt",
128+
},
129+
}
130+
131+
for _, tc := range tests {
132+
t.Run(tc.name, func(t *testing.T) {
133+
result := translateToGuestPath(tc.hostPath, tc.symlinks, tc.locations)
134+
assert.Equal(t, result, tc.expected)
135+
})
136+
}
137+
}

0 commit comments

Comments
 (0)