diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 032856173e..da9f714c9d 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,24 @@ 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 + } else 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 +284,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 +337,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 { + mountpoints := make([]MountPoint, 0, len(spMounts)) + 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) + mountpoints = append(mountpoints, mp) + } + + return mountpoints +} + 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..1c772c4daa --- /dev/null +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -0,0 +1,236 @@ +/* + 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 ( + "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) { + 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", + }, + }, + }, + // 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", + }, + }, + }, + } + + for _, tc := range testcase { + d, _ := ContainerFromNative(tc.n) + assert.DeepEqual(t, d, tc.expected) + } +}