down.go 11 KB

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