diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 032856173e..6787fec65e 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -32,6 +32,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "time" "github.com/containerd/containerd" @@ -210,6 +211,7 @@ type NetworkEndpointSettings struct { // ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container. func ContainerFromNative(n *native.Container) (*Container, error) { + var hostname string c := &Container{ ID: n.ID, Created: n.CreatedAt.Format(time.RFC3339Nano), @@ -231,15 +233,25 @@ func ContainerFromNative(n *native.Container) (*Container, error) { } c.AppArmorProfile = p.ApparmorProfile } + c.Mounts = mountsFromNative(sp.Mounts) + for _, mount := range c.Mounts { + if mount.Destination == "/etc/resolv.conf" { + c.ResolvConfPath = mount.Source + } + if mount.Destination == "/etc/hostname" { + c.HostnamePath = mount.Source + } + } + hostname = sp.Hostname } if nerdctlStateDir := n.Labels[labels.StateDir]; nerdctlStateDir != "" { - c.ResolvConfPath = filepath.Join(nerdctlStateDir, "resolv.conf") - if _, err := os.Stat(c.ResolvConfPath); err != nil { - c.ResolvConfPath = "" + resolvConfPath := filepath.Join(nerdctlStateDir, "resolv.conf") + if _, err := os.Stat(resolvConfPath); err == nil { + c.ResolvConfPath = resolvConfPath } - c.HostnamePath = filepath.Join(nerdctlStateDir, "hostname") - if _, err := os.Stat(c.HostnamePath); err != nil { - c.HostnamePath = "" + hostnamePath := filepath.Join(nerdctlStateDir, "hostname") + if _, err := os.Stat(hostnamePath); err == nil { + c.HostnamePath = hostnamePath } c.LogPath = filepath.Join(nerdctlStateDir, n.ID+"-json.log") if _, err := os.Stat(c.LogPath); err != nil { @@ -273,9 +285,12 @@ func ContainerFromNative(n *native.Container) (*Container, error) { } c.State = cs c.Config = &Config{ - Hostname: n.Labels[labels.Hostname], - Labels: n.Labels, + Labels: n.Labels, } + if n.Labels[labels.Hostname] != "" { + hostname = n.Labels[labels.Hostname] + } + c.Config.Hostname = hostname return c, nil } @@ -323,6 +338,27 @@ func ImageFromNative(n *native.Image) (*Image, error) { i.Size = n.Size return i, nil } + +// mountsFromNative only filters bind mount to transform from native container. +// Because native container shows all types of mounts, such as tmpfs, proc, sysfs. +func mountsFromNative(spMounts []specs.Mount) []MountPoint { + var mountpoins []MountPoint + for _, m := range spMounts { + var mp MountPoint + if m.Type != "bind" { + continue + } + mp.Type = m.Type + mp.Source = m.Source + mp.Destination = m.Destination + mp.Mode = strings.Join(m.Options, ",") + mp.RW, mp.Propagation = ParseMountProperties(m.Options) + mountpoins = append(mountpoins, mp) + } + + return mountpoins +} + func statusFromNative(x containerd.Status, labels map[string]string) string { switch s := x.Status; s { case containerd.Stopped: diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go new file mode 100644 index 0000000000..a54faef223 --- /dev/null +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -0,0 +1,217 @@ +package dockercompat + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/containers" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + + "github.com/opencontainers/runtime-spec/specs-go" + "gotest.tools/v3/assert" +) + +func TestContainerFromNative(t *testing.T) { + 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": "/mock-state-dir", + "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: "/mock-state-dir/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": "/mock-state-dir", + "nerdctl/hostname": "host1", + }, + Hostname: "host1", + }, + }, + }, + // 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: "/mock-sandbox-dir/resolv.conf", + HostnamePath: "/mock-sandbox-dir/hostname", + State: &ContainerState{ + Status: "running", + Running: true, + Pid: 10000, + FinishedAt: "0001-01-01T00:00:00Z", + }, + Mounts: []MountPoint{ + { + Type: "bind", + Source: "/mock-sandbox-dir/resolv.conf", + Destination: "/etc/resolv.conf", + Mode: "rbind,rprivate,rw", + RW: true, + Propagation: "rprivate", + }, + { + Type: "bind", + Source: "/mock-sandbox-dir/hostname", + Destination: "/etc/hostname", + Mode: "rbind,rprivate,rw", + RW: true, + Propagation: "rprivate", + }, + { + Type: "bind", + Source: "/mnt/foo", + Destination: "/mnt/foo", + Mode: "rbind,rslave,rw", + RW: true, + Propagation: "rslave", + }, + // ignore sysfs mountpoint + }, + Config: &Config{}, + }, + }, + // 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: "host1", + 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", + }, + Mounts: []MountPoint{ + { + Type: "bind", + Source: "/mnt/foo", + Destination: "/mnt/foo", + Mode: "rbind,rslave,rw", + RW: true, + Propagation: "rslave", + }, + // ignore sysfs mountpoint + }, + Config: &Config{ + Hostname: "host1", + }, + }, + }, + } + + os.Mkdir("/mock-state-dir", 0755) + os.WriteFile(filepath.Join("/mock-state-dir", "resolv.conf"), []byte("hello"), 0600) + defer os.RemoveAll("/mock-state-dir") + + for _, tc := range testcase { + d, _ := ContainerFromNative(tc.n) + assert.DeepEqual(t, d, tc.expected) + } +}