Browse code

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 authored on 2015/08/20 16:57:15
Showing 5 changed files
... ...
@@ -6,7 +6,9 @@ import (
6 6
 	"strconv"
7 7
 	"strings"
8 8
 
9
+	"github.com/Sirupsen/logrus"
9 10
 	"github.com/docker/docker/api/types"
11
+	"github.com/docker/docker/image"
10 12
 	"github.com/docker/docker/pkg/graphdb"
11 13
 	"github.com/docker/docker/pkg/nat"
12 14
 	"github.com/docker/docker/pkg/parsers/filters"
... ...
@@ -28,13 +30,15 @@ type ContainersConfig struct {
28 28
 
29 29
 func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container, error) {
30 30
 	var (
31
-		foundBefore bool
32
-		displayed   int
33
-		all         = config.All
34
-		n           = config.Limit
35
-		psFilters   filters.Args
36
-		filtExited  []int
31
+		foundBefore    bool
32
+		displayed      int
33
+		ancestorFilter bool
34
+		all            = config.All
35
+		n              = config.Limit
36
+		psFilters      filters.Args
37
+		filtExited     []int
37 38
 	)
39
+	imagesFilter := map[string]bool{}
38 40
 	containers := []*types.Container{}
39 41
 
40 42
 	psFilters, err := filters.FromParam(config.Filters)
... ...
@@ -61,6 +65,27 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container,
61 61
 			}
62 62
 		}
63 63
 	}
64
+
65
+	if ancestors, ok := psFilters["ancestor"]; ok {
66
+		ancestorFilter = true
67
+		byParents := daemon.Graph().ByParent()
68
+		// The idea is to walk the graph down the most "efficient" way.
69
+		for _, ancestor := range ancestors {
70
+			// First, get the imageId of the ancestor filter (yay)
71
+			image, err := daemon.Repositories().LookupImage(ancestor)
72
+			if err != nil {
73
+				logrus.Warnf("Error while looking up for image %v", ancestor)
74
+				continue
75
+			}
76
+			if imagesFilter[ancestor] {
77
+				// Already seen this ancestor, skip it
78
+				continue
79
+			}
80
+			// Then walk down the graph and put the imageIds in imagesFilter
81
+			populateImageFilterByParents(imagesFilter, image.ID, byParents)
82
+		}
83
+	}
84
+
64 85
 	names := map[string][]string{}
