down.go 11 KB

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