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