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 1 commit
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
Next Next commit
Process multiple paths for watch config
Signed-off-by: Joana Hrotko <joana.hrotko@gmail.com>
  • Loading branch information
jhrotko committed Feb 25, 2025
commit b7f6d335afff9042d8ccbaa7099decf998dd1711
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 => ../compose-go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jhrotko 👋
You need to replace compose-go with a version of your fork 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coucou @glours ! 😉
Done

4 changes: 2 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=
@@ -494,6 +492,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=
205 changes: 119 additions & 86 deletions pkg/compose/watch.go
Original file line number Diff line number Diff line change
@@ -84,9 +84,15 @@ type watchRule struct {
service string
}

func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
func (r watchRule) Matches(event watch.FileEvent) []*sync.PathMapping {
hostPath := string(event)
if !pathutil.IsChild(r.Path, hostPath) {
childPaths := []string{}
for _, p := range r.Path {
if pathutil.IsChild(p, hostPath) {
childPaths = append(childPaths, p)
}
}
if len(childPaths) == 0 {
return nil
}
isIgnored, err := r.ignore.Matches(hostPath)
@@ -100,20 +106,29 @@ func (r watchRule) Matches(event watch.FileEvent) *sync.PathMapping {
return nil
}

var containerPath string
if r.Target != "" {
rel, err := filepath.Rel(r.Path, hostPath)
if r.Target == "" {
return []*sync.PathMapping{
&sync.PathMapping{
HostPath: hostPath,
},
}
}

var res []*sync.PathMapping
for _, p := range childPaths {
rel, err := filepath.Rel(p, 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, p, err)
return nil
}
// always use Unix-style paths for inside the container
containerPath = path.Join(r.Target, filepath.ToSlash(rel))
}
return &sync.PathMapping{
HostPath: hostPath,
ContainerPath: containerPath,
containerPath := path.Join(r.Target, filepath.ToSlash(rel))
res = append(res, &sync.PathMapping{
HostPath: hostPath,
ContainerPath: containerPath,
})
}
return res
}

func (s *composeService) watch(ctx context.Context, syncChannel chan bool, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
@@ -161,9 +176,13 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
}

for _, trigger := range config.Watch {
if isSync(trigger) && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
continue
if isSync(trigger) {
for _, p := range trigger.Path {
if checkIfPathAlreadyBindMounted(p, service.Volumes) {
logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", p)
continue
}
}
} else {
var initialSync bool
success, err := trigger.Extensions.Get("x-initialSync", &initialSync)
@@ -175,7 +194,7 @@ func (s *composeService) watch(ctx context.Context, syncChannel chan bool, proje
}
}
}
paths = append(paths, trigger.Path)
paths = append(paths, trigger.Path...)
}

serviceWatchRules, err := getWatchRules(config, service)
@@ -238,18 +257,21 @@ func getWatchRules(config *types.DevelopConfig, service types.ServiceConfig) ([]
}

for _, trigger := range config.Watch {
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
if err != nil {
return nil, err
var ignorePaths []watch.PathMatcher

for _, p := range trigger.Path {
ignore, err := watch.NewDockerPatternMatcher(p, trigger.Ignore)
if err != nil {
return nil, err
}
ignorePaths = append(ignorePaths, ignore)
}

ignorePaths = append(ignorePaths, dockerIgnores, dotGitIgnore, watch.EphemeralPathMatcher())
rules = append(rules, watchRule{
Trigger: trigger,
ignore: watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
ignore,
ignorePaths...,
),
service: service.Name,
})
@@ -305,16 +327,18 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
}

