diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go index 0af3650ede5..07f0813b23e 100644 --- a/pkg/compose/watch.go +++ b/pkg/compose/watch.go @@ -138,6 +138,10 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, opti return wait() } +func selectWatchServices(project *types.Project, services []string) (*types.Project, error) { + return project.WithSelectedServices(services, types.IgnoreDependencies) +} + type watchRule struct { types.Trigger include watch.PathMatcher @@ -188,7 +192,7 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping { func (s *composeService) watch(ctx context.Context, project *types.Project, options api.WatchOptions) (func() error, error) { //nolint: gocyclo var err error - if project, err = project.WithSelectedServices(options.Services); err != nil { + if project, err = selectWatchServices(project, options.Services); err != nil { return nil, err } syncer, err := s.getSyncImplementation(project) @@ -636,6 +640,7 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services)) // restrict the build to ONLY this service, not any of its dependencies options.Build.Services = services + options.Build.Deps = false options.Build.Progress = string(progressui.PlainMode) options.Build.Out = cutils.GetWriter(func(line string) { options.LogTo.Log(api.WatchLogger, line) diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go index 0c59b884ba6..18357b93ae2 100644 --- a/pkg/compose/watch_test.go +++ b/pkg/compose/watch_test.go @@ -180,6 +180,37 @@ func TestWatch_Sync(t *testing.T) { // TODO: there's not a great way to assert that the rebuild attempt happened } +func TestSelectWatchServicesIgnoresDependencies(t *testing.T) { + project := &types.Project{ + Name: "myProjectName", + Services: types.Services{ + "backend": { + Name: "backend", + }, + "stats": { + Name: "stats", + DependsOn: types.DependsOnConfig{ + "backend": { + Condition: types.ServiceConditionStarted, + Restart: true, + Required: true, + }, + }, + }, + }, + } + + selected, err := selectWatchServices(project, []string{"stats"}) + assert.NilError(t, err) + + _, ok := selected.Services["stats"] + assert.Assert(t, ok) + _, ok = selected.Services["backend"] + assert.Assert(t, !ok) + assert.Assert(t, len(selected.Services["stats"].DependsOn) == 0) + assert.Assert(t, len(project.Services["stats"].DependsOn) != 0) +} + type fakeSyncer struct { synced chan []*sync.PathMapping } diff --git a/pkg/e2e/fixtures/watch/rebuild-deps.yaml b/pkg/e2e/fixtures/watch/rebuild-deps.yaml new file mode 100644 index 00000000000..f32a8e9e72e --- /dev/null +++ b/pkg/e2e/fixtures/watch/rebuild-deps.yaml @@ -0,0 +1,19 @@ +services: + backend: + build: + dockerfile_inline: | + FROM nginx + RUN mkdir /data + COPY backend /data/backend + frontend: + build: + dockerfile_inline: | + FROM nginx + RUN mkdir /data + COPY frontend /data/frontend + depends_on: + - backend + develop: + watch: + - path: frontend + action: rebuild diff --git a/pkg/e2e/watch_test.go b/pkg/e2e/watch_test.go index 1716be05d03..7b55681909d 100644 --- a/pkg/e2e/watch_test.go +++ b/pkg/e2e/watch_test.go @@ -368,6 +368,61 @@ func TestWatchMultiServices(t *testing.T) { c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9") } +func TestWatchRebuildIgnoresDependencies(t *testing.T) { + c := NewCLI(t) + const projectName = "test_watch_rebuild_deps" + + defer c.cleanupWithDown(t, projectName) + + tmpdir := t.TempDir() + composeFilePath := filepath.Join(tmpdir, "compose.yaml") + CopyFile(t, filepath.Join("fixtures", "watch", "rebuild-deps.yaml"), composeFilePath) + + for _, svc := range []string{"backend", "frontend"} { + assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, svc), []byte("v1"), 0o600)) + } + + cmd := c.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--build", "--watch") + buffer := bytes.NewBuffer(nil) + cmd.Stdout = buffer + watch := icmd.StartCmd(cmd) + t.Cleanup(func() { + if watch.Cmd.Process != nil { + _ = watch.Cmd.Process.Kill() + } + }) + + poll.WaitOn(t, func(l poll.LogT) poll.Result { + if strings.Contains(watch.Stdout(), "Attaching to ") { + return poll.Success() + } + return poll.Continue("%v", watch.Stdout()) + }, poll.WithTimeout(90*time.Second)) + + containerID := func(service string) string { + res := c.RunDockerComposeCmd(t, "-p", projectName, "ps", "-q", service) + return strings.TrimSpace(res.Stdout()) + } + backendID := containerID("backend") + assert.Assert(t, backendID != "") + + t.Log("editing frontend code only") + assert.NilError(t, os.WriteFile(filepath.Join(tmpdir, "frontend"), []byte("v2"), 0o600)) + + poll.WaitOn(t, func(l poll.LogT) poll.Result { + cat := c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "exec", "frontend", "cat", "/data/frontend") + if strings.Contains(cat.Stdout(), "v2") { + return poll.Success() + } + return poll.Continue("%v", cat.Combined()) + }, poll.WithTimeout(90*time.Second)) + + t.Log("backend must not be rebuilt nor recreated") + assert.Equal(t, backendID, containerID("backend")) + + c.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9") +} + func TestWatchIncludes(t *testing.T) { c := NewCLI(t) const projectName = "test_watch_includes"