From 48824a763e7f7199f5eeea227dea890b60c1b943 Mon Sep 17 00:00:00 2001 From: Sam Chew Date: Fri, 16 Aug 2024 09:54:46 -0700 Subject: [PATCH] Include init NetworkSettings within inspect response Related to [containerd#3310](https://github.com/containerd/nerdctl/issues/3310) New behavior will always initialize a NetworkSettings entity for the inspect response, including Ports child member. If inspecting a running container with published ports, then all information will be correctly returned. If inspecting a running container without published ports, then NetworkSettings will include an initialized Ports member. If inspecting a stopped/exited container, then an entirely initialized, "empty-value" NetworkSettings is returned. Signed-off-by: Sam Chew --- pkg/inspecttypes/dockercompat/dockercompat.go | 16 +- .../dockercompat/dockercompat_test.go | 325 ++++++++++++++++++ 2 files changed, 336 insertions(+), 5 deletions(-) create mode 100644 pkg/inspecttypes/dockercompat/dockercompat_test.go diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 2312274f9af..359e2f90d1b 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -173,7 +173,7 @@ type ContainerState struct { } type NetworkSettings struct { - Ports *nat.PortMap `json:",omitempty"` + Ports *nat.PortMap DefaultNetworkSettings Networks map[string]*NetworkEndpointSettings } @@ -337,12 +337,15 @@ func statusFromNative(x containerd.Status, labels map[string]string) string { } func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSettings, error) { - if n == nil { - return nil, nil - } res := &NetworkSettings{ Networks: make(map[string]*NetworkEndpointSettings), } + resPortMap := make(nat.PortMap) + res.Ports = &resPortMap + if n == nil { + return res, nil + } + var primary *NetworkEndpointSettings for _, x := range n.Interfaces { if x.Interface.Flags&net.FlagLoopback != 0 { @@ -386,8 +389,11 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting if err != nil { return nil, err } - res.Ports = nports + for portLabel, portBindings := range *nports { + resPortMap[portLabel] = portBindings + } } + if x.Index == n.PrimaryInterface { primary = nes } diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go new file mode 100644 index 00000000000..0a89aa7898a --- /dev/null +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -0,0 +1,325 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package dockercompat + +import ( + "net" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/opencontainers/runtime-spec/specs-go" + "gotest.tools/v3/assert" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + + "github.com/containerd/nerdctl/pkg/inspecttypes/native" +) + +func TestContainerFromNative(t *testing.T) { + tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw") + if err != nil { + t.Fatal(err) + } + os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) + defer os.RemoveAll(tempStateDir) + + testcase := []struct { + name string + n *native.Container + expected *Container + }{ + // nerdctl container, mount /mnt/foo:/mnt/foo:rw,rslave; ResolvConfPath; hostname + { + name: "container from nerdctl", + n: &native.Container{ + Container: containers.Container{ + Labels: map[string]string{ + "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/state-dir": tempStateDir, + "nerdctl/hostname": "host1", + }, + }, + Spec: &specs.Spec{}, + Process: &native.Process{ + Pid: 10000, + Status: containerd.Status{ + Status: "running", + }, + }, + }, + expected: &Container{ + Created: "0001-01-01T00:00:00Z", + Platform: runtime.GOOS, + ResolvConfPath: tempStateDir + "/resolv.conf", + State: &ContainerState{ + Status: "running", + Running: true, + Pid: 10000, + FinishedAt: "0001-01-01T00:00:00Z", + }, + Mounts: []MountPoint{ + { + Type: "bind", + Source: "/mnt/foo", + Destination: "/mnt/foo", + Mode: "rshared,rw", + RW: true, + Propagation: "rshared", + }, + }, + Config: &Config{ + Labels: map[string]string{ + "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/state-dir": tempStateDir, + "nerdctl/hostname": "host1", + }, + Hostname: "host1", + }, + NetworkSettings: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{}, + }, + }, + }, + // cri container, mount /mnt/foo:/mnt/foo:rw,rslave; mount resolv.conf and hostname; internal sysfs mount + { + name: "container from cri", + n: &native.Container{ + Container: containers.Container{}, + Spec: &specs.Spec{ + Mounts: []specs.Mount{ + { + Destination: "/etc/resolv.conf", + Type: "bind", + Source: "/mock-sandbox-dir/resolv.conf", + Options: []string{"rbind", "rprivate", "rw"}, + }, + { + Destination: "/etc/hostname", + Type: "bind", + Source: "/mock-sandbox-dir/hostname", + Options: []string{"rbind", "rprivate", "rw"}, + }, + { + Destination: "/mnt/foo", + Type: "bind", + Source: "/mnt/foo", + Options: []string{"rbind", "rslave", "rw"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "ro"}, + }, + }, + }, + Process: &native.Process{ + Pid: 10000, + Status: containerd.Status{ + Status: "running", + }, + }, + }, + expected: &Container{ + Created: "0001-01-01T00:00:00Z", + Platform: runtime.GOOS, + ResolvConfPath: "", + HostnamePath: "", + State: &ContainerState{ + Status: "running", + Running: true, + Pid: 10000, + FinishedAt: "0001-01-01T00:00:00Z", + }, + Config: &Config{}, + NetworkSettings: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{}, + }, + }, + }, + // ctr container, mount /mnt/foo:/mnt/foo:rw,rslave; internal sysfs mount; hostname + { + name: "container from ctr", + n: &native.Container{ + Container: containers.Container{}, + Spec: &specs.Spec{ + Hostname: "", + Mounts: []specs.Mount{ + { + Destination: "/mnt/foo", + Type: "bind", + Source: "/mnt/foo", + Options: []string{"rbind", "rslave", "rw"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev", "ro"}, + }, + }, + }, + Process: &native.Process{ + Pid: 10000, + Status: containerd.Status{ + Status: "running", + }, + }, + }, + expected: &Container{ + Created: "0001-01-01T00:00:00Z", + Platform: runtime.GOOS, + State: &ContainerState{ + Status: "running", + Running: true, + Pid: 10000, + FinishedAt: "0001-01-01T00:00:00Z", + }, + Config: &Config{ + Hostname: "", + }, + NetworkSettings: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{}, + }, + }, + }, + } + + for _, tc := range testcase { + t.Run(tc.name, func(tt *testing.T) { + d, _ := ContainerFromNative(tc.n) + assert.DeepEqual(tt, d, tc.expected) + }) + } +} + +func TestNetworkSettingsFromNative(t *testing.T) { + tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw") + if err != nil { + t.Fatal(err) + } + os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644) + defer os.RemoveAll(tempStateDir) + + testcase := []struct { + name string + n *native.NetNS + s *specs.Spec + expected *NetworkSettings + }{ + // Given null native.NetNS, Return initialized NetworkSettings + // UseCase: Inspect a Stopped Container + { + name: "Given Null NetNS, Return initialized NetworkSettings", + n: nil, + s: &specs.Spec{}, + expected: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{}, + }, + }, + // Given native.NetNS with single Interface with Port Annotations, Return populated NetworkSettings + // UseCase: Inspect a Running Container with published ports + { + name: "Given NetNS with single Interface with Port Annotation, Return populated NetworkSettings", + n: &native.NetNS{ + Interfaces: []native.NetInterface{ + { + Interface: net.Interface{ + Index: 1, + MTU: 1500, + Name: "eth0.100", + Flags: net.FlagUp, + }, + HardwareAddr: "xx:xx:xx:xx:xx:xx", + Flags: []string{}, + Addrs: []string{"10.0.4.30/24"}, + }, + }, + }, + s: &specs.Spec{ + Annotations: map[string]string{ + "nerdctl/ports": "[{\"HostPort\":8075,\"ContainerPort\":77,\"Protocol\":\"tcp\",\"HostIP\":\"127.0.0.1\"}]", + }, + }, + expected: &NetworkSettings{ + Ports: &nat.PortMap{ + nat.Port("77/tcp"): []nat.PortBinding{ + { + HostIP: "127.0.0.1", + HostPort: "8075", + }, + }, + }, + Networks: map[string]*NetworkEndpointSettings{ + "unknown-eth0.100": { + IPAddress: "10.0.4.30", + IPPrefixLen: 24, + MacAddress: "xx:xx:xx:xx:xx:xx", + }, + }, + }, + }, + // Given native.NetNS with single Interface without Port Annotations, Return valid NetworkSettings w/ empty Ports + // UseCase: Inspect a Running Container without published ports + { + name: "Given NetNS with single Interface without Port Annotations, Return valid NetworkSettings w/ empty Ports", + n: &native.NetNS{ + Interfaces: []native.NetInterface{ + { + Interface: net.Interface{ + Index: 1, + MTU: 1500, + Name: "eth0.100", + Flags: net.FlagUp, + }, + HardwareAddr: "xx:xx:xx:xx:xx:xx", + Flags: []string{}, + Addrs: []string{"10.0.4.30/24"}, + }, + }, + }, + s: &specs.Spec{ + Annotations: map[string]string{}, + }, + expected: &NetworkSettings{ + Ports: &nat.PortMap{}, + Networks: map[string]*NetworkEndpointSettings{ + "unknown-eth0.100": { + IPAddress: "10.0.4.30", + IPPrefixLen: 24, + MacAddress: "xx:xx:xx:xx:xx:xx", + }, + }, + }, + }, + } + + for _, tc := range testcase { + t.Run(tc.name, func(tt *testing.T) { + d, _ := networkSettingsFromNative(tc.n, tc.s) + assert.DeepEqual(tt, d, tc.expected) + }) + } +}