Skip to content

Commit dadc635

Browse files
committed
fix: allow single file syncing
1 parent e443359 commit dadc635

7 files changed

Lines changed: 170 additions & 39 deletions

File tree

e2e/tests/sync/sync.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,4 +695,84 @@ var _ = DevSpaceDescribe("sync", func() {
695695
// wait for the command to finish
696696
waitGroup.Wait()
697697
})
698+
699+
ginkgo.It("should sync single file to a container", func() {
700+
tempDir, err := framework.CopyToTempDir("tests/sync/testdata/sync-single-file")
701+
framework.ExpectNoError(err)
702+
defer framework.CleanupTempDir(initialDir, tempDir)
703+
704+
ns, err := kubeClient.CreateNamespace("sync")
705+
framework.ExpectNoError(err)
706+
defer func() {
707+
err := kubeClient.DeleteNamespace(ns)
708+
framework.ExpectNoError(err)
709+
}()
710+
711+
// deploy app to sync
712+
deployCmd := &cmd.RunPipelineCmd{
713+
GlobalFlags: &flags.GlobalFlags{
714+
NoWarn: true,
715+
Namespace: ns,
716+
ConfigPath: "single.yaml",
717+
},
718+
Pipeline: "deploy",
719+
}
720+
err = deployCmd.RunDefault(f)
721+
framework.ExpectNoError(err)
722+
723+
cancelCtx, stop := context.WithCancel(context.Background())
724+
defer stop()
725+
726+
// sync command
727+
syncCmd := &cmd.SyncCmd{
728+
GlobalFlags: &flags.GlobalFlags{
729+
NoWarn: true,
730+
Namespace: ns,
731+
ConfigPath: "single.yaml",
732+
},
733+
Wait: true,
734+
Ctx: cancelCtx,
735+
}
736+
737+
// start the command
738+
waitGroup := sync.WaitGroup{}
739+
waitGroup.Add(1)
740+
go func() {
741+
defer ginkgo.GinkgoRecover()
742+
defer waitGroup.Done()
743+
err = syncCmd.Run(f)
744+
framework.ExpectNoError(err)
745+
}()
746+
747+
// check that uploadExcludePaths folder was not synced
748+
framework.ExpectRemoteFileContents("alpine", ns, "/watch/test.txt", "Hello World")
749+
framework.ExpectRemoteFileNotFound("alpine", ns, "/watch/single.yaml")
750+
751+
// write a file and check that it got synced
752+
payload := randutil.GenerateRandomString(10000)
753+
err = ioutil.WriteFile(filepath.Join(tempDir, "other-folder", "test.txt"), []byte(payload), 0666)
754+
framework.ExpectNoError(err)
755+
err = ioutil.WriteFile(filepath.Join(tempDir, "other-folder", "test123.txt"), []byte(payload), 0666)
756+
framework.ExpectNoError(err)
757+
framework.ExpectRemoteFileContents("alpine", ns, "/watch/test.txt", payload)
758+
framework.ExpectRemoteFileNotFound("alpine", ns, "/watch/test123.txt")
759+
760+
// check that file is not created but updated
761+
_, err = kubeClient.ExecByImageSelector("alpine", ns, []string{
762+
"sh", "-c", "echo -n 'Hello World' > /watch/test.test",
763+
})
764+
framework.ExpectNoError(err)
765+
_, err = kubeClient.ExecByImageSelector("alpine", ns, []string{
766+
"sh", "-c", "echo -n 'Hello DevSpace' > /watch/test.txt",
767+
})
768+
framework.ExpectNoError(err)
769+
framework.ExpectLocalFileContents(filepath.Join(tempDir, "other-folder", "test.txt"), "Hello DevSpace")
770+
framework.ExpectLocalFileNotFound(filepath.Join(tempDir, "other-folder", "test.test"))
771+
772+
// stop command
773+
stop()
774+
775+
// wait for the command to finish
776+
waitGroup.Wait()
777+
})
698778
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Hello World
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: v2beta1
2+
vars:
3+
IMAGE: alpine
4+
deployments:
5+
test:
6+
helm:
7+
values:
8+
containers:
9+
- image: ${IMAGE}
10+
command: ["sleep"]
11+
args: ["999999999999"]
12+
dev:
13+
sync:
14+
imageSelector: ${IMAGE}
15+
sync:
16+
- path: other-folder/test.txt:/watch/test.txt

helper/cmd/sync/downstream.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ type DownstreamCmd struct {
1313

1414
Throttle int64
1515

16-
Polling bool
16+
Polling bool
17+
RecursiveWatch bool
1718
}
1819

1920
// NewDownstreamCmd creates a new downstream command
@@ -29,6 +30,7 @@ func NewDownstreamCmd() *cobra.Command {
2930
downstreamCmd.Flags().StringSliceVar(&cmd.Exclude, "exclude", []string{}, "The exclude paths for downstream watching")
3031
downstreamCmd.Flags().Int64Var(&cmd.Throttle, "throttle", 5, "The amount of milliseconds to throttle change detection per 100 files")
3132
downstreamCmd.Flags().BoolVar(&cmd.Polling, "polling", false, "If true, DevSpace will use polling instead of inotify")
33+
downstreamCmd.Flags().BoolVar(&cmd.RecursiveWatch, "recursive-watch", true, "If false, DevSpace will not watch recursively")
3234
return downstreamCmd
3335
}
3436

