Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle glob pattern in watch configuration path #12557

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -168,6 +168,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/zclconf/go-cty v1.16.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
@@ -198,3 +199,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/compose-spec/compose-go/v2 v2.4.8 => github.com/jhrotko/compose-go/v2 v2.0.0-20250225153415-9ce61ad83a27
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -81,8 +81,6 @@ github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/compose-spec/compose-go/v2 v2.4.8 h1:7Myl8wDRl/4mRz77S+eyDJymGGEHu0diQdGSSeyq90A=
github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
@@ -251,6 +249,8 @@ github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1Gd
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jhrotko/compose-go/v2 v2.0.0-20250225153415-9ce61ad83a27 h1:zt9TD5EqlE4d/RQ6hspiLj2VaoviTBrETfS8kr2YT30=
github.com/jhrotko/compose-go/v2 v2.0.0-20250225153415-9ce61ad83a27/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
@@ -494,6 +494,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
89 changes: 47 additions & 42 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
@@ -80,15 +80,30 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv

type watchRule struct {
types.Trigger
ignore watch.PathMatcher
service string
ignore watch.PathMatcher
service string
globPattern watch.PathMatcher
}

func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
hostPath := string(event)
if !pathutil.IsChild(r.Path, hostPath) {

isGlob := r.IsGlobPath()
if !isGlob && !pathutil.IsChild(r.Path, hostPath) {
return nil
}

if isGlob {
isMatch, err := r.globPattern.Matches(hostPath)
if err != nil {
logrus.Warnf("error while pattern matching %q: %v", hostPath, err)
return nil
}
if !isMatch {
return nil
}
}

isIgnored, err := r.ignore.Matches(hostPath)
if err != nil {
logrus.Warnf("error ignore matching %q: %v", hostPath, err)
@@ -102,9 +117,9 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {

var containerPath string
if r.Target != "" {
rel, err := filepath.Rel(r.Path, hostPath)
rel, err := filepath.Rel(r.AnchorPath(), hostPath)
if err != nil {
logrus.Warnf("error making %s relative to %s: %v", hostPath, r.Path, err)
logrus.Warnf("error making %s relative to %s: %v", hostPath, r.AnchorPath(), err)
return nil
}
// always use Unix-style paths for inside the container
@@ -161,21 +176,21 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
}

for _, trigger := range config.Watch {
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
if trigger.IsSyncAction() && isPathBindMounted(trigger.AnchorPath(), service.Volumes) {
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
continue
} else {
var initialSync bool
success, err := trigger.Extensions.Get("x-initialSync", &initialSync)
if err == nil && success && initialSync && isSync(trigger) {
if err == nil && success && initialSync && trigger.IsSyncAction() {
// Need to check initial files are in container that are meant to be synched from watch action
err := s.initialSync(ctx, project, service, trigger, syncer)
if err != nil {
return err
}
}
}
paths = append(paths, trigger.Path)
paths = append(paths, trigger.AnchorPath())
}

serviceWatchRules, err := getWatchRules(config, service)
@@ -224,15 +239,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]watchRule, error) {
var rules []watchRule

dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
if err != nil {
return nil, err
}

// add a hardcoded set of ignores on top of what came from .dockerignore
// some of this should likely be configurable (e.g. there could be cases
// where you want `.git` to be synced) but this is suitable for now
dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
general, err := watch.GeneralIgnorePatterns(service)
if err != nil {
return nil, err
}
@@ -242,25 +249,27 @@ func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]
if err != nil {
return nil, err
}
var glob watch.PathMatcher = watch.EmptyMatcher{}
if trigger.IsGlobPath() {
glob, err = watch.NewDockerPatternMatcher(trigger.AnchorPath(), []string{trigger.Path})
if err != nil {
return nil, err
}
}

rules = append(rules, watchRule{
Trigger: trigger,
ignore: watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
general,
ignore,
),
service: service.Name,
globPattern: glob,
service: service.Name,
})
}
return rules, nil
}

func isSync(trigger types.Trigger) bool {
return trigger.Action == types.WatchActionSync || trigger.Action == types.WatchActionSyncRestart
}

func (s *composeService) watchEvents(ctx context.Context, project *types.Project, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, rules []watchRule) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@@ -329,7 +338,7 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
return &config, nil
}

