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>
| ... | ... |
@@ -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*. |