down.go 11 KB

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