Skip to content

Commit 792f8be

Browse files
committed
Fix initial watch sync for existing files
Signed-off-by: trinhchien <[email protected]>
1 parent d518da2 commit 792f8be

2 files changed

Lines changed: 61 additions & 45 deletions

File tree

pkg/compose/watch.go

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti
248248

249249
if shouldInitialSync && isSync(trigger) {
250250
// Need to check initial files are in container that are meant to be synced from watch action
251-
err := s.initialSync(ctx, project, service, trigger, syncer)
251+
err := s.initialSync(ctx, service, trigger, syncer)
252252
if err != nil {
253253
return nil, err
254254
}
@@ -730,7 +730,7 @@ func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, proje
730730

731731
// Walks develop.watch.path and checks which files should be copied inside the container
732732
// ignores develop.watch.ignore, Dockerfile, compose files, bind mounted paths and .git
733-
func (s *composeService) initialSync(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, syncer sync.Syncer) error {
733+
func (s *composeService) initialSync(ctx context.Context, service types.ServiceConfig, trigger types.Trigger, syncer sync.Syncer) error {
734734
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
735735
if err != nil {
736736
return err
@@ -752,26 +752,20 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project
752752
dotGitIgnore,
753753
triggerIgnore)
754754

755-
pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
755+
pathsToCopy, err := s.initialSyncFiles(service, trigger, ignoreInitialSync)
756756
if err != nil {
757757
return err
758758
}
759759

760760
return syncer.Sync(ctx, service.Name, pathsToCopy)
761761
}
762762

763-
// Syncs files from develop.watch.path if thy have been modified after the image has been created
764-
//
765-
//nolint:gocyclo
766-
func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) {
763+
// Syncs files from develop.watch.path, ignoring bind-mounted and excluded paths.
764+
func (s *composeService) initialSyncFiles(service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) {
767765
fi, err := os.Stat(trigger.Path)
768766
if err != nil {
769767
return nil, err
770768
}
771-
timeImageCreated, err := s.imageCreatedTime(ctx, project, service.Name)
772-
if err != nil {
773-
return nil, err
774-
}
775769
var pathsToCopy []*sync.PathMapping
776770
switch mode := fi.Mode(); {
777771
case mode.IsDir():
@@ -793,15 +787,7 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
793787
}
794788
return nil // skip file
795789
}
796-
info, err := d.Info()
797-
if err != nil {
798-
return err
799-
}
800790
if !d.IsDir() {
801-
if info.ModTime().Before(timeImageCreated) {
802-
// skip file if it was modified before image creation
803-
return nil
804-
}
805791
rel, err := filepath.Rel(trigger.Path, path)
806792
if err != nil {
807793
return err
@@ -816,7 +802,7 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
816802
})
817803
case mode.IsRegular():
818804
// process file
819-
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
805+
if !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
820806
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
821807
HostPath: trigger.Path,
822808
ContainerPath: trigger.Target,
@@ -831,28 +817,3 @@ func shouldIgnore(name string, ignore watch.PathMatcher) bool {
831817
// ignore files that match any ignore pattern
832818
return shouldIgnore
833819
}
834-
835-
// gets the image creation time for a service
836-
func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Project, serviceName string) (time.Time, error) {
837-
res, err := s.apiClient().ContainerList(ctx, client.ContainerListOptions{
838-
All: true,
839-
Filters: projectFilter(project.Name).Add("label", serviceFilter(serviceName)),
840-
})
841-
if err != nil {
842-
return time.Now(), err
843-
}
844-
if len(res.Items) == 0 {
845-
return time.Now(), fmt.Errorf("could not get created time for service's image")
846-
}
847-
848-
img, err := s.apiClient().ImageInspect(ctx, res.Items[0].ImageID)
849-
if err != nil {
850-
return time.Now(), err
851-
}
852-
// Need to get the oldest one?
853-
timeCreated, err := time.Parse(time.RFC3339Nano, img.Created)
854-
if err != nil {
855-
return time.Now(), err
856-
}
857-
return timeCreated, nil
858-
}

pkg/compose/watch_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"fmt"
2121
"os"
22+
"path/filepath"
2223
"slices"
2324
"testing"
2425
"time"
@@ -194,3 +195,57 @@ func (f *fakeSyncer) Sync(ctx context.Context, service string, paths []*sync.Pat
194195
f.synced <- paths
195196
return nil
196197
}
198+
199+
func TestInitialSyncFiles_DirectoryIncludesExistingFilesEvenIfOlderThanImage(t *testing.T) {
200+
dir := t.TempDir()
201+
hostDir := filepath.Join(dir, "src")
202+
assert.NilError(t, os.MkdirAll(hostDir, 0o755))
203+
hostFile := filepath.Join(hostDir, "test.txt")
204+
assert.NilError(t, os.WriteFile(hostFile, []byte("hello"), 0o644))
205+
206+
fileTime := time.Now().Add(-24 * time.Hour)
207+
assert.NilError(t, os.Chtimes(hostFile, fileTime, fileTime))
208+
209+
service := composeService{}
210+
trigger := types.Trigger{
211+
Path: hostDir,
212+
Action: types.WatchActionSync,
213+
Target: "/app/src",
214+
InitialSync: true,
215+
}
216+
217+
got, err := service.initialSyncFiles(types.ServiceConfig{Name: "test"}, trigger, watch.EmptyMatcher{})
218+
assert.NilError(t, err)
219+
220+
expected := []*sync.PathMapping{{
221+
HostPath: hostFile,
222+
ContainerPath: "/app/src/test.txt",
223+
}}
224+
assert.DeepEqual(t, expected, got)
225+
}
226+
227+
func TestInitialSyncFiles_FileIncludesExistingFileEvenIfOlderThanImage(t *testing.T) {
228+
dir := t.TempDir()
229+
hostFile := filepath.Join(dir, "test.txt")
230+
assert.NilError(t, os.WriteFile(hostFile, []byte("hello"), 0o644))
231+
232+
fileTime := time.Now().Add(-24 * time.Hour)
233+
assert.NilError(t, os.Chtimes(hostFile, fileTime, fileTime))
234+
235+
service := composeService{}
236+
trigger := types.Trigger{
237+
Path: hostFile,
238+
Action: types.WatchActionSync,
239+
Target: "/app/test.txt",
240+
InitialSync: true,
241+
}
242+
243+
got, err := service.initialSyncFiles(types.ServiceConfig{Name: "test"}, trigger, watch.EmptyMatcher{})
244+
assert.NilError(t, err)
245+
246+
expected := []*sync.PathMapping{{
247+
HostPath: hostFile,
248+
ContainerPath: "/app/test.txt",
249+
}}
250+
assert.DeepEqual(t, expected, got)
251+
}

0 commit comments

Comments
 (0)