Browse code

Add `publish` and `expose` filter for `docker ps --filter`

This fix tries to address the enhancement proposal raised in
27178 for filtering based on published or exposed ports of
`docker ps --filter`.

In this fix, two filter options, `publish` and `expose` have
been added to take either `<port>[/<protocol>]` or `<from>-<to>[/<protocol>]`
and filtering on containers.

An integration test has been added to cover the changes.

This fix fixes 27178.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>

Yong Tang authored on 2016/10/16 07:53:06
Showing 3 changed files
... ...
@@ -38,6 +38,8 @@ var acceptedPsFilterTags = map[string]bool{
38 38
 	"volume":    true,
39 39
 	"network":   true,
40 40
 	"is-task":   true,
41
+	"publish":   true,
42
+	"expose":    true,
41 43
 }
42 44
 
43 45
 // iterationAction represents possible outcomes happening during the container iteration.
... ...
@@ -89,6 +91,12 @@ type listContext struct {
89 89
 	taskFilter bool
90 90
 	// isTask tells us if the we should filter container that are a task (true) or not (false)
91 91
 	isTask bool
92
+
93
+	// publish is a list of published ports to filter with
94
+	publish map[nat.Port]bool
95
+	// expose is a list of exposed ports to filter with
96
+	expose map[nat.Port]bool
97
+
92 98
 	// ContainerListOptions is the filters set by the user
93 99
 	*types.ContainerListOptions
94 100
 }
... ...
@@ -311,6 +319,54 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte
311 311
 		})
312 312
 	}
313 313
 
314
+	publishFilter := map[nat.Port]bool{}
315
+	err = psFilters.WalkValues("publish", func(value string) error {
316
+		if strings.Contains(value, ":") {
317
+			return fmt.Errorf("filter for 'publish' should not contain ':': %v", value)
318
+		}
319
+		//support two formats, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
320
+		proto, port := nat.SplitProtoPort(value)
321
+		start, end, err := nat.ParsePortRange(port)
322
+		if err != nil {
323
+			return fmt.Errorf("error while looking up for publish %v: %s", value, err)
324
+		}
325
+		for i := start; i <= end; i++ {
326
+			p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
327
+			if err != nil {
328
+				return fmt.Errorf("error while looking up for publish %v: %s", value, err)
329
+			}
330
+			publishFilter[p] = true
331
+		}
332
+		return nil
333
+	})
334
+	if err != nil {
335
+		return nil, err
336
+	}
337
+
338
+	exposeFilter := map[nat.Port]bool{}
339
+	err = psFilters.WalkValues("expose", func(value string) error {
340
+		if strings.Contains(value, ":") {
341
+			return fmt.Errorf("filter for 'expose' should not contain ':': %v", value)
342
+		}
343
+		//support two formats, original format <portnum>/[<proto>] or <startport-endport>/[<proto>]
344
+		proto, port := nat.SplitProtoPort(value)
345
+		start, end, err := nat.ParsePortRange(port)
346
+		if err != nil {
347
+			return fmt.Errorf("error while looking up for 'expose' %v: %s", value, err)
348
+		}
349
+		for i := start; i <= end; i++ {
350
+			p, err := nat.NewPort(proto, strconv.FormatUint(i, 10))
351
+			if err != nil {
352
+				return fmt.Errorf("error while looking up for 'expose' %v: %s", value, err)
353
+			}
354
+			exposeFilter[p] = true
355
+		}
356
+		return nil
357
+	})
358
+	if err != nil {
359
+		return nil, err
360
+	}
361
+
314 362
 	return &listContext{
315 363
 		filters:              psFilters,
316 364
 		ancestorFilter:       ancestorFilter,
... ...
@@ -320,6 +376,8 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte
320 320
 		sinceFilter:          sinceContFilter,
321 321
 		taskFilter:           taskFilter,
322 322
 		isTask:               isTask,
323
+		publish:              publishFilter,
324
+		expose:               exposeFilter,
323 325
 		ContainerListOptions: config,
324 326
 		names:                daemon.nameIndex.GetAll(),
325 327
 	}, nil
... ...
@@ -459,6 +517,32 @@ func includeContainerInList(container *container.Container, ctx *listContext) it
459 459
 		}
460 460
 	}
461 461
 
462
+	if len(ctx.publish) > 0 {
463
+		shouldSkip := true
464
+		for port := range ctx.publish {
465
+			if _, ok := container.HostConfig.PortBindings[port]; ok {
466
+				shouldSkip = false
467
+				break
468
+			}
469
+		}
470
+		if shouldSkip {
471
+			return excludeContainer
472
+		}
473
+	}
474
+
475
+	if len(ctx.expose) > 0 {
476
+		shouldSkip := true
477
+		for port := range ctx.expose {
478
+			if _, ok := container.Config.ExposedPorts[port]; ok {
479
+				shouldSkip = false
480
+				break
481
+			}
482
+		}
483
+		if shouldSkip {
484
+			return excludeContainer
485
+		}
486
+	}
487
+
462 488
 	return includeContainer
