watch.go 21 KB

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