Skip to content

Commit a11515e

Browse files
committed
introduce ignore attribute for watch triggers
Signed-off-by: Nicolas De Loof <[email protected]>
1 parent 6c1f06e commit a11515e

File tree

3 files changed

+225
-84
lines changed

3 files changed

+225
-84
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ jobs:
123123
set: |
124124
*.cache-from=type=gha,scope=test
125125
*.cache-to=type=gha,scope=test
126+
-
127+
name: Upload coverage to Codecov
128+
uses: codecov/codecov-action@v3
126129

127130
e2e:
128131
runs-on: ubuntu-latest

pkg/compose/watch.go

Lines changed: 126 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ const (
4646
)
4747

4848
type Trigger struct {
49-
Path string `json:"path,omitempty"`
50-
Action string `json:"action,omitempty"`
51-
Target string `json:"target,omitempty"`
49+
Path string `json:"path,omitempty"`
50+
Action string `json:"action,omitempty"`
51+
Target string `json:"target,omitempty"`
52+
Ignore []string `json:"ignore,omitempty"`
5253
}
5354

5455
const quietPeriod = 2 * time.Second
@@ -58,23 +59,23 @@ const quietPeriod = 2 * time.Second
5859
// For file sync, the container path is also included.
5960
// For rebuild, there is no container path, so it is always empty.
6061
type fileMapping struct {
61-
// service that the file event is for.
62-
service string
63-
// hostPath that was created/modified/deleted outside the container.
62+
// Service that the file event is for.
63+
Service string
64+
// HostPath that was created/modified/deleted outside the container.
6465
//
6566
// This is the path as seen from the user's perspective, e.g.
6667
// - C:\Users\moby\Documents\hello-world\main.go
6768
// - /Users/moby/Documents/hello-world/main.go
68-
hostPath string
69-
// containerPath for the target file inside the container (only populated
69+
HostPath string
70+
// ContainerPath for the target file inside the container (only populated
7071
// for sync events, not rebuild).
7172
//
7273
// This is the path as used in Docker CLI commands, e.g.
7374
// - /workdir/main.go
74-
containerPath string
75+
ContainerPath string
7576
}
7677

