diff --git a/daemon/list.go b/daemon/list.go index 259ca0c26d..a4a0fd7ee4 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -6,7 +6,9 @@ import ( "strconv" "strings" + "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" + "github.com/docker/docker/image" "github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/nat" "github.com/docker/docker/pkg/parsers/filters" @@ -37,13 +39,15 @@ type ContainersConfig struct { // Containers returns a list of all the containers. func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container, error) { var ( - foundBefore bool - displayed int - all = config.All - n = config.Limit - psFilters filters.Args - filtExited []int + foundBefore bool + displayed int + ancestorFilter bool + all = config.All + n = config.Limit + psFilters filters.Args + filtExited []int ) + imagesFilter := map[string]bool{} containers := []*types.Container{} psFilters, err := filters.FromParam(config.Filters) @@ -70,6 +74,27 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container, } } } + + if ancestors, ok := psFilters["ancestor"]; ok { + ancestorFilter = true + byParents := daemon.Graph().ByParent() + // The idea is to walk the graph down the most "efficient" way. + for _, ancestor := range ancestors { + // First, get the imageId of the ancestor filter (yay) + image, err := daemon.Repositories().LookupImage(ancestor) + if err != nil { + logrus.Warnf("Error while looking up for image %v", ancestor) + continue + } + if imagesFilter[ancestor] { + // Already seen this ancestor, skip it + continue + } + // Then walk down the graph and put the imageIds in imagesFilter + populateImageFilterByParents(imagesFilter, image.ID, byParents) + } + } + names := map[string][]string{} daemon.containerGraph().Walk("/", func(p string, e *graphdb.Entity) error { names[e.ID()] = append(names[e.ID()], p) @@ -140,6 +165,16 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container, if !psFilters.Match("status", container.State.StateString()) { return nil } + + if ancestorFilter { + if len(imagesFilter) == 0 { + return nil + } + if !imagesFilter[container.ImageID] { + return nil + } + } + displayed++ newC := &types.Container{ ID: container.ID, @@ -254,3 +289,14 @@ func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) { } return volumesOut, nil } + +func populateImageFilterByParents(ancestorMap map[string]bool, imageID string, byParents map[string][]*image.Image) { + if !ancestorMap[imageID] { + if images, ok := byParents[imageID]; ok { + for _, image := range images { + populateImageFilterByParents(ancestorMap, image.ID, byParents) + } + } + ancestorMap[imageID] = true + } +} diff --git a/docs/reference/commandline/ps.md b/docs/reference/commandline/ps.md index 0e3335483b..328cac76b4 100644 --- a/docs/reference/commandline/ps.md +++ b/docs/reference/commandline/ps.md @@ -50,6 +50,7 @@ The currently supported filters are: * name (container's name) * exited (int - the code of exited containers. Only useful with `--all`) * status (created|restarting|running|paused|exited) +* ancestor (`[:]`, `` or ``) - filters containers that were created from the given image or a descendant. #### Label diff --git a/integration-cli/docker_cli_by_digest_test.go b/integration-cli/docker_cli_by_digest_test.go index 9f2d3173ea..7204a2f9ba 100644 --- a/integration-cli/docker_cli_by_digest_test.go +++ b/integration-cli/docker_cli_by_digest_test.go @@ -387,6 +387,43 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) { } } +func (s *DockerRegistrySuite) TestPsListContainersFilterAncestorImageByDigest(c *check.C) { + digest, err := setupImage(c) + c.Assert(err, check.IsNil, check.Commentf("error setting up image: %v", err)) + + imageReference := fmt.Sprintf("%s@%s", repoName, digest) + + // pull from the registry using the @ reference + dockerCmd(c, "pull", imageReference) + + // build a image from it + imageName1 := "images_ps_filter_test" + _, err = buildImage(imageName1, fmt.Sprintf( + `FROM %s + LABEL match me 1`, imageReference), true) + c.Assert(err, check.IsNil) + + // run a container based on that + out, _ := dockerCmd(c, "run", "-d", imageReference, "echo", "hello") + expectedID := strings.TrimSpace(out) + + // run a container based on the a descendant of that too + out, _ = dockerCmd(c, "run", "-d", imageName1, "echo", "hello") + expectedID1 := strings.TrimSpace(out) + + expectedIDs := []string{expectedID, expectedID1} + + // Invalid imageReference + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", fmt.Sprintf("--filter=ancestor=busybox@%s", digest)) + if strings.TrimSpace(out) != "" { + c.Fatalf("Expected filter container for %s ancestor filter to be empty, got %v", fmt.Sprintf("busybox@%s", digest), strings.TrimSpace(out)) + } + + // Valid imageReference + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageReference) + checkPsAncestorFilterOutput(c, out, imageReference, expectedIDs) +} + func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C) { pushDigest, err := setupImage(c) if err != nil { diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index 6d376719e6..6125b7b7c9 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -12,6 +12,9 @@ import ( "time" "github.com/go-check/check" + "sort" + + "github.com/docker/docker/pkg/stringid" ) func (s *DockerSuite) TestPsListContainers(c *check.C) { @@ -278,6 +281,113 @@ func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) { } +// Test for the ancestor filter for ps. +// There is also the same test but with image:tag@digest in docker_cli_by_digest_test.go +// +// What the test setups : +// - Create 2 image based on busybox using the same repository but different tags +// - Create an image based on the previous image (images_ps_filter_test2) +// - Run containers for each of those image (busybox, images_ps_filter_test1, images_ps_filter_test2) +// - Filter them out :P +func (s *DockerSuite) TestPsListContainersFilterAncestorImage(c *check.C) { + // Build images + imageName1 := "images_ps_filter_test1" + imageID1, err := buildImage(imageName1, + `FROM busybox + LABEL match me 1`, true) + c.Assert(err, check.IsNil) + + imageName1Tagged := "images_ps_filter_test1:tag" + imageID1Tagged, err := buildImage(imageName1Tagged, + `FROM busybox + LABEL match me 1 tagged`, true) + c.Assert(err, check.IsNil) + + imageName2 := "images_ps_filter_test2" + imageID2, err := buildImage(imageName2, + fmt.Sprintf(`FROM %s + LABEL match me 2`, imageName1), true) + c.Assert(err, check.IsNil) + + // start containers + out, _ := dockerCmd(c, "run", "-d", "busybox", "echo", "hello") + firstID := strings.TrimSpace(out) + + // start another container + out, _ = dockerCmd(c, "run", "-d", "busybox", "echo", "hello") + secondID := strings.TrimSpace(out) + + // start third container + out, _ = dockerCmd(c, "run", "-d", imageName1, "echo", "hello") + thirdID := strings.TrimSpace(out) + + // start fourth container + out, _ = dockerCmd(c, "run", "-d", imageName1Tagged, "echo", "hello") + fourthID := strings.TrimSpace(out) + + // start fifth container + out, _ = dockerCmd(c, "run", "-d", imageName2, "echo", "hello") + fifthID := strings.TrimSpace(out) + + var filterTestSuite = []struct { + filterName string + expectedIDs []string + }{ + // non existent stuff + {"nonexistent", []string{}}, + {"nonexistent:tag", []string{}}, + // image + {"busybox", []string{firstID, secondID, thirdID, fourthID, fifthID}}, + {imageName1, []string{thirdID, fifthID}}, + {imageName2, []string{fifthID}}, + // image:tag + {fmt.Sprintf("%s:latest", imageName1), []string{thirdID, fifthID}}, + {imageName1Tagged, []string{fourthID}}, + // short-id + {stringid.TruncateID(imageID1), []string{thirdID, fifthID}}, + {stringid.TruncateID(imageID2), []string{fifthID}}, + // full-id + {imageID1, []string{thirdID, fifthID}}, + {imageID1Tagged, []string{fourthID}}, + {imageID2, []string{fifthID}}, + } + + for _, filter := range filterTestSuite { + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+filter.filterName) + checkPsAncestorFilterOutput(c, out, filter.filterName, filter.expectedIDs) + } + + // Multiple ancestor filter + out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageName2, "--filter=ancestor="+imageName1Tagged) + checkPsAncestorFilterOutput(c, out, imageName2+","+imageName1Tagged, []string{fourthID, fifthID}) +} + +func checkPsAncestorFilterOutput(c *check.C, out string, filterName string, expectedIDs []string) { + actualIDs := []string{} + if out != "" { + actualIDs = strings.Split(out[:len(out)-1], "\n") + } + sort.Strings(actualIDs) + sort.Strings(expectedIDs) + + if len(actualIDs) != len(expectedIDs) { + c.Fatalf("Expected filtered container(s) for %s ancestor filter to be %v:%v, got %v:%v", filterName, len(expectedIDs), expectedIDs, len(actualIDs), actualIDs) + } + if len(expectedIDs) > 0 { + same := true + for i := range expectedIDs { + if actualIDs[i] != expectedIDs[i] { + c.Logf("%s, %s", actualIDs[i], expectedIDs[i]) + same = false + break + } + } + if !same { + c.Fatalf("Expected filtered container(s) for %s ancestor filter to be %v, got %v", filterName, expectedIDs, actualIDs) + } + } +} + func (s *DockerSuite) TestPsListContainersFilterLabel(c *check.C) { // start container out, _ := dockerCmd(c, "run", "-d", "-l", "match=me", "-l", "second=tag", "busybox") diff --git a/man/docker-ps.1.md b/man/docker-ps.1.md index 0fdf7ccc93..5e21926d73 100644 --- a/man/docker-ps.1.md +++ b/man/docker-ps.1.md @@ -41,6 +41,8 @@ the running containers. status=(created|restarting|running|paused|exited) name= - container's name id= - container's ID + ancestor=([:tag]||) - filters containers that were + created from the given image or a descendant. **-l**, **--latest**=*true*|*false* Show only the latest created container, include non-running ones. The default is *false*.