down.go 12 KB

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