diff --git a/go.mod b/go.mod index 96fb967f96..b9a8e3be65 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/buger/goterm v1.0.4 - github.com/compose-spec/compose-go/v2 v2.15.1-0.20260918184426-f18e211cbeaf + github.com/compose-spec/compose-go/v2 v2.16.1 github.com/containerd/console v1.0.5 github.com/containerd/containerd/v2 v2.3.5 github.com/containerd/errdefs v1.0.0 @@ -19,7 +19,6 @@ require ( github.com/docker/go-units v0.5.0 github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 github.com/fsnotify/fsevents v0.2.0 - github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.9.0 @@ -77,6 +76,7 @@ require ( github.com/fvbommel/sortorder v1.2.0 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gofrs/flock v0.13.1 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/go.sum b/go.sum index 1d9c2d48b7..650e1ba260 100644 --- a/go.sum +++ b/go.sum @@ -28,8 +28,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/compose-spec/compose-go/v2 v2.15.1-0.20260918184426-f18e211cbeaf h1:ot3ZuEYubwVMNT5sO8/5MONyTkGi+GNxezgUT2Wwf9E= -github.com/compose-spec/compose-go/v2 v2.15.1-0.20260918184426-f18e211cbeaf/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= +github.com/compose-spec/compose-go/v2 v2.16.1 h1:xuEQu32ghB2AK023Beumm//K8bz8u1AHC9P0zKp8jlw= +github.com/compose-spec/compose-go/v2 v2.16.1/go.mod h1:Q1+qtN4vhzEjGrnqRtzx1xa8raDZQlMUe3WJxndYNiQ= github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 220d4ca29e..ec1a924615 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -33,7 +33,6 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/utils" ccli "github.com/docker/cli/cli/command/container" - "github.com/go-viper/mapstructure/v2" "github.com/moby/buildkit/util/progress/progressui" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/image" @@ -206,15 +205,7 @@ func (s *composeService) watch(ctx context.Context, project *types.Project, opti paths []string ) for serviceName, service := range project.Services { - config, err := loadDevelopmentConfig(service, project) - if err != nil { - return nil, err - } - - if service.Develop != nil { - config = service.Develop - } - + config := service.Develop if config == nil { continue } @@ -307,19 +298,10 @@ func (s *composeService) watchTriggerPaths(ctx context.Context, service types.Se return paths, nil } -// initialSyncRequested tells whether a sync trigger requests an initial sync, -// honoring the DEPRECATED x-initialSync extension attribute +// initialSyncRequested tells whether a sync trigger requests an initial sync. +// compose-go promotes the legacy x-initialSync extension into InitialSync at load time. func initialSyncRequested(trigger types.Trigger) bool { - if trigger.InitialSync { - return true - } - var legacyInitialSync bool - success, err := trigger.Extensions.Get("x-initialSync", &legacyInitialSync) - if err == nil && success && legacyInitialSync { - logrus.Warnf("x-initialSync is DEPRECATED, please use the official `initial_sync` attribute\n") - return true - } - return false + return trigger.InitialSync } func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) { @@ -430,47 +412,6 @@ func (s *composeService) watchEvents(ctx context.Context, project *types.Project } } -func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (*types.DevelopConfig, error) { - var config types.DevelopConfig - y, ok := service.Extensions["x-develop"] - if !ok { - return nil, nil - } - logrus.Warnf("x-develop is DEPRECATED, please use the official `develop` attribute") - err := mapstructure.Decode(y, &config) - if err != nil { - return nil, err - } - baseDir, err := filepath.EvalSymlinks(project.WorkingDir) - if err != nil { - return nil, fmt.Errorf("resolving symlink for %q: %w", project.WorkingDir, err) - } - - for i, trigger := range config.Watch { - if !filepath.IsAbs(trigger.Path) { - trigger.Path = filepath.Join(baseDir, trigger.Path) - } - if p, err := filepath.EvalSymlinks(trigger.Path); err == nil { - // this might fail because the path doesn't exist, etc. - trigger.Path = p - } - trigger.Path = filepath.Clean(trigger.Path) - if trigger.Path == "" { - return nil, errors.New("watch rules MUST define a path") - } - - if trigger.Action == types.WatchActionRebuild && service.Build == nil { - return nil, fmt.Errorf("service %s doesn't have a build section, can't apply %s on watch", types.WatchActionRebuild, service.Name) - } - if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 { - return nil, fmt.Errorf("can't watch with action %q on service %s without a command", types.WatchActionSyncExec, service.Name) - } - - config.Watch[i] = trigger - } - return &config, nil -} - func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool { for _, volume := range volumes { if volume.Bind != nil { diff --git a/pkg/e2e/fixtures/watch/x-initialSync.yaml b/pkg/e2e/fixtures/watch/x-initialSync.yaml index 6a954bd6ae..3407edf45d 100644 --- a/pkg/e2e/fixtures/watch/x-initialSync.yaml +++ b/pkg/e2e/fixtures/watch/x-initialSync.yaml @@ -1,15 +1,12 @@ services: test: build: - dockerfile_inline: FROM alpine - command: ping localhost - volumes: - - /data + dockerfile_inline: | + FROM nginx + RUN mkdir /data/ develop: watch: - path: . - target: /data - action: sync+exec - exec: - command: echo "SUCCESS" - x-initialSync: true \ No newline at end of file + target: /data/ + action: sync + x-initialSync: true diff --git a/pkg/e2e/watch_test.go b/pkg/e2e/watch_test.go index 4ecc1b8586..f729133e83 100644 --- a/pkg/e2e/watch_test.go +++ b/pkg/e2e/watch_test.go @@ -550,26 +550,41 @@ func TestWatchIncludes(t *testing.T) { c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9") } -func TestCheckWarningXInitialSyn(t *testing.T) { +// TestWatchLegacyXInitialSync checks that the deprecated x-initialSync extension +// still triggers an initial sync, now that compose-go promotes it to the +// official initial_sync attribute at load time instead of leaving it as an +// extension for compose to interpret. A file already present before `up --watch` +// starts must show up in the container without any subsequent filesystem event. +func TestWatchLegacyXInitialSync(t *testing.T) { c := NewCLI(t) - const projectName = "test_watch_warn_initial_syn" + const projectName = "test_watch_legacy_initial_sync" defer c.cleanupWithDown(t, projectName) tmpdir := t.TempDir() composeFilePath := filepath.Join(tmpdir, "compose.yaml") CopyFile(t, filepath.Join("fixtures", "watch", "x-initialSync.yaml"), composeFilePath) - cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "--verbose", "up", "--watch") + assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "preexisting.txt"), []byte("preexisting"), 0o600)) + + cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch") buffer := bytes.NewBuffer(nil) cmd.Stdout = buffer watch := icmd.StartCmd(cmd) poll.WaitOn(t, func(l poll.LogT) poll.Result { - if strings.Contains(watch.Combined(), "x-initialSync is DEPRECATED, please use the official `initial_sync` attribute") { + if strings.Contains(watch.Stdout(), "Attaching to ") { return poll.Success() } return poll.Continue("%v", watch.Stdout()) }) + poll.WaitOn(t, func(l poll.LogT) poll.Result { + cat := c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "exec", "test", "cat", "/data/preexisting.txt") + if strings.Contains(cat.Stdout(), "preexisting") { + return poll.Success() + } + return poll.Continue("%v", cat.Combined()) + }) + c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9") }