@@ -46,9 +46,10 @@ const (
4646)
4747
4848type 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
5455const 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.
6061type 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
214257func (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