Skip to content

Commit d7b0b2b

Browse files
authored
watch: build & launch the project at start (docker#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 <[email protected]>
1 parent e0f39eb commit d7b0b2b

File tree

7 files changed

+82
-56
lines changed

7 files changed

+82
-56
lines changed

cmd/compose/watch.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ import (
3131
type watchOptions struct {
3232
*ProjectOptions
3333
quiet bool
34+
noUp bool
3435
}
3536

3637
func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command {
37-
opts := watchOptions{
38+
watchOpts := watchOptions{
39+
ProjectOptions: p,
40+
}
41+
buildOpts := buildOptions{
3842
ProjectOptions: p,
3943
}
4044
cmd := &cobra.Command{
@@ -44,22 +48,33 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service)
4448
return nil
4549
}),
4650
RunE: Adapt(func(ctx context.Context, args []string) error {
47-
return runWatch(ctx, dockerCli, backend, opts, args)
51+
return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
4852
}),
4953
ValidArgsFunction: completeServiceNames(dockerCli, p),
5054
}
5155

52-
cmd.Flags().BoolVar(&opts.quiet, "quiet", false, "hide build output")
56+
cmd.Flags().BoolVar(&watchOpts.quiet, "quiet", false, "hide build output")
57+
cmd.Flags().BoolVar(&watchOpts.noUp, "no-up", false, "Do not build & start services before watching")
5358
return cmd
5459
}
5560

56-
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, opts watchOptions, services []string) error {
61+
func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
5762
fmt.Fprintln(os.Stderr, "watch command is EXPERIMENTAL")
58-
project, err := opts.ToProject(dockerCli, nil)
63+
project, err := watchOpts.ToProject(dockerCli, nil)
64+
if err != nil {
65+
return err
66+
}
67+
68+
if err := applyPlatforms(project, true); err != nil {
69+
return err
70+
}
71+
72+
build, err := buildOpts.toAPIBuildOptions(nil)
5973
if err != nil {
6074
return err
6175
}
6276

77+
// validation done -- ensure we have the lockfile for this project before doing work
6378
l, err := locker.NewPidfile(project.Name)
6479
if err != nil {
6580
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
6883
return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err)
6984
}
7085

71-
return backend.Watch(ctx, project, services, api.WatchOptions{})
86+
if !watchOpts.noUp {
87+
upOpts := api.UpOptions{
88+
Create: api.CreateOptions{
89+
Build: &build,
90+
Services: services,
91+
RemoveOrphans: false,
92+
Recreate: api.RecreateDiverged,
93+
RecreateDependencies: api.RecreateNever,
94+
Inherit: true,
95+
QuietPull: watchOpts.quiet,
96+
},
97+
Start: api.StartOptions{
98+
Project: project,
99+
Attach: nil,
100+
CascadeStop: false,
101+
Services: services,
102+
},
103+
}
104+
if err := backend.Up(ctx, project, upOpts); err != nil {
105+
return err
106+
}
107+
}
108+
return backend.Watch(ctx, project, services, api.WatchOptions{
109+
Build: build,
110+
})
72111
}

docs/reference/compose_alpha_watch.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ EXPERIMENTAL - Watch build context for service and rebuild/refresh containers wh
55

66
### Options
77

8-
| Name | Type | Default | Description |
9-
|:------------|:-----|:--------|:--------------------------------|
10-
| `--dry-run` | | | Execute command in dry run mode |
11-
| `--quiet` | | | hide build output |
8+
| Name | Type | Default | Description |
9+
|:------------|:-----|:--------|:----------------------------------------------|
10+
| `--dry-run` | | | Execute command in dry run mode |
11+
| `--no-up` | | | Do not build & start services before watching |
12+
| `--quiet` | | | hide build output |
1213

1314

1415
<!---MARKER_GEN_END-->

docs/reference/docker_compose_alpha_watch.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ usage: docker compose alpha watch [SERVICE...]
77
pname: docker compose alpha
88
plink: docker_compose_alpha.yaml
99
options:
10+
- option: no-up
11+
value_type: bool
12+
default_value: "false"
13+
description: Do not build & start services before watching
14+
deprecated: false
15+
hidden: false
16+
experimental: false
17+
experimentalcli: false
18+
kubernetes: false
19+
swarm: false
1020
- option: quiet
1121
value_type: bool
1222
default_value: "false"

pkg/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type VizOptions struct {
110110

111111
// WatchOptions group options of the Watch API
112112
type WatchOptions struct {
113+
Build BuildOptions
113114
}
114115

115116
// BuildOptions group options of the Build API

pkg/compose/watch.go

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,16 @@ import (
2626
"strings"
2727
"time"
2828

29-
moby "github.com/docker/docker/api/types"
30-
31-
"github.com/docker/compose/v2/internal/sync"
32-
3329
"github.com/compose-spec/compose-go/types"
30+
"github.com/docker/compose/v2/internal/sync"
31+
"github.com/docker/compose/v2/pkg/api"
32+
"github.com/docker/compose/v2/pkg/watch"
33+
moby "github.com/docker/docker/api/types"
3434
"github.com/jonboulle/clockwork"
3535
"github.com/mitchellh/mapstructure"
3636
"github.com/pkg/errors"
3737
"github.com/sirupsen/logrus"
3838
"golang.org/x/sync/errgroup"
39-
40-
"github.com/docker/compose/v2/pkg/api"
41-
"github.com/docker/compose/v2/pkg/watch"
4239
)
4340

4441
type DevelopmentConfig struct {
@@ -84,7 +81,7 @@ func (s *composeService) getSyncImplementation(project *types.Project) sync.Sync
8481
return sync.NewDockerCopy(project.Name, s, s.stdinfo())
8582
}
8683

87-
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
84+
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
8885
if err := project.ForServices(services); err != nil {
8986
return err
9087
}
@@ -161,7 +158,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
161158

162159
eg.Go(func() error {
163160
defer watcher.Close() //nolint:errcheck
164-
return s.watch(ctx, project, service.Name, watcher, syncer, config.Watch)
161+
return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
165162
})
166163
}
167164

