down.go 11 KB

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