Przeglądaj źródła

Add 'ancestor' ps filter for image

Makes it possible to filter containers by image, using
--filter=ancestor=busybox and get all the container running busybox
image and image based on busybox (to the bottom).

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Vincent Demeester 10 lat temu
rodzic
commit
c1af0ac082

+ 52 - 6
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"
@@ -28,13 +30,15 @@ type ContainersConfig struct {
 
 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)
@@ -61,6 +65,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)
@@ -131,6 +156,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,
@@ -243,3 +278,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
+	}
+}

+ 1 - 0
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 (`<image-name>[:<tag>]`,  `<image id>` or `<image@digest>`) - filters containers that were created from the given image or a descendant.
 
 ## Successfully exited containers
 

+ 37 - 0
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 <name>@<digest> 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 {

+ 110 - 0
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")

+ 2 - 0
man/docker-ps.1.md

@@ -41,6 +41,8 @@ the running containers.
                           status=(created|restarting|running|paused|exited)
                           name=<string> - container's name
                           id=<ID> - container's ID
+                          ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - 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*.