@@ -172,14 +169,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
172169
return eg.Wait()
173170
}
174171

175-
func (s *composeService) watch(
176-
ctx context.Context,
177-
project *types.Project,
178-
name string,
179-
watcher watch.Notify,
180-
syncer sync.Syncer,
181-
triggers []Trigger,
182-
) error {
172+
func (s *composeService) watch(ctx context.Context, project *types.Project, name string, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, triggers []Trigger) error {
183173
ctx, cancel := context.WithCancel(ctx)
184174
defer cancel()
185175

@@ -202,7 +192,7 @@ func (s *composeService) watch(
202192
case batch := <-batchEvents:
203193
start := time.Now()
204194
logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch))
205-
if err := s.handleWatchBatch(ctx, project, name, batch, syncer); err != nil {
195+
if err := s.handleWatchBatch(ctx, project, name, options.Build, batch, syncer); err != nil {
206196
logrus.Warnf("Error handling changed files for service %s: %v", name, err)
207197
}
208198
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
436426
return nil
437427
}
438428

439-
func (s *composeService) handleWatchBatch(
440-
ctx context.Context,
441-
project *types.Project,
442-
serviceName string,
443-
batch []fileEvent,
444-
syncer sync.Syncer,
445-
) error {
429+
func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, serviceName string, build api.BuildOptions, batch []fileEvent, syncer sync.Syncer) error {
446430
pathMappings := make([]sync.PathMapping, len(batch))
447431
for i := range batch {
448432
if batch[i].Action == WatchActionRebuild {
@@ -452,14 +436,11 @@ func (s *composeService) handleWatchBatch(
452436
serviceName,
453437
strings.Join(append([]string{""}, batch[i].HostPath), "\n - "),
454438
)
439+
// restrict the build to ONLY this service, not any of its dependencies
440+
build.Services = []string{serviceName}
455441
err := s.Up(ctx, project, api.UpOptions{
456442
Create: api.CreateOptions{
457-
Build: &api.BuildOptions{
458-
Pull: false,
459-
Push: false,
460-
// restrict the build to ONLY this service, not any of its dependencies
461-
Services: []string{serviceName},
462-
},
443+
Build: &build,
463444
Services: []string{serviceName},
464445
Inherit: true,
465446
},

pkg/compose/watch_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,14 @@ import (
2121
"time"
2222

2323
"github.com/compose-spec/compose-go/types"
24+
"github.com/docker/compose/v2/internal/sync"
25+
"github.com/docker/compose/v2/pkg/api"
2426
"github.com/docker/compose/v2/pkg/mocks"
27+
"github.com/docker/compose/v2/pkg/watch"
2528
moby "github.com/docker/docker/api/types"
2629
"github.com/golang/mock/gomock"
27-
2830
"github.com/jonboulle/clockwork"
2931
"github.com/stretchr/testify/require"
30-
31-
"github.com/docker/compose/v2/internal/sync"
32-
33-
"github.com/docker/compose/v2/pkg/watch"
3432
"gotest.tools/v3/assert"
3533
)
3634

@@ -126,7 +124,7 @@ func TestWatch_Sync(t *testing.T) {
126124
dockerCli: cli,
127125
clock: clock,
128126
}
129-
err := service.watch(ctx, &proj, "test", watcher, syncer, []Trigger{
127+
err := service.watch(ctx, &proj, "test", api.WatchOptions{}, watcher, syncer, []Trigger{
130128
{
131129
Path: "/sync",
132130
Action: "sync",

pkg/e2e/watch_test.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,13 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
8282

8383
cli := NewCLI(t, WithEnv(env...))
8484

85+
// important that --rmi is used to prune the images and ensure that watch builds on launch
8586
cleanup := func() {
86-
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes")
87+
cli.RunDockerComposeCmd(t, "down", svcName, "--timeout=0", "--remove-orphans", "--volumes", "--rmi=local")
8788
}
8889
cleanup()
8990
t.Cleanup(cleanup)
9091

91-
cli.RunDockerComposeCmd(t, "up", svcName, "--wait", "--build")
92-
9392
cmd := cli.NewDockerComposeCmd(t, "--verbose", "alpha", "watch", svcName)
9493
// stream output since watch runs in the background
9594
cmd.Stdout = os.Stdout
@@ -161,14 +160,12 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
161160
Assert(t, icmd.Expected{
162161
ExitCode: 1,
163162
Err: "No such file or directory",
164-
},
165-
)
163+
})
166164
cli.RunDockerComposeCmdNoCheck(t, "exec", svcName, "stat", "/app/data/ignored").
167165
Assert(t, icmd.Expected{
168166
ExitCode: 1,
169167
Err: "No such file or directory",
170-
},
171-
)
168+
})
172169

173170
t.Logf("Creating subdirectory")
174171
require.NoError(t, os.Mkdir(filepath.Join(dataDir, "subdir"), 0o700))
@@ -196,8 +193,7 @@ func doTest(t *testing.T, svcName string, tarSync bool) {
196193
Assert(t, icmd.Expected{
197194
ExitCode: 1,
198195
Err: "No such file or directory",
199-
},
200-
)
196+
})
201197

202198
testComplete.Store(true)
203199
}

0 commit comments

Comments
 (0)