65 86
 	daemon.ContainerGraph().Walk("/", func(p string, e *graphdb.Entity) error {
66 87
 		names[e.ID()] = append(names[e.ID()], p)
... ...
@@ -131,6 +156,16 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container,
131 131
 		if !psFilters.Match("status", container.State.StateString()) {
132 132
 			return nil
133 133
 		}
134
+
135
+		if ancestorFilter {
136
+			if len(imagesFilter) == 0 {
137
+				return nil
138
+			}
139
+			if !imagesFilter[container.ImageID] {
140
+				return nil
141
+			}
142
+		}
143
+
134 144
 		displayed++
135 145
 		newC := &types.Container{
136 146
 			ID:    container.ID,
... ...
@@ -243,3 +278,14 @@ func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) {
243 243
 	}
244 244
 	return volumesOut, nil
245 245
 }
246
+
247
+func populateImageFilterByParents(ancestorMap map[string]bool, imageID string, byParents map[string][]*image.Image) {
248
+	if !ancestorMap[imageID] {
249
+		if images, ok := byParents[imageID]; ok {
250
+			for _, image := range images {
251
+				populateImageFilterByParents(ancestorMap, image.ID, byParents)
252
+			}
253
+		}
254
+		ancestorMap[imageID] = true
255
+	}
256
+}
... ...
@@ -50,6 +50,7 @@ The currently supported filters are:
50 50
 * name (container's name)
51 51
 * exited (int - the code of exited containers. Only useful with `--all`)
52 52
 * status (created|restarting|running|paused|exited)
53
+* ancestor (`<image-name>[:<tag>]`,  `<image id>` or `<image@digest>`) - filters containers that were created from the given image or a descendant.
53 54
 
54 55
 ## Successfully exited containers
55 56
 
... ...
@@ -387,6 +387,43 @@ func (s *DockerRegistrySuite) TestListImagesWithDigests(c *check.C) {
387 387
 	}
388 388
 }
389 389
 
390
+func (s *DockerRegistrySuite) TestPsListContainersFilterAncestorImageByDigest(c *check.C) {
391
+	digest, err := setupImage(c)
392
+	c.Assert(err, check.IsNil, check.Commentf("error setting up image: %v", err))
393
+
394
+	imageReference := fmt.Sprintf("%s@%s", repoName, digest)
395
+
396
+	// pull from the registry using the <name>@<digest> reference
397
+	dockerCmd(c, "pull", imageReference)
398
+
399
+	// build a image from it
400
+	imageName1 := "images_ps_filter_test"
401
+	_, err = buildImage(imageName1, fmt.Sprintf(
402
+		`FROM %s
403
+		 LABEL match me 1`, imageReference), true)
404
+	c.Assert(err, check.IsNil)
405
+
406
+	// run a container based on that
407
+	out, _ := dockerCmd(c, "run", "-d", imageReference, "echo", "hello")
408
+	expectedID := strings.TrimSpace(out)
409
+
410
+	// run a container based on the a descendant of that too
411
+	out, _ = dockerCmd(c, "run", "-d", imageName1, "echo", "hello")
412
+	expectedID1 := strings.TrimSpace(out)
413
+
414
+	expectedIDs := []string{expectedID, expectedID1}
415
+
416
+	// Invalid imageReference
417
+	out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", fmt.Sprintf("--filter=ancestor=busybox@%s", digest))
418
+	if strings.TrimSpace(out) != "" {
419
+		c.Fatalf("Expected filter container for %s ancestor filter to be empty, got %v", fmt.Sprintf("busybox@%s", digest), strings.TrimSpace(out))
420
+	}
421
+
422
+	// Valid imageReference
423
+	out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageReference)
424
+	checkPsAncestorFilterOutput(c, out, imageReference, expectedIDs)
425
+}
426
+
390 427
 func (s *DockerRegistrySuite) TestDeleteImageByIDOnlyPulledByDigest(c *check.C) {
391 428
 	pushDigest, err := setupImage(c)
392 429
 	if err != nil {
... ...
@@ -12,6 +12,9 @@ import (
12 12
 	"time"
13 13
 
14 14
 	"github.com/go-check/check"
15
+	"sort"
16
+
17
+	"github.com/docker/docker/pkg/stringid"
15 18
 )
16 19
 
17 20
 func (s *DockerSuite) TestPsListContainers(c *check.C) {
... ...
@@ -278,6 +281,113 @@ func (s *DockerSuite) TestPsListContainersFilterName(c *check.C) {
278 278
 
279 279
 }
280 280
 
281
+// Test for the ancestor filter for ps.
282
+// There is also the same test but with image:tag@digest in docker_cli_by_digest_test.go
283
+//
284
+// What the test setups :
285
+// - Create 2 image based on busybox using the same repository but different tags
286
+// - Create an image based on the previous image (images_ps_filter_test2)
287
+// - Run containers for each of those image (busybox, images_ps_filter_test1, images_ps_filter_test2)
288
+// - Filter them out :P
289
+func (s *DockerSuite) TestPsListContainersFilterAncestorImage(c *check.C) {
290
+	// Build images
291
+	imageName1 := "images_ps_filter_test1"
292
+	imageID1, err := buildImage(imageName1,
293
+		`FROM busybox
294
+		 LABEL match me 1`, true)
295
+	c.Assert(err, check.IsNil)
296
+
297
+	imageName1Tagged := "images_ps_filter_test1:tag"
298
+	imageID1Tagged, err := buildImage(imageName1Tagged,
299
+		`FROM busybox
300
+		 LABEL match me 1 tagged`, true)
301
+	c.Assert(err, check.IsNil)
302
+
303
+	imageName2 := "images_ps_filter_test2"
304
+	imageID2, err := buildImage(imageName2,
305
+		fmt.Sprintf(`FROM %s
306
+		 LABEL match me 2`, imageName1), true)
307
+	c.Assert(err, check.IsNil)
308
+
309
+	// start containers
310
+	out, _ := dockerCmd(c, "run", "-d", "busybox", "echo", "hello")
311
+	firstID := strings.TrimSpace(out)
312
+
313
+	// start another container
314
+	out, _ = dockerCmd(c, "run", "-d", "busybox", "echo", "hello")
315
+	secondID := strings.TrimSpace(out)
316
+
317
+	// start third container
318
+	out, _ = dockerCmd(c, "run", "-d", imageName1, "echo", "hello")
319
+	thirdID := strings.TrimSpace(out)
320
+
321
+	// start fourth container
322
+	out, _ = dockerCmd(c, "run", "-d", imageName1Tagged, "echo", "hello")
323
+	fourthID := strings.TrimSpace(out)
324
+
325
+	// start fifth container
326
+	out, _ = dockerCmd(c, "run", "-d", imageName2, "echo", "hello")
327
+	fifthID := strings.TrimSpace(out)
328
+
329
+	var filterTestSuite = []struct {
330
+		filterName  string
331
+		expectedIDs []string
332
+	}{
333
+		// non existent stuff
334
+		{"nonexistent", []string{}},
335
+		{"nonexistent:tag", []string{}},
336
+		// image
337
+		{"busybox", []string{firstID, secondID, thirdID, fourthID, fifthID}},
338
+		{imageName1, []string{thirdID, fifthID}},
339
+		{imageName2, []string{fifthID}},
340
+		// image:tag
341
+		{fmt.Sprintf("%s:latest", imageName1), []string{thirdID, fifthID}},
342
+		{imageName1Tagged, []string{fourthID}},
343
+		// short-id
344
+		{stringid.TruncateID(imageID1), []string{thirdID, fifthID}},
345
+		{stringid.TruncateID(imageID2), []string{fifthID}},
346
+		// full-id
347
+		{imageID1, []string{thirdID, fifthID}},
348
+		{imageID1Tagged, []string{fourthID}},
349
+		{imageID2, []string{fifthID}},
350
+	}
351
+
352
+	for _, filter := range filterTestSuite {
353
+		out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+filter.filterName)
354
+		checkPsAncestorFilterOutput(c, out, filter.filterName, filter.expectedIDs)
355
+	}
356
+
357
+	// Multiple ancestor filter
358
+	out, _ = dockerCmd(c, "ps", "-a", "-q", "--no-trunc", "--filter=ancestor="+imageName2, "--filter=ancestor="+imageName1Tagged)
359
+	checkPsAncestorFilterOutput(c, out, imageName2+","+imageName1Tagged, []string{fourthID, fifthID})
360
+}
361
+
362
+func checkPsAncestorFilterOutput(c *check.C, out string, filterName string, expectedIDs []string) {
363
+	actualIDs := []string{}
364
+	if out != "" {
365
+		actualIDs = strings.Split(out[:len(out)-1], "\n")
366
+	}
367
+	sort.Strings(actualIDs)
368
+	sort.Strings(expectedIDs)
369
+
370
+	if len(actualIDs) != len(expectedIDs) {
371
+		c.Fatalf("Expected filtered container(s) for %s ancestor filter to be %v:%v, got %v:%v", filterName, len(expectedIDs), expectedIDs, len(actualIDs), actualIDs)
372
+	}
373
+	if len(expectedIDs) > 0 {
374
+		same := true
375
+		for i := range expectedIDs {
376
+			if actualIDs[i] != expectedIDs[i] {
377
+				c.Logf("%s, %s", actualIDs[i], expectedIDs[i])
378
+				same = false
379
+				break
380
+			}
381
+		}
382
+		if !same {
383
+			c.Fatalf("Expected filtered container(s) for %s ancestor filter to be %v, got %v", filterName, expectedIDs, actualIDs)
384
+		}
385
+	}
386
+}
387
+
281 388
 func (s *DockerSuite) TestPsListContainersFilterLabel(c *check.C) {
282 389
 	// start container
283 390
 	out, _ := dockerCmd(c, "run", "-d", "-l", "match=me", "-l", "second=tag", "busybox")
... ...
@@ -41,6 +41,8 @@ the running containers.
41 41
                           status=(created|restarting|running|paused|exited)
42 42
                           name=<string> - container's name
43 43
                           id=<ID> - container's ID
44
+                          ancestor=(<image-name>[:tag]|<image-id>|<image@digest>) - filters containers that were
45
+                          created from the given image or a descendant.
44 46
 
45 47
 **-l**, **--latest**=*true*|*false*
46 48
    Show only the latest created container, include non-running ones. The default is *false*.