diff --git a/cmd/compose/alpha.go b/cmd/compose/alpha.go new file mode 100644 index 000000000..561407064 --- /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 55846dfb3..120726f07 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 000000000..6e895edc1 --- /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 f97cc2a91..8ca43277d 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 000000000..b3b7ec056 --- /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 000000000..4b154c308 --- /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 000000000..34e5dd0e2 --- /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 000000000..5e6639fc4 --- /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 e680bc8fc..c28198a80 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/compose-spec/compose-go v1.8.2 github.com/containerd/console v1.0.3 github.com/containerd/containerd v1.6.15 + github.com/cucumber/godog v0.0.0-00010101000000-000000000000 github.com/distribution/distribution/v3 v3.0.0-20221201083218-92d136e113cf github.com/docker/buildx v0.9.1 // when updating, also update the replace rules accordingly github.com/docker/cli v20.10.20+incompatible // replaced; see replace rule for actual version @@ -15,11 +16,12 @@ require ( github.com/docker/docker v20.10.20+incompatible // replaced; see replace rule for actual version github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.5.0 + github.com/fsnotify/fsnotify v1.6.0 github.com/golang/mock v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 - github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 + github.com/mitchellh/mapstructure v1.5.0 github.com/moby/buildkit v0.10.4 // replaced; see replace rule for actual version github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f github.com/morikuni/aec v1.0.0 @@ -42,11 +44,15 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bugsnag/bugsnag-go v1.5.0 // indirect github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cloudflare/cfssl v1.4.1 // indirect github.com/containerd/continuity v0.3.0 // indirect github.com/containerd/ttrpc v1.1.0 // indirect github.com/containerd/typeurl v1.0.2 // indirect + github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect + github.com/cucumber/messages-go/v16 v16.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect @@ -57,6 +63,7 @@ require ( github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/flock v0.8.0 // indirect + github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -68,17 +75,21 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.2 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jinzhu/gorm v1.9.11 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect 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/moby/locker v1.0.1 // indirect github.com/moby/patternmatcher v0.5.0 // indirect github.com/moby/spdystream v0.2.0 // indirect @@ -96,6 +107,7 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect + github.com/spf13/viper v1.4.0 // indirect github.com/tonistiigi/fsutil v0.0.0-20220930225714-4638ad635be5 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect @@ -119,7 +131,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,21 +149,6 @@ require ( sigs.k8s.io/yaml v1.2.0 // indirect ) -require github.com/cucumber/godog v0.0.0-00010101000000-000000000000 - -require ( - github.com/bugsnag/bugsnag-go v1.5.0 // indirect - github.com/cloudflare/cfssl v1.4.1 // indirect - github.com/cucumber/gherkin-go/v19 v19.0.3 // indirect - github.com/cucumber/messages-go/v16 v16.0.1 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-memdb v1.3.2 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/jinzhu/gorm v1.9.11 // indirect - github.com/spf13/viper v1.4.0 // indirect -) - replace ( // Override for e2e tests github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7 diff --git a/go.sum b/go.sum index a988c591e..c1b986895 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 76bb6a478..baded095f 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 1ed80b26c..34acd2cc9 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 000000000..c8ad285fa --- /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 73874f8dc..c42295d62 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