watch.go 22 KB

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