From 6a44f0384b4e2bca3bae90bcf56aa5242110af40 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 11 Jan 2023 11:52:19 +0100 Subject: [PATCH] introduce experimental watch command (skeletton) Signed-off-by: Nicolas De Loof --- cmd/compose/alpha.go | 31 ++++++++ cmd/compose/compose.go | 2 + cmd/compose/watch.go | 61 ++++++++++++++ docs/reference/compose.md | 1 + docs/reference/compose_alpha.md | 15 ++++ docs/reference/compose_alpha_watch.md | 14 ++++ docs/reference/docker_compose_alpha.yaml | 15 ++++ .../reference/docker_compose_alpha_watch.yaml | 25 ++++++ go.mod | 9 ++- go.sum | 8 +- pkg/api/api.go | 6 ++ pkg/api/proxy.go | 10 +++ pkg/compose/watch.go | 79 +++++++++++++++++++ pkg/mocks/mock_docker_compose_api.go | 14 ++++ 14 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 cmd/compose/alpha.go create mode 100644 cmd/compose/watch.go create mode 100644 docs/reference/compose_alpha.md create mode 100644 docs/reference/compose_alpha_watch.md create mode 100644 docs/reference/docker_compose_alpha.yaml create mode 100644 docs/reference/docker_compose_alpha_watch.yaml create mode 100644 pkg/compose/watch.go diff --git a/cmd/compose/alpha.go b/cmd/compose/alpha.go new file mode 100644 index 00000000000..5614070647c --- /dev/null +++ b/cmd/compose/alpha.go @@ -0,0 +1,31 @@ +/* + + 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 ( + "github.com/docker/compose/v2/pkg/api" + "github.com/spf13/cobra" +) + +// alphaCommand groups all experimental subcommands +func alphaCommand(p *ProjectOptions, backend api.Service) *cobra.Command { + cmd := &cobra.Command{ + Short: "Experimental commands", + Use: "alpha [COMMAND]", + Hidden: true, + } + cmd.AddCommand(watchCommand(p, backend)) + return cmd +} diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 55846dfb37a..120726f07ed 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -365,7 +365,9 @@ func RootCommand(streams api.Streams, backend api.Service) *cobra.Command { //no pullCommand(&opts, backend), createCommand(&opts, backend), copyCommand(&opts, backend), + alphaCommand(&opts, backend), ) + c.Flags().SetInterspersed(false) opts.addProjectFlags(c.Flags()) c.RegisterFlagCompletionFunc( //nolint:errcheck diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go new file mode 100644 index 00000000000..6e895edc1bd --- /dev/null +++ b/cmd/compose/watch.go @@ -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" + "fmt" + "os" + + "github.com/docker/compose/v2/pkg/api" + "github.com/spf13/cobra" +) + +type watchOptions struct { + *ProjectOptions + quiet bool +} + +func watchCommand(p *ProjectOptions, backend api.Service) *cobra.Command { + opts := watchOptions{ + ProjectOptions: p, + } + cmd := &cobra.Command{ + Use: "watch [SERVICE...]", + Short: "EXPERIMENTAL - Watch build context for service 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: completeServiceNames(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 { + fmt.Fprintln(os.Stderr, "watch command is EXPERIMENTAL") + project, err := opts.ToProject(nil) + if err != nil { + return err + } + + return backend.Watch(ctx, project, services, api.WatchOptions{}) +} diff --git a/docs/reference/compose.md b/docs/reference/compose.md index f97cc2a91a1..8ca43277d42 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -7,6 +7,7 @@ Docker Compose | Name | Description | |:--------------------------------|:------------------------------------------------------------------------| +| [`alpha`](compose_alpha.md) | Experimental commands | | [`build`](compose_build.md) | Build or rebuild services | | [`convert`](compose_convert.md) | Converts the compose file to platform's canonical format | | [`cp`](compose_cp.md) | Copy files/folders between a service container and the local filesystem | diff --git a/docs/reference/compose_alpha.md b/docs/reference/compose_alpha.md new file mode 100644 index 00000000000..b3b7ec05642 --- /dev/null +++ b/docs/reference/compose_alpha.md @@ -0,0 +1,15 @@ +# docker compose alpha + + +Experimental commands + +### Subcommands + +| Name | Description | +|:----------------------------------|:-----------------------------------------------------------------------------------------------------| +| [`watch`](compose_alpha_watch.md) | EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated | + + + + + diff --git a/docs/reference/compose_alpha_watch.md b/docs/reference/compose_alpha_watch.md new file mode 100644 index 00000000000..4b154c308dc --- /dev/null +++ b/docs/reference/compose_alpha_watch.md @@ -0,0 +1,14 @@ +# docker compose alpha watch + + +EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated + +### Options + +| Name | Type | Default | Description | +|:----------|:-----|:--------|:------------------| +| `--quiet` | | | hide build output | + + + + diff --git a/docs/reference/docker_compose_alpha.yaml b/docs/reference/docker_compose_alpha.yaml new file mode 100644 index 00000000000..34e5dd0e2b8 --- /dev/null +++ b/docs/reference/docker_compose_alpha.yaml @@ -0,0 +1,15 @@ +command: docker compose alpha +short: Experimental commands +long: Experimental commands +pname: docker compose +plink: docker_compose.yaml +cname: + - docker compose alpha watch +clink: + - docker_compose_alpha_watch.yaml +deprecated: false +experimental: false +experimentalcli: false +kubernetes: false +swarm: false + diff --git a/docs/reference/docker_compose_alpha_watch.yaml b/docs/reference/docker_compose_alpha_watch.yaml new file mode 100644 index 00000000000..5e6639fc4da --- /dev/null +++ b/docs/reference/docker_compose_alpha_watch.yaml @@ -0,0 +1,25 @@ +command: docker compose alpha watch +short: | + EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated +long: | + EXPERIMENTAL - Watch build context for service and rebuild/refresh containers when files are updated +usage: docker compose alpha watch [SERVICE...] +pname: docker compose alpha +plink: docker_compose_alpha.yaml +options: + - option: quiet + value_type: bool + default_value: "false" + description: hide build output + deprecated: false + hidden: false + experimental: false + experimentalcli: false + kubernetes: false + swarm: false +deprecated: false +experimental: false +experimentalcli: false +kubernetes: false +swarm: false + diff --git a/go.mod b/go.mod index e680bc8fcec..b859264859f 100644 --- a/go.mod +++ b/go.mod @@ -78,7 +78,7 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 github.com/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -119,7 +119,7 @@ require ( golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect golang.org/x/net v0.4.0 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect - golang.org/x/sys v0.3.0 // indirect + golang.org/x/sys v0.4.0 // indirect golang.org/x/term v0.3.0 // indirect golang.org/x/text v0.6.0 // indirect golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect @@ -137,7 +137,10 @@ require ( sigs.k8s.io/yaml v1.2.0 // indirect ) -require github.com/cucumber/godog v0.0.0-00010101000000-000000000000 +require ( + github.com/cucumber/godog v0.0.0-00010101000000-000000000000 + github.com/fsnotify/fsnotify v1.6.0 +) require ( github.com/bugsnag/bugsnag-go v1.5.0 // indirect diff --git a/go.sum b/go.sum index a988c591ea6..c1b98689550 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,9 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= 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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= @@ -903,8 +904,9 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/api/api.go b/pkg/api/api.go index 76bb6a478af..baded095ff1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -77,6 +77,12 @@ type Service interface { Images(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) // MaxConcurrency defines upper limit for concurrent operations against engine API MaxConcurrency(parallel int) + // Watch services' development context and sync/notify/rebuild/restart on changes + Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error +} + +// WatchOptions group options of the Watch API +type WatchOptions struct { } // BuildOptions group options of the Build API diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index 1ed80b26c70..34acd2cc9a2 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -50,6 +50,7 @@ type ServiceProxy struct { EventsFn func(ctx context.Context, project string, options EventsOptions) error PortFn func(ctx context.Context, project string, service string, port uint16, options PortOptions) (string, int, error) ImagesFn func(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) + WatchFn func(ctx context.Context, project *types.Project, services []string, options WatchOptions) error MaxConcurrencyFn func(parallel int) interceptors []Interceptor } @@ -88,6 +89,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy { s.EventsFn = service.Events s.PortFn = service.Port s.ImagesFn = service.Images + s.WatchFn = service.Watch s.MaxConcurrencyFn = service.MaxConcurrency return s } @@ -311,6 +313,14 @@ 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, services []string, options WatchOptions) error { + if s.WatchFn == nil { + return ErrNotImplemented + } + return s.WatchFn(ctx, project, services, options) +} + func (s *ServiceProxy) MaxConcurrency(i int) { s.MaxConcurrencyFn(i) } diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go new file mode 100644 index 00000000000..c8ad285faa7 --- /dev/null +++ b/pkg/compose/watch.go @@ -0,0 +1,79 @@ +/* + + 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" + "log" + + "github.com/compose-spec/compose-go/types" + "github.com/docker/compose/v2/pkg/api" + "github.com/fsnotify/fsnotify" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +type DevelopmentConfig struct { +} + +func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { + fmt.Fprintln(s.stderr(), "not implemented yet") + + eg, ctx := errgroup.WithContext(ctx) + err := project.WithServices(services, func(service types.ServiceConfig) error { + var config DevelopmentConfig + if y, ok := service.Extensions["x-develop"]; ok { + err := mapstructure.Decode(y, &config) + if err != nil { + return err + } + } + if service.Build == nil { + return errors.New("can't watch a service without a build section") + } + context := service.Build.Context + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + fmt.Println("watching " + context) + err = watcher.Add(context) + if err != nil { + return err + } + eg.Go(func() error { + defer watcher.Close() //nolint:errcheck + for { + select { + case <-ctx.Done(): + return nil + case event := <-watcher.Events: + log.Println("fs event :", event.String()) + case err := <-watcher.Errors: + return err + } + } + }) + return nil + }) + if err != nil { + return err + } + + return eg.Wait() +} diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index 73874f8dc37..c42295d6238 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -393,6 +393,20 @@ func (mr *MockServiceMockRecorder) Up(ctx, project, options interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Up", reflect.TypeOf((*MockService)(nil).Up), ctx, project, options) } +// Watch mocks base method. +func (m *MockService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Watch", ctx, project, services, options) + ret0, _ := ret[0].(error) + return ret0 +} + +// Watch indicates an expected call of Watch. +func (mr *MockServiceMockRecorder) Watch(ctx, project, services, options interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockService)(nil).Watch), ctx, project, services, options) +} + // MockLogConsumer is a mock of LogConsumer interface. type MockLogConsumer struct { ctrl *gomock.Controller