mirror of https://github.com/docker/compose.git
watch: build & launch the project at start (#10957)
The `alpha watch` command current "attaches" to an already-running Compose project, so it's necessary to run something like `docker compose up --wait` first. Now, we'll do the equivalent of an `up --build` before starting the watch, so that we know the project is up-to-date and running. Additionally, unlike an interactive `up`, the services are not stopped when `watch` exits (e.g. via `Ctrl-C`). This prevents the need to start from scratch each time the command is run - if some services are already running and up-to-date, they can be used as-is. A `down` can always be used to destroy everything, and we can consider introducing a flag like `--down-on-exit` to `watch` or changing the default. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
This commit is contained in:
parent
e0f39ebbef
commit
d7b0b2bd7d
|
@ -31,10 +31,14 @@ import (
|
||||||
type watchOptions struct {
|
type watchOptions struct {
|
||||||
*ProjectOptions
|
*ProjectOptions
|
||||||
quiet bool
|
quiet 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 {
|
||||||
opts := watchOptions{
|
watchOpts := watchOptions{
|
||||||
|
ProjectOptions: p,
|
||||||
|
}
|
||||||
|
buildOpts := buildOptions{
|
||||||
ProjectOptions: p,
|
ProjectOptions: p,
|
||||||
}
|
}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
@ -44,22 +48,33 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
RunE: Adapt(func(ctx context.Context, args []string) error {
|
RunE: Adapt(func(ctx context.Context, args []string) error {
|
||||||
return runWatch(ctx, dockerCli, backend, opts, args)
|
return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
|
||||||
}),
|
}),
|
||||||
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
ValidArgsFunction: completeServiceNames(dockerCli, p),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().BoolVar(&opts.quiet, "quiet", false, "hide build output")
|
cmd.Flags().BoolVar(&watchOpts.quiet, "quiet", false, "hide build output")
|
||||||
|
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, opts watchOptions, services []string) error {
|
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
|
||||||
fmt.Fprintln(os.Stderr, "watch command is EXPERIMENTAL")
|
fmt.Fprintln(os.Stderr, "watch command is EXPERIMENTAL")
|
||||||
project, err := opts.ToProject(dockerCli, nil)
|
project, err := watchOpts.ToProject(dockerCli, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := applyPlatforms(project, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := buildOpts.toAPIBuildOptions(nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validation done -- ensure we have the lockfile for this project before doing work
|
||||||
l, err := locker.NewPidfile(project.Name)
|
l, err := locker.NewPidfile(project.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
||||||
|
@ -68,5 +83,29 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, o
|
||||||
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return backend.Watch(ctx, project, services, api.WatchOptions{})
|
if !watchOpts.noUp {
|
||||||
|
upOpts := api.UpOptions{
|
||||||
|
Create: api.CreateOptions{
|
||||||
|
Build: &build,
|
||||||
|
Services: services,
|
||||||
|
RemoveOrphans: false,
|
||||||
|
Recreate: api.RecreateDiverged,
|
||||||
|
RecreateDependencies: api.RecreateNever,
|
||||||
|
Inherit: true,
|
||||||
|
QuietPull: watchOpts.quiet,
|
||||||
|
},
|
||||||
|
Start: api.StartOptions{
|
||||||
|
Project: project,
|
||||||
|
Attach: nil,
|
||||||
|
CascadeStop: false,
|
||||||
|
Services: services,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := backend.Up(ctx, project, upOpts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return backend.Watch(ctx, project, services, api.WatchOptions{
|
||||||
|
Build: build,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,11 @@ EXPERIMENTAL - Watch build context for service and rebuild/refresh containers wh
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:------------|:-----|:--------|:--------------------------------|
|
|:------------|:-----|:--------|:----------------------------------------------|
|
||||||
| `--dry-run` | | | Execute command in dry run mode |
|
| `--dry-run` | | | Execute command in dry run mode |
|
||||||
| `--quiet` | | | hide build output |
|
| `--no-up` | | | Do not build & start services before watching |
|
||||||
|
| `--quiet` | | | hide build output |
|
||||||
|
|
||||||
|
|
||||||
<!---MARKER_GEN_END-->
|
<!---MARKER_GEN_END-->
|
||||||
|
|
|
@ -7,6 +7,16 @@ usage: docker compose alpha watch [SERVICE...]
|
||||||
pname: docker compose alpha
|
pname: docker compose alpha
|
||||||
plink: docker_compose_alpha.yaml
|
plink: docker_compose_alpha.yaml
|
||||||
options:
|
options:
|
||||||
|
- option: no-up
|
||||||
|
value_type: bool
|
||||||
|
default_value: "false"
|
||||||
|
description: Do not build & start services before watching
|
||||||
|
deprecated: false
|
||||||
|
hidden: false
|
||||||
|
experimental: false
|
||||||
|
experimentalcli: false
|
||||||
|
kubernetes: false
|
||||||
|
swarm: false
|
||||||
- option: quiet
|
- option: quiet
|
||||||
value_type: bool
|
value_type: bool
|
||||||
default_value: "false"
|
default_value: "false"
|
||||||
|
|
|
@ -110,6 +110,7 @@ type VizOptions struct {
|
||||||
|
|
||||||
// WatchOptions group options of the Watch API
|
// WatchOptions group options of the Watch API
|
||||||
type WatchOptions struct {
|
type WatchOptions struct {
|
||||||
|
Build BuildOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildOptions group options of the Build API
|
// BuildOptions group options of the Build API
|
||||||
|
|
|
@ -26,19 +26,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
moby "github.com/docker/docker/api/types"
|
|
||||||
|
|
||||||
"github.com/docker/compose/v2/internal/sync"
|
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
|
"github.com/docker/compose/v2/internal/sync"
|
||||||
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
|
"github.com/docker/compose/v2/pkg/watch"
|
||||||
|
moby "github.com/docker/docker/api/types"
|
||||||
"github.com/jonboulle/clockwork"
|
"github.com/jonboulle/clockwork"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/docker/compose/v2/pkg/api"
|
|
||||||
"github.com/docker/compose/v2/pkg/watch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DevelopmentConfig struct {
|
type DevelopmentConfig struct {
|
||||||
|
@ -84,7 +81,7 @@ func (s *composeService) getSyncImplementation(project *types.Project) sync.Sync
|
||||||
return sync.NewDockerCopy(project.Name, s, s.stdinfo())
|
return sync.NewDockerCopy(project.Name, s, s.stdinfo())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
|
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
|
||||||
if err := project.ForServices(services); err != nil {
|
if err := project.ForServices(services); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -161,7 +158,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
defer watcher.Close() //nolint:errcheck
|
defer watcher.Close() //nolint:errcheck
|
||||||
return s.watch(ctx, project, service.Name, watcher, syncer, config.Watch)
|
return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,14 +169,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
|
||||||
return eg.Wait()
|
return eg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) watch(
|
func (s *composeService) watch(ctx context.Context, project *types.Project, name string, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, triggers []Trigger) error {
|
||||||
ctx context.Context,
|
|
||||||
project *types.Project,
|
|
||||||
name string,
|
|
||||||
watcher watch.Notify,
|
|
||||||
syncer sync.Syncer,
|
|
||||||
triggers []Trigger,
|
|
||||||
) error {
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -202,7 +192,7 @@ func (s *composeService) watch(
|
||||||
case batch := <-batchEvents:
|
case batch := <-batchEvents:
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch))
|
logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch))
|
||||||
if err := s.handleWatchBatch(ctx, project, name, batch, syncer); err != nil {
|
if err := s.handleWatchBatch(ctx, project, name, options.Build, batch, syncer); err != nil {
|
||||||
logrus.Warnf("Error handling changed files for service %s: %v", name, err)
|
logrus.Warnf("Error handling changed files for service %s: %v", name, err)
|
||||||
}
|
}
|
||||||
logrus.Debugf("batch complete: service[%s] duration[%s] count[%d]",
|
logrus.Debugf("batch complete: service[%s] duration[%s] count[%d]",
|
||||||
|
@ -436,13 +426,7 @@ func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []str
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) handleWatchBatch(
|
func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, serviceName string, build api.BuildOptions, batch []fileEvent, syncer sync.Syncer) error {
|
||||||
ctx context.Context,
|
|
||||||
project *types.Project,
|
|
||||||
serviceName string,
|
|
||||||
batch []fileEvent,
|
|
||||||
syncer sync.Syncer,
|
|
||||||
) error {
|
|
||||||
pathMappings := make([]sync.PathMapping, len(batch))
|
pathMappings := make([]sync.PathMapping, len(batch))
|
||||||
for i := range batch {
|
for i := range batch {
|
||||||
if batch[i].Action == WatchActionRebuild {
|
if batch[i].Action == WatchActionRebuild {
|
||||||
|
@ -452,14 +436,11 @@ func (s *composeService) handleWatchBatch(
|
||||||
serviceName,
|
serviceName,
|
||||||
strings.Join(append([]string{""}, batch[i].HostPath), "\n - "),
|
strings.Join(append([]string{""}, batch[i].HostPath), "\n - "),
|
||||||
)
|
)
|
||||||
|
// restrict the build to ONLY this service, not any of its dependencies
|
||||||
|
build.Services = []string{serviceName}
|
||||||
err := s.Up(ctx, project, api.UpOptions{
|
err := s.Up(ctx, project, api.UpOptions{
|
||||||
Create: api.CreateOptions{
|
Create: api.CreateOptions{
|
||||||
Build: &api.BuildOptions{
|
Build: &build,
|
||||||
Pull: false,
|
|
||||||
Push: false,
|
|
||||||
// restrict the build to ONLY this service, not any of its dependencies
|
|
||||||
Services: []string{serviceName},
|
|
||||||
},
|
|
||||||
Services: []string{serviceName},
|
Services: []string{serviceName},
|
||||||
Inherit: true,
|
Inherit: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,16 +21,14 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
|
"github.com/docker/compose/v2/internal/sync"
|
||||||
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
"github.com/docker/compose/v2/pkg/mocks"
|
"github.com/docker/compose/v2/pkg/mocks"
|
||||||
|
"github.com/docker/compose/v2/pkg/watch"
|
||||||
moby "github.com/docker/docker/api/types"
|
moby "github.com/docker/docker/api/types"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
"github.com/jonboulle/clockwork"
|
"github.com/jonboulle/clockwork"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/docker/compose/v2/internal/sync"
|
|
||||||
|
|
||||||
"github.com/docker/compose/v2/pkg/watch"
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -126,7 +124,7 @@ func TestWatch_Sync(t *testing.T) {
|
||||||
dockerCli: cli,
|
dockerCli: cli,
|
||||||
clock: clock,
|
clock: clock,
|
||||||
}
|
}
|
||||||
err := service.watch(ctx, &proj, "test", watcher, syncer, []Trigger{
|
err := service.watch(ctx, &proj, "test", api.WatchOptions{}, watcher, syncer, []Trigger{
|
||||||
{
|
{
|
||||||
Path: "/sync",
|
Path: "/sync",
|
||||||
Action: "sync",
|
Action: "sync",
|
||||||
|
|
|
@ -82,14 +82,13 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
|
||||||
|
|
||||||
cli := NewCLI(t, WithEnv(env...))
|
cli := NewCLI(t, WithEnv(env...))
|
||||||
|
|
||||||
|
// important that --rmi is used to prune the images and ensure that watch builds on launch
|
||||||
cleanup := func() {
|
cleanup := func() {
|
||||||
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes")
|
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes", "--rmi=local")
|
||||||
}
|
}
|
||||||
cleanup()
|
cleanup()
|
||||||
t.Cleanup(cleanup)
|
t.Cleanup(cleanup)
|
||||||
|
|
||||||
cli.RunDockerComposeCmd(t, "up", svcName, "--wait", "--build")
|
|
||||||
|
|
||||||
cmd := cli.NewDockerComposeCmd(t, "--verbose", "alpha", "watch", svcName)
|
cmd := cli.NewDockerComposeCmd(t, "--verbose", "alpha", "watch", svcName)
|
||||||
// stream output since watch runs in the background
|
// stream output since watch runs in the background
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
|
@ -161,14 +160,12 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
|
||||||
Assert(t, icmd.Expected{
|
Assert(t, icmd.Expected{
|
||||||
ExitCode: 1,
|
ExitCode: 1,
|
||||||
Err: "No such file or directory",
|
Err: "No such file or directory",
|
||||||
},
|
})
|
||||||
)
|
|
||||||
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
|
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
|
||||||
Assert(t, icmd.Expected{
|
Assert(t, icmd.Expected{
|
||||||
ExitCode: 1,
|
ExitCode: 1,
|
||||||
Err: "No such file or directory",
|
Err: "No such file or directory",
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
t.Logf("Creating subdirectory")
|
t.Logf("Creating subdirectory")
|
||||||
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
|
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
|
||||||
|
@ -196,8 +193,7 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
|
||||||
Assert(t, icmd.Expected{
|
Assert(t, icmd.Expected{
|
||||||
ExitCode: 1,
|
ExitCode: 1,
|
||||||
Err: "No such file or directory",
|
Err: "No such file or directory",
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
testComplete.Store(true)
|
testComplete.Store(true)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue