up.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. "os"
  19. "os/signal"
  20. "sync/atomic"
  21. "syscall"
  22. "github.com/compose-spec/compose-go/v2/types"
  23. cerrdefs "github.com/containerd/errdefs"
  24. "github.com/docker/cli/cli"
  25. "github.com/docker/compose/v2/cmd/formatter"
  26. "github.com/docker/compose/v2/internal/tracing"
  27. "github.com/docker/compose/v2/pkg/api"
  28. "github.com/docker/compose/v2/pkg/progress"
  29. "github.com/docker/docker/errdefs"
  30. "github.com/eiannone/keyboard"
  31. "github.com/hashicorp/go-multierror"
  32. "github.com/sirupsen/logrus"
  33. )
  34. func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
  35. err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
  36. err := s.create(ctx, project, options.Create)
  37. if err != nil {
  38. return err
  39. }
  40. if options.Start.Attach == nil {
  41. return s.start(ctx, project.Name, options.Start, nil)
  42. }
  43. return nil
  44. }), s.stdinfo())
  45. if err != nil {
  46. return err
  47. }
  48. if options.Start.Attach == nil {
  49. return err
  50. }
  51. if s.dryRun {
  52. _, _ = fmt.Fprintln(s.stdout(), "end of 'compose up' output, interactive run is not supported in dry-run mode")
  53. return err
  54. }
  55. var eg multierror.Group
  56. // if we get a second signal during shutdown, we kill the services
  57. // immediately, so the channel needs to have sufficient capacity or
  58. // we might miss a signal while setting up the second channel read
  59. // (this is also why signal.Notify is used vs signal.NotifyContext)
  60. signalChan := make(chan os.Signal, 2)
  61. defer close(signalChan)
  62. signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
  63. defer signal.Stop(signalChan)
  64. var isTerminated atomic.Bool
  65. var (
  66. logConsumer = options.Start.Attach
  67. navigationMenu *formatter.LogKeyboard
  68. kEvents <-chan keyboard.KeyEvent
  69. )
  70. if options.Start.NavigationMenu {
  71. kEvents, err = keyboard.GetKeys(100)
  72. if err != nil {
  73. logrus.Warnf("could not start menu, an error occurred while starting: %v", err)
  74. options.Start.NavigationMenu = false
  75. } else {
  76. defer keyboard.Close() //nolint:errcheck
  77. isDockerDesktopActive := s.isDesktopIntegrationActive()
  78. tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive)
  79. navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan)
  80. logConsumer = navigationMenu.Decorate(logConsumer)
  81. }
  82. }
  83. tui := formatter.NewStopping(logConsumer)
  84. defer tui.Close()
  85. logConsumer = tui
  86. watcher, err := NewWatcher(project, options, s.watch, logConsumer)
  87. if err != nil && options.Start.Watch {
  88. return err
  89. }
  90. if navigationMenu != nil && watcher != nil {
  91. navigationMenu.EnableWatch(options.Start.Watch, watcher)
  92. }
  93. printer := newLogPrinter(logConsumer)
  94. doneCh := make(chan bool)
  95. eg.Go(func() error {
  96. first := true
  97. gracefulTeardown := func() {
  98. tui.ApplicationTermination()
  99. eg.Go(func() error {
  100. return progress.RunWithLog(context.WithoutCancel(ctx), func(ctx context.Context) error {
  101. return s.stop(ctx, project.Name, api.StopOptions{
  102. Services: options.Create.Services,
  103. Project: project,
  104. }, printer.HandleEvent)
  105. }, s.stdinfo(), logConsumer)
  106. })
  107. isTerminated.Store(true)
  108. first = false
  109. }
  110. for {
  111. select {
  112. case <-doneCh:
  113. if watcher != nil {
  114. return watcher.Stop()
  115. }
  116. return nil
  117. case <-ctx.Done():
  118. if first {
  119. gracefulTeardown()
  120. }
  121. case <-signalChan:
  122. if first {
  123. keyboard.Close() //nolint:errcheck
  124. gracefulTeardown()
  125. break
  126. }
  127. eg.Go(func() error {
  128. err := s.kill(context.WithoutCancel(ctx), project.Name, api.KillOptions{
  129. Services: options.Create.Services,
  130. Project: project,
  131. All: true,
  132. })
  133. // Ignore errors indicating that some of the containers were already stopped or removed.
  134. if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
  135. return nil
  136. }
  137. return err
  138. })
  139. return nil
  140. case event := <-kEvents:
  141. navigationMenu.HandleKeyEvents(ctx, event, project, options)
  142. }
  143. }
  144. })
  145. if options.Start.Watch && watcher != nil {
  146. err = watcher.Start(ctx)
  147. if err != nil {
  148. return err
  149. }
  150. }
  151. monitor := newMonitor(s.apiClient(), project.Name)
  152. if len(options.Start.Services) > 0 {
  153. monitor.withServices(options.Start.Services)
  154. } else {
  155. monitor.withServices(project.ServiceNames())
  156. }
  157. monitor.withListener(printer.HandleEvent)
  158. var exitCode int
  159. if options.Start.OnExit != api.CascadeIgnore {
  160. once := true
  161. // detect first container to exit to trigger application shutdown
  162. monitor.withListener(func(event api.ContainerEvent) {
  163. if once && event.Type == api.ContainerEventExited {
  164. if options.Start.OnExit == api.CascadeFail && event.ExitCode == 0 {
  165. return
  166. }
  167. once = false
  168. exitCode = event.ExitCode
  169. _, _ = fmt.Fprintln(s.stdinfo(), progress.ErrorColor("Aborting on container exit..."))
  170. eg.Go(func() error {
  171. return progress.RunWithLog(context.WithoutCancel(ctx), func(ctx context.Context) error {
  172. return s.stop(ctx, project.Name, api.StopOptions{
  173. Services: options.Create.Services,
  174. Project: project,
  175. }, printer.HandleEvent)
  176. }, s.stdinfo(), logConsumer)
  177. })
  178. }
  179. })
  180. }
  181. if options.Start.ExitCodeFrom != "" {
  182. once := true
  183. // capture exit code from first container to exit with selected service
  184. monitor.withListener(func(event api.ContainerEvent) {
  185. if once && event.Type == api.ContainerEventExited && event.Service == options.Start.ExitCodeFrom {
  186. exitCode = event.ExitCode
  187. once = false
  188. }
  189. })
  190. }
  191. monitor.withListener(func(event api.ContainerEvent) {
  192. if event.Type != api.ContainerEventStarted {
  193. return
  194. }
  195. if event.Restarting || event.Container.Labels[api.ContainerReplaceLabel] != "" {
  196. eg.Go(func() error {
  197. ctr, err := s.apiClient().ContainerInspect(ctx, event.ID)
  198. if err != nil {
  199. return err
  200. }
  201. err = s.doLogContainer(ctx, options.Start.Attach, event.Source, ctr, api.LogOptions{
  202. Follow: true,
  203. Since: ctr.State.StartedAt,
  204. })
  205. var notImplErr errdefs.ErrNotImplemented
  206. if errors.As(err, &notImplErr) {
  207. // container may be configured with logging_driver: none
  208. // as container already started, we might miss the very first logs. But still better than none
  209. return s.doAttachContainer(ctx, event.Service, event.ID, event.Source, printer.HandleEvent)
  210. }
  211. return err
  212. })
  213. }
  214. })
  215. eg.Go(func() error {
  216. err := monitor.Start(ctx)
  217. // Signal for the signal-handler goroutines to stop
  218. close(doneCh)
  219. return err
  220. })
  221. // We use the parent context without cancellation as we manage sigterm to stop the stack
  222. err = s.start(context.WithoutCancel(ctx), project.Name, options.Start, printer.HandleEvent)
  223. if err != nil && !isTerminated.Load() { // Ignore error if the process is terminated
  224. return err
  225. }
  226. err = eg.Wait().ErrorOrNil()
  227. if exitCode != 0 {
  228. errMsg := ""
  229. if err != nil {
  230. errMsg = err.Error()
  231. }
  232. return cli.StatusError{StatusCode: exitCode, Status: errMsg}
  233. }
  234. return err
  235. }