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 }