Skip to content

Commit a3fd624

Browse files
committed
introduce sync+exec watch action
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent a8469db commit a3fd624

File tree

5 files changed

+97
-20
lines changed

5 files changed

+97
-20
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,5 @@ require (
196196
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
197197
sigs.k8s.io/yaml v1.3.0 // indirect
198198
)
199+
200+
replace github.com/compose-spec/compose-go/v2 => github.com/ndeloof/compose-go/v2 v2.0.1-0.20241127110655-b1321070b3ab

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8E
8585
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
8686
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
8787
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
88-
github.com/compose-spec/compose-go/v2 v2.4.5 h1:p4ih4Jb6VgGPLPxh3fSFVKAjFHtZd+7HVLCSFzcFx9Y=
89-
github.com/compose-spec/compose-go/v2 v2.4.5/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
9088
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
9189
github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0=
9290
github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE=
@@ -359,6 +357,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
359357
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
360358
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
361359
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
360+
github.com/ndeloof/compose-go/v2 v2.0.1-0.20241127110655-b1321070b3ab h1:3Q4/1sAnPv4nMpak/lIzWsQJjX8X5zKZRkDd6mlf2mc=
361+
github.com/ndeloof/compose-go/v2 v2.0.1-0.20241127110655-b1321070b3ab/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
362362
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
363363
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
364364
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=

pkg/compose/watch.go

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import (
2323
"os"
2424
"path"
2525
"path/filepath"
26-
"sort"
2726
"strconv"
2827
"strings"
2928
"time"
3029

3130
"github.com/compose-spec/compose-go/v2/types"
31+
ccli "github.com/docker/cli/cli/command/container"
3232
pathutil "github.com/docker/compose/v2/internal/paths"
3333
"github.com/docker/compose/v2/internal/sync"
3434
"github.com/docker/compose/v2/pkg/api"
@@ -48,7 +48,7 @@ const quietPeriod = 500 * time.Millisecond
4848
// fileEvent contains the Compose service and modified host system path.
4949
type fileEvent struct {
5050
sync.PathMapping
51-
Action types.WatchAction
51+
Trigger types.Trigger
5252
}
5353

5454
// getSyncImplementation returns an appropriate sync implementation for the
@@ -298,7 +298,7 @@ func maybeFileEvent(trigger types.Trigger, hostPath string, ignore watch.PathMat
298298
}
299299

300300
return &fileEvent{
301-
Action: trigger.Action,
301+
Trigger: trigger,
302302
PathMapping: sync.PathMapping{
303303
HostPath: hostPath,
304304
ContainerPath: containerPath,
@@ -338,6 +338,9 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
338338
if trigger.Action == types.WatchActionRebuild && service.Build == nil {
339339
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply 'rebuild' on watch", service.Name)
340340
}
341+
if trigger.Action == types.WatchActionSyncExec && len(trigger.Exec.Command) == 0 {
342+
return nil, fmt.Errorf("can't watch with action 'sync+exec' on service %s wihtout a command", service.Name)
343+
}
341344

342345
config.Watch[i] = trigger
343346
}
@@ -352,24 +355,17 @@ func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.
352355
out := make(chan []fileEvent)
353356
go func() {
354357
defer close(out)
355-
seen := make(map[fileEvent]time.Time)
358+
seen := make(map[sync.PathMapping]fileEvent)
356359
flushEvents := func() {
357360
if len(seen) == 0 {
358361
return
359362
}
360363
events := make([]fileEvent, 0, len(seen))
361-
for e := range seen {
364+
for _, e := range seen {
362365
events = append(events, e)
363366
}
364-
// sort batch by oldest -> newest
365-
// (if an event is seen > 1 per batch, it gets the latest timestamp)
366-
sort.SliceStable(events, func(i, j int) bool {
367-
x := events[i]
368-
y := events[j]
369-
return seen[x].Before(seen[y])
370-
})
371367
out <- events
372-
seen = make(map[fileEvent]time.Time)
368+
seen = make(map[sync.PathMapping]fileEvent)
373369
}
374370

375371
t := clock.NewTicker(delay)
@@ -386,7 +382,7 @@ func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.
386382
flushEvents()
387383
return
388384
}
389-
seen[e] = time.Now()
385+
seen[e.PathMapping] = e
390386
t.Reset(delay)
391387
}
392388
}
@@ -485,7 +481,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
485481
pathMappings := make([]sync.PathMapping, len(batch))
486482
restartService := false
487483
for i := range batch {
488-
if batch[i].Action == types.WatchActionRebuild {
484+
if batch[i].Trigger.Action == types.WatchActionRebuild {
489485
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service %q after changes were detected...", serviceName))
490486
// restrict the build to ONLY this service, not any of its dependencies
491487
options.Build.Services = []string{serviceName}
@@ -527,7 +523,7 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
527523
}
528524
return nil
529525
}
530-
if batch[i].Action == types.WatchActionSyncRestart {
526+
if batch[i].Trigger.Action == types.WatchActionSyncRestart {
531527
restartService = true
532528
}
533529
pathMappings[i] = batch[i].PathMapping
@@ -554,9 +550,34 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
554550
options.LogTo.Log(
555551
api.WatchLogger,
556552
fmt.Sprintf("service %q restarted", serviceName))
557-
558553
}
559-
return nil
554+
eg, ctx := errgroup.WithContext(ctx)
555+
for _, b := range batch {
556+
if b.Trigger.Action == "sync+exec" {
557+
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, serviceName)
558+
if err != nil {
559+
return err
560+
}
561+
x := b.Trigger.Exec
562+
for _, c := range containers {
563+
eg.Go(func() error {
564+
exec := ccli.NewExecOptions()
565+
exec.User = x.User
566+
exec.Privileged = x.Privileged
567+
exec.Command = x.Command
568+
exec.Workdir = x.WorkingDir
569+
for _, v := range x.Environment.ToMapping().Values() {
570+
err := exec.Env.Set(v)
571+
if err != nil {
572+
return err
573+
}
574+
}
575+
return ccli.RunExec(ctx, s.dockerCli, c.ID, exec)
576+
})
577+
}
578+
}
579+
}
580+
return eg.Wait()
560581
}
561582

