Browse Source

feat(watch): Add --prune option to docker-compose watch command

Signed-off-by: Suleiman Dibirov <[email protected]>
Suleiman Dibirov 1 năm trước cách đây
mục cha
commit
9549a213ba
4 tập tin đã thay đổi với 59 bổ sung3 xóa
  1. 4 1
      cmd/compose/watch.go
  2. 1 0
      pkg/api/api.go
  3. 37 2
      pkg/compose/watch.go
  4. 17 0
      pkg/compose/watch_test.go

+ 4 - 1
cmd/compose/watch.go

@@ -32,7 +32,8 @@ import (
 
 type watchOptions struct {
 	*ProjectOptions
-	noUp bool
+	prune bool
+	noUp  bool
 }
 
 func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
@@ -58,6 +59,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
 	}
 
 	cmd.Flags().BoolVar(&buildOpts.quiet, "quiet", false, "hide build output")
+	cmd.Flags().BoolVar(&watchOpts.prune, "prune", false, "Prune dangling images on rebuild")
 	cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
 	return cmd
 }
@@ -118,5 +120,6 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
 	return backend.Watch(ctx, project, services, api.WatchOptions{
 		Build: &build,
 		LogTo: consumer,
+		Prune: watchOpts.prune,
 	})
 }

+ 1 - 0
pkg/api/api.go

@@ -121,6 +121,7 @@ const WatchLogger = "#watch"
 type WatchOptions struct {
 	Build *BuildOptions
 	LogTo LogConsumer
+	Prune bool
 }
 
 // BuildOptions group options of the Build API

+ 37 - 2
pkg/compose/watch.go

@@ -34,6 +34,8 @@ import (
 	"github.com/docker/compose/v2/pkg/watch"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/image"
 	"github.com/jonboulle/clockwork"
 	"github.com/mitchellh/mapstructure"
 	"github.com/sirupsen/logrus"
@@ -175,7 +177,11 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
 		}
 		watching = true
 		eg.Go(func() error {
-			defer watcher.Close() //nolint:errcheck
+			defer func() {
+				if err := watcher.Close(); err != nil {
+					logrus.Debugf("Error closing watcher for service %s: %v", service.Name, err)
+				}
+			}()
 			return s.watchEvents(ctx, project, service.Name, options, watcher, syncer, config.Watch)
 		})
 	}
@@ -471,11 +477,17 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
 			options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service %q after changes were detected...", serviceName))
 			// restrict the build to ONLY this service, not any of its dependencies
 			options.Build.Services = []string{serviceName}
-			_, err := s.build(ctx, project, *options.Build, nil)
+			imageNameToIdMap, err := s.build(ctx, project, *options.Build, nil)
+
 			if err != nil {
 				options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
 				return err
 			}
+
+			if options.Prune {
+				s.pruneDanglingImagesOnRebuild(ctx, project.Name, imageNameToIdMap)
+			}
+
 			options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service %q successfully built", serviceName))
 
 			err = s.create(ctx, project, api.CreateOptions{
@@ -539,3 +551,26 @@ func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings
 		log.Log(api.WatchLogger, fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings)))
 	}
 }
+
+func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, projectName string, imageNameToIdMap map[string]string) {
+	images, err := s.apiClient().ImageList(ctx, image.ListOptions{
+		Filters: filters.NewArgs(
+			filters.Arg("dangling", "true"),
+			filters.Arg("label", api.ProjectLabel+"="+projectName),
+		),
+	})
+
+	if err != nil {
+		logrus.Debugf("Failed to list images: %v", err)
+		return
+	}
+
+	for _, img := range images {
+		if _, ok := imageNameToIdMap[img.ID]; !ok {
+			_, err := s.apiClient().ImageRemove(ctx, img.ID, image.RemoveOptions{})
+			if err != nil {
+				logrus.Debugf("Failed to remove image %s: %v", img.ID, err)
+			}
+		}
+	}
+}

+ 17 - 0
pkg/compose/watch_test.go

@@ -28,6 +28,8 @@ import (
 	"github.com/docker/compose/v2/pkg/mocks"
 	"github.com/docker/compose/v2/pkg/watch"
 	moby "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/image"
 	"github.com/jonboulle/clockwork"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
@@ -120,12 +122,26 @@ func TestWatch_Sync(t *testing.T) {
 	apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return([]moby.Container{
 		testContainer("test", "123", false),
 	}, nil).AnyTimes()
+	// we expect the image to be pruned
+	apiClient.EXPECT().ImageList(gomock.Any(), image.ListOptions{
+		Filters: filters.NewArgs(
+			filters.Arg("dangling", "true"),
+			filters.Arg("label", api.ProjectLabel+"=myProjectName"),
+		),
+	}).Return([]image.Summary{
+		{ID: "123"},
+		{ID: "456"},
+	}, nil).Times(1)
+	apiClient.EXPECT().ImageRemove(gomock.Any(), "123", image.RemoveOptions{}).Times(1)
+	apiClient.EXPECT().ImageRemove(gomock.Any(), "456", image.RemoveOptions{}).Times(1)
+	//
 	cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 	ctx, cancelFunc := context.WithCancel(context.Background())
 	t.Cleanup(cancelFunc)
 
 	proj := types.Project{
+		Name: "myProjectName",
 		Services: types.Services{
 			"test": {
 				Name: "test",
@@ -148,6 +164,7 @@ func TestWatch_Sync(t *testing.T) {
 		err := service.watchEvents(ctx, &proj, "test", api.WatchOptions{
 			Build: &api.BuildOptions{},
 			LogTo: stdLogger{},
+			Prune: true,
 		}, watcher, syncer, []types.Trigger{
 			{
 				Path:   "/sync",