Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce compose watch to rebuild/restart service on change to build context #9449

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command {
pullCommand(&opts, backend),
createCommand(&opts, backend),
copyCommand(&opts, backend),
watchCommand(&opts, backend),
)
command.Flags().SetInterspersed(false)
opts.addProjectFlags(command.Flags())
Expand Down
61 changes: 61 additions & 0 deletions cmd/compose/watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
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

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"context"

"github.com/spf13/cobra"

"github.com/docker/compose/v2/pkg/api"
)

type watchOptions struct {
*projectOptions
quiet bool
}

func watchCommand(p *projectOptions, backend api.Service) *cobra.Command {
opts := watchOptions{
projectOptions: p,
}
cmd := &cobra.Command{
Use: "watch [SERVICES...]",
Short: "Watch build context for service(s) and rebuild/refresh containers when files are updated",
PreRunE: Adapt(func(ctx context.Context, args []string) error {
return nil
}),
RunE: Adapt(func(ctx context.Context, args []string) error {
return runWatch(ctx, backend, opts, args)
}),
ValidArgsFunction: serviceCompletion(p),
}

cmd.Flags().BoolVar(&opts.quiet, "quiet", false, "hide build output")
return cmd
}

func runWatch(ctx context.Context, backend api.Service, opts watchOptions, services []string) error {
project, err := opts.toProject(services)
if err != nil {
return err
}

return backend.Watch(ctx, project, api.WatchOptions{
Quiet: opts.quiet,
})
}
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0
github.com/fsnotify/fsnotify v1.5.1
github.com/golang/mock v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-version v1.3.0
Expand All @@ -35,7 +36,7 @@ require (
github.com/theupdateframework/notary v0.6.1
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.1.0
gotest.tools/v3 v3.2.0
)

require (
Expand Down Expand Up @@ -78,7 +79,7 @@ require (
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/signal v0.6.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
Expand Down Expand Up @@ -133,6 +134,7 @@ require (
)

replace (
github.com/compose-spec/compose-go => /Users/nicolas/go/src/github.com/compose-spec/compose-go
github.com/docker/cli => github.com/docker/cli v20.10.3-0.20220309205733-2b52f62e9627+incompatible
github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220309172631-83b51522df43+incompatible

Expand Down
16 changes: 6 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,6 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h
github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/compose-spec/compose-go v1.0.8/go.mod h1:REnCbBugoIdHB7S1sfkN/aJ7AJpNApGNjNiVjA9L8x4=
github.com/compose-spec/compose-go v1.2.4 h1:nzTFqM8+2J7Veao5Pq5U451thinv3U1wChIvcjX59/A=
github.com/compose-spec/compose-go v1.2.4/go.mod h1:pAy7Mikpeft4pxkFU565/DRHEbDfR84G6AQuiL+Hdg8=
github.com/compose-spec/godotenv v1.1.1/go.mod h1:zF/3BOa18Z24tts5qnO/E9YURQanJTBUf7nlcCTNsyc=
github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
Expand Down Expand Up @@ -544,8 +540,9 @@ github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
Expand Down Expand Up @@ -1032,9 +1029,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ=
github.com/moby/buildkit v0.10.0-rc2.0.20220308185020-fdecd0ae108b h1:plbnJxjht8Z6D3c/ga79D1+VaA/IUfNVp08J3lcDgI8=
Expand Down Expand Up @@ -2148,8 +2144,8 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=
gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ type Service interface {
Port(ctx context.Context, projectName string, service string, port int, options PortOptions) (string, int, error)
// Images executes the equivalent of a `compose images`
Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
// Watch services' build context and rebuild/restart image on changes
Watch(ctx context.Context, project *types.Project, options WatchOptions) error
}

// WatchOptions group options of the Watch API
type WatchOptions struct {
Quiet bool
}

// BuildOptions group options of the Build API
Expand Down
10 changes: 10 additions & 0 deletions pkg/api/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type ServiceProxy struct {
EventsFn func(ctx context.Context, project string, options EventsOptions) error
PortFn func(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error)
ImagesFn func(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error)
WatchFn func(ctx context.Context, project *types.Project, options WatchOptions) error
interceptors []Interceptor
}

Expand Down Expand Up @@ -87,6 +88,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy {
s.EventsFn = service.Events
s.PortFn = service.Port
s.ImagesFn = service.Images
s.WatchFn = service.Watch
return s
}

Expand Down Expand Up @@ -308,3 +310,11 @@ func (s *ServiceProxy) Images(ctx context.Context, project string, options Image
}
return s.ImagesFn(ctx, project, options)
}

// Watch implements Service interface
func (s *ServiceProxy) Watch(ctx context.Context, project *types.Project, options WatchOptions) error {
if s.WatchFn == nil {
return ErrNotImplemented
}
return s.WatchFn(ctx, project, options)
}
6 changes: 5 additions & 1 deletion pkg/compose/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
}
}

