diff --git a/pkg/hostagent/inotify.go b/pkg/hostagent/inotify.go index 4a9fec00b95..3971b0cf4cb 100644 --- a/pkg/hostagent/inotify.go +++ b/pkg/hostagent/inotify.go @@ -20,8 +20,9 @@ import ( const CacheSize = 10000 var ( - inotifyCache = make(map[string]int64) - mountSymlinks = make(map[string]string) + inotifyCache = make(map[string]int64) + mountSymlinks = make(map[string]string) + mountLocations = make(map[string]string) ) func (a *HostAgent) startInotify(ctx context.Context) error { @@ -54,11 +55,8 @@ func (a *HostAgent) startInotify(ctx context.Context) error { continue } - for k, v := range mountSymlinks { - if strings.HasPrefix(watchPath, k) { - watchPath = strings.ReplaceAll(watchPath, k, v) - } - } + watchPath = translateToGuestPath(watchPath, mountSymlinks, mountLocations) + utcTimestamp := timestamppb.New(stat.ModTime().UTC()) event := &guestagentapi.Inotify{MountPath: watchPath, Time: utcTimestamp} err = inotifyClient.Send(event) @@ -81,6 +79,9 @@ func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error { if m.Location != symlink { mountSymlinks[symlink] = m.Location } + if m.MountPoint != nil && m.Location != *m.MountPoint { + mountLocations[m.Location] = *m.MountPoint + } logrus.Infof("enable inotify for writable mount: %s", m.Location) err = notify.Watch(path.Join(m.Location, "..."), events, GetNotifyEvent()) @@ -91,6 +92,24 @@ func (a *HostAgent) setupWatchers(events chan notify.EventInfo) error { return nil } +func translateToGuestPath(hostPath string, symlinks, locations map[string]string) string { + result := hostPath + + for symlink, original := range symlinks { + if strings.HasPrefix(result, symlink) { + result = strings.ReplaceAll(result, symlink, original) + } + } + + for location, mountPoint := range locations { + if suffix, ok := strings.CutPrefix(result, location); ok { + return mountPoint + suffix + } + } + + return result +} + func filterEvents(event notify.EventInfo, stat os.FileInfo) bool { currTime := stat.ModTime() eventPath := event.Path() diff --git a/pkg/hostagent/inotify_test.go b/pkg/hostagent/inotify_test.go new file mode 100644 index 00000000000..6adf0353261 --- /dev/null +++ b/pkg/hostagent/inotify_test.go @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package hostagent + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestTranslateToGuestPath(t *testing.T) { + tests := []struct { + name string + hostPath string + symlinks map[string]string + locations map[string]string + expected string + }{ + { + name: "no translation needed - empty maps", + hostPath: "/Users/user/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{}, + expected: "/Users/user/file.txt", + }, + { + name: "no translation needed - location equals mountPoint", + hostPath: "/Users/user/project/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user": "/Users/user"}, + expected: "/Users/user/project/file.txt", + }, + { + name: "translate location to different mountPoint", + hostPath: "/Users/user/source/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/dest"}, + expected: "/mnt/dest/file.txt", + }, + { + name: "translate location to different mountPoint - nested path", + hostPath: "/Users/user/source/subdir/deep/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/dest"}, + expected: "/mnt/dest/subdir/deep/file.txt", + }, + { + name: "translate location to different mountPoint - root file", + hostPath: "/Users/user/source/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/dest"}, + expected: "/mnt/dest/file.txt", + }, + { + name: "symlink resolution only", + hostPath: "/private/tmp/file.txt", + symlinks: map[string]string{"/private/tmp": "/tmp"}, + locations: map[string]string{}, + expected: "/tmp/file.txt", + }, + { + name: "symlink resolution with location translation", + hostPath: "/private/var/folders/source/file.txt", + symlinks: map[string]string{"/private/var": "/var"}, + locations: map[string]string{"/var/folders/source": "/mnt/dest"}, + expected: "/mnt/dest/file.txt", + }, + { + name: "more specific location matches", + hostPath: "/Users/user/source/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/source"}, + expected: "/mnt/source/file.txt", + }, + { + name: "less specific location matches when more specific not present", + hostPath: "/Users/user/other/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user": "/mnt/home"}, + expected: "/mnt/home/other/file.txt", + }, + { + name: "path not matching any location", + hostPath: "/other/path/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/dest"}, + expected: "/other/path/file.txt", + }, + { + name: "exact location match - file at mount root", + hostPath: "/Users/user/source", + symlinks: map[string]string{}, + locations: map[string]string{"/Users/user/source": "/mnt/dest"}, + expected: "/mnt/dest", + }, + { + name: "multiple locations - non-overlapping", + hostPath: "/Users/user/project/file.txt", + symlinks: map[string]string{}, + locations: map[string]string{ + "/Users/user/project": "/mnt/project", + "/Users/user/other": "/mnt/other", + }, + expected: "/mnt/project/file.txt", + }, + { + name: "multiple symlinks", + hostPath: "/private/var/tmp/file.txt", + symlinks: map[string]string{ + "/private/var": "/var", + "/private/tmp": "/tmp", + }, + locations: map[string]string{}, + expected: "/var/tmp/file.txt", + }, + { + name: "multiple locations and symlinks combined", + hostPath: "/private/var/folders/source/file.txt", + symlinks: map[string]string{ + "/private/var": "/var", + }, + locations: map[string]string{ + "/var/folders/source": "/mnt/dest1", + "/tmp/test": "/mnt/dest2", + }, + expected: "/mnt/dest1/file.txt", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := translateToGuestPath(tc.hostPath, tc.symlinks, tc.locations) + assert.Equal(t, result, tc.expected) + }) + } +}