down.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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/types"
  20. "github.com/distribution/distribution/v3/reference"
  21. moby "github.com/docker/docker/api/types"
  22. "github.com/docker/docker/api/types/filters"
  23. "github.com/docker/docker/errdefs"
  24. "github.com/pkg/errors"
  25. "golang.org/x/sync/errgroup"
  26. "github.com/docker/compose/v2/pkg/api"
  27. "github.com/docker/compose/v2/pkg/progress"
  28. "github.com/docker/compose/v2/pkg/utils"
  29. )
  30. type downOp func() error
  31. func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
  32. return progress.Run(ctx, func(ctx context.Context) error {
  33. return s.down(ctx, strings.ToLower(projectName), options)
  34. })
  35. }
  36. func (s *composeService) down(ctx context.Context, projectName string, options api.DownOptions) error {
  37. w := progress.ContextWriter(ctx)
  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. if len(containers) > 0 {
  55. resourceToRemove = true
  56. }
  57. err = InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
  58. serviceContainers := containers.filter(isService(service))
  59. err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
  60. return err
  61. })
  62. if err != nil {
  63. return err
  64. }
  65. orphans := containers.filter(isNotService(project.ServiceNames()...))
  66. if options.RemoveOrphans && len(orphans) > 0 {
  67. err := s.removeContainers(ctx, w, orphans, options.Timeout, false)
  68. if err != nil {
  69. return err
  70. }
  71. }
  72. ops := s.ensureNetworksDown(ctx, project, w)
  73. if options.Images != "" {
  74. imgOps, err := s.ensureImagesDown(ctx, project, options, w)
  75. if err != nil {
  76. return err
  77. }
  78. ops = append(ops, imgOps...)
  79. }
  80. if options.Volumes {
  81. ops = append(ops, s.ensureVolumesDown(ctx, project, w)...)
  82. }
  83. if !resourceToRemove && len(ops) == 0 {
  84. fmt.Fprintf(s.stderr(), "Warning: No resource found to remove for project %q.\n", projectName)
  85. }
  86. eg, _ := errgroup.WithContext(ctx)
  87. for _, op := range ops {
  88. eg.Go(op)
  89. }
  90. return eg.Wait()
  91. }
  92. func (s *composeService) ensureVolumesDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  93. var ops []downOp
  94. for _, vol := range project.Volumes {
  95. if vol.External.External {
  96. continue
  97. }
  98. volumeName := vol.Name
  99. ops = append(ops, func() error {
  100. return s.removeVolume(ctx, volumeName, w)
  101. })
  102. }
  103. return ops
  104. }
  105. func (s *composeService) ensureImagesDown(ctx context.Context, project *types.Project, options api.DownOptions, w progress.Writer) ([]downOp, error) {
  106. images, err := s.getServiceImagesToRemove(ctx, options, project)
  107. if err != nil {
  108. return nil, err
  109. }
  110. var ops []downOp
  111. for i := range images {
  112. img := images[i]
  113. ops = append(ops, func() error {
  114. return s.removeImage(ctx, img, w)
  115. })
  116. }
  117. return ops, nil
  118. }
  119. func (s *composeService) ensureNetworksDown(ctx context.Context, project *types.Project, w progress.Writer) []downOp {
  120. var ops []downOp
  121. for _, n := range project.Networks {
  122. if n.External.External {
  123. continue
  124. }
  125. // loop capture variable for op closure
  126. networkName := n.Name
  127. ops = append(ops, func() error {
  128. return s.removeNetwork(ctx, networkName, w)
  129. })
  130. }
  131. return ops
  132. }
  133. func (s *composeService) removeNetwork(ctx context.Context, name string, w progress.Writer) error {
  134. // networks are guaranteed to have unique IDs but NOT names, so it's
  135. // possible to get into a situation where a compose down will fail with
  136. // an error along the lines of:
  137. // failed to remove network test: Error response from daemon: network test is ambiguous (2 matches found based on name)
  138. // as a workaround here, the delete is done by ID after doing a list using
  139. // the name as a filter (99.9% of the time this will return a single result)
  140. networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{
  141. Filters: filters.NewArgs(filters.Arg("name", name)),
  142. })
  143. if err != nil {
  144. return errors.Wrapf(err, fmt.Sprintf("failed to inspect network %s", name))
  145. }
  146. if len(networks) == 0 {
  147. return nil
  148. }
  149. eventName := fmt.Sprintf("Network %s", name)
  150. w.Event(progress.RemovingEvent(eventName))
  151. var removed int
  152. for _, net := range networks {
  153. if net.Name == name {
  154. if err := s.apiClient().NetworkRemove(ctx, net.ID); err != nil {
  155. if errdefs.IsNotFound(err) {
  156. continue
  157. }
  158. w.Event(progress.ErrorEvent(eventName))
  159. return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", name))
  160. }
  161. removed++
  162. }
  163. }
  164. if removed == 0 {
  165. // in practice, it's extremely unlikely for this to ever occur, as it'd
  166. // mean the network was present when we queried at the start of this
  167. // method but was then deleted by something else in the interim
  168. w.Event(progress.NewEvent(eventName, progress.Done, "Warning: No resource found to remove"))
  169. return nil
  170. }
  171. w.Event(progress.RemovedEvent(eventName))
  172. return nil
  173. }
  174. //nolint:gocyclo
  175. func (s *composeService) getServiceImagesToRemove(ctx context.Context, options api.DownOptions, project *types.Project) ([]string, error) {
  176. if options.Images == "" {
  177. return nil, nil
  178. }
  179. var localServiceImages []string
  180. var imagesToRemove []string
  181. addImageToRemove := func(img string, checkExistence bool) {
  182. // since some references come from user input (service.image) and some
  183. // come from the engine API, we standardize them, opting for the
  184. // familiar name format since they'll also be displayed in the CLI
  185. ref, err := reference.ParseNormalizedNamed(img)
  186. if err != nil {
  187. return
  188. }
  189. ref = reference.TagNameOnly(ref)
  190. img = reference.FamiliarString(ref)
  191. if utils.StringContains(imagesToRemove, img) {
  192. return
  193. }
  194. if checkExistence {
  195. _, _, err := s.apiClient().ImageInspectWithRaw(ctx, img)
  196. if errdefs.IsNotFound(err) {
  197. // err on the side of caution: only skip if we successfully
  198. // queried the API and got back a definitive "not exists"
  199. return
  200. }
  201. }
  202. imagesToRemove = append(imagesToRemove, img)
  203. }
  204. imageListOpts := moby.ImageListOptions{
  205. Filters: filters.NewArgs(
  206. projectFilter(project.Name),
  207. // TODO(milas): we should really clean up the dangling images as
  208. // well (historically we have NOT); need to refactor this to handle
  209. // it gracefully without producing confusing CLI output, i.e. we
  210. // do not want to print out a bunch of untagged/dangling image IDs,
  211. // they should be grouped into a logical operation for the relevant
  212. // service
  213. filters.Arg("dangling", "false"),
  214. ),
  215. }
  216. projectImages, err := s.apiClient().ImageList(ctx, imageListOpts)
  217. if err != nil {
  218. return nil, err
  219. }
  220. // 1. Remote / custom-named images - only deleted on `--rmi="all"`
  221. for _, service := range project.Services {
  222. if service.Image == "" {
  223. localServiceImages = append(localServiceImages, service.Name)
  224. continue
  225. }
  226. if options.Images == "all" {
  227. addImageToRemove(service.Image, true)
  228. }
  229. }
  230. // 2. *LABELED* Locally-built images with implicit image names
  231. //
  232. // If `--remove-orphans` is being used, then ALL images for the project
  233. // will be selected for removal. Otherwise, only those that match a known
  234. // service based on the loaded project will be included.
  235. for _, img := range projectImages {
  236. if len(img.RepoTags) == 0 {
  237. // currently, we're only removing the tagged references, but
  238. // if we start removing the dangling images and grouping by
  239. // service, we can remove this (and should rely on `Image::ID`)
  240. continue
  241. }
  242. shouldRemove := options.RemoveOrphans
  243. for _, service := range localServiceImages {
  244. if img.Labels[api.ServiceLabel] == service {
  245. shouldRemove = true
  246. break
  247. }
  248. }
  249. if shouldRemove {
  250. addImageToRemove(img.RepoTags[0], false)
  251. }
  252. }
  253. // 3. *UNLABELED* Locally-built images with implicit image names
  254. //
  255. // This is a fallback for (2) to handle images built by previous
  256. // versions of Compose, which did not label their built images.
  257. for _, serviceName := range localServiceImages {
  258. service, err := project.GetService(serviceName)
  259. if err != nil || service.Image != "" {
  260. continue
  261. }
  262. imgName := api.GetImageNameOrDefault(service, project.Name)
  263. addImageToRemove(imgName, true)
  264. }
  265. return imagesToRemove, nil
  266. }
  267. func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
  268. id := fmt.Sprintf("Image %s", image)
  269. w.Event(progress.NewEvent(id, progress.Working, "Removing"))
  270. _, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
  271. if err == nil {
  272. w.Event(progress.NewEvent(id, progress.Done, "Removed"))
  273. return nil
  274. }
  275. if errdefs.IsNotFound(err) {
  276. w.Event(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
  277. return nil
  278. }
  279. return err
  280. }
  281. func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
  282. resource := fmt.Sprintf("Volume %s", id)
  283. w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
  284. err := s.apiClient().VolumeRemove(ctx, id, true)
  285. if err == nil {
  286. w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
  287. return nil
  288. }
  289. if errdefs.IsNotFound(err) {
  290. w.Event(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
  291. return nil
  292. }
  293. return err
  294. }
  295. func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration) error {
  296. eg, ctx := errgroup.WithContext(ctx)
  297. for _, container := range containers {
  298. container := container
  299. eg.Go(func() error {
  300. eventName := getContainerProgressName(container)
  301. w.Event(progress.StoppingEvent(eventName))
  302. err := s.apiClient().ContainerStop(ctx, container.ID, timeout)
  303. if err != nil {
  304. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  305. return err
  306. }
  307. w.Event(progress.StoppedEvent(eventName))
  308. return nil
  309. })
  310. }
  311. return eg.Wait()
  312. }
  313. func (s *composeService) removeContainers(ctx context.Context, w progress.Writer, containers []moby.Container, timeout *time.Duration, volumes bool) error {
  314. eg, _ := errgroup.WithContext(ctx)
  315. for _, container := range containers {
  316. container := container
  317. eg.Go(func() error {
  318. eventName := getContainerProgressName(container)
  319. w.Event(progress.StoppingEvent(eventName))
  320. err := s.stopContainers(ctx, w, []moby.Container{container}, timeout)
  321. if err != nil {
  322. w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
  323. return err
  324. }
  325. w.Event(progress.RemovingEvent(eventName))
  326. err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
  327. Force: true,
  328. RemoveVolumes: volumes,
  329. })
  330. if err != nil {
  331. w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
  332. return err
  333. }
  334. w.Event(progress.RemovedEvent(eventName))
  335. return nil
  336. })
  337. }
  338. return eg.Wait()
  339. }
  340. func (s *composeService) getProjectWithResources(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
  341. containers = containers.filter(isNotOneOff)
  342. project, err := s.projectFromName(containers, projectName)
  343. if err != nil && !api.IsNotFoundError(err) {
  344. return nil, err
  345. }
  346. volumes, err := s.actualVolumes(ctx, projectName)
  347. if err != nil {
  348. return nil, err
  349. }
  350. project.Volumes = volumes
  351. networks, err := s.actualNetworks(ctx, projectName)
  352. if err != nil {
  353. return nil, err
  354. }
  355. project.Networks = networks
  356. return project, nil
  357. }