Skip to content

Commit

Permalink
inspect: show mounts info from CRI/ctr containers
Browse files Browse the repository at this point in the history
Fix containerd#2934

Containers created by CRI or ctr with bind mounts
don't have label "nerdct/mounts".

However, 'inspect' with dockercompat mode only parses mounts
from label "nerdct/mounts".

After this patch, 'inspect' with dockercompat mode can
show CRI and ctr container bind mounts.

ResolvConfPath, HostnamePath and Hostname are taken to the same
consideration.

Signed-off-by: baijia <baijia.wr@antgroup.com>
  • Loading branch information
baijia committed Apr 25, 2024
1 parent 4ef3190 commit b379bc1
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 8 deletions.
52 changes: 44 additions & 8 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"github.com/containerd/containerd"
Expand Down Expand Up @@ -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),
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand Down
217 changes: 217 additions & 0 deletions pkg/inspecttypes/dockercompat/dockercompat_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit b379bc1

Please sign in to comment.