for i, trigger := range config.Watch {
if !filepath.IsAbs(trigger.Path) {
trigger.Path = filepath.Join(baseDir, trigger.Path)
}
if p, err := filepath.EvalSymlinks(trigger.Path); err == nil {
// this might fail because the path doesn't exist, etc.
trigger.Path = p
}
trigger.Path = filepath.Clean(trigger.Path)
if trigger.Path == "" {
return nil, errors.New("watch rules MUST define a path")
for j, p := range trigger.Path {
if !filepath.IsAbs(p) {
trigger.Path[j] = filepath.Join(baseDir, p)
}
if p, err := filepath.EvalSymlinks(p); err == nil {
// this might fail because the path doesn't exist, etc.
trigger.Path[j] = p
}
trigger.Path[j] = filepath.Clean(p)
if trigger.Path[j] == "" {
return nil, errors.New("watch rules MUST define a path")
}
}

if trigger.Action == types.WatchActionRebuild && service.Build == nil {
@@ -427,22 +451,22 @@ func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Pr
for _, event := range batch {
for i, rule := range rules {
mapping := rule.Matches(event)
if mapping == nil {
if len(mapping) == 0 {
continue
}

switch rule.Action {
case types.WatchActionRebuild:
rebuild[rule.service] = true
case types.WatchActionSync:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
syncfiles[rule.service] = append(syncfiles[rule.service], mapping...)
case types.WatchActionRestart:
restart[rule.service] = true
case types.WatchActionSyncRestart:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
syncfiles[rule.service] = append(syncfiles[rule.service], mapping...)
restart[rule.service] = true
case types.WatchActionSyncExec:
syncfiles[rule.service] = append(syncfiles[rule.service], mapping)
syncfiles[rule.service] = append(syncfiles[rule.service], mapping...)
// We want to run exec hooks only once after syncfiles if multiple file events match
// as we can't compare ServiceHook to sort and compact a slice, collect rule indexes
exec[rule.service] = append(exec[rule.service], i)
@@ -607,26 +631,32 @@ 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 {
var allIgnores []watch.PathMatcher

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

dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
if err != nil {
return err
}
allIgnores = append(allIgnores, dotGitIgnore)
allIgnores = append(allIgnores, watch.EphemeralPathMatcher())

triggerIgnore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
if err != nil {
return err
for _, p := range trigger.Path {
triggerIgnore, err := watch.NewDockerPatternMatcher(p, trigger.Ignore)
if err != nil {
return err
}
allIgnores = append(allIgnores, triggerIgnore)
}
// FIXME .dockerignore
ignoreInitialSync := watch.NewCompositeMatcher(
dockerIgnores,
watch.EphemeralPathMatcher(),
dotGitIgnore,
triggerIgnore)
allIgnores...,
)

pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
if err != nil {
@@ -640,63 +670,66 @@ 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)
if err != nil {
return nil, err
}
timeImageCreated, err := s.imageCreatedTime(ctx, project, service.Name)
if err != nil {
return nil, err
}
var pathsToCopy []*sync.PathMapping
switch mode := fi.Mode(); {
case mode.IsDir():
// process directory
err = filepath.WalkDir(trigger.Path, 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 {
// walk starts at the root directory
return nil
}
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
// By definition sync ignores bind mounted paths
if d.IsDir() {
// skip folder
return fs.SkipDir

for _, p := range trigger.Path {
fi, err := os.Stat(p)
if err != nil {
return nil, err
}
switch mode := fi.Mode(); {
case mode.IsDir():
// process directory
err = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error {
if err != nil {
// handle possible path err, just in case...
return err
}
return nil // skip file
}
info, err := d.Info()
if err != nil {
return err
}
if !d.IsDir() {
if info.ModTime().Before(timeImageCreated) {
// skip file if it was modified before image creation
if p == path {
// walk starts at the root directory
return nil
}
rel, err := filepath.Rel(trigger.Path, path)
if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
// By definition sync ignores bind mounted paths
if d.IsDir() {
// skip folder
return fs.SkipDir
}
return nil // skip file
}
info, err := d.Info()
if err != nil {
return err
}
// only copy files (and not full directories)
if !d.IsDir() {
if info.ModTime().Before(timeImageCreated) {
// skip file if it was modified before image creation
return nil
}
rel, err := filepath.Rel(p, path)
if err != nil {
return err
}
// only copy files (and not full directories)
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: path,
ContainerPath: filepath.Join(trigger.Target, rel),
})
}
return nil
})
case mode.IsRegular():
// process file
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(p), ignore) && !checkIfPathAlreadyBindMounted(p, service.Volumes) {
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: path,
ContainerPath: filepath.Join(trigger.Target, rel),
HostPath: p,
ContainerPath: trigger.Target,
})
}
return nil
})
case mode.IsRegular():
// process file
if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
pathsToCopy = append(pathsToCopy, &sync.PathMapping{
HostPath: trigger.Path,
ContainerPath: trigger.Target,
})
}
}
return pathsToCopy, err