watch.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "io"
  19. "io/fs"
  20. "os"
  21. "path"
  22. "path/filepath"
  23. "sort"
  24. "strconv"
  25. "strings"
  26. "time"
  27. "github.com/compose-spec/compose-go/v2/types"
  28. pathutil "github.com/docker/compose/v2/internal/paths"
  29. "github.com/docker/compose/v2/internal/sync"
  30. "github.com/docker/compose/v2/pkg/api"
  31. "github.com/docker/compose/v2/pkg/watch"
  32. moby "github.com/docker/docker/api/types"
  33. "github.com/docker/docker/api/types/container"
  34. "github.com/docker/docker/api/types/filters"
  35. "github.com/docker/docker/api/types/image"
  36. "github.com/jonboulle/clockwork"
  37. "github.com/mitchellh/mapstructure"
  38. "github.com/sirupsen/logrus"
  39. "golang.org/x/sync/errgroup"
  40. )
  41. const quietPeriod = 500 * time.Millisecond
  42. // fileEvent contains the Compose service and modified host system path.
  43. type fileEvent struct {
  44. sync.PathMapping
  45. Action types.WatchAction
  46. }
  47. // getSyncImplementation returns an appropriate sync implementation for the
  48. // project.
  49. //
  50. // Currently, an implementation that batches files and transfers them using
  51. // the Moby `Untar` API.
  52. func (s *composeService) getSyncImplementation(project *types.Project) (sync.Syncer, error) {
  53. var useTar bool
  54. if useTarEnv, ok := os.LookupEnv("COMPOSE_EXPERIMENTAL_WATCH_TAR"); ok {
  55. useTar, _ = strconv.ParseBool(useTarEnv)
  56. } else {
  57. useTar = true
  58. }
  59. if !useTar {
  60. return nil, errors.New("no available sync implementation")
  61. }
  62. return sync.NewTar(project.Name, tarDockerClient{s: s}), nil
  63. }
  64. func (s *composeService) shouldWatch(project *types.Project) bool {
  65. var shouldWatch bool
  66. for i := range project.Services {
  67. service := project.Services[i]
  68. if service.Develop != nil && service.Develop.Watch != nil {
  69. shouldWatch = true
  70. }
  71. }
  72. return shouldWatch
  73. }
  74. func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
  75. return s.watch(ctx, nil, project, services, options)
  76. }
  77. func (s *composeService) watch(ctx context.Context, syncChannel chan bool, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
  78. var err error
  79. if project, err = project.WithSelectedServices(services); err != nil {
  80. return err
  81. }
  82. syncer, err := s.getSyncImplementation(project)
  83. if err != nil {
  84. return err
  85. }
  86. eg, ctx := errgroup.WithContext(ctx)
  87. watching := false
  88. options.LogTo.Register(api.WatchLogger)
  89. for i := range project.Services {
  90. service := project.Services[i]
  91. config, err := loadDevelopmentConfig(service, project)
  92. if err != nil {
  93. return err
  94. }
  95. if service.Develop != nil {
  96. config = service.Develop
  97. }
  98. if config == nil {
  99. continue
  100. }
  101. for _, trigger := range config.Watch {
  102. if trigger.Action == types.WatchActionRebuild {
  103. if service.Build == nil {
  104. return fmt.Errorf("can't watch service %q with action %s without a build context", service.Name, types.WatchActionRebuild)
  105. }
  106. if options.Build == nil {
  107. return fmt.Errorf("--no-build is incompatible with watch action %s in service %s", types.WatchActionRebuild, service.Name)
  108. }
  109. }
  110. }
  111. if len(services) > 0 && service.Build == nil {
  112. // service explicitly selected for watch has no build section
  113. return fmt.Errorf("can't watch service %q without a build context", service.Name)
  114. }
  115. if len(services) == 0 && service.Build == nil {
  116. continue
  117. }
  118. // set the service to always be built - watch triggers `Up()` when it receives a rebuild event
  119. service.PullPolicy = types.PullPolicyBuild
  120. project.Services[i] = service
  121. dockerIgnores, err := watch.LoadDockerIgnore(service.Build.Context)
  122. if err != nil {
  123. return err
  124. }
  125. // add a hardcoded set of ignores on top of what came from .dockerignore
  126. // some of this should likely be configurable (e.g. there could be cases
  127. // where you want `.git` to be synced) but this is suitable for now
  128. dotGitIgnore, err := watch.NewDockerPatternMatcher("/", []string{".git/"})
  129. if err != nil {
  130. return err
  131. }
  132. ignore := watch.NewCompositeMatcher(
  133. dockerIgnores,
  134. watch.EphemeralPathMatcher(),
  135. dotGitIgnore,
  136. )
  137. var paths, pathLogs []string
  138. for _, trigger := range config.Watch {
  139. if trigger.Action != types.WatchActionRebuild && checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
  140. logrus.Warnf("path '%s' also declared by a bind mount volume, this path won't be monitored!\n", trigger.Path)
  141. continue
  142. } else {
  143. var initialSync bool
  144. success, err := trigger.Extensions.Get("x-initialSync", &initialSync)
  145. if err == nil && success && initialSync && (trigger.Action == types.WatchActionSync || trigger.Action == types.WatchActionSyncRestart) {
  146. // Need to check initial files are in container that are meant to be synched from watch action
  147. err := s.initialSync(ctx, project, service, trigger, ignore, syncer)
  148. if err != nil {
  149. return err
  150. }
  151. }
  152. }
  153. paths = append(paths, trigger.Path)
  154. pathLogs = append(pathLogs, fmt.Sprintf("Action %s for path %q", trigger.Action, trigger.Path))
  155. }
  156. watcher, err := watch.NewWatcher(paths, ignore)
  157. if err != nil {
  158. return err
  159. }
  160. logrus.Debugf("Watch configuration for service %q:%s\n",
  161. service.Name,
  162. strings.Join(append([]string{""}, pathLogs...), "\n - "),
  163. )
  164. err = watcher.Start()
  165. if err != nil {
  166. return err
  167. }
  168. watching = true
  169. eg.Go(func() error {
  170. defer func() {
  171. if err := watcher.Close(); err != nil {
  172. logrus.Debugf("Error closing watcher for service %s: %v", service.Name, err)
  173. }
  174. }()
  175. return s.watchEvents(ctx, project, service.Name, options, watcher, syncer, config.Watch)
  176. })
  177. }
  178. if !watching {
  179. return fmt.Errorf("none of the selected services is configured for watch, consider setting an 'develop' section")
  180. }
  181. options.LogTo.Log(api.WatchLogger, "Watch enabled")
  182. for {
  183. select {
  184. case <-ctx.Done():
  185. return eg.Wait()
  186. case <-syncChannel:
  187. options.LogTo.Log(api.WatchLogger, "Watch disabled")
  188. return nil
  189. }
  190. }
  191. }
  192. func (s *composeService) watchEvents(ctx context.Context, project *types.Project, name string, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, triggers []types.Trigger) error {
  193. ctx, cancel := context.WithCancel(ctx)
  194. defer cancel()
  195. ignores := make([]watch.PathMatcher, len(triggers))
  196. for i, trigger := range triggers {
  197. ignore, err := watch.NewDockerPatternMatcher(trigger.Path, trigger.Ignore)
  198. if err != nil {
  199. return err
  200. }
  201. ignores[i] = ignore
  202. }
  203. events := make(chan fileEvent)
  204. batchEvents := batchDebounceEvents(ctx, s.clock, quietPeriod, events)
  205. quit := make(chan bool)
  206. go func() {
  207. for {
  208. select {
  209. case <-ctx.Done():
  210. quit <- true
  211. return
  212. case batch := <-batchEvents:
  213. start := time.Now()
  214. logrus.Debugf("batch start: service[%s] count[%d]", name, len(batch))
  215. if err := s.handleWatchBatch(ctx, project, name, options, batch, syncer); err != nil {
  216. logrus.Warnf("Error handling changed files for service %s: %v", name, err)
  217. }
  218. logrus.Debugf("batch complete: service[%s] duration[%s] count[%d]",
  219. name, time.Since(start), len(batch))
  220. }
  221. }
  222. }()
  223. for {
  224. select {
  225. case <-quit:
  226. options.LogTo.Log(api.WatchLogger, "Watch disabled")
  227. return nil
  228. case err := <-watcher.Errors():
  229. options.LogTo.Err(api.WatchLogger, "Watch disabled with errors")
  230. return err
  231. case event := <-watcher.Events():
  232. hostPath := event.Path()
  233. for i, trigger := range triggers {
  234. logrus.Debugf("change for %s - comparing with %s", hostPath, trigger.Path)
  235. if fileEvent := maybeFileEvent(trigger, hostPath, ignores[i]); fileEvent != nil {
  236. events <- *fileEvent
  237. }
  238. }
  239. }
  240. }
  241. }
  242. // maybeFileEvent returns a file event object if hostPath is valid for the provided trigger and ignore
  243. // rules.
  244. //
  245. // Any errors are logged as warnings and nil (no file event) is returned.
  246. func maybeFileEvent(trigger types.Trigger, hostPath string, ignore watch.PathMatcher) *fileEvent {
  247. if !pathutil.IsChild(trigger.Path, hostPath) {
  248. return nil
  249. }
  250. isIgnored, err := ignore.Matches(hostPath)
  251. if err != nil {
  252. logrus.Warnf("error ignore matching %q: %v", hostPath, err)
  253. return nil
  254. }
  255. if isIgnored {
  256. logrus.Debugf("%s is matching ignore pattern", hostPath)
  257. return nil
  258. }
  259. var containerPath string
  260. if trigger.Target != "" {
  261. rel, err := filepath.Rel(trigger.Path, hostPath)
  262. if err != nil {
  263. logrus.Warnf("error making %s relative to %s: %v", hostPath, trigger.Path, err)
  264. return nil
  265. }
  266. // always use Unix-style paths for inside the container
  267. containerPath = path.Join(trigger.Target, rel)
  268. }
  269. return &fileEvent{
  270. Action: trigger.Action,
  271. PathMapping: sync.PathMapping{
  272. HostPath: hostPath,
  273. ContainerPath: containerPath,
  274. },
  275. }
  276. }
  277. func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project) (*types.DevelopConfig, error) {
  278. var config types.DevelopConfig
  279. y, ok := service.Extensions["x-develop"]
  280. if !ok {
  281. return nil, nil
  282. }
  283. logrus.Warnf("x-develop is DEPRECATED, please use the official `develop` attribute")
  284. err := mapstructure.Decode(y, &config)
  285. if err != nil {
  286. return nil, err
  287. }
  288. baseDir, err := filepath.EvalSymlinks(project.WorkingDir)
  289. if err != nil {
  290. return nil, fmt.Errorf("resolving symlink for %q: %w", project.WorkingDir, err)
  291. }
  292. for i, trigger := range config.Watch {
  293. if !filepath.IsAbs(trigger.Path) {
  294. trigger.Path = filepath.Join(baseDir, trigger.Path)
  295. }
  296. if p, err := filepath.EvalSymlinks(trigger.Path); err == nil {
  297. // this might fail because the path doesn't exist, etc.
  298. trigger.Path = p
  299. }
  300. trigger.Path = filepath.Clean(trigger.Path)
  301. if trigger.Path == "" {
  302. return nil, errors.New("watch rules MUST define a path")
  303. }
  304. if trigger.Action == types.WatchActionRebuild && service.Build == nil {
  305. return nil, fmt.Errorf("service %s doesn't have a build section, can't apply 'rebuild' on watch", service.Name)
  306. }
  307. config.Watch[i] = trigger
  308. }
  309. return &config, nil
  310. }
  311. // batchDebounceEvents groups identical file events within a sliding time window and writes the results to the returned
  312. // channel.
  313. //
  314. // The returned channel is closed when the debouncer is stopped via context cancellation or by closing the input channel.
  315. func batchDebounceEvents(ctx context.Context, clock clockwork.Clock, delay time.Duration, input <-chan fileEvent) <-chan []fileEvent {
  316. out := make(chan []fileEvent)
  317. go func() {
  318. defer close(out)
  319. seen := make(map[fileEvent]time.Time)
  320. flushEvents := func() {
  321. if len(seen) == 0 {
  322. return
  323. }
  324. events := make([]fileEvent, 0, len(seen))
  325. for e := range seen {
  326. events = append(events, e)
  327. }
  328. // sort batch by oldest -> newest
  329. // (if an event is seen > 1 per batch, it gets the latest timestamp)
  330. sort.SliceStable(events, func(i, j int) bool {
  331. x := events[i]
  332. y := events[j]
  333. return seen[x].Before(seen[y])
  334. })
  335. out <- events
  336. seen = make(map[fileEvent]time.Time)
  337. }
  338. t := clock.NewTicker(delay)
  339. defer t.Stop()
  340. for {
  341. select {
  342. case <-ctx.Done():
  343. return
  344. case <-t.Chan():
  345. flushEvents()
  346. case e, ok := <-input:
  347. if !ok {
  348. // input channel was closed
  349. flushEvents()
  350. return
  351. }
  352. seen[e] = time.Now()
  353. t.Reset(delay)
  354. }
  355. }
  356. }()
  357. return out
  358. }
  359. func checkIfPathAlreadyBindMounted(watchPath string, volumes []types.ServiceVolumeConfig) bool {
  360. for _, volume := range volumes {
  361. if volume.Bind != nil && strings.HasPrefix(watchPath, volume.Source) {
  362. return true
  363. }
  364. }
  365. return false
  366. }
  367. type tarDockerClient struct {
  368. s *composeService
  369. }
  370. func (t tarDockerClient) ContainersForService(ctx context.Context, projectName string, serviceName string) ([]moby.Container, error) {
  371. containers, err := t.s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
  372. if err != nil {
  373. return nil, err
  374. }
  375. return containers, nil
  376. }
  377. func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []string, in io.Reader) error {
  378. execCfg := container.ExecOptions{
  379. Cmd: cmd,
  380. AttachStdout: false,
  381. AttachStderr: true,
  382. AttachStdin: in != nil,
  383. Tty: false,
  384. }
  385. execCreateResp, err := t.s.apiClient().ContainerExecCreate(ctx, containerID, execCfg)
  386. if err != nil {
  387. return err
  388. }
  389. startCheck := container.ExecStartOptions{Tty: false, Detach: false}
  390. conn, err := t.s.apiClient().ContainerExecAttach(ctx, execCreateResp.ID, startCheck)
  391. if err != nil {
  392. return err
  393. }
  394. defer conn.Close()
  395. var eg errgroup.Group
  396. if in != nil {
  397. eg.Go(func() error {
  398. defer func() {
  399. _ = conn.CloseWrite()
  400. }()
  401. _, err := io.Copy(conn.Conn, in)
  402. return err
  403. })
  404. }
  405. eg.Go(func() error {
  406. _, err := io.Copy(t.s.stdinfo(), conn.Reader)
  407. return err
  408. })
  409. err = t.s.apiClient().ContainerExecStart(ctx, execCreateResp.ID, startCheck)
  410. if err != nil {
  411. return err
  412. }
  413. // although the errgroup is not tied directly to the context, the operations
  414. // in it are reading/writing to the connection, which is tied to the context,
  415. // so they won't block indefinitely
  416. if err := eg.Wait(); err != nil {
  417. return err
  418. }
  419. execResult, err := t.s.apiClient().ContainerExecInspect(ctx, execCreateResp.ID)
  420. if err != nil {
  421. return err
  422. }
  423. if execResult.Running {
  424. return errors.New("process still running")
  425. }
  426. if execResult.ExitCode != 0 {
  427. return fmt.Errorf("exit code %d", execResult.ExitCode)
  428. }
  429. return nil
  430. }
  431. func (t tarDockerClient) Untar(ctx context.Context, id string, archive io.ReadCloser) error {
  432. return t.s.apiClient().CopyToContainer(ctx, id, "/", archive, container.CopyToContainerOptions{
  433. CopyUIDGID: true,
  434. })
  435. }
  436. func (s *composeService) handleWatchBatch(ctx context.Context, project *types.Project, serviceName string, options api.WatchOptions, batch []fileEvent, syncer sync.Syncer) error {
  437. pathMappings := make([]sync.PathMapping, len(batch))
  438. restartService := false
  439. for i := range batch {
  440. if batch[i].Action == types.WatchActionRebuild {
  441. options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service %q after changes were detected...", serviceName))
  442. // restrict the build to ONLY this service, not any of its dependencies
  443. options.Build.Services = []string{serviceName}
  444. imageNameToIdMap, err := s.build(ctx, project, *options.Build, nil)
  445. if err != nil {
  446. options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Build failed. Error: %v", err))
  447. return err
  448. }
  449. if options.Prune {
  450. s.pruneDanglingImagesOnRebuild(ctx, project.Name, imageNameToIdMap)
  451. }
  452. options.LogTo.Log(api.WatchLogger, fmt.Sprintf("service %q successfully built", serviceName))
  453. err = s.create(ctx, project, api.CreateOptions{
  454. Services: []string{serviceName},
  455. Inherit: true,
  456. Recreate: api.RecreateForce,
  457. })
  458. if err != nil {
  459. options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Failed to recreate service after update. Error: %v", err))
  460. return err
  461. }
  462. err = s.start(ctx, project.Name, api.StartOptions{
  463. Project: project,
  464. Services: []string{serviceName},
  465. }, nil)
  466. if err != nil {
  467. options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Application failed to start after update. Error: %v", err))
  468. }
  469. return nil
  470. }
  471. if batch[i].Action == types.WatchActionSyncRestart {
  472. restartService = true
  473. }
  474. pathMappings[i] = batch[i].PathMapping
  475. }
  476. writeWatchSyncMessage(options.LogTo, serviceName, pathMappings)
  477. service, err := project.GetService(serviceName)
  478. if err != nil {
  479. return err
  480. }
  481. if err := syncer.Sync(ctx, service, pathMappings); err != nil {
  482. return err
  483. }
  484. if restartService {
  485. return s.Restart(ctx, project.Name, api.RestartOptions{
  486. Services: []string{serviceName},
  487. Project: project,
  488. NoDeps: false,
  489. })
  490. }
  491. return nil
  492. }
  493. // writeWatchSyncMessage prints out a message about the sync for the changed paths.
  494. func writeWatchSyncMessage(log api.LogConsumer, serviceName string, pathMappings []sync.PathMapping) {
  495. const maxPathsToShow = 10
  496. if len(pathMappings) <= maxPathsToShow || logrus.IsLevelEnabled(logrus.DebugLevel) {
  497. hostPathsToSync := make([]string, len(pathMappings))
  498. for i := range pathMappings {
  499. hostPathsToSync[i] = pathMappings[i].HostPath
  500. }
  501. log.Log(api.WatchLogger, fmt.Sprintf("Syncing %q after changes were detected", serviceName))
  502. } else {
  503. hostPathsToSync := make([]string, len(pathMappings))
  504. for i := range pathMappings {
  505. hostPathsToSync[i] = pathMappings[i].HostPath
  506. }
  507. log.Log(api.WatchLogger, fmt.Sprintf("Syncing service %q after %d changes were detected", serviceName, len(pathMappings)))
  508. }
  509. }
  510. func (s *composeService) pruneDanglingImagesOnRebuild(ctx context.Context, projectName string, imageNameToIdMap map[string]string) {
  511. images, err := s.apiClient().ImageList(ctx, image.ListOptions{
  512. Filters: filters.NewArgs(
  513. filters.Arg("dangling", "true"),
  514. filters.Arg("label", api.ProjectLabel+"="+projectName),
  515. ),
  516. })
  517. if err != nil {
  518. logrus.Debugf("Failed to list images: %v", err)
  519. return
  520. }
  521. for _, img := range images {
  522. if _, ok := imageNameToIdMap[img.ID]; !ok {
  523. _, err := s.apiClient().ImageRemove(ctx, img.ID, image.RemoveOptions{})
  524. if err != nil {
  525. logrus.Debugf("Failed to remove image %s: %v", img.ID, err)
  526. }
  527. }
  528. }
  529. }
  530. // Walks develop.watch.path and checks which files should be copied inside the container
  531. // ignores develop.watch.ignore, Dockerfile, compose files, bind mounted paths and .git
  532. func (s *composeService) initialSync(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher, syncer sync.Syncer) error {
  533. dockerFileIgnore, err := watch.NewDockerPatternMatcher("/", []string{"Dockerfile", "*compose*.y*ml"})
  534. if err != nil {
  535. return err
  536. }
  537. triggerIgnore, err := watch.NewDockerPatternMatcher("/", trigger.Ignore)
  538. if err != nil {
  539. return err
  540. }
  541. ignoreInitialSync := watch.NewCompositeMatcher(ignore, dockerFileIgnore, triggerIgnore)
  542. pathsToCopy, err := s.initialSyncFiles(ctx, project, service, trigger, ignoreInitialSync)
  543. if err != nil {
  544. return err
  545. }
  546. return syncer.Sync(ctx, service, pathsToCopy)
  547. }
  548. // Syncs files from develop.watch.path if thy have been modified after the image has been created
  549. //
  550. //nolint:gocyclo
  551. func (s *composeService) initialSyncFiles(ctx context.Context, project *types.Project, service types.ServiceConfig, trigger types.Trigger, ignore watch.PathMatcher) ([]sync.PathMapping, error) {
  552. fi, err := os.Stat(trigger.Path)
  553. if err != nil {
  554. return nil, err
  555. }
  556. timeImageCreated, err := s.imageCreatedTime(ctx, project, service.Name)
  557. if err != nil {
  558. return nil, err
  559. }
  560. var pathsToCopy []sync.PathMapping
  561. switch mode := fi.Mode(); {
  562. case mode.IsDir():
  563. // process directory
  564. err = filepath.WalkDir(trigger.Path, func(path string, d fs.DirEntry, err error) error {
  565. if err != nil {
  566. // handle possible path err, just in case...
  567. return err
  568. }
  569. if trigger.Path == path {
  570. // walk starts at the root directory
  571. return nil
  572. }
  573. if shouldIgnore(filepath.Base(path), ignore) || checkIfPathAlreadyBindMounted(path, service.Volumes) {
  574. // By definition sync ignores bind mounted paths
  575. if d.IsDir() {
  576. // skip folder
  577. return fs.SkipDir
  578. }
  579. return nil // skip file
  580. }
  581. info, err := d.Info()
  582. if err != nil {
  583. return err
  584. }
  585. if !d.IsDir() {
  586. if info.ModTime().Before(timeImageCreated) {
  587. // skip file if it was modified before image creation
  588. return nil
  589. }
  590. rel, err := filepath.Rel(trigger.Path, path)
  591. if err != nil {
  592. return err
  593. }
  594. // only copy files (and not full directories)
  595. pathsToCopy = append(pathsToCopy, sync.PathMapping{
  596. HostPath: path,
  597. ContainerPath: filepath.Join(trigger.Target, rel),
  598. })
  599. }
  600. return nil
  601. })
  602. case mode.IsRegular():
  603. // process file
  604. if fi.ModTime().After(timeImageCreated) && !shouldIgnore(filepath.Base(trigger.Path), ignore) && !checkIfPathAlreadyBindMounted(trigger.Path, service.Volumes) {
  605. pathsToCopy = append(pathsToCopy, sync.PathMapping{
  606. HostPath: trigger.Path,
  607. ContainerPath: trigger.Target,
  608. })
  609. }
  610. }
  611. return pathsToCopy, err
  612. }
  613. func shouldIgnore(name string, ignore watch.PathMatcher) bool {
  614. shouldIgnore, _ := ignore.Matches(name)
  615. // ignore files that match any ignore pattern
  616. return shouldIgnore
  617. }
  618. // gets the image creation time for a service
  619. func (s *composeService) imageCreatedTime(ctx context.Context, project *types.Project, serviceName string) (time.Time, error) {
  620. containers, err := s.apiClient().ContainerList(ctx, container.ListOptions{
  621. All: true,
  622. Filters: filters.NewArgs(
  623. filters.Arg("label", fmt.Sprintf("%s=%s", api.ProjectLabel, project.Name)),
  624. filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName))),
  625. })
  626. if err != nil {
  627. return time.Now(), err
  628. }
  629. if len(containers) == 0 {
  630. return time.Now(), fmt.Errorf("Could not get created time for service's image")
  631. }
  632. img, _, err := s.apiClient().ImageInspectWithRaw(ctx, containers[0].ImageID)
  633. if err != nil {
  634. return time.Now(), err
  635. }
  636. // Need to get oldest one?
  637. timeCreated, err := time.Parse(time.RFC3339Nano, img.Created)
  638. if err != nil {
  639. return time.Now(), err
  640. }
  641. return timeCreated, nil
  642. }