@@ -43,9 +45,10 @@ func (cmd *DownstreamCmd) Run(cobraCmd *cobra.Command, args []string) error {
4345
RemotePath: absolutePath,
4446
ExcludePaths: cmd.Exclude,
4547

46-
Throttle: cmd.Throttle,
47-
Polling: cmd.Polling,
48-
ExitOnClose: true,
49-
Ping: true,
48+
Throttle: cmd.Throttle,
49+
Polling: cmd.Polling,
50+
ExitOnClose: true,
51+
NoRecursiveWatch: !cmd.RecursiveWatch,
52+
Ping: true,
5053
})
5154
}

helper/server/downstream.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ const rescanPeriod = time.Minute * 15
3131

3232
// DownstreamOptions holds the options for the downstream server
3333
type DownstreamOptions struct {
34-
RemotePath string
35-
ExcludePaths []string
36-
ExitOnClose bool
37-
Throttle int64
34+
RemotePath string
35+
ExcludePaths []string
36+
ExitOnClose bool
37+
NoRecursiveWatch bool
38+
Throttle int64
3839

3940
Polling bool
4041
Ping bool
@@ -78,7 +79,12 @@ func StartDownstreamServer(reader io.Reader, writer io.Writer, options *Downstre
7879

7980
go func() {
8081
// set up a watchpoint listening for events within a directory tree rooted at specified directory
81-
err := notify.WatchWithFilter(options.RemotePath+"/...", downStream.events, func(s string) bool {
82+
watchPath := options.RemotePath + "/..."
83+
if options.NoRecursiveWatch {
84+
watchPath = options.RemotePath
85+
}
86+
87+
err := notify.WatchWithFilter(watchPath, downStream.events, func(s string) bool {
8288
if ignoreMatcher == nil || ignoreMatcher.RequireFullScan() {
8389
return false
8490
}
@@ -229,7 +235,7 @@ func (d *Downstream) ChangesCount(context.Context, *remote.Empty) (*remote.Chang
229235

230236
// Walk through the dir
231237
if d.options.Polling {
232-
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, throttle)
238+
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, d.options.NoRecursiveWatch, throttle)
233239
}
234240

235241
changeAmount := int64(0)
@@ -272,7 +278,7 @@ func (d *Downstream) getWatchState() map[string]*remote.Change {
272278
d.lastRescan = &now
273279

274280
newState := make(map[string]*remote.Change)
275-
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, 0)
281+
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, d.options.NoRecursiveWatch, 0)
276282
return newState
277283
} else if changeAmount == 0 {
278284
return nil
@@ -305,7 +311,7 @@ func (d *Downstream) Changes(empty *remote.Empty, stream remote.Downstream_Chang
305311
if !d.options.Polling {
306312
newState = d.getWatchState()
307313
} else {
308-
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, throttle)
314+
walkDir(d.options.RemotePath, d.options.RemotePath, d.ignoreMatcher, newState, d.options.NoRecursiveWatch, throttle)
309315
}
310316

311317
if newState != nil {
@@ -384,7 +390,7 @@ func (d *Downstream) applyChange(newState map[string]*remote.Change, fullPath st
384390
}
385391
}
386392

387-
walkDir(d.options.RemotePath, fullPath, d.ignoreMatcher, newState, time.Duration(d.options.Throttle)*time.Millisecond)
393+
walkDir(d.options.RemotePath, fullPath, d.ignoreMatcher, newState, d.options.NoRecursiveWatch, time.Duration(d.options.Throttle)*time.Millisecond)
388394
} else {
389395
if d.ignoreMatcher == nil || !d.ignoreMatcher.RequireFullScan() || !d.ignoreMatcher.Matches(relativePath, false) {
390396
newState[fullPath] = &remote.Change{
@@ -525,7 +531,7 @@ func streamChanges(basePath string, oldState map[string]*remote.Change, newState
525531
return changeAmount, nil
526532
}
527533

528-
func walkDir(basePath string, path string, ignoreMatcher ignoreparser.IgnoreParser, state map[string]*remote.Change, throttle time.Duration) {
534+
func walkDir(basePath string, path string, ignoreMatcher ignoreparser.IgnoreParser, state map[string]*remote.Change, noRecursive bool, throttle time.Duration) {
529535
files, err := ioutil.ReadDir(path)
530536
if err != nil {
531537
// We ignore errors here
@@ -566,7 +572,9 @@ func walkDir(basePath string, path string, ignoreMatcher ignoreparser.IgnorePars
566572
}
567573
}
568574

569-
walkDir(basePath, absolutePath, ignoreMatcher, state, throttle)
575+
if !noRecursive {
576+
walkDir(basePath, absolutePath, ignoreMatcher, state, noRecursive, throttle)
577+
}
570578
} else {
571579
// Check if not ignored
572580
if ignoreMatcher == nil || !ignoreMatcher.RequireFullScan() || !ignoreMatcher.Matches(absolutePath[len(basePath):], false) {

pkg/devspace/services/sync/controller.go

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/mgutz/ansi"
1010
"io"
1111
"os"
12+
"path"
1213
"path/filepath"
1314
"runtime"
1415
"strings"
@@ -329,24 +330,6 @@ func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch,
329330
// make sure we resolve it correctly
330331
localPath = ctx.ResolvePath(localPath)
331332

332-
// check if local path exists
333-
_, err = os.Stat(localPath)
334-
if err != nil {
335-
if !os.IsNotExist(err) {
336-
return nil, err
337-
}
338-
339-
err = os.MkdirAll(localPath, os.ModePerm)
340-
if err != nil {
341-
return nil, err
342-
}
343-
}
344-
345-
err = inject.InjectDevSpaceHelper(ctx.Context(), ctx.KubeClient(), pod, container, arch, customLog)
346-
if err != nil {
347-
return nil, err
348-
}
349-
350333
upstreamDisabled := syncConfig.DisableUpload
351334
downstreamDisabled := syncConfig.DisableDownload
352335
compareBy := latest.InitialSyncCompareByMTime
@@ -368,6 +351,36 @@ func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch,
368351
},
369352
}
370353

354+
if len(syncConfig.ExcludePaths) > 0 {
355+
options.ExcludePaths = syncConfig.ExcludePaths
356+
}
357+
358+
// check if local path exists
359+
stat, err := os.Stat(localPath)
360+
if err != nil {
361+
if !os.IsNotExist(err) {
362+
return nil, err
363+
}
364+
365+
err = os.MkdirAll(localPath, os.ModePerm)
366+
if err != nil {
367+
return nil, err
368+
}
369+
} else if !stat.IsDir() {
370+
if path.Base(filepath.ToSlash(localPath)) != path.Base(containerPath) {
371+
return nil, fmt.Errorf("if you want to sync a single file, make sure the filename matches on the local and container path. E.g.: local-path/my-file.txt:remote-path/my-file.txt")
372+
}
373+
374+
fileName := path.Base(localPath)
375+
localPath = path.Dir(localPath)
376+
containerPath = path.Dir(containerPath)
377+
options.ExcludePaths = []string{
378+
"**",
379+
"!/" + fileName,
380+
}
381+
options.NoRecursiveWatch = true
382+
}
383+
371384
// Initialize log
372385
if options.Log == nil {
373386
options.Log = logpkg.GetFileLogger("sync")
@@ -378,8 +391,10 @@ func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch,
378391
options.Exec = syncConfig.OnUpload.Exec
379392
}
380393

381-
if len(syncConfig.ExcludePaths) > 0 {
382-
options.ExcludePaths = syncConfig.ExcludePaths
394+
// inject devspace helper
395+
err = inject.InjectDevSpaceHelper(ctx.Context(), ctx.KubeClient(), pod, container, arch, customLog)
396+
if err != nil {
397+
return nil, err
383398
}
384399

385400
if syncConfig.ExcludeFile != "" {
@@ -494,6 +509,9 @@ func (c *controller) initClient(ctx devspacecontext.Context, pod *v1.Pod, arch,
494509
for _, exclude := range options.ExcludePaths {
495510
downstreamArgs = append(downstreamArgs, "--exclude", exclude)
496511
}
512+
if options.NoRecursiveWatch {
513+
downstreamArgs = append(downstreamArgs, "--recursive-watch=false")
514+
}
497515
downstreamArgs = append(downstreamArgs, containerPath)
498516

499517
downStdinReader, downStdinWriter := io.Pipe()

pkg/devspace/sync/sync.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const waitForMoreChangesTimeout = time.Minute
2525

2626
// Options holds the sync options
2727
type Options struct {
28-
Polling bool
28+
Polling bool
29+
NoRecursiveWatch bool
2930

3031
Exec []latest.SyncExec
3132

@@ -85,7 +86,7 @@ type Sync struct {
8586
readyChan chan bool
8687
}
8788

88-
// NewSync creates a new sync for the given
89+
// NewSync creates a new sync for the given local path
8990
func NewSync(ctx context.Context, localPath string, options Options) (*Sync, error) {
9091
cancelCtx, cancel := context.WithCancel(ctx)
9192

@@ -233,7 +234,11 @@ func (s *Sync) startUpstream() {
233234
s.tree = notify.NewTree()
234235

235236
// Set up a watchpoint listening for events within a directory tree rooted at specified directory
236-
err := s.tree.Watch(s.LocalPath+"/...", s.upstream.events, func(path string) bool {
237+
watchPath := s.LocalPath + "/..."
238+
if s.Options.NoRecursiveWatch {
239+
watchPath = s.LocalPath
240+
}
241+
err := s.tree.Watch(watchPath, s.upstream.events, func(path string) bool {
237242
if s.ignoreMatcher == nil || s.ignoreMatcher.RequireFullScan() {
238243
return false
239244
}

0 commit comments

Comments
 (0)