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