up.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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. "strconv"
  21. "strings"
  22. "syscall"
  23. "time"
  24. "github.com/compose-spec/compose-go/types"
  25. "github.com/sirupsen/logrus"
  26. "github.com/spf13/cobra"
  27. "golang.org/x/sync/errgroup"
  28. "github.com/docker/compose-cli/api/client"
  29. "github.com/docker/compose-cli/api/compose"
  30. "github.com/docker/compose-cli/api/context/store"
  31. "github.com/docker/compose-cli/api/progress"
  32. "github.com/docker/compose-cli/cli/cmd"
  33. "github.com/docker/compose-cli/cli/formatter"
  34. )
  35. // composeOptions hold options common to `up` and `run` to run compose project
  36. type composeOptions struct {
  37. *projectOptions
  38. Build bool
  39. noBuild bool
  40. // ACI only
  41. DomainName string
  42. }
  43. type upOptions struct {
  44. *composeOptions
  45. Detach bool
  46. Environment []string
  47. removeOrphans bool
  48. forceRecreate bool
  49. noRecreate bool
  50. recreateDeps bool
  51. noStart bool
  52. noDeps bool
  53. cascadeStop bool
  54. exitCodeFrom string
  55. scale []string
  56. noColor bool
  57. noPrefix bool
  58. timeChanged bool
  59. timeout int
  60. noInherit bool
  61. attachDependencies bool
  62. }
  63. func (opts upOptions) recreateStrategy() string {
  64. if opts.noRecreate {
  65. return compose.RecreateNever
  66. }
  67. if opts.forceRecreate {
  68. return compose.RecreateForce
  69. }
  70. return compose.RecreateDiverged
  71. }
  72. func (opts upOptions) dependenciesRecreateStrategy() string {
  73. if opts.noRecreate {
  74. return compose.RecreateNever
  75. }
  76. if opts.recreateDeps {
  77. return compose.RecreateForce
  78. }
  79. return compose.RecreateDiverged
  80. }
  81. func (opts upOptions) GetTimeout() *time.Duration {
  82. if opts.timeChanged {
  83. t := time.Duration(opts.timeout) * time.Second
  84. return &t
  85. }
  86. return nil
  87. }
  88. func (opts upOptions) apply(project *types.Project, services []string) error {
  89. if opts.noDeps {
  90. enabled, err := project.GetServices(services...)
  91. if err != nil {
  92. return err
  93. }
  94. for _, s := range project.Services {
  95. if !contains(services, s.Name) {
  96. project.DisabledServices = append(project.DisabledServices, s)
  97. }
  98. }
  99. project.Services = enabled
  100. }
  101. if opts.exitCodeFrom != "" {
  102. _, err := project.GetService(opts.exitCodeFrom)
  103. if err != nil {
  104. return err
  105. }
  106. }
  107. for _, scale := range opts.scale {
  108. split := strings.Split(scale, "=")
  109. if len(split) != 2 {
  110. return fmt.Errorf("invalid --scale option %q. Should be SERVICE=NUM", scale)
  111. }
  112. name := split[0]
  113. replicas, err := strconv.Atoi(split[1])
  114. if err != nil {
  115. return err
  116. }
  117. err = setServiceScale(project, name, replicas)
  118. if err != nil {
  119. return err
  120. }
  121. }
  122. return nil
  123. }
  124. func upCommand(p *projectOptions, contextType string) *cobra.Command {
  125. opts := upOptions{
  126. composeOptions: &composeOptions{
  127. projectOptions: p,
  128. },
  129. }
  130. upCmd := &cobra.Command{
  131. Use: "up [SERVICE...]",
  132. Short: "Create and start containers",
  133. RunE: func(cmd *cobra.Command, args []string) error {
  134. opts.timeChanged = cmd.Flags().Changed("timeout")
  135. switch contextType {
  136. case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
  137. if opts.exitCodeFrom != "" {
  138. opts.cascadeStop = true
  139. }
  140. if opts.Build && opts.noBuild {
  141. return fmt.Errorf("--build and --no-build are incompatible")
  142. }
  143. if opts.Detach && (opts.attachDependencies || opts.cascadeStop) {
  144. return fmt.Errorf("--detach cannot be combined with --abort-on-container-exit or --attach-dependencies")
  145. }
  146. if opts.forceRecreate && opts.noRecreate {
  147. return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
  148. }
  149. if opts.recreateDeps && opts.noRecreate {
  150. return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
  151. }
  152. return runCreateStart(cmd.Context(), opts, args)
  153. default:
  154. return runUp(cmd.Context(), opts, args)
  155. }
  156. },
  157. }
  158. flags := upCmd.Flags()
  159. flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
  160. flags.BoolVarP(&opts.Detach, "detach", "d", false, "Detached mode: Run containers in the background")
  161. flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")
  162. flags.BoolVar(&opts.noBuild, "no-build", false, "Don't build an image, even if it's missing.")
  163. flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
  164. flags.StringArrayVar(&opts.scale, "scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
  165. flags.BoolVar(&opts.noColor, "no-color", false, "Produce monochrome output.")
  166. flags.BoolVar(&opts.noPrefix, "no-log-prefix", false, "Don't print prefix in logs.")
  167. switch contextType {
  168. case store.AciContextType:
  169. flags.StringVar(&opts.DomainName, "domainname", "", "Container NIS domain name")
  170. case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
  171. flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
  172. flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
  173. flags.BoolVar(&opts.noStart, "no-start", false, "Don't start the services after creating them.")
  174. flags.BoolVar(&opts.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
  175. flags.StringVar(&opts.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
  176. flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Use this timeout in seconds for container shutdown when attached or when containers are already running.")
  177. flags.BoolVar(&opts.noDeps, "no-deps", false, "Don't start linked services.")
  178. flags.BoolVar(&opts.recreateDeps, "always-recreate-deps", false, "Recreate dependent containers. Incompatible with --no-recreate.")
  179. flags.BoolVarP(&opts.noInherit, "renew-anon-volumes", "V", false, "Recreate anonymous volumes instead of retrieving data from the previous containers.")
  180. flags.BoolVar(&opts.attachDependencies, "attach-dependencies", false, "Attach to dependent containers.")
  181. }
  182. return upCmd
  183. }
  184. func runUp(ctx context.Context, opts upOptions, services []string) error {
  185. c, project, err := setup(ctx, *opts.composeOptions, services)
  186. if err != nil {
  187. return err
  188. }
  189. err = opts.apply(project, services)
  190. if err != nil {
  191. return err
  192. }
  193. _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
  194. return "", c.ComposeService().Up(ctx, project, compose.UpOptions{
  195. Detach: opts.Detach,
  196. })
  197. })
  198. return err
  199. }
  200. func runCreateStart(ctx context.Context, opts upOptions, services []string) error {
  201. c, project, err := setup(ctx, *opts.composeOptions, services)
  202. if err != nil {
  203. return err
  204. }
  205. err = opts.apply(project, services)
  206. if err != nil {
  207. return err
  208. }
  209. _, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
  210. err := c.ComposeService().Create(ctx, project, compose.CreateOptions{
  211. Services: services,
  212. RemoveOrphans: opts.removeOrphans,
  213. Recreate: opts.recreateStrategy(),
  214. RecreateDependencies: opts.dependenciesRecreateStrategy(),
  215. Inherit: !opts.noInherit,
  216. Timeout: opts.GetTimeout(),
  217. })
  218. if err != nil {
  219. return "", err
  220. }
  221. if opts.Detach {
  222. err = c.ComposeService().Start(ctx, project, compose.StartOptions{})
  223. }
  224. return "", err
  225. })
  226. if err != nil {
  227. return err
  228. }
  229. if opts.noStart {
  230. return nil
  231. }
  232. if opts.attachDependencies {
  233. services = nil
  234. }
  235. if opts.Detach {
  236. return nil
  237. }
  238. queue := make(chan compose.ContainerEvent)
  239. printer := printer{
  240. queue: queue,
  241. }
  242. stopFunc := func() error {
  243. ctx := context.Background()
  244. _, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
  245. return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{})
  246. })
  247. return err
  248. }
  249. signalChan := make(chan os.Signal, 1)
  250. signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
  251. go func() {
  252. <-signalChan
  253. queue <- compose.ContainerEvent{
  254. Type: compose.UserCancel,
  255. }
  256. fmt.Println("Gracefully stopping...")
  257. stopFunc() // nolint:errcheck
  258. }()
  259. consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
  260. var exitCode int
  261. eg, ctx := errgroup.WithContext(ctx)
  262. eg.Go(func() error {
  263. code, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, consumer, stopFunc)
  264. exitCode = code
  265. return err
  266. })
  267. err = c.ComposeService().Start(ctx, project, compose.StartOptions{
  268. Attach: func(event compose.ContainerEvent) {
  269. queue <- event
  270. },
  271. Services: services,
  272. })
  273. if err != nil {
  274. return err
  275. }
  276. err = eg.Wait()
  277. if exitCode != 0 {
  278. return cmd.ExitCodeError{ExitCode: exitCode}
  279. }
  280. return err
  281. }
  282. func setServiceScale(project *types.Project, name string, replicas int) error {
  283. for i, s := range project.Services {
  284. if s.Name == name {
  285. service, err := project.GetService(name)
  286. if err != nil {
  287. return err
  288. }
  289. if service.Deploy == nil {
  290. service.Deploy = &types.DeployConfig{}
  291. }
  292. count := uint64(replicas)
  293. service.Deploy.Replicas = &count
  294. project.Services[i] = service
  295. return nil
  296. }
  297. }
  298. return fmt.Errorf("unknown service %q", name)
  299. }
  300. func setup(ctx context.Context, opts composeOptions, services []string) (*client.Client, *types.Project, error) {
  301. c, err := client.NewWithDefaultLocalBackend(ctx)
  302. if err != nil {
  303. return nil, nil, err
  304. }
  305. project, err := opts.toProject(services)
  306. if err != nil {
  307. return nil, nil, err
  308. }
  309. if opts.DomainName != "" {
  310. // arbitrarily set the domain name on the first service ; ACI backend will expose the entire project
  311. project.Services[0].DomainName = opts.DomainName
  312. }
  313. if opts.Build {
  314. for i, service := range project.Services {
  315. service.PullPolicy = types.PullPolicyBuild
  316. project.Services[i] = service
  317. }
  318. }
  319. if opts.noBuild {
  320. for i, service := range project.Services {
  321. service.Build = nil
  322. project.Services[i] = service
  323. }
  324. }
  325. if opts.EnvFile != "" {
  326. var services types.Services
  327. for _, s := range project.Services {
  328. ef := opts.EnvFile
  329. if ef != "" {
  330. if !filepath.IsAbs(ef) {
  331. ef = filepath.Join(project.WorkingDir, opts.EnvFile)
  332. }
  333. if s.Labels == nil {
  334. s.Labels = make(map[string]string)
  335. }
  336. s.Labels[compose.EnvironmentFileLabel] = ef
  337. services = append(services, s)
  338. }
  339. }
  340. project.Services = services
  341. }
  342. return c, project, nil
  343. }
  344. type printer struct {
  345. queue chan compose.ContainerEvent
  346. }
  347. func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, consumer compose.LogConsumer, stopFn func() error) (int, error) { //nolint:unparam
  348. var aborting bool
  349. var count int
  350. for {
  351. event := <-p.queue
  352. switch event.Type {
  353. case compose.UserCancel:
  354. aborting = true
  355. case compose.ContainerEventAttach:
  356. consumer.Register(event.Name, event.Source)
  357. count++
  358. case compose.ContainerEventExit:
  359. if !aborting {
  360. consumer.Status(event.Name, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
  361. }
  362. if cascadeStop {
  363. if !aborting {
  364. aborting = true
  365. fmt.Println("Aborting on container exit...")
  366. err := stopFn()
  367. if err != nil {
  368. return 0, err
  369. }
  370. }
  371. if exitCodeFrom == "" || exitCodeFrom == event.Service {
  372. logrus.Error(event.ExitCode)
  373. return event.ExitCode, nil
  374. }
  375. }
  376. count--
  377. if count == 0 {
  378. // Last container terminated, done
  379. return 0, nil
  380. }
  381. case compose.ContainerEventLog:
  382. if !aborting {
  383. consumer.Log(event.Name, event.Service, event.Source, event.Line)
  384. }
  385. }
  386. }
  387. }