up.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. "fmt"
  17. "os"
  18. "os/signal"
  19. "path/filepath"
  20. "syscall"
  21. "github.com/docker/compose-cli/api/client"
  22. "github.com/docker/compose-cli/api/compose"
  23. "github.com/docker/compose-cli/api/context/store"
  24. "github.com/docker/compose-cli/api/progress"
  25. "github.com/docker/compose-cli/cli/cmd"
  26. "github.com/docker/compose-cli/cli/formatter"
  27. "github.com/compose-spec/compose-go/types"
  28. "github.com/sirupsen/logrus"
  29. "github.com/spf13/cobra"
  30. )
  31. // composeOptions hold options common to `up` and `run` to run compose project
  32. type composeOptions struct {
  33. *projectOptions
  34. Build bool
  35. // ACI only
  36. DomainName string
  37. }
  38. type upOptions struct {
  39. *composeOptions
  40. Detach bool
  41. Environment []string
  42. removeOrphans bool
  43. forceRecreate bool
  44. noRecreate bool
  45. noStart bool
  46. cascadeStop bool
  47. exitCodeFrom string
  48. }
  49. func (o upOptions) recreateStrategy() string {
  50. if o.noRecreate {
  51. return compose.RecreateNever
  52. }
  53. if o.forceRecreate {
  54. return compose.RecreateForce
  55. }
  56. return compose.RecreateDiverged
  57. }
  58. func upCommand(p *projectOptions, contextType string) *cobra.Command {
  59. opts := upOptions{
  60. composeOptions: &composeOptions{
  61. projectOptions: p,
  62. },
  63. }
  64. upCmd := &cobra.Command{
  65. Use: "up [SERVICE...]",
  66. Short: "Create and start containers",
  67. RunE: func(cmd *cobra.Command, args []string) error {
  68. switch contextType {
  69. case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
  70. if opts.exitCodeFrom != "" {
  71. opts.cascadeStop = true
  72. }
  73. if opts.cascadeStop && opts.Detach {
  74. return fmt.Errorf("--abort-on-container-exit and --detach are incompatible")
  75. }
  76. if opts.forceRecreate && opts.noRecreate {
  77. return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
  78. }
  79. return runCreateStart(cmd.Context(), opts, args)
  80. default:
  81. return runUp(cmd.Context(), opts, args)
  82. }
  83. },
  84. }
  85. flags := upCmd.Flags()
  86. flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
  87. flags.BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
  88. flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
  89. flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
  90. switch contextType {
  91. case store.AciContextType:
  92. flags.StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
  93. case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
  94. flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
  95. flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
  96. flags.BoolVar(&opts.noStart, "no-start", false, "Don't start the services after creating them.")
  97. flags.BoolVar(&opts.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
  98. flags.StringVar(&opts.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
  99. }
  100. return upCmd
  101. }
  102. func runUp(ctx context.Context, opts upOptions, services []string) error {
  103. c, project, err := setup(ctx, *opts.composeOptions, services)
  104. if err != nil {
  105. return err
  106. }
  107. _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
  108. return "", c.ComposeService().Up(ctx, project, compose.UpOptions{
  109. Detach: opts.Detach,
  110. })
  111. })
  112. return err
  113. }
  114. func runCreateStart(ctx context.Context, opts upOptions, services []string) error {
  115. c, project, err := setup(ctx, *opts.composeOptions, services)
  116. if err != nil {
  117. return err
  118. }
  119. if opts.exitCodeFrom != "" {
  120. _, err := project.GetService(opts.exitCodeFrom)
  121. if err != nil {
  122. return err
  123. }
  124. }
  125. _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
  126. err := c.ComposeService().Create(ctx, project, compose.CreateOptions{
  127. RemoveOrphans: opts.removeOrphans,
  128. Recreate: opts.recreateStrategy(),
  129. })
  130. if err != nil {
  131. return "", err
  132. }
  133. if opts.Detach {
  134. err = c.ComposeService().Start(ctx, project, compose.StartOptions{})
  135. }
  136. return "", err
  137. })
  138. if err != nil {
  139. return err
  140. }
  141. if opts.noStart {
  142. return nil
  143. }
  144. if opts.Detach {
  145. return nil
  146. }
  147. queue := make(chan compose.ContainerEvent)
  148. printer := printer{
  149. queue: queue,
  150. }
  151. stopFunc := func() error {
  152. ctx := context.Background()
  153. _, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
  154. return "", c.ComposeService().Stop(ctx, project)
  155. })
  156. return err
  157. }
  158. signalChan := make(chan os.Signal, 1)
  159. signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
  160. go func() {
  161. <-signalChan
  162. fmt.Println("Gracefully stopping...")
  163. stopFunc() // nolint:errcheck
  164. }()
  165. err = c.ComposeService().Start(ctx, project, compose.StartOptions{
  166. Attach: func(event compose.ContainerEvent) {
  167. queue <- event
  168. },
  169. })
  170. if err != nil {
  171. return err
  172. }
  173. exitCode, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, stopFunc)
  174. if exitCode != 0 {
  175. return cmd.ExitCodeError{ExitCode: exitCode}
  176. }
  177. return err
  178. }
  179. func setup(ctx context.Context, opts composeOptions, services []string) (*client.Client, *types.Project, error) {
  180. c, err := client.NewWithDefaultLocalBackend(ctx)
  181. if err != nil {
  182. return nil, nil, err
  183. }
  184. project, err := opts.toProject(services)
  185. if err != nil {
  186. return nil, nil, err
  187. }
  188. if opts.DomainName != "" {
  189. // arbitrarily set the domain name on the first service ; ACI backend will expose the entire project
  190. project.Services[0].DomainName = opts.DomainName
  191. }
  192. if opts.Build {
  193. for _, service := range project.Services {
  194. service.PullPolicy = types.PullPolicyBuild
  195. }
  196. }
  197. if opts.EnvFile != "" {
  198. var services types.Services
  199. for _, s := range project.Services {
  200. ef := opts.EnvFile
  201. if ef != "" {
  202. if !filepath.IsAbs(ef) {
  203. ef = filepath.Join(project.WorkingDir, opts.EnvFile)
  204. }
  205. if s.Labels == nil {
  206. s.Labels = make(map[string]string)
  207. }
  208. s.Labels[compose.EnvironmentFileLabel] = ef
  209. services = append(services, s)
  210. }
  211. }
  212. project.Services = services
  213. }
  214. return c, project, nil
  215. }
  216. type printer struct {
  217. queue chan compose.ContainerEvent
  218. }
  219. func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { //nolint:unparam
  220. consumer := formatter.NewLogConsumer(ctx, os.Stdout)
  221. var aborting bool
  222. for {
  223. event := <-p.queue
  224. switch event.Type {
  225. case compose.ContainerEventExit:
  226. if !aborting {
  227. consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
  228. }
  229. if cascadeStop && !aborting {
  230. aborting = true
  231. fmt.Println("Aborting on container exit...")
  232. err := stopFn()
  233. if err != nil {
  234. return 0, err
  235. }
  236. }
  237. if exitCodeFrom == "" || exitCodeFrom == event.Service {
  238. logrus.Error(event.ExitCode)
  239. return event.ExitCode, nil
  240. }
  241. case compose.ContainerEventLog:
  242. if !aborting {
  243. consumer.Log(event.Service, event.Source, event.Line)
  244. }
  245. }
  246. }
  247. }