562583
// writeWatchSyncMessage prints out a message about the sync for the changed paths.

pkg/e2e/fixtures/watch/exec.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
test:
3+
build:
4+
dockerfile_inline: FROM alpine
5+
command: ping localhost
6+
volumes:
7+
- /data
8+
develop:
9+
watch:
10+
- path: .
11+
target: /data
12+
action: sync+exec
13+
exec:
14+
command: echo "SUCCESS"

pkg/e2e/watch_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package e2e
1818

1919
import (
20+
"bytes"
2021
"crypto/rand"
2122
"fmt"
2223
"os"
@@ -293,3 +294,42 @@ func doTest(t *testing.T, svcName string) {
293294

294295
testComplete.Store(true)
295296
}
297+
298+
func TestWatchExec(t *testing.T) {
299+
cli := NewCLI(t)
300+
const projectName = "test_watch_exec"
301+
302+
t.Cleanup(func() {
303+
cli.RunDockerComposeCmd(t, "-p", projectName, "down")
304+
})
305+
306+
tmpdir := t.TempDir()
307+
composeFilePath := filepath.Join(tmpdir, "compose.yaml")
308+
CopyFile(t, filepath.Join("fixtures", "watch", "exec.yaml"), composeFilePath)
309+
cmd := cli.NewDockerComposeCmd(t, "-p", projectName, "-f", composeFilePath, "up", "--watch")
310+
buffer := bytes.NewBuffer(nil)
311+
cmd.Stdout = buffer
312+
watch := icmd.StartCmd(cmd)
313+
314+
poll.WaitOn(t, func(l poll.LogT) poll.Result {
315+
out := buffer.String()
316+
if strings.Contains(out, "64 bytes from") {
317+
return poll.Success()
318+
}
319+
return poll.Continue("%v", watch.Stdout())
320+
})
321+
322+
t.Logf("Create new file")
323+
324+
testFile := filepath.Join(tmpdir, "test")
325+
require.NoError(t, os.WriteFile(testFile, []byte("test\n"), 0o600))
326+
327+
poll.WaitOn(t, func(l poll.LogT) poll.Result {
328+
out := buffer.String()
329+
if strings.Contains(out, "SUCCESS") {
330+
return poll.Success()
331+
}
332+
return poll.Continue("%v", out)
333+
})
334+
cli.RunDockerComposeCmdNoCheck(t, "-p", projectName, "kill", "-s", "9")
335+
}

0 commit comments

Comments
 (0)