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
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
89 changes: 47 additions & 42 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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,
})
}
Expand Down
13 changes: 11 additions & 2 deletions pkg/compose/watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions pkg/watch/dockerignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading