1
0

down.go 12 KB

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