@@ -46,9 +46,10 @@ const (
46
46
)
47
47
48
48
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"`
52
53
}
53
54
54
55
const quietPeriod = 2 * time .Second
@@ -58,23 +59,23 @@ const quietPeriod = 2 * time.Second
58
59
// For file sync, the container path is also included.
59
60
// For rebuild, there is no container path, so it is always empty.
60
61
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.
64
65
//
65
66
// This is the path as seen from the user's perspective, e.g.
66
67
// - C:\Users\moby\Documents\hello-world\main.go
67
68
// - /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
70
71
// for sync events, not rebuild).
71
72
//
72
73
// This is the path as used in Docker CLI commands, e.g.
73
74
// - /workdir/main.go
74
- containerPath string
75
+ ContainerPath string
75
76
}
76
77
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 {
78
79
needRebuild := make (chan fileMapping )
79
80
needSync := make (chan fileMapping )
80
81
@@ -96,20 +97,26 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
96
97
if err != nil {
97
98
return err
98
99
}
100
+ watching := false
99
101
for _ , service := range ss {
100
102
config , err := loadDevelopmentConfig (service , project )
101
103
if err != nil {
102
104
return err
103
105
}
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
+ },
109
117
}
110
- logrus .Infof ("service %s ignored. Can't watch a service without a build section" , name )
111
- continue
112
118
}
119
+ name := service .Name
113
120
bc := service .Build .Context
114
121
115
122
dockerIgnores , err := watch .LoadDockerIgnore (bc )
@@ -140,75 +147,111 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
140
147
if err != nil {
141
148
return err
142
149
}
150
+ watching = true
143
151
144
152
eg .Go (func () error {
145
153
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 )
187
155
})
188
156
}
189
157
158
+ if ! watching {
159
+ return fmt .Errorf ("none of the selected services is configured for watch, consider setting an 'x-develop' section" )
160
+ }
161
+
190
162
return eg .Wait ()
191
163
}
192
164
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 )
197
169
if err != nil {
198
- return config , err
170
+ return err
199
171
}
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
+ }
207
222
}
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" )
209
246
}
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
210
253
}
211
- return config , nil
254
+ return & config , nil
212
255
}
213
256
214
257
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,
264
307
case <- ctx .Done ():
265
308
return nil
266
309
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 () {
268
311
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 ),
271
314
})
272
315
if err != nil {
273
316
return err
274
317
}
275
- fmt .Fprintf (s .stderr (), "%s updated\n " , opt .containerPath )
318
+ fmt .Fprintf (s .stderr (), "%s updated\n " , opt .ContainerPath )
276
319
} else if errors .Is (statErr , fs .ErrNotExist ) {
277
320
_ , 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 },
280
323
Index : 1 ,
281
324
})
282
325
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 )
284
327
}
285
- fmt .Fprintf (s .stderr (), "%s deleted from container\n " , opt .containerPath )
328
+ fmt .Fprintf (s .stderr (), "%s deleted from container\n " , opt .ContainerPath )
286
329
}
287
330
}
288
331
}
@@ -306,12 +349,12 @@ func debounce(ctx context.Context, clock clockwork.Clock, delay time.Duration, i
306
349
return
307
350
case e := <- input :
308
351
t .Reset (delay )
309
- svc , ok := services [e .service ]
352
+ svc , ok := services [e .Service ]
310
353
if ! ok {
311
354
svc = make (utils.Set [string ])
312
- services [e .service ] = svc
355
+ services [e .Service ] = svc
313
356
}
314
- svc .Add (e .hostPath )
357
+ svc .Add (e .HostPath )
315
358
}
316
359
}
317
360
}
0 commit comments