Browse code

Merge pull request #14570 from vdemeester/13365-ps-image-filter

Add docker ps ancestor filter for image

Sebastiaan van Stijn authored on 2015/08/29 02:47:43
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"
... ...
@@ -37,13 +39,15 @@ type ContainersConfig struct {
37 37
 // Containers returns a list of all the containers.
38 38
 func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container, error) {
39 39
 	var (
40
-		foundBefore bool
41
-		displayed   int
42
-		all         = config.All
43
-		n           = config.Limit
44
-		psFilters   filters.Args
45
-		filtExited  []int
40
+		foundBefore    bool
41
+		displayed      int
42
+		ancestorFilter bool
43
+		all            = config.All
44
+		n              = config.Limit
45
+		psFilters      filters.Args
46
+		filtExited     []int
46 47
 	)
48
+	imagesFilter := map[string]bool{}
47 49
 	containers := []*types.Container{}
48 50
 
49 51
 	psFilters, err := filters.FromParam(config.Filters)
... ...
@@ -70,6 +74,27 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container,
70 70
 			}
71 71
 		}
72 72
 	}
73
+
74
+	if ancestors, ok := psFilters["ancestor"]; ok {
75
+		ancestorFilter = true
76
+		byParents := daemon.Graph().ByParent()
77
+		// The idea is to walk the graph down the most "efficient" way.
78
+		for _, ancestor := range ancestors {
79
+			// First, get the imageId of the ancestor filter (yay)
80
+			image, err := daemon.Repositories().LookupImage(ancestor)
81
+			if err != nil {
82
+				logrus.Warnf("Error while looking up for image %v", ancestor)
83
+				continue
84
+			}
85
+			if imagesFilter[ancestor] {
86
+				// Already seen this ancestor, skip it
87
+				continue
88
+			}
89
+			// Then walk down the graph and put the imageIds in imagesFilter
90
+			populateImageFilterByParents(imagesFilter, image.ID, byParents)
91
+		}
92
+	}
93
+
73 94
 	names := map[string][]string{}
74 95
 	daemon.containerGraph().Walk("/", func(p string, e *graphdb.Entity) error {
75 96
 		names[e.ID()] = append(names[e.ID()], p)
... ...
@@ -140,6 +165,16 @@ func (daemon *Daemon) Containers(config *ContainersConfig) ([]*types.Container,
140 140
 		if !psFilters.Match("status", container.State.StateString()) {
141 141
 			return nil
142 142
 		}
143
+
144
+		if ancestorFilter {
145
+			if len(imagesFilter) == 0 {
146
+				return nil
147
+			}
148
+			if !imagesFilter[container.ImageID] {
149
+				return nil
150
+			}
151
+		}
152
+
143 153
 		displayed++
144 154
 		newC := &types.Container{
145 155
 			ID:    container.ID,
... ...
@@ -254,3 +289,14 @@ func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) {
254 254
 	}
255 255
 	return volumesOut, nil
256 256
 }
257
+
258
+func populateImageFilterByParents(ancestorMap map[string]bool, imageID string, byParents map[string][]*image.Image) {
259
+	if !ancestorMap[imageID] {
260
+		if images, ok := byParents[imageID]; ok {
261
+			for _, image := range images {
262
+				populateImageFilterByParents(ancestorMap, image.ID, byParents)
263
+			}
264
+		}
265
+		ancestorMap[imageID] = true
266
+	}
267
+}
... ...
@@ -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
 
55 56
 #### Label
... ...
@@ -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*.