convergence.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. // +build local
  2. /*
  3. Copyright 2020 Docker Compose CLI authors
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. */
  14. package local
  15. import (
  16. "context"
  17. "fmt"
  18. "github.com/compose-spec/compose-go/types"
  19. "github.com/docker/compose-cli/api/containers"
  20. "github.com/docker/compose-cli/progress"
  21. moby "github.com/docker/docker/api/types"
  22. "github.com/docker/docker/api/types/filters"
  23. "github.com/docker/docker/api/types/network"
  24. "golang.org/x/sync/errgroup"
  25. "strconv"
  26. )
  27. func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
  28. actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
  29. Filters: filters.NewArgs(
  30. filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)),
  31. filters.Arg("label", fmt.Sprintf("%s=%s", serviceLabel, service.Name)),
  32. ),
  33. })
  34. if err != nil {
  35. return err
  36. }
  37. scale := getScale(service)
  38. eg, ctx := errgroup.WithContext(ctx)
  39. if len(actual) < scale {
  40. next, err := nextContainerNumber(actual)
  41. if err != nil {
  42. return err
  43. }
  44. missing := scale - len(actual)
  45. for i := 0; i < missing; i++ {
  46. number := next + i
  47. name := fmt.Sprintf("%s_%s_%d", project.Name, service.Name, number)
  48. eg.Go(func() error {
  49. return s.createContainer(ctx, project, service, name, number)
  50. })
  51. }
  52. }
  53. if len(actual) > scale {
  54. for i := scale; i < len(actual); i++ {
  55. container := actual[i]
  56. eg.Go(func() error {
  57. err := s.containerService.Stop(ctx, container.ID, nil)
  58. if err != nil {
  59. return err
  60. }
  61. return s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
  62. })
  63. }
  64. actual = actual[:scale]
  65. }
  66. expected, err := jsonHash(service)
  67. if err != nil {
  68. return err
  69. }
  70. for _, container := range actual {
  71. container := container
  72. diverged := container.Labels[configHashLabel] != expected
  73. if diverged {
  74. eg.Go(func() error {
  75. return s.recreateContainer(ctx, project, service, container)
  76. })
  77. continue
  78. }
  79. if container.State == "running" {
  80. // already running, skip
  81. continue
  82. }
  83. eg.Go(func() error {
  84. return s.restartContainer(ctx, service, container)
  85. })
  86. }
  87. return eg.Wait()
  88. }
  89. func nextContainerNumber(containers []moby.Container) (int, error) {
  90. max := 0
  91. for _, c := range containers {
  92. n, err := strconv.Atoi(c.Labels[containerNumberLabel])
  93. if err != nil {
  94. return 0, err
  95. }
  96. if n > max {
  97. max = n
  98. }
  99. }
  100. return max + 1, nil
  101. }
  102. func getScale(config types.ServiceConfig) int {
  103. if config.Deploy != nil && config.Deploy.Replicas != nil {
  104. return int(*config.Deploy.Replicas)
  105. }
  106. if config.Scale != 0 {
  107. return config.Scale
  108. }
  109. return 1
  110. }
  111. func (s *local) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int) error {
  112. w := progress.ContextWriter(ctx)
  113. w.Event(progress.Event{
  114. ID: fmt.Sprintf("Service %q", service.Name),
  115. Status: progress.Working,
  116. StatusText: "Create",
  117. Done: false,
  118. })
  119. err := s.runContainer(ctx, project, service, name, number, nil)
  120. if err != nil {
  121. return err
  122. }
  123. w.Event(progress.Event{
  124. ID: fmt.Sprintf("Service %q", service.Name),
  125. Status: progress.Done,
  126. StatusText: "Created",
  127. Done: true,
  128. })
  129. return nil
  130. }
  131. func (s *local) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container) error {
  132. w := progress.ContextWriter(ctx)
  133. w.Event(progress.Event{
  134. ID: fmt.Sprintf("Service %q", service.Name),
  135. Status: progress.Working,
  136. StatusText: "Recreate",
  137. Done: false,
  138. })
  139. err := s.containerService.Stop(ctx, container.ID, nil)
  140. if err != nil {
  141. return err
  142. }
  143. name := getContainerName(container)
  144. tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name)
  145. err = s.containerService.apiClient.ContainerRename(ctx, container.ID, tmpName)
  146. if err != nil {
  147. return err
  148. }
  149. number, err := strconv.Atoi(container.Labels[containerNumberLabel])
  150. if err != nil {
  151. return err
  152. }
  153. err = s.runContainer(ctx, project, service, name, number, &container)
  154. if err != nil {
  155. return err
  156. }
  157. err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{})
  158. if err != nil {
  159. return err
  160. }
  161. w.Event(progress.Event{
  162. ID: fmt.Sprintf("Service %q", service.Name),
  163. Status: progress.Done,
  164. StatusText: "Recreated",
  165. Done: true,
  166. })
  167. return nil
  168. }
  169. func (s *local) restartContainer(ctx context.Context, service types.ServiceConfig, container moby.Container) error {
  170. w := progress.ContextWriter(ctx)
  171. w.Event(progress.Event{
  172. ID: fmt.Sprintf("Service %q", service.Name),
  173. Status: progress.Working,
  174. StatusText: "Restart",
  175. Done: false,
  176. })
  177. err := s.containerService.Start(ctx, container.ID)
  178. if err != nil {
  179. return err
  180. }
  181. w.Event(progress.Event{
  182. ID: fmt.Sprintf("Service %q", service.Name),
  183. Status: progress.Done,
  184. StatusText: "Restarted",
  185. Done: true,
  186. })
  187. return nil
  188. }
  189. func (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, container *moby.Container) error {
  190. containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, number, container)
  191. if err != nil {
  192. return err
  193. }
  194. id, err := s.containerService.create(ctx, containerConfig, hostConfig, networkingConfig, name)
  195. if err != nil {
  196. return err
  197. }
  198. for net := range service.Networks {
  199. name := fmt.Sprintf("%s_%s", project.Name, net)
  200. err = s.connectContainerToNetwork(ctx, id, service.Name, name)
  201. if err != nil {
  202. return err
  203. }
  204. }
  205. err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{})
  206. if err != nil {
  207. return err
  208. }
  209. return nil
  210. }
  211. func (s *local) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error {
  212. err := s.containerService.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{
  213. Aliases: []string{service},
  214. })
  215. if err != nil {
  216. return err
  217. }
  218. return nil
  219. }