down.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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. "strings"
  18. "time"
  19. "github.com/compose-spec/compose-go/v2/types"
  20. "github.com/docker/compose/v2/internal/desktop"
  21. "github.com/docker/compose/v2/pkg/api"
  22. "github.com/docker/compose/v2/pkg/progress"
  23. "github.com/docker/compose/v2/pkg/utils"
  24. moby "github.com/docker/docker/api/types"
  25. containerType "github.com/docker/docker/api/types/container"
  26. "github.com/docker/docker/api/types/filters"
  27. imageapi "github.com/docker/docker/api/types/image"
  28. "github.com/docker/docker/api/types/network"
  29. "github.com/docker/docker/errdefs"
  30. "github.com/sirupsen/logrus"
  31. "golang.org/x/sync/errgroup"
  32. )
  33. type downOp func() error
  34. func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
  35. return progress.Run(ctx, func(ctx context.Context) error {
  36. return s.down(ctx, strings.ToLower(projectName), options)
  37. }, s.stdinfo())
  38. }
  39. func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error { //nolint:gocyclo
  40. w := progress.ContextWriter(ctx)
  41. resourceToRemove := false
  42. include := oneOffExclude
  43. if options.RemoveOrphans {
  44. include = oneOffInclude
  45. }
  46. containers, err := s.getContainers(ctx, projectName, include, true)
  47. if err != nil {
  48. return err
  49. }
  50. project := options.Project
  51. if project == nil {
  52. project, err = s.getProjectWithResources(ctx, containers, projectName)
  53. if err != nil {
  54. return err
  55. }
  56. }
  57. // Check requested services exists in model
  58. options.Services, err = checkSelectedServices(options, project)
  59. if err != nil {
  60. return err
  61. }
  62. if len(containers) > 0 {
  63. resourceToRemove = true
  64. }
  65. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
  66. serviceContainers := containers.filter(isService(service))
  67. err := s.removeContainers(ctx, serviceContainers, options.Timeout, options.Volumes)
  68. return err
  69. }, WithRootNodesAndDown(options.Services))
  70. if err != nil {
  71. return err
  72. }
  73. orphans := containers.filter(isOrphaned(project))
  74. if options.RemoveOrphans && len(orphans) > 0 {
  75. err := s.removeContainers(ctx, orphans, options.Timeout, false)
  76. if err != nil {
  77. return err
  78. }
  79. }
  80. ops := s.ensureNetworksDown(ctx, project, w)
  81. if options.Images != "" {
  82. imgOps, err := s.ensureImagesDown(ctx, project, options, w)
  83. if err != nil {
  84. return err
  85. }
  86. ops = append(ops, imgOps...)
  87. }
  88. if options.Volumes {
  89. ops = append(ops, s.ensureVolumesDown(ctx, project, w)...)
  90. }
  91. if !resourceToRemove && len(ops) == 0 {
  92. logrus.Warnf("Warning: No resource found to remove for project %q.", projectName)
  93. }
  94. eg, _ := errgroup.WithContext(ctx)
  95. for _, op := range ops {
  96. eg.Go(op)
  97. }
  98. return eg.Wait()
  99. }
  100. func checkSelectedServices(options api.DownOptions, project *types.Project) ([]string, error) {
  101. var services []string
  102. for _, service := range options.Services {
  103. _, err := project.GetService(service)
  104. if err != nil {
  105. if options.Project != nil {
  106. // ran with an explicit compose.yaml file, so we should not ignore
  107. return nil, err
  108. }
  109. // ran without an explicit compose.yaml file, so can't distinguish typo vs container already removed
  110. } else {
  111. services = append(services, service)
  112. }
  113. }
  114. return services, nil
  115. }
  116. func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  117. var ops []downOp
  118. for _, vol := range project.Volumes {
  119. if vol.External {
  120. continue
  121. }
  122. volumeName := vol.Name
  123. ops = append(ops, func() error {
  124. return s.removeVolume(ctx, volumeName, w)
  125. })
  126. }
  127. if s.manageDesktopFileSharesEnabled(ctx) {
  128. ops = append(ops, func() error {
  129. desktop.RemoveFileSharesForProject(ctx, s.desktopCli, project.Name)
  130. return nil
  131. })
  132. }
  133. return ops
  134. }
  135. func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) ([]downOp, error) {
  136. imagePruner := NewImagePruner(s.apiClient(), project)
  137. pruneOpts := ImagePruneOptions{
  138. Mode: ImagePruneMode(options.Images),
  139. RemoveOrphans: options.RemoveOrphans,
  140. }
  141. images, err := imagePruner.ImagesToPrune(ctx, pruneOpts)
  142. if err != nil {
  143. return nil, err
  144. }
  145. var ops []downOp
  146. for i := range images {
  147. img := images[i]
  148. ops = append(ops, func() error {
  149. return s.removeImage(ctx, img, w)
  150. })
  151. }
  152. return ops, nil
  153. }
  154. func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  155. var ops []downOp
  156. for key, n := range project.Networks {
  157. if n.External {
  158. continue
  159. }
  160. // loop capture variable for op closure
  161. networkKey := key
  162. idOrName := n.Name
  163. ops = append(ops, func() error {
  164. return s.removeNetwork(ctx, networkKey, project.Name, idOrName, w)
  165. })
  166. }
  167. return ops
  168. }
  169. func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName string, projectName string, name string, w progress.Writer) error {
  170. networks, err := s.apiClient().NetworkList(ctx, network.ListOptions{
  171. Filters: filters.NewArgs(
  172. projectFilter(projectName),
  173. networkFilter(composeNetworkName)),
  174. })
  175. if err != nil {
  176. return fmt.Errorf("failed to list networks: %w", err)
  177. }
  178. if len(networks) == 0 {
  179. return nil
  180. }
  181. eventName := fmt.Sprintf("Network %s", name)
  182. w.Event(progress.RemovingEvent(eventName))
  183. var found int
  184. for _, net := range networks {
  185. if net.Name != name {
  186. continue
  187. }
  188. nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
  189. if errdefs.IsNotFound(err) {
  190. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  191. return nil
  192. }
  193. if err != nil {
  194. return err
  195. }
  196. if len(nw.Containers) > 0 {
  197. w.Event(progress.NewEvent(eventName, progress.Warning, "Resource is still in use"))
  198. found++
  199. continue
  200. }
  201. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  202. if errdefs.IsNotFound(err) {
  203. continue
  204. }
  205. w.Event(progress.ErrorEvent(eventName))
  206. return fmt.Errorf("failed to remove network %s: %w", name, err)
  207. }
  208. w.Event(progress.RemovedEvent(eventName))
  209. found++
  210. }
  211. if found == 0 {
  212. // in practice, it's extremely unlikely for this to ever occur, as it'd
  213. // mean the network was present when we queried at the start of this
  214. // method but was then deleted by something else in the interim
  215. w.Event(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
  216. return nil
  217. }
  218. return nil
  219. }
  220. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  221. id := fmt.Sprintf("Image %s", image)
  222. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  223. _, err := s.apiClient().ImageRemove(ctx, image, imageapi.RemoveOptions{})
  224. if err == nil {
  225. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  226. return nil
  227. }
  228. if errdefs.IsConflict(err) {
  229. w.Event(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
  230. return nil
  231. }
  232. if errdefs.IsNotFound(err) {
  233. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  234. return nil
  235. }
  236. return err
  237. }
  238. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  239. resource := fmt.Sprintf("Volume %s", id)
  240. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  241. err := s.apiClient().VolumeRemove(ctx, id, true)
  242. if err == nil {
  243. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  244. return nil
  245. }
  246. if errdefs.IsConflict(err) {
  247. w.Event(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
  248. return nil
  249. }
  250. if errdefs.IsNotFound(err) {
  251. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  252. return nil
  253. }
  254. return err
  255. }
  256. func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, container moby.Container, timeout *time.Duration) error {
  257. eventName := getContainerProgressName(container)
  258. w.Event(progress.StoppingEvent(eventName))
  259. timeoutInSecond := utils.DurationSecondToInt(timeout)
  260. err := s.apiClient().ContainerStop(ctx, container.ID, containerType.StopOptions{Timeout: timeoutInSecond})
  261. if err != nil {
  262. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  263. return err
  264. }
  265. w.Event(progress.StoppedEvent(eventName))
  266. return nil
  267. }
  268. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  269. eg, ctx := errgroup.WithContext(ctx)
  270. for _, container := range containers {
  271. container := container
  272. eg.Go(func() error {
  273. return s.stopContainer(ctx, w, container, timeout)
  274. })
  275. }
  276. return eg.Wait()
  277. }
  278. func (s *composeService) removeContainers(ctx context.Context, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  279. eg, _ := errgroup.WithContext(ctx)
  280. for _, container := range containers {
  281. container := container
  282. eg.Go(func() error {
  283. return s.stopAndRemoveContainer(ctx, container, timeout, volumes)
  284. })
  285. }
  286. return eg.Wait()
  287. }
  288. func (s *composeService) stopAndRemoveContainer(ctx context.Context, container moby.Container, timeout *time.Duration, volumes bool) error {
  289. w := progress.ContextWriter(ctx)
  290. eventName := getContainerProgressName(container)
  291. err := s.stopContainer(ctx, w, container, timeout)
  292. if errdefs.IsNotFound(err) {
  293. w.Event(progress.RemovedEvent(eventName))
  294. return nil
  295. }
  296. if err != nil {
  297. return err
  298. }
  299. w.Event(progress.RemovingEvent(eventName))
  300. err = s.apiClient().ContainerRemove(ctx, container.ID, containerType.RemoveOptions{
  301. Force: true,
  302. RemoveVolumes: volumes,
  303. })
  304. if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
  305. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  306. return err
  307. }
  308. w.Event(progress.RemovedEvent(eventName))
  309. return nil
  310. }
  311. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  312. containers = containers.filter(isNotOneOff)
  313. p, err := s.projectFromName(containers, projectName)
  314. if err != nil && !api.IsNotFoundError(err) {
  315. return nil, err
  316. }
  317. project, err := p.WithServicesTransform(func(name string, service types.ServiceConfig) (types.ServiceConfig, error) {
  318. for k := range service.DependsOn {
  319. if dependency, ok := service.DependsOn[k]; ok {
  320. dependency.Required = false
  321. service.DependsOn[k] = dependency
  322. }
  323. }
  324. return service, nil
  325. })
  326. if err != nil {
  327. return nil, err
  328. }
  329. volumes, err := s.actualVolumes(ctx, projectName)
  330. if err != nil {
  331. return nil, err
  332. }
  333. project.Volumes = volumes
  334. networks, err := s.actualNetworks(ctx, projectName)
  335. if err != nil {
  336. return nil, err
  337. }
  338. project.Networks = networks
  339. return project, nil
  340. }