github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/inspecttypes/dockercompat/dockercompat.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  /*
    18     Portions from https://github.com/moby/moby/blob/v20.10.1/api/types/types.go
    19     Copyright (C) Docker/Moby authors.
    20     Licensed under the Apache License, Version 2.0
    21     NOTICE: https://github.com/moby/moby/blob/v20.10.1/NOTICE
    22  */
    23  
    24  // Package dockercompat mimics `docker inspect` objects.
    25  package dockercompat
    26  
    27  import (
    28  	"encoding/json"
    29  	"fmt"
    30  	"net"
    31  	"os"
    32  	"path/filepath"
    33  	"runtime"
    34  	"strconv"
    35  	"strings"
    36  	"time"
    37  
    38  	"github.com/containerd/containerd"
    39  	"github.com/containerd/containerd/runtime/restart"
    40  	gocni "github.com/containerd/go-cni"
    41  	"github.com/containerd/log"
    42  	"github.com/containerd/nerdctl/v2/pkg/imgutil"
    43  	"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
    44  	"github.com/containerd/nerdctl/v2/pkg/labels"
    45  	"github.com/containerd/nerdctl/v2/pkg/ocihook/state"
    46  	"github.com/docker/go-connections/nat"
    47  	"github.com/opencontainers/runtime-spec/specs-go"
    48  	"github.com/tidwall/gjson"
    49  )
    50  
    51  // Image mimics a `docker image inspect` object.
    52  // From https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L340-L374
    53  type Image struct {
    54  	ID          string `json:"Id"`
    55  	RepoTags    []string
    56  	RepoDigests []string
    57  	// TODO: Parent      string
    58  	Comment string
    59  	Created string
    60  	// TODO: Container   string
    61  	// TODO: ContainerConfig *container.Config
    62  	// TODO: DockerVersion string
    63  	Author       string
    64  	Config       *Config
    65  	Architecture string
    66  	// TODO: Variant       string `json:",omitempty"`
    67  	Os string
    68  	// TODO: OsVersion     string `json:",omitempty"`
    69  	Size int64 // Size is the unpacked size of the image
    70  	// TODO: GraphDriver     GraphDriverData
    71  	RootFS   RootFS
    72  	Metadata ImageMetadata
    73  }
    74  
    75  type RootFS struct {
    76  	Type      string
    77  	Layers    []string `json:",omitempty"`
    78  	BaseLayer string   `json:",omitempty"`
    79  }
    80  
    81  type ImageMetadata struct {
    82  	LastTagTime time.Time `json:",omitempty"`
    83  }
    84  
    85  // Container mimics a `docker container inspect` object.
    86  // From https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L340-L374
    87  type Container struct {
    88  	ID             string `json:"Id"`
    89  	Created        string
    90  	Path           string
    91  	Args           []string
    92  	State          *ContainerState
    93  	Image          string
    94  	ResolvConfPath string
    95  	HostnamePath   string
    96  	// TODO: HostsPath      string
    97  	LogPath string
    98  	// Unimplemented: Node            *ContainerNode `json:",omitempty"` // Node is only propagated by Docker Swarm standalone API
    99  	Name         string
   100  	RestartCount int
   101  	Driver       string
   102  	Platform     string
   103  	// TODO: MountLabel      string
   104  	// TODO: ProcessLabel    string
   105  	AppArmorProfile string
   106  	// TODO: ExecIDs         []string
   107  	// TODO: HostConfig      *container.HostConfig
   108  	// TODO: GraphDriver     GraphDriverData
   109  	SizeRw     *int64 `json:",omitempty"`
   110  	SizeRootFs *int64 `json:",omitempty"`
   111  
   112  	Mounts          []MountPoint
   113  	Config          *Config
   114  	NetworkSettings *NetworkSettings
   115  }
   116  
   117  // From https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L416-L427
   118  // MountPoint represents a mount point configuration inside the container.
   119  // This is used for reporting the mountpoints in use by a container.
   120  type MountPoint struct {
   121  	Type        string `json:",omitempty"`
   122  	Name        string `json:",omitempty"`
   123  	Source      string
   124  	Destination string
   125  	Driver      string `json:",omitempty"`
   126  	Mode        string
   127  	RW          bool
   128  	Propagation string
   129  }
   130  
   131  // config is from https://github.com/moby/moby/blob/8dbd90ec00daa26dc45d7da2431c965dec99e8b4/api/types/container/config.go#L37-L69
   132  type Config struct {
   133  	Hostname string `json:",omitempty"` // Hostname
   134  	// TODO: Domainname   string      // Domainname
   135  	User        string `json:",omitempty"` // User that will run the command(s) inside the container, also support user:group
   136  	AttachStdin bool   // Attach the standard input, makes possible user interaction
   137  	// TODO: AttachStdout bool        // Attach the standard output
   138  	// TODO: AttachStderr bool        // Attach the standard error
   139  	ExposedPorts nat.PortSet `json:",omitempty"` // List of exposed ports
   140  	// TODO: Tty          bool        // Attach standard streams to a tty, including stdin if it is not closed.
   141  	// TODO: OpenStdin    bool        // Open stdin
   142  	// TODO: StdinOnce    bool        // If true, close stdin after the 1 attached client disconnects.
   143  	Env []string `json:",omitempty"` // List of environment variable to set in the container
   144  	Cmd []string `json:",omitempty"` // Command to run when starting the container
   145  	// TODO Healthcheck     *HealthConfig       `json:",omitempty"` // Healthcheck describes how to check the container is healthy
   146  	// TODO: ArgsEscaped     bool                `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific).
   147  	// TODO: Image           string              // Name of the image as it was passed by the operator (e.g. could be symbolic)
   148  	Volumes    map[string]struct{} `json:",omitempty"` // List of volumes (mounts) used for the container
   149  	WorkingDir string              `json:",omitempty"` // Current directory (PWD) in the command will be launched
   150  	Entrypoint []string            `json:",omitempty"` // Entrypoint to run when starting the container
   151  	// TODO: NetworkDisabled bool                `json:",omitempty"` // Is network disabled
   152  	// TODO: MacAddress      string              `json:",omitempty"` // Mac Address of the container
   153  	// TODO: OnBuild         []string            // ONBUILD metadata that were defined on the image Dockerfile
   154  	Labels map[string]string `json:",omitempty"` // List of labels set to this container
   155  	// TODO: StopSignal      string              `json:",omitempty"` // Signal to stop a container
   156  	// TODO: StopTimeout     *int                `json:",omitempty"` // Timeout (in seconds) to stop a container
   157  	// TODO: Shell           []string            `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
   158  }
   159  
   160  // ContainerState is from https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L313-L326
   161  type ContainerState struct {
   162  	Status     string // String representation of the container state. Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead"
   163  	Running    bool
   164  	Paused     bool
   165  	Restarting bool
   166  	// TODO: OOMKilled  bool
   167  	// TODO:	Dead       bool
   168  	Pid        int
   169  	ExitCode   int
   170  	Error      string
   171  	StartedAt  string
   172  	FinishedAt string
   173  	// TODO: Health     *Health `json:",omitempty"`
   174  }
   175  
   176  type NetworkSettings struct {
   177  	Ports *nat.PortMap `json:",omitempty"`
   178  	DefaultNetworkSettings
   179  	Networks map[string]*NetworkEndpointSettings
   180  }
   181  
   182  // DefaultNetworkSettings is from https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L405-L414
   183  type DefaultNetworkSettings struct {
   184  	// TODO EndpointID          string // EndpointID uniquely represents a service endpoint in a Sandbox
   185  	// TODO Gateway             string // Gateway holds the gateway address for the network
   186  	GlobalIPv6Address   string // GlobalIPv6Address holds network's global IPv6 address
   187  	GlobalIPv6PrefixLen int    // GlobalIPv6PrefixLen represents mask length of network's global IPv6 address
   188  	IPAddress           string // IPAddress holds the IPv4 address for the network
   189  	IPPrefixLen         int    // IPPrefixLen represents mask length of network's IPv4 address
   190  	// TODO IPv6Gateway         string // IPv6Gateway holds gateway address specific for IPv6
   191  	MacAddress string // MacAddress holds the MAC address for the network
   192  }
   193  
   194  // NetworkEndpointSettings is from https://github.com/moby/moby/blob/v20.10.1/api/types/network/network.go#L49-L65
   195  type NetworkEndpointSettings struct {
   196  	// Configurations
   197  	// TODO IPAMConfig *EndpointIPAMConfig
   198  	// TODO Links      []string
   199  	// TODO Aliases    []string
   200  	// Operational data
   201  	// TODO NetworkID           string
   202  	// TODO EndpointID          string
   203  	// TODO Gateway             string
   204  	IPAddress   string
   205  	IPPrefixLen int
   206  	// TODO IPv6Gateway         string
   207  	GlobalIPv6Address   string
   208  	GlobalIPv6PrefixLen int
   209  	MacAddress          string
   210  	// TODO DriverOpts          map[string]string
   211  }
   212  
   213  // ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container.
   214  func ContainerFromNative(n *native.Container) (*Container, error) {
   215  	var hostname string
   216  	c := &Container{
   217  		ID:      n.ID,
   218  		Created: n.CreatedAt.Format(time.RFC3339Nano),
   219  		Image:   n.Image,
   220  		Name:    n.Labels[labels.Name],
   221  		Driver:  n.Snapshotter,
   222  		// XXX is this always right? what if the container OS is NOT the same as the host OS?
   223  		Platform: runtime.GOOS, // for Docker compatibility, this Platform string does NOT contain arch like "/amd64"
   224  	}
   225  	if n.Labels[restart.StatusLabel] == string(containerd.Running) {
   226  		c.RestartCount, _ = strconv.Atoi(n.Labels[restart.CountLabel])
   227  	}
   228  	containerAnnotations := make(map[string]string)
   229  	if sp, ok := n.Spec.(*specs.Spec); ok {
   230  		containerAnnotations = sp.Annotations
   231  		if p := sp.Process; p != nil {
   232  			if len(p.Args) > 0 {
   233  				c.Path = p.Args[0]
   234  				if len(p.Args) > 1 {
   235  					c.Args = p.Args[1:]
   236  				}
   237  			}
   238  			c.AppArmorProfile = p.ApparmorProfile
   239  		}
   240  		c.Mounts = mountsFromNative(sp.Mounts)
   241  		for _, mount := range c.Mounts {
   242  			if mount.Destination == "/etc/resolv.conf" {
   243  				c.ResolvConfPath = mount.Source
   244  			} else if mount.Destination == "/etc/hostname" {
   245  				c.HostnamePath = mount.Source
   246  			}
   247  		}
   248  		hostname = sp.Hostname
   249  	}
   250  	if nerdctlStateDir := n.Labels[labels.StateDir]; nerdctlStateDir != "" {
   251  		resolvConfPath := filepath.Join(nerdctlStateDir, "resolv.conf")
   252  		if _, err := os.Stat(resolvConfPath); err == nil {
   253  			c.ResolvConfPath = resolvConfPath
   254  		}
   255  		hostnamePath := filepath.Join(nerdctlStateDir, "hostname")
   256  		if _, err := os.Stat(hostnamePath); err == nil {
   257  			c.HostnamePath = hostnamePath
   258  		}
   259  		c.LogPath = filepath.Join(nerdctlStateDir, n.ID+"-json.log")
   260  		if _, err := os.Stat(c.LogPath); err != nil {
   261  			c.LogPath = ""
   262  		}
   263  	}
   264  
   265  	if nerdctlMounts := n.Labels[labels.Mounts]; nerdctlMounts != "" {
   266  		mounts, err := parseMounts(nerdctlMounts)
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		c.Mounts = mounts
   271  	}
   272  
   273  	cs := new(ContainerState)
   274  	cs.Restarting = n.Labels[restart.StatusLabel] == string(containerd.Running)
   275  	cs.Error = n.Labels[labels.Error]
   276  	if n.Process != nil {
   277  		cs.Status = statusFromNative(n.Process.Status, n.Labels)
   278  		cs.Running = n.Process.Status.Status == containerd.Running
   279  		cs.Paused = n.Process.Status.Status == containerd.Paused
   280  		cs.Pid = n.Process.Pid
   281  		cs.ExitCode = int(n.Process.Status.ExitStatus)
   282  		if containerAnnotations[labels.StateDir] != "" {
   283  			lf := state.NewLifecycleState(containerAnnotations[labels.StateDir])
   284  			if err := lf.WithLock(lf.Load); err == nil && !time.Time.IsZero(lf.StartedAt) {
   285  				cs.StartedAt = lf.StartedAt.UTC().Format(time.RFC3339Nano)
   286  			}
   287  		}
   288  		if !n.Process.Status.ExitTime.IsZero() {
   289  			cs.FinishedAt = n.Process.Status.ExitTime.Format(time.RFC3339Nano)
   290  		}
   291  		nSettings, err := networkSettingsFromNative(n.Process.NetNS, n.Spec.(*specs.Spec))
   292  		if err != nil {
   293  			return nil, err
   294  		}
   295  		c.NetworkSettings = nSettings
   296  	}
   297  	c.State = cs
   298  	c.Config = &Config{
   299  		Labels: n.Labels,
   300  	}
   301  	if n.Labels[labels.Hostname] != "" {
   302  		hostname = n.Labels[labels.Hostname]
   303  	}
   304  	c.Config.Hostname = hostname
   305  
   306  	return c, nil
   307  }
   308  
   309  func ImageFromNative(n *native.Image) (*Image, error) {
   310  	i := &Image{}
   311  
   312  	imgoci := n.ImageConfig
   313  
   314  	i.RootFS.Type = imgoci.RootFS.Type
   315  	diffIDs := imgoci.RootFS.DiffIDs
   316  	for _, d := range diffIDs {
   317  		i.RootFS.Layers = append(i.RootFS.Layers, d.String())
   318  	}
   319  	if len(imgoci.History) > 0 {
   320  		i.Comment = imgoci.History[len(imgoci.History)-1].Comment
   321  		i.Created = imgoci.History[len(imgoci.History)-1].Created.Format(time.RFC3339Nano)
   322  		i.Author = imgoci.History[len(imgoci.History)-1].Author
   323  	}
   324  	i.Architecture = imgoci.Architecture
   325  	i.Os = imgoci.OS
   326  
   327  	portSet := make(nat.PortSet)
   328  	for k := range imgoci.Config.ExposedPorts {
   329  		portSet[nat.Port(k)] = struct{}{}
   330  	}
   331  
   332  	i.Config = &Config{
   333  		Cmd:          imgoci.Config.Cmd,
   334  		Volumes:      imgoci.Config.Volumes,
   335  		Env:          imgoci.Config.Env,
   336  		User:         imgoci.Config.User,
   337  		WorkingDir:   imgoci.Config.WorkingDir,
   338  		Entrypoint:   imgoci.Config.Entrypoint,
   339  		Labels:       imgoci.Config.Labels,
   340  		ExposedPorts: portSet,
   341  	}
   342  
   343  	i.ID = n.ImageConfigDesc.Digest.String() // Docker ID (digest of platform-specific config), not containerd ID (digest of multi-platform index or manifest)
   344  
   345  	repository, tag := imgutil.ParseRepoTag(n.Image.Name)
   346  
   347  	i.RepoTags = []string{fmt.Sprintf("%s:%s", repository, tag)}
   348  	i.RepoDigests = []string{fmt.Sprintf("%s@%s", repository, n.Image.Target.Digest.String())}
   349  	i.Size = n.Size
   350  	return i, nil
   351  }
   352  
   353  // mountsFromNative only filters bind mount to transform from native container.
   354  // Because native container shows all types of mounts, such as tmpfs, proc, sysfs.
   355  func mountsFromNative(spMounts []specs.Mount) []MountPoint {
   356  	mountpoints := make([]MountPoint, 0, len(spMounts))
   357  	for _, m := range spMounts {
   358  		var mp MountPoint
   359  		if m.Type != "bind" {
   360  			continue
   361  		}
   362  		mp.Type = m.Type
   363  		mp.Source = m.Source
   364  		mp.Destination = m.Destination
   365  		mp.Mode = strings.Join(m.Options, ",")
   366  		mp.RW, mp.Propagation = ParseMountProperties(m.Options)
   367  		mountpoints = append(mountpoints, mp)
   368  	}
   369  
   370  	return mountpoints
   371  }
   372  
   373  func statusFromNative(x containerd.Status, labels map[string]string) string {
   374  	switch s := x.Status; s {
   375  	case containerd.Stopped:
   376  		if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(x, labels) {
   377  			return "restarting"
   378  		}
   379  		return "exited"
   380  	default:
   381  		return string(s)
   382  	}
   383  }
   384  
   385  func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSettings, error) {
   386  	if n == nil {
   387  		return nil, nil
   388  	}
   389  	res := &NetworkSettings{
   390  		Networks: make(map[string]*NetworkEndpointSettings),
   391  	}
   392  	var primary *NetworkEndpointSettings
   393  	for _, x := range n.Interfaces {
   394  		if x.Interface.Flags&net.FlagLoopback != 0 {
   395  			continue
   396  		}
   397  		if x.Interface.Flags&net.FlagUp == 0 {
   398  			continue
   399  		}
   400  		nes := &NetworkEndpointSettings{}
   401  		nes.MacAddress = x.HardwareAddr
   402  
   403  		for _, a := range x.Addrs {
   404  			ip, ipnet, err := net.ParseCIDR(a)
   405  			if err != nil {
   406  				log.L.WithError(err).WithField("name", x.Name).Warnf("failed to parse %q", a)
   407  				continue
   408  			}
   409  			if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
   410  				continue
   411  			}
   412  			ones, _ := ipnet.Mask.Size()
   413  			if ip4 := ip.To4(); ip4 != nil {
   414  				nes.IPAddress = ip4.String()
   415  				nes.IPPrefixLen = ones
   416  			} else if ip16 := ip.To16(); ip16 != nil {
   417  				nes.GlobalIPv6Address = ip16.String()
   418  				nes.GlobalIPv6PrefixLen = ones
   419  			}
   420  		}
   421  		// TODO: set CNI name when possible
   422  		fakeDockerNetworkName := fmt.Sprintf("unknown-%s", x.Name)
   423  		res.Networks[fakeDockerNetworkName] = nes
   424  
   425  		if portsLabel, ok := sp.Annotations[labels.Ports]; ok {
   426  			var ports []gocni.PortMapping
   427  			err := json.Unmarshal([]byte(portsLabel), &ports)
   428  			if err != nil {
   429  				return nil, err
   430  			}
   431  			nports, err := convertToNatPort(ports)
   432  			if err != nil {
   433  				return nil, err
   434  			}
   435  			res.Ports = nports
   436  		}
   437  		if x.Index == n.PrimaryInterface {
   438  			primary = nes
   439  		}
   440  
   441  	}
   442  	if primary != nil {
   443  		res.DefaultNetworkSettings.MacAddress = primary.MacAddress
   444  		res.DefaultNetworkSettings.IPAddress = primary.IPAddress
   445  		res.DefaultNetworkSettings.IPPrefixLen = primary.IPPrefixLen
   446  		res.DefaultNetworkSettings.GlobalIPv6Address = primary.GlobalIPv6Address
   447  		res.DefaultNetworkSettings.GlobalIPv6PrefixLen = primary.GlobalIPv6PrefixLen
   448  	}
   449  	return res, nil
   450  }
   451  
   452  func convertToNatPort(portMappings []gocni.PortMapping) (*nat.PortMap, error) {
   453  	portMap := make(nat.PortMap)
   454  	for _, portMapping := range portMappings {
   455  		ports := []nat.PortBinding{}
   456  		p := nat.PortBinding{
   457  			HostIP:   portMapping.HostIP,
   458  			HostPort: strconv.FormatInt(int64(portMapping.HostPort), 10),
   459  		}
   460  		newP, err := nat.NewPort(portMapping.Protocol, strconv.FormatInt(int64(portMapping.ContainerPort), 10))
   461  		if err != nil {
   462  			return nil, err
   463  		}
   464  		ports = append(ports, p)
   465  		portMap[newP] = ports
   466  	}
   467  	return &portMap, nil
   468  }
   469  
   470  type IPAMConfig struct {
   471  	Subnet  string `json:"Subnet,omitempty"`
   472  	Gateway string `json:"Gateway,omitempty"`
   473  	IPRange string `json:"IPRange,omitempty"`
   474  }
   475  
   476  type IPAM struct {
   477  	// Driver is omitted
   478  	Config []IPAMConfig `json:"Config,omitempty"`
   479  }
   480  
   481  // Network mimics a `docker network inspect` object.
   482  // From https://github.com/moby/moby/blob/v20.10.7/api/types/types.go#L430-L448
   483  type Network struct {
   484  	Name   string            `json:"Name"`
   485  	ID     string            `json:"Id,omitempty"` // optional in nerdctl
   486  	IPAM   IPAM              `json:"IPAM,omitempty"`
   487  	Labels map[string]string `json:"Labels"`
   488  	// Scope, Driver, etc. are omitted
   489  }
   490  
   491  func NetworkFromNative(n *native.Network) (*Network, error) {
   492  	var res Network
   493  
   494  	nameResult := gjson.GetBytes(n.CNI, "name")
   495  	if s, ok := nameResult.Value().(string); ok {
   496  		res.Name = s
   497  	}
   498  
   499  	// flatten twice to get ipamRangesResult=[{ "subnet": "10.4.19.0/24", "gateway": "10.4.19.1" }]
   500  	ipamRangesResult := gjson.GetBytes(n.CNI, "plugins.#.ipam.ranges|@flatten|@flatten")
   501  	for _, f := range ipamRangesResult.Array() {
   502  		m := f.Map()
   503  		var cfg IPAMConfig
   504  		if x, ok := m["subnet"]; ok {
   505  			cfg.Subnet = x.String()
   506  		}
   507  		if x, ok := m["gateway"]; ok {
   508  			cfg.Gateway = x.String()
   509  		}
   510  		if x, ok := m["ipRange"]; ok {
   511  			cfg.IPRange = x.String()
   512  		}
   513  		res.IPAM.Config = append(res.IPAM.Config, cfg)
   514  	}
   515  
   516  	if n.NerdctlID != nil {
   517  		res.ID = *n.NerdctlID
   518  	}
   519  
   520  	if n.NerdctlLabels != nil {
   521  		res.Labels = *n.NerdctlLabels
   522  	}
   523  
   524  	return &res, nil
   525  }
   526  
   527  func parseMounts(nerdctlMounts string) ([]MountPoint, error) {
   528  	var mounts []MountPoint
   529  	err := json.Unmarshal([]byte(nerdctlMounts), &mounts)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  
   534  	return mounts, nil
   535  }
   536  
   537  func ParseMountProperties(option []string) (rw bool, propagation string) {
   538  	rw = true
   539  	for _, opt := range option {
   540  		switch opt {
   541  		case "ro", "rro":
   542  			rw = false
   543  		case "private", "rprivate", "shared", "rshared", "slave", "rslave":
   544  			propagation = opt
   545  		default:
   546  		}
   547  	}
   548  	return
   549  }