|
|
@@ -1,6 +1,6 @@
|
|
|
/*
|
|
|
-
|
|
|
Copyright 2020 Docker Compose CLI authors
|
|
|
+
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
you may not use this file except in compliance with the License.
|
|
|
You may obtain a copy of the License at
|
|
|
@@ -17,13 +17,13 @@ package compose
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
- "io/fs"
|
|
|
- "os"
|
|
|
"path"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
+ "github.com/docker/compose/v2/internal/sync"
|
|
|
+
|
|
|
"github.com/compose-spec/compose-go/types"
|
|
|
"github.com/jonboulle/clockwork"
|
|
|
"github.com/mitchellh/mapstructure"
|
|
|
@@ -54,11 +54,8 @@ type Trigger struct {
|
|
|
|
|
|
const quietPeriod = 2 * time.Second
|
|
|
|
|
|
-// fileMapping contains the Compose service and modified host system path.
|
|
|
-//
|
|
|
-// For file sync, the container path is also included.
|
|
|
-// For rebuild, there is no container path, so it is always empty.
|
|
|
-type fileMapping struct {
|
|
|
+// fileEvent contains the Compose service and modified host system path.
|
|
|
+type fileEvent struct {
|
|
|
// Service that the file event is for.
|
|
|
Service string
|
|
|
// HostPath that was created/modified/deleted outside the container.
|
|
|
@@ -67,17 +64,11 @@ type fileMapping struct {
|
|
|
// - C:\Users\moby\Documents\hello-world\main.go
|
|
|
// - /Users/moby/Documents/hello-world/main.go
|
|
|
HostPath string
|
|
|
- // ContainerPath for the target file inside the container (only populated
|
|
|
- // for sync events, not rebuild).
|
|
|
- //
|
|
|
- // This is the path as used in Docker CLI commands, e.g.
|
|
|
- // - /workdir/main.go
|
|
|
- ContainerPath string
|
|
|
}
|
|
|
|
|
|
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
|
|
|
- needRebuild := make(chan fileMapping)
|
|
|
- needSync := make(chan fileMapping)
|
|
|
+ needRebuild := make(chan fileEvent)
|
|
|
+ needSync := make(chan sync.PathMapping)
|
|
|
|
|
|
_, err := s.prepareProjectForBuild(project, nil)
|
|
|
if err != nil {
|
|
|
@@ -175,7 +166,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
|
|
return eg.Wait()
|
|
|
}
|
|
|
|
|
|
-func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan fileMapping, needRebuild chan fileMapping) error {
|
|
|
+func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan sync.PathMapping, needRebuild chan fileEvent) error {
|
|
|
ignores := make([]watch.PathMatcher, len(triggers))
|
|
|
for i, trigger := range triggers {
|
|
|
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
|
|
|
@@ -209,11 +200,6 @@ WATCH:
|
|
|
|
|
|
fmt.Fprintf(s.stdinfo(), "change detected on %s\n", hostPath)
|
|
|
|
|
|
- f := fileMapping{
|
|
|
- HostPath: hostPath,
|
|
|
- Service: name,
|
|
|
- }
|
|
|
-
|
|
|
switch trigger.Action {
|
|
|
case WatchActionSync:
|
|
|
logrus.Debugf("modified file %s triggered sync", hostPath)
|
|
|
@@ -221,12 +207,18 @@ WATCH:
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- // always use Unix-style paths for inside the container
|
|
|
- f.ContainerPath = path.Join(trigger.Target, rel)
|
|
|
- needSync <- f
|
|
|
+ needSync <- sync.PathMapping{
|
|
|
+ Service: name,
|
|
|
+ HostPath: hostPath,
|
|
|
+ // always use Unix-style paths for inside the container
|
|
|
+ ContainerPath: path.Join(trigger.Target, rel),
|
|
|
+ }
|
|
|
case WatchActionRebuild:
|
|
|
logrus.Debugf("modified file %s requires image to be rebuilt", hostPath)
|
|
|
- needRebuild <- f
|
|
|
+ needRebuild <- fileEvent{
|
|
|
+ HostPath: hostPath,
|
|
|
+ Service: name,
|
|
|
+ }
|
|
|
default:
|
|
|
return fmt.Errorf("watch action %q is not supported", trigger)
|
|
|
}
|
|
|
@@ -304,57 +296,25 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project, needSync <-chan fileMapping) func() error {
|
|
|
+func (s *composeService) makeSyncFn(
|
|
|
+ ctx context.Context,
|
|
|
+ project *types.Project,
|
|
|
+ needSync <-chan sync.PathMapping,
|
|
|
+) func() error {
|
|
|
+ syncer := sync.NewDockerCopy(project.Name, s, s.stdinfo())
|
|
|
+
|
|
|
return func() error {
|
|
|
for {
|
|
|
select {
|
|
|
case <-ctx.Done():
|
|
|
return nil
|
|
|
- case opt := <-needSync:
|
|
|
- service, err := project.GetService(opt.Service)
|
|
|
+ case pathMapping := <-needSync:
|
|
|
+ service, err := project.GetService(pathMapping.Service)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- scale := 1
|
|
|
- if service.Deploy != nil && service.Deploy.Replicas != nil {
|
|
|
- scale = int(*service.Deploy.Replicas)
|
|
|
- }
|
|
|
-
|
|
|
- if fi, statErr := os.Stat(opt.HostPath); statErr == nil {
|
|
|
- if fi.IsDir() {
|
|
|
- for i := 1; i <= scale; i++ {
|
|
|
- _, err := s.Exec(ctx, project.Name, api.RunOptions{
|
|
|
- Service: opt.Service,
|
|
|
- Command: []string{"mkdir", "-p", opt.ContainerPath},
|
|
|
- Index: i,
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- logrus.Warnf("failed to create %q from %s: %v", opt.ContainerPath, opt.Service, err)
|
|
|
- }
|
|
|
- }
|
|
|
- fmt.Fprintf(s.stdinfo(), "%s created\n", opt.ContainerPath)
|
|
|
- } else {
|
|
|
- err := s.Copy(ctx, project.Name, api.CopyOptions{
|
|
|
- Source: opt.HostPath,
|
|
|
- Destination: fmt.Sprintf("%s:%s", opt.Service, opt.ContainerPath),
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- fmt.Fprintf(s.stdinfo(), "%s updated\n", opt.ContainerPath)
|
|
|
- }
|
|
|
- } else if errors.Is(statErr, fs.ErrNotExist) {
|
|
|
- for i := 1; i <= scale; i++ {
|
|
|
- _, err := s.Exec(ctx, project.Name, api.RunOptions{
|
|
|
- Service: opt.Service,
|
|
|
- Command: []string{"rm", "-rf", opt.ContainerPath},
|
|
|
- Index: i,
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
|
|
|
- }
|
|
|
- }
|
|
|
- fmt.Fprintf(s.stdinfo(), "%s deleted from service\n", opt.ContainerPath)
|
|
|
+ if err := syncer.Sync(ctx, service, []sync.PathMapping{pathMapping}); err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -363,7 +323,7 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project,
|
|
|
|
|
|
type rebuildServices map[string]utils.Set[string]
|
|
|
|
|
|
-func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileMapping, fn func(services rebuildServices)) {
|
|
|
+func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileEvent, fn func(services rebuildServices)) {
|
|
|
services := make(rebuildServices)
|
|
|
t := clock.NewTimer(delay)
|
|
|
defer t.Stop()
|