func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
func isPathBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
for _, volume := range volumes {
if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {
return true
@@ -607,12 +616,7 @@ func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, proje
// Walks develop.watch.path and checks which files should be copied inside the container
// ignores develop.watch.ignore, Dockerfile, compose files, bind mounted paths and .git
func (s *composeService) initialSync(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, syncer sync.Syncer) error {
dockerIgnores, err := watch.LoadDockerIgnore(service.Build)
if err != nil {
return err
}

dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
ignore, err := watch.GeneralIgnorePatterns(service)
if err != nil {
return err
}
@@ -623,9 +627,7 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project
}
// FIXME .dockerignore
ignoreInitialSync := watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
ignore,
triggerIgnore)

pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
@@ -640,7 +642,9 @@ func (s *composeService) initialSync(ctx context.Context, project *types.Project
//
//nolint:gocyclo
func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]*sync.PathMapping, error) {
fi, err := os.Stat(trigger.Path)
sourcePath := trigger.AnchorPath()

fi, err := os.Stat(sourcePath)
if err != nil {
return nil, err
}
@@ -652,16 +656,17 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
switch mode := fi.Mode(); {
case mode.IsDir():
// process directory
err = filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error {
err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// handle possible path err, just in case...
return err
}
if trigger.Path == path {
if sourcePath == path {
// walk starts at the root directory
return nil
}
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {

if shouldIgnore(filepath.Base(path), ignore) || isPathBindMounted(path, service.Volumes) {
// By definition sync ignores bind mounted paths
if d.IsDir() {
// skip folder
@@ -678,7 +683,7 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
// skip file if it was modified before image creation
return nil
}
rel, err := filepath.Rel(trigger.Path, path)
rel, err := filepath.Rel(sourcePath, path)
if err != nil {
return err
}
@@ -692,9 +697,9 @@ func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Pr
})
case mode.IsRegular():
// process file
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(sourcePath), ignore) && !isPathBindMounted(sourcePath, service.Volumes) {
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: trigger.Path,
HostPath: sourcePath,
ContainerPath: trigger.Target,
})
}
13 changes: 11 additions & 2 deletions pkg/compose/watch_test.go
Original file line number Diff line number Diff line change
@@ -129,6 +129,11 @@ func TestWatch_Sync(t *testing.T) {
Target: "/work",
Ignore: []string{"ignore"},
},
{
Path: "/restart/*/sub",
Action: "sync",
Target: "/foo",
},
{
Path: "/rebuild",
Action: "rebuild",
@@ -147,22 +152,26 @@ func TestWatch_Sync(t *testing.T) {

watcher.Events() <- watch.NewFileEvent("/sync/changed")
watcher.Events() <- watch.NewFileEvent("/sync/changed/sub")
err := clock.BlockUntilContext(ctx, 3)

watcher.Events() <- watch.NewFileEvent("/restart/changed")
watcher.Events() <- watch.NewFileEvent("/restart/changed/sub")
err := clock.BlockUntilContext(ctx, 5)
assert.NilError(t, err)
clock.Advance(watch.QuietPeriod)
select {
case actual := <-syncer.synced:
require.ElementsMatch(t, []*sync.PathMapping{
{HostPath: "/sync/changed", ContainerPath: "/work/changed"},
{HostPath: "/sync/changed/sub", ContainerPath: "/work/changed/sub"},
{HostPath: "/restart/changed/sub", ContainerPath: "/foo/changed/sub"},
}, actual)
case <-time.After(100 * time.Millisecond):
t.Error("timeout")
}

watcher.Events() <- watch.NewFileEvent("/rebuild")
watcher.Events() <- watch.NewFileEvent("/sync/changed")
err = clock.BlockUntilContext(ctx, 4)
err = clock.BlockUntilContext(ctx, 7)
assert.NilError(t, err)
clock.Advance(watch.QuietPeriod)
select {
17 changes: 17 additions & 0 deletions pkg/watch/dockerignore.go
Original file line number Diff line number Diff line change
@@ -96,6 +96,23 @@ func LoadDockerIgnore(build *types.BuildConfig) (PathMatcher, error) {
return NewDockerPatternMatcher(absRoot, patterns)
}

func GeneralIgnorePatterns(service types.ServiceConfig) (PathMatcher, error) {
dockerIgnores, err := LoadDockerIgnore(service.Build)
if err != nil {
return nil, err
}

// add a hardcoded set of ignores on top of what came from .dockerignore
// some of this should likely be configurable (e.g. there could be cases
// where you want `.git` to be synced) but this is suitable for now
dotGitIgnore, err := NewDockerPatternMatcher("/", []string{".git/"})
if err != nil {
return nil, err
}

return NewCompositeMatcher(dockerIgnores, dotGitIgnore, EphemeralPathMatcher()), nil
}

// Make all the patterns use absolute paths.
func absPatterns(absRoot string, patterns []string) []string {
absPatterns := make([]string, 0, len(patterns))
Loading
Oops, something went wrong.