77-
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint:gocyclo
78+
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error {
7879
needRebuild := make(chan fileMapping)
7980
needSync := make(chan fileMapping)
8081

@@ -96,20 +97,26 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
9697
if err != nil {
9798
return err
9899
}
100+
watching := false
99101
for _, service := range ss {
100102
config, err := loadDevelopmentConfig(service, project)
101103
if err != nil {
102104
return err
103105
}
104-
name := service.Name
105-
if service.Build == nil {
106-
if len(services) != 0 || len(config.Watch) != 0 {
107-
// watch explicitly requested on service, but no build section set
108-
return fmt.Errorf("service %s doesn't have a build section", name)
106+
if config == nil {
107+
if service.Build == nil {
108+
continue
109+
}
110+
config = &DevelopmentConfig{
111+
Watch: []Trigger{
112+
{
113+
Path: service.Build.Context,
114+
Action: WatchActionRebuild,
115+
},
116+
},
109117
}
110-
logrus.Infof("service %s ignored. Can't watch a service without a build section", name)
111-
continue
112118
}
119+
name := service.Name
113120
bc := service.Build.Context
114121

115122
dockerIgnores, err := watch.LoadDockerIgnore(bc)
@@ -140,75 +147,111 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
140147
if err != nil {
141148
return err
142149
}
150+
watching = true
143151

144152
eg.Go(func() error {
145153
defer watcher.Close() //nolint:errcheck
146-
WATCH:
147-
for {
148-
select {
149-
case <-ctx.Done():
150-
return nil
151-
case event := <-watcher.Events():
152-
hostPath := event.Path()
153-
154-
for _, trigger := range config.Watch {
155-
logrus.Debugf("change detected on %s - comparing with %s", hostPath, trigger.Path)
156-
if watch.IsChild(trigger.Path, hostPath) {
157-
fmt.Fprintf(s.stderr(), "change detected on %s\n", hostPath)
158-
159-
f := fileMapping{
160-
hostPath: hostPath,
161-
service: name,
162-
}
163-
164-
switch trigger.Action {
165-
case WatchActionSync:
166-
logrus.Debugf("modified file %s triggered sync", hostPath)
167-
rel, err := filepath.Rel(trigger.Path, hostPath)
168-
if err != nil {
169-
return err
170-
}
171-
// always use Unix-style paths for inside the container
172-
f.containerPath = path.Join(trigger.Target, rel)
173-
needSync <- f
174-
case WatchActionRebuild:
175-
logrus.Debugf("modified file %s requires image to be rebuilt", hostPath)
176-
needRebuild <- f
177-
default:
178-
return fmt.Errorf("watch action %q is not supported", trigger)
179-
}
180-
continue WATCH
181-
}
182-
}
183-
case err := <-watcher.Errors():
184-
return err
185-
}
186-
}
154+
return s.watch(ctx, name, watcher, config.Watch, needSync, needRebuild)
187155
})
188156
}
189157

158+
if !watching {
159+
return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'x-develop' section")
160+
}
161+
190162
return eg.Wait()
191163
}
192164

193-
func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (DevelopmentConfig, error) {
194-
var config DevelopmentConfig
195-
if y, ok := service.Extensions["x-develop"]; ok {
196-
err := mapstructure.Decode(y, &config)
165+
func (s *composeService) watch(ctx context.Context, name string, watcher watch.Notify, triggers []Trigger, needSync chan fileMapping, needRebuild chan fileMapping) error {
166+
ignores := make([]watch.PathMatcher, len(triggers))
167+
for i, trigger := range triggers {
168+
ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
197169
if err != nil {
198-
return config, err
170+
return err
199171
}
200-
for i, trigger := range config.Watch {
201-
if !filepath.IsAbs(trigger.Path) {
202-
trigger.Path = filepath.Join(project.WorkingDir, trigger.Path)
203-
}
204-
trigger.Path = filepath.Clean(trigger.Path)
205-
if trigger.Path == "" {
206-
return config, errors.New("watch rules MUST define a path")
172+
ignores[i] = ignore
173+
}
174+
175+
WATCH:
176+
for {
177+
select {
178+
case <-ctx.Done():
179+
return nil
180+
case event := <-watcher.Events():
181+
hostPath := event.Path()
182+
183+
for i, trigger := range triggers {
184+
logrus.Debugf("change detected on %s - comparing with %s", hostPath, trigger.Path)
185+
if watch.IsChild(trigger.Path, hostPath) {
186+
187+
match, err := ignores[i].Matches(hostPath)
188+
if err != nil {
189+
return err
190+
}
191+
192+
if match {
193+
logrus.Debugf("%s is matching ignore pattern", hostPath)
194+
continue
195+
}
196+
197+
fmt.Fprintf(s.stderr(), "change detected on %s\n", hostPath)
198+
199+
f := fileMapping{
200+
HostPath: hostPath,
201+
Service: name,
202+
}
203+
204+
switch trigger.Action {
205+
case WatchActionSync:
206+
logrus.Debugf("modified file %s triggered sync", hostPath)
207+
rel, err := filepath.Rel(trigger.Path, hostPath)
208+
if err != nil {
209+
return err
210+
}
211+
// always use Unix-style paths for inside the container
212+
f.ContainerPath = path.Join(trigger.Target, rel)
213+
needSync <- f
214+
case WatchActionRebuild:
215+
logrus.Debugf("modified file %s requires image to be rebuilt", hostPath)
216+
needRebuild <- f
217+
default:
218+
return fmt.Errorf("watch action %q is not supported", trigger)
219+
}
220+
continue WATCH
221+
}
207222
}
208-
config.Watch[i] = trigger
223+
case err := <-watcher.Errors():
224+
return err
225+
}
226+
}
227+
}
228+
229+
func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (*DevelopmentConfig, error) {
230+
var config DevelopmentConfig
231+
y, ok := service.Extensions["x-develop"]
232+
if !ok {
233+
return nil, nil
234+
}
235+
err := mapstructure.Decode(y, &config)
236+
if err != nil {
237+
return nil, err
238+
}
239+
for i, trigger := range config.Watch {
240+
if !filepath.IsAbs(trigger.Path) {
241+
trigger.Path = filepath.Join(project.WorkingDir, trigger.Path)
242+
}
243+
trigger.Path = filepath.Clean(trigger.Path)
244+
if trigger.Path == "" {
245+
return nil, errors.New("watch rules MUST define a path")
209246
}
247+
248+
if trigger.Action == WatchActionRebuild && service.Build == nil {
249+
return nil, fmt.Errorf("service %s doesn't have a build section, can't apply 'rebuild' on watch", service.Name)
250+
}
251+
252+
config.Watch[i] = trigger
210253
}
211-
return config, nil
254+
return &config, nil
212255
}
213256

214257
func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Project) func(services rebuildServices) {
@@ -264,25 +307,25 @@ func (s *composeService) makeSyncFn(ctx context.Context, project *types.Project,
264307
case <-ctx.Done():
265308
return nil
266309
case opt := <-needSync:
267-
if fi, statErr := os.Stat(opt.hostPath); statErr == nil && !fi.IsDir() {
310+
if fi, statErr := os.Stat(opt.HostPath); statErr == nil && !fi.IsDir() {
268311
err := s.Copy(ctx, project.Name, api.CopyOptions{
269-
Source: opt.hostPath,
270-
Destination: fmt.Sprintf("%s:%s", opt.service, opt.containerPath),
312+
Source: opt.HostPath,
313+
Destination: fmt.Sprintf("%s:%s", opt.Service, opt.ContainerPath),
271314
})
272315
if err != nil {
273316
return err
274317
}
275-
fmt.Fprintf(s.stderr(), "%s updated\n", opt.containerPath)
318+
fmt.Fprintf(s.stderr(), "%s updated\n", opt.ContainerPath)
276319
} else if errors.Is(statErr, fs.ErrNotExist) {
277320
_, err := s.Exec(ctx, project.Name, api.RunOptions{
278-
Service: opt.service,
279-
Command: []string{"rm", "-rf", opt.containerPath},
321+
Service: opt.Service,
322+
Command: []string{"rm", "-rf", opt.ContainerPath},
280323
Index: 1,
281324
})
282325
if err != nil {
283-
logrus.Warnf("failed to delete %q from %s: %v", opt.containerPath, opt.service, err)
326+
logrus.Warnf("failed to delete %q from %s: %v", opt.ContainerPath, opt.Service, err)
284327
}
285-
fmt.Fprintf(s.stderr(), "%s deleted from container\n", opt.containerPath)
328+
fmt.Fprintf(s.stderr(), "%s deleted from container\n", opt.ContainerPath)
286329
}
287330
}
288331
}
@@ -306,12 +349,12 @@ func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, i
306349
return
307350
case e := <-input:
308351
t.Reset(delay)
309-
svc, ok := services[e.service]
352+
svc, ok := services[e.Service]
310353
if !ok {
311354
svc = make(utils.Set[string])
312-
services[e.service] = svc
355+
services[e.Service] = svc
313356
}
314-
svc.Add(e.hostPath)
357+
svc.Add(e.HostPath)
315358
}
316359
}
317360
}

0 commit comments

Comments
 (0)