feat(watch): Add --prune option to docker-compose watch command
Signed-off-by: Suleiman Dibirov <idsulik@gmail.com>
This commit is contained in:
parent
da434013e3
commit
9549a213ba
@ -32,7 +32,8 @@ import (
|
|||||||
|
|
||||||
type watchOptions struct {
|
type watchOptions struct {
|
||||||
*ProjectOptions
|
*ProjectOptions
|
||||||
noUp bool
|
prune bool
|
||||||
|
noUp bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
|
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(&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")
|
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
|
||||||
return cmd
|
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{
|
return backend.Watch(ctx, project, services, api.WatchOptions{
|
||||||
Build: &build,
|
Build: &build,
|
||||||
LogTo: consumer,
|
LogTo: consumer,
|
||||||
|
Prune: watchOpts.prune,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,7 @@ const WatchLogger = "#watch"
|
|||||||
type WatchOptions struct {
|
type WatchOptions struct {
|
||||||
Build *BuildOptions
|
Build *BuildOptions
|
||||||
LogTo LogConsumer
|
LogTo LogConsumer
|
||||||
|
Prune bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildOptions group options of the Build API
|
// BuildOptions group options of the Build API
|
||||||
|
@ -34,6 +34,8 @@ import (
|
|||||||
"github.com/docker/compose/v2/pkg/watch"
|
"github.com/docker/compose/v2/pkg/watch"
|
||||||
moby "github.com/docker/docker/api/types"
|
moby "github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"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/jonboulle/clockwork"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -175,7 +177,11 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
|
|||||||
}
|
}
|
||||||
watching = true
|
watching = true
|
||||||
eg.Go(func() error {
|
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)
|
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))
|
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
|
// restrict the build to ONLY this service, not any of its dependencies
|
||||||
options.Build.Services = []string{serviceName}
|
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 {
|
if err != nil {
|
||||||
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
|
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if options.Prune {
|
||||||
|
s.pruneDanglingImagesOnRebuild(ctx, project.Name, imageNameToIdMap)
|
||||||
|
}
|
||||||
|
|
||||||
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service %q successfully built", serviceName))
|
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service %q successfully built", serviceName))
|
||||||
|
|
||||||
err = s.create(ctx, project, api.CreateOptions{
|
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)))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,6 +28,8 @@ import (
|
|||||||
"github.com/docker/compose/v2/pkg/mocks"
|
"github.com/docker/compose/v2/pkg/mocks"
|
||||||
"github.com/docker/compose/v2/pkg/watch"
|
"github.com/docker/compose/v2/pkg/watch"
|
||||||
moby "github.com/docker/docker/api/types"
|
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/jonboulle/clockwork"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/mock/gomock"
|
"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{
|
apiClient.EXPECT().ContainerList(gomock.Any(), gomock.Any()).Return([]moby.Container{
|
||||||
testContainer("test", "123", false),
|
testContainer("test", "123", false),
|
||||||
}, nil).AnyTimes()
|
}, 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()
|
cli.EXPECT().Client().Return(apiClient).AnyTimes()
|
||||||
|
|
||||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
t.Cleanup(cancelFunc)
|
t.Cleanup(cancelFunc)
|
||||||
|
|
||||||
proj := types.Project{
|
proj := types.Project{
|
||||||
|
Name: "myProjectName",
|
||||||
Services: types.Services{
|
Services: types.Services{
|
||||||
"test": {
|
"test": {
|
||||||
Name: "test",
|
Name: "test",
|
||||||
@ -148,6 +164,7 @@ func TestWatch_Sync(t *testing.T) {
|
|||||||
err := service.watchEvents(ctx, &proj, "test", api.WatchOptions{
|
err := service.watchEvents(ctx, &proj, "test", api.WatchOptions{
|
||||||
Build: &api.BuildOptions{},
|
Build: &api.BuildOptions{},
|
||||||
LogTo: stdLogger{},
|
LogTo: stdLogger{},
|
||||||
|
Prune: true,
|
||||||
}, watcher, syncer, []types.Trigger{
|
}, watcher, syncer, []types.Trigger{
|
||||||
{
|
{
|
||||||
Path: "/sync",
|
Path: "/sync",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user