if options.Quiet {
options.Progress = xprogress.PrinterModeQuiet
}

_, err = s.doBuild(ctx, project, opts, options.Progress)
if err == nil {
if len(imagesToBuild) > 0 && !options.Quiet {
Expand Down Expand Up @@ -291,7 +295,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
Platforms: plats,
Labels: service.Build.Labels,
NetworkMode: service.Build.Network,
ExtraHosts: service.Build.ExtraHosts,
ExtraHosts: service.Build.ExtraHosts.AsList(),
Session: sessionConfig,
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
DNS: service.DNS,
DNSSearch: service.DNSSearch,
DNSOptions: service.DNSOpts,
ExtraHosts: service.ExtraHosts,
ExtraHosts: service.ExtraHosts.AsList(),
SecurityOpt: securityOpts,
UsernsMode: container.UsernsMode(service.UserNSMode),
Privileged: service.Privileged,
Expand Down
166 changes: 166 additions & 0 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
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

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package compose

import (
"context"
"fmt"
"time"

"github.com/docker/compose/v2/pkg/api"

"github.com/compose-spec/compose-go/types"
buildx "github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli/command/image/build"
"github.com/docker/compose/v2/pkg/progress"
"github.com/docker/docker/pkg/fileutils"
"github.com/fsnotify/fsnotify"
"golang.org/x/sync/errgroup"
)

//nolint:gocyclo
func (s *composeService) Watch(ctx context.Context, project *types.Project, options api.WatchOptions) error {
eg, ctx := errgroup.WithContext(ctx)
for _, service := range project.Services {
service := service
if service.Develop == nil {
continue
}

if service.Develop.Watch.Update != types.WatchBuild {
fmt.Fprintf(s.stderr(), "Unsupported update policy %q\n", service.Develop.Watch.Update)
continue
}

if service.Build == nil {
// ignored
continue
}

qp := service.Develop.Watch.QuietPeriod
if qp == "" {
qp = "500ms"
}
quietPeriod, err := time.ParseDuration(qp)
if err != nil {
return err
}

path := service.Build.Context
fmt.Printf("watching build context %s for service %s\n", path, service.Name)
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
err = watcher.Add(path)
if err != nil {
return err
}
defer watcher.Close() //nolint:errcheck

excludes, err := build.ReadDockerignore(path)
if err != nil {
return err
}
pm, err := fileutils.NewPatternMatcher(excludes)
if err != nil {
return err
}

eg.Go(func() error {
triggered := make(chan bool)

// use as a guard to enforce we run a single concurrent `refresh`
ready := make(chan bool, 1)
ready <- true
refresh := func() {
select {
case <-ready:
eg.Go(func() error {
triggered <- true
err := s.refresh(ctx, project, service.Name, options.Quiet)
if err != nil {
return err
}
ready <- true
return nil
})
default:
}
}

for {
var changes []string

select {
case event := <-watcher.Events:
ignore, err := pm.MatchesOrParentMatches(event.Name)
if err != nil {
return err
}
if ignore {
continue
}
changes = append(changes, event.Name)
if len(changes) == 1 {
// change detected, trigger a refresh but apply a quiet period waiting for more changes in a row
eg.Go(func() error {
time.Sleep(quietPeriod)
refresh()
return nil
})
} else {
refresh()
}
case <-triggered:
// a refresh has just started, reset the pending changes list
changes = nil
case err := <-watcher.Errors:
return err
case <-ctx.Done():
return watcher.Close()
}
}
})
}
return eg.Wait()
}

func (s *composeService) refresh(ctx context.Context, project *types.Project, service string, quiet bool) error {
return progress.Run(ctx, func(ctx context.Context) error {
err := s.build(ctx, project, api.BuildOptions{
Services: []string{service},
Quiet: quiet,
Progress: buildx.PrinterModeAuto,
})
if err != nil {
return err
}

err = s.create(ctx, project, api.CreateOptions{
Services: []string{service},
Recreate: api.RecreateForce,
})
if err != nil {
return err
}

return s.start(ctx, project.Name, api.StartOptions{
Project: project,
}, nil)
})
}