463 489
 }
464 490
 
... ...
@@ -32,6 +32,8 @@ Options:
32 32
                         - since=(<container-name>|<container-id>)
33 33
                         - ancestor=(<image-name>[:tag]|<image-id>|<image@digest>)
34 34
                           containers created from an image or a descendant.
35
+                        - publish=(<port>[/<proto>]|<startport-endport>/[<proto>])
36
+                        - expose=(<port>[/<proto>]|<startport-endport>/[<proto>])
35 37
                         - is-task=(true|false)
36 38
                         - health=(starting|healthy|unhealthy|none)
37 39
       --format string   Pretty-print containers using a Go template
... ...
@@ -83,6 +85,8 @@ The currently supported filters are:
83 83
 * volume (volume name or mount point) - filters containers that mount volumes.
84 84
 * network (network id or name) - filters containers connected to the provided network
85 85
 * health (starting|healthy|unhealthy|none) - filters containers based on healthcheck status
86
+* publish=(container's published port) - filters published ports by containers
87
+* expose=(container's exposed port) - filters exposed ports by containers
86 88
 
87 89
 #### Label
88 90
 
... ...
@@ -328,6 +332,44 @@ CONTAINER ID        IMAGE       COMMAND       CREATED             STATUS
328 328
 9d4893ed80fe        ubuntu      "top"         10 minutes ago      Up 10 minutes                           test1
329 329
 ```
330 330
 
331
+#### Publish and Expose
332
+
333
+The `publish` and `expose` filters show only containers that have published or exposed port with a given port
334
+number, port range, and/or protocol. The default protocol is `tcp` when not specified.
335
+
336
+The following filter matches all containers that have published port of 80:
337
+
338
+```bash
339
+$ docker run -d --publish=80 busybox top
340
+$ docker run -d --expose=8080 busybox top
341
+
342
+$ docker ps -a
343
+
344
+CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                   NAMES
345
+9833437217a5        busybox             "top"               5 seconds ago       Up 4 seconds        8080/tcp                dreamy_mccarthy
346
+fc7e477723b7        busybox             "top"               50 seconds ago      Up 50 seconds       0.0.0.0:32768->80/tcp   admiring_roentgen
347
+
348
+$ docker ps --filter publish=80
349
+
350
+CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS                   NAMES
351
+fc7e477723b7        busybox             "top"               About a minute ago   Up About a minute   0.0.0.0:32768->80/tcp   admiring_roentgen
352
+```
353
+
354
+The following filter matches all containers that have exposed TCP port in the range of `8000-8080`:
355
+```bash
356
+$ docker ps --filter expose=8000-8080/tcp
357
+
358
+CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
359
+9833437217a5        busybox             "top"               21 seconds ago      Up 19 seconds       8080/tcp            dreamy_mccarthy
360
+```
361
+
362
+The following filter matches all containers that have exposed UDP port `80`:
363
+```bash
364
+$ docker ps --filter publish=80/udp
365
+
366
+CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
367
+```
368
+
331 369
 ## Formatting
332 370
 
333 371
 The formatting option (`--format`) pretty-prints container output using a Go
... ...
@@ -924,3 +924,37 @@ func (s *DockerSuite) TestPsFormatTemplateWithArg(c *check.C) {
924 924
 	out, _ := dockerCmd(c, "ps", "--format", `{{.Names}} {{.Label "some.label"}}`)
925 925
 	c.Assert(strings.TrimSpace(out), checker.Equals, "top label.foo-bar")
926 926
 }
927
+
928
+func (s *DockerSuite) TestPsListContainersFilterPorts(c *check.C) {
929
+	testRequires(c, DaemonIsLinux)
930
+
931
+	out, _ := dockerCmd(c, "run", "-d", "--publish=80", "busybox", "top")
932
+	id1 := strings.TrimSpace(out)
933
+
934
+	out, _ = dockerCmd(c, "run", "-d", "--expose=8080", "busybox", "top")
935
+	id2 := strings.TrimSpace(out)
936
+
937
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q")
938
+	c.Assert(strings.TrimSpace(out), checker.Contains, id1)
939
+	c.Assert(strings.TrimSpace(out), checker.Contains, id2)
940
+
941
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter", "publish=80-8080/udp")
942
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id1)
943
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id2)
944
+
945
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter", "expose=8081")
946
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id1)
947
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id2)
948
+
949
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter", "publish=80-81")
950
+	c.Assert(strings.TrimSpace(out), checker.Equals, id1)
951
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id2)
952
+
953
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter", "expose=80/tcp")
954
+	c.Assert(strings.TrimSpace(out), checker.Equals, id1)
955
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id2)
956
+
957
+	out, _ = dockerCmd(c, "ps", "--no-trunc", "-q", "--filter", "expose=8080/tcp")
958
+	c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), id1)
959
+	c.Assert(strings.TrimSpace(out), checker.Equals, id2)
960
+}