watch.go 21 KB

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