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 }