From 1242e7b7a612b1dcae0e09a7fd10e266d34cf9e2 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Sat, 9 Jan 2021 15:54:07 +0100 Subject: [PATCH 1/3] Add network filter for podman ps and pod ps Allow to filter on the network name or full id. For pod ps it will filter on the infra container networks. Signed-off-by: Paul Holzinger --- cmd/podman/common/completion.go | 10 ++++++---- docs/source/markdown/podman-pod-ps.1.md | 1 + docs/source/markdown/podman-ps.1.md | 1 + pkg/domain/filters/containers.go | 19 +++++++++++++++++++ pkg/domain/filters/pods.go | 24 ++++++++++++++++++++++++ test/e2e/pod_ps_test.go | 25 +++++++++++++++++++++++++ test/e2e/ps_test.go | 25 +++++++++++++++++++++++++ 7 files changed, 101 insertions(+), 4 deletions(-) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 83fe0723c3..d018429986 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -982,9 +982,10 @@ func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string) return []string{define.HealthCheckHealthy, define.HealthCheckUnhealthy}, cobra.ShellCompDirectiveNoFileComp }, - "label=": nil, - "exited=": nil, - "until=": nil, + "network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s) }, + "label=": nil, + "exited=": nil, + "until=": nil, } return completeKeyValues(toComplete, kv) } @@ -1004,7 +1005,8 @@ func AutocompletePodPsFilters(cmd *cobra.Command, args []string, toComplete stri "ctr-status=": func(_ string) ([]string, cobra.ShellCompDirective) { return containerStatuses, cobra.ShellCompDirectiveNoFileComp }, - "label=": nil, + "network=": func(s string) ([]string, cobra.ShellCompDirective) { return getNetworks(cmd, s) }, + "label=": nil, } return completeKeyValues(toComplete, kv) } diff --git a/docs/source/markdown/podman-pod-ps.1.md b/docs/source/markdown/podman-pod-ps.1.md index e1d60d765b..aa9319ef36 100644 --- a/docs/source/markdown/podman-pod-ps.1.md +++ b/docs/source/markdown/podman-pod-ps.1.md @@ -93,6 +93,7 @@ Valid filters are listed below: | name | [Name] Pod's name (accepts regex) | | label | [Key] or [Key=Value] Label assigned to a container | | status | Pod's status: `stopped`, `running`, `paused`, `exited`, `dead`, `created`, `degraded` | +| network | [Network] name or full ID of network | | ctr-names | Container name within the pod (accepts regex) | | ctr-ids | Container ID within the pod (accepts regex) | | ctr-status | Container status within the pod | diff --git a/docs/source/markdown/podman-ps.1.md b/docs/source/markdown/podman-ps.1.md index 28212b92c7..70774140c7 100644 --- a/docs/source/markdown/podman-ps.1.md +++ b/docs/source/markdown/podman-ps.1.md @@ -58,6 +58,7 @@ Valid filters are listed below: | volume | [VolumeName] or [MountpointDestination] Volume mounted in container | | health | [Status] healthy or unhealthy | | pod | [Pod] name or full or partial ID of pod | +| network | [Network] name or full ID of network | #### **--format**=*format* diff --git a/pkg/domain/filters/containers.go b/pkg/domain/filters/containers.go index ce6c12b715..09ef6201a3 100644 --- a/pkg/domain/filters/containers.go +++ b/pkg/domain/filters/containers.go @@ -7,6 +7,7 @@ import ( "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/timetype" "github.com/containers/podman/v2/pkg/util" "github.com/pkg/errors" @@ -233,6 +234,24 @@ func GenerateContainerFilterFuncs(filter string, filterValues []string, r *libpo } return false }, nil + case "network": + return func(c *libpod.Container) bool { + networks, _, err := c.Networks() + // if err or no networks, quick out + if err != nil || len(networks) == 0 { + return false + } + for _, net := range networks { + netID := network.GetNetworkID(net) + for _, val := range filterValues { + // match by network name or id + if val == net || val == netID { + return true + } + } + } + return false + }, nil } return nil, errors.Errorf("%s is an invalid filter", filter) } diff --git a/pkg/domain/filters/pods.go b/pkg/domain/filters/pods.go index 7e6b7f2cc1..685c182bad 100644 --- a/pkg/domain/filters/pods.go +++ b/pkg/domain/filters/pods.go @@ -6,6 +6,7 @@ import ( "github.com/containers/podman/v2/libpod" "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/util" "github.com/pkg/errors" ) @@ -134,6 +135,29 @@ func GeneratePodFilterFunc(filter string, filterValues []string) ( } return true }, nil + case "network": + return func(p *libpod.Pod) bool { + infra, err := p.InfraContainer() + // no infra, quick out + if err != nil { + return false + } + networks, _, err := infra.Networks() + // if err or no networks, quick out + if err != nil || len(networks) == 0 { + return false + } + for _, net := range networks { + netID := network.GetNetworkID(net) + for _, val := range filterValues { + // match by network name or id + if val == net || val == netID { + return true + } + } + } + return false + }, nil } return nil, errors.Errorf("%s is an invalid filter", filter) } diff --git a/test/e2e/pod_ps_test.go b/test/e2e/pod_ps_test.go index 225da785cc..6a740ed097 100644 --- a/test/e2e/pod_ps_test.go +++ b/test/e2e/pod_ps_test.go @@ -6,6 +6,7 @@ import ( "sort" . "github.com/containers/podman/v2/test/utils" + "github.com/containers/storage/pkg/stringid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" @@ -280,6 +281,30 @@ var _ = Describe("Podman ps", func() { Expect(session.OutputToString()).To(Not(ContainSubstring(podid3))) }) + It("podman pod ps filter network", func() { + net := stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", net}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net) + + session = podmanTest.Podman([]string{"pod", "create", "--network", net}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + podWithNet := session.OutputToString() + + session = podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + podWithoutNet := session.OutputToString() + + session = podmanTest.Podman([]string{"pod", "ps", "--no-trunc", "--filter", "network=" + net}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + Expect(session.OutputToString()).To(ContainSubstring(podWithNet)) + Expect(session.OutputToString()).To(Not(ContainSubstring(podWithoutNet))) + }) + It("pod no infra should ps", func() { session := podmanTest.Podman([]string{"pod", "create", "--infra=false"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index 0c5d817bad..0d2bed485e 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -8,6 +8,7 @@ import ( "strings" . "github.com/containers/podman/v2/test/utils" + "github.com/containers/storage/pkg/stringid" "github.com/docker/go-units" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -724,4 +725,28 @@ var _ = Describe("Podman ps", func() { }) + It("podman ps filter network", func() { + net := stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", net}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net) + + session = podmanTest.Podman([]string{"create", "--network", net, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + ctrWithNet := session.OutputToString() + + session = podmanTest.Podman([]string{"create", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + ctrWithoutNet := session.OutputToString() + + session = podmanTest.Podman([]string{"ps", "--all", "--no-trunc", "--filter", "network=" + net}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + Expect(session.OutputToString()).To(ContainSubstring(ctrWithNet)) + Expect(session.OutputToString()).To(Not(ContainSubstring(ctrWithoutNet))) + }) + }) From 38baf3d5e2bc5c1e8b216dc072ea1d3eaf5053b8 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Sat, 9 Jan 2021 16:54:41 +0100 Subject: [PATCH 2/3] Add Networks format placeholder to podman ps and pod ps `podman ps --format {{.Networks}}` will show all connected networks for this container. For `pod ps` it will show the infra container networks. Signed-off-by: Paul Holzinger --- cmd/podman/containers/ps.go | 5 ++++ cmd/podman/pods/ps.go | 5 ++++ docs/source/markdown/podman-pod-ps.1.md | 2 ++ docs/source/markdown/podman-ps.1.md | 1 + pkg/domain/entities/container_ps.go | 2 ++ pkg/domain/entities/pods.go | 6 ++-- pkg/domain/infra/abi/pods.go | 12 ++++++++ pkg/ps/ps.go | 6 ++++ test/e2e/pod_ps_test.go | 39 +++++++++++++++++++++++++ test/e2e/ps_test.go | 39 +++++++++++++++++++++++++ 10 files changed, 115 insertions(+), 2 deletions(-) diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 5d08e61638..d23771fc5f 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -392,6 +392,11 @@ func (l psReporter) Names() string { return l.ListContainer.Names[0] } +// Networks returns the container network names in string format +func (l psReporter) Networks() string { + return strings.Join(l.ListContainer.Networks, ",") +} + // Ports converts from Portmappings to the string form // required by ps func (l psReporter) Ports() string { diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 99d3244113..a27ab48590 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -191,6 +191,11 @@ func (l ListPodReporter) Labels() map[string]string { return l.ListPodsReport.Labels } +// Networks returns the infra container network names in string format +func (l ListPodReporter) Networks() string { + return strings.Join(l.ListPodsReport.Networks, ",") +} + // NumberOfContainers returns an int representation for // the number of containers belonging to the pod func (l ListPodReporter) NumberOfContainers() int { diff --git a/docs/source/markdown/podman-pod-ps.1.md b/docs/source/markdown/podman-pod-ps.1.md index aa9319ef36..ab250e1ff1 100644 --- a/docs/source/markdown/podman-pod-ps.1.md +++ b/docs/source/markdown/podman-pod-ps.1.md @@ -72,6 +72,8 @@ Valid placeholders for the Go template are listed below: | .Cgroup | Cgroup path of pod | | .Created | Creation time of pod | | .InfraID | Pod infra container ID | +| .Networks | Show all networks connected to the infra container | + #### **--sort** Sort by created, ID, name, status, or number of containers diff --git a/docs/source/markdown/podman-ps.1.md b/docs/source/markdown/podman-ps.1.md index 70774140c7..bb8001ad96 100644 --- a/docs/source/markdown/podman-ps.1.md +++ b/docs/source/markdown/podman-ps.1.md @@ -80,6 +80,7 @@ Valid placeholders for the Go template are listed below: | .Ports | Exposed ports | | .Size | Size of container | | .Names | Name of container | +| .Networks | Show all networks connected to the container | | .Labels | All the labels assigned to the container | | .Mounts | Volumes mounted in the container | diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index ff3b087ed3..6709ca48a9 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -43,6 +43,8 @@ type ListContainer struct { // Namespaces the container belongs to. Requires the // namespace boolean to be true Namespaces ListContainerNamespaces + // The network names assigned to the container + Networks []string // The process id of the container Pid int // If the container is part of Pod, the Pod ID. Requires the pod diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 4264198337..edb0af15a3 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -28,8 +28,10 @@ type ListPodsReport struct { InfraId string //nolint Name string Namespace string - Status string - Labels map[string]string + // Network names connected to infra container + Networks []string + Status string + Labels map[string]string } type ListPodContainer struct { diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index f108b770c3..2a8445c9f8 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -333,6 +333,17 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti if err != nil { return nil, err } + networks := []string{} + if len(infraID) > 0 { + infra, err := p.InfraContainer() + if err != nil { + return nil, err + } + networks, _, err = infra.Networks() + if err != nil { + return nil, err + } + } reports = append(reports, &entities.ListPodsReport{ Cgroup: p.CgroupParent(), Containers: lpcs, @@ -341,6 +352,7 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti InfraId: infraID, Name: p.Name(), Namespace: p.Namespace(), + Networks: networks, Status: status, Labels: p.Labels(), }) diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index 9e0dcb7281..dc577890a6 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -178,6 +178,11 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities return entities.ListContainer{}, err } + networks, _, err := ctr.Networks() + if err != nil { + return entities.ListContainer{}, err + } + ps := entities.ListContainer{ AutoRemove: ctr.AutoRemove(), Command: conConfig.Command, @@ -192,6 +197,7 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities Labels: conConfig.Labels, Mounts: ctr.UserVolumes(), Names: []string{conConfig.Name}, + Networks: networks, Pid: pid, Pod: conConfig.Pod, Ports: portMappings, diff --git a/test/e2e/pod_ps_test.go b/test/e2e/pod_ps_test.go index 6a740ed097..9f63c1d5d0 100644 --- a/test/e2e/pod_ps_test.go +++ b/test/e2e/pod_ps_test.go @@ -305,6 +305,45 @@ var _ = Describe("Podman ps", func() { Expect(session.OutputToString()).To(Not(ContainSubstring(podWithoutNet))) }) + It("podman pod ps --format networks", func() { + session := podmanTest.Podman([]string{"pod", "create"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + + session = podmanTest.Podman([]string{"pod", "ps", "--format", "{{ .Networks }}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + if isRootless() { + // rootless container don't have a network by default + Expect(session.OutputToString()).To(Equal("")) + } else { + // default network name is podman + Expect(session.OutputToString()).To(Equal("podman")) + } + + net1 := stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", net1}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net1) + net2 := stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", net2}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net2) + + session = podmanTest.Podman([]string{"pod", "create", "--network", net1 + "," + net2}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + pid := session.OutputToString() + + session = podmanTest.Podman([]string{"pod", "ps", "--format", "{{ .Networks }}", "--filter", "id=" + pid}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + // the output is not deterministic so check both possible orders + Expect(session.OutputToString()).To(Or(Equal(net1+","+net2), Equal(net2+","+net1))) + }) + It("pod no infra should ps", func() { session := podmanTest.Podman([]string{"pod", "create", "--infra=false"}) session.WaitWithDefaultTimeout() diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go index 0d2bed485e..13701fc3b4 100644 --- a/test/e2e/ps_test.go +++ b/test/e2e/ps_test.go @@ -749,4 +749,43 @@ var _ = Describe("Podman ps", func() { Expect(session.OutputToString()).To(Not(ContainSubstring(ctrWithoutNet))) }) + It("podman ps --format networks", func() { + session := podmanTest.Podman([]string{"create", ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + + session = podmanTest.Podman([]string{"ps", "--all", "--format", "{{ .Networks }}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + if isRootless() { + // rootless container don't have a network by default + Expect(session.OutputToString()).To(Equal("")) + } else { + // default network name is podman + Expect(session.OutputToString()).To(Equal("podman")) + } + + net1 := stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", net1}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net1) + net2 := stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", net2}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(net2) + + session = podmanTest.Podman([]string{"create", "--network", net1 + "," + net2, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + cid := session.OutputToString() + + session = podmanTest.Podman([]string{"ps", "--all", "--format", "{{ .Networks }}", "--filter", "id=" + cid}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + // the output is not deterministic so check both possible orders + Expect(session.OutputToString()).To(Or(Equal(net1+","+net2), Equal(net2+","+net1))) + }) + }) From 82bce7d906c5b5aa1d98782bed8d5e77637fc0ef Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Sat, 9 Jan 2021 23:38:26 +0100 Subject: [PATCH 3/3] Use abi PodPs implementation for libpod/pods/json endpoint This removes unnecessary code duplication. Signed-off-by: Paul Holzinger --- pkg/api/handlers/libpod/pods.go | 7 ++- pkg/api/handlers/utils/pods.go | 87 --------------------------------- 2 files changed, 6 insertions(+), 88 deletions(-) delete mode 100644 pkg/api/handlers/utils/pods.go diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index de373f05f3..2409d3a20d 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -43,6 +43,7 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { } func Pods(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { Filters map[string][]string `schema:"filters"` @@ -55,7 +56,11 @@ func Pods(w http.ResponseWriter, r *http.Request) { return } - pods, err := utils.GetPods(w, r) + containerEngine := abi.ContainerEngine{Libpod: runtime} + podPSOptions := entities.PodPSOptions{ + Filters: query.Filters, + } + pods, err := containerEngine.PodPs(r.Context(), podPSOptions) if err != nil { utils.Error(w, "Something went wrong", http.StatusInternalServerError, err) return diff --git a/pkg/api/handlers/utils/pods.go b/pkg/api/handlers/utils/pods.go deleted file mode 100644 index 0fe3a308ba..0000000000 --- a/pkg/api/handlers/utils/pods.go +++ /dev/null @@ -1,87 +0,0 @@ -package utils - -import ( - "net/http" - - "github.com/containers/podman/v2/libpod" - "github.com/containers/podman/v2/pkg/domain/entities" - dfilters "github.com/containers/podman/v2/pkg/domain/filters" - "github.com/gorilla/schema" -) - -func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport, error) { - var ( - pods []*libpod.Pod - ) - runtime := r.Context().Value("runtime").(*libpod.Runtime) - decoder := r.Context().Value("decoder").(*schema.Decoder) - - query := struct { - All bool - Filters map[string][]string `schema:"filters"` - Digests bool - }{} - - if err := decoder.Decode(&query, r.URL.Query()); err != nil { - return nil, err - } - if _, found := r.URL.Query()["digests"]; found && query.Digests { - UnSupportedParameter("digests") - } - - filters := make([]libpod.PodFilter, 0, len(query.Filters)) - for k, v := range query.Filters { - f, err := dfilters.GeneratePodFilterFunc(k, v) - if err != nil { - return nil, err - } - filters = append(filters, f) - } - pods, err := runtime.Pods(filters...) - if err != nil { - return nil, err - } - - if len(pods) == 0 { - return []*entities.ListPodsReport{}, nil - } - - lps := make([]*entities.ListPodsReport, 0, len(pods)) - for _, pod := range pods { - status, err := pod.GetPodStatus() - if err != nil { - return nil, err - } - ctrs, err := pod.AllContainers() - if err != nil { - return nil, err - } - infraID, err := pod.InfraContainerID() - if err != nil { - return nil, err - } - lp := entities.ListPodsReport{ - Cgroup: pod.CgroupParent(), - Created: pod.CreatedTime(), - Id: pod.ID(), - Name: pod.Name(), - Namespace: pod.Namespace(), - Status: status, - InfraId: infraID, - Labels: pod.Labels(), - } - for _, ctr := range ctrs { - state, err := ctr.State() - if err != nil { - return nil, err - } - lp.Containers = append(lp.Containers, &entities.ListPodContainer{ - Id: ctr.ID(), - Names: ctr.Name(), - Status: state.String(), - }) - } - lps = append(lps, &lp) - } - return lps, nil -}