package libpod import ( "strings" "time" "github.com/containers/image/manifest" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/driver" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // InspectContainerData provides a detailed record of a container's configuration // and state as viewed by Libpod. // Large portions of this structure are defined such that the output is // compatible with `docker inspect` JSON, but additional fields have been added // as required to share information not in the original output. type InspectContainerData struct { ID string `json:"Id"` Created time.Time `json:"Created"` Path string `json:"Path"` Args []string `json:"Args"` State *InspectContainerState `json:"State"` ImageID string `json:"Image"` ImageName string `json:"ImageName"` Rootfs string `json:"Rootfs"` ResolvConfPath string `json:"ResolvConfPath"` HostnamePath string `json:"HostnamePath"` HostsPath string `json:"HostsPath"` StaticDir string `json:"StaticDir"` OCIConfigPath string `json:"OCIConfigPath,omitempty"` OCIRuntime string `json:"OCIRuntime,omitempty"` LogPath string `json:"LogPath"` ConmonPidFile string `json:"ConmonPidFile"` Name string `json:"Name"` RestartCount int32 `json:"RestartCount"` Driver string `json:"Driver"` MountLabel string `json:"MountLabel"` ProcessLabel string `json:"ProcessLabel"` AppArmorProfile string `json:"AppArmorProfile"` EffectiveCaps []string `json:"EffectiveCaps"` BoundingCaps []string `json:"BoundingCaps"` ExecIDs []string `json:"ExecIDs"` GraphDriver *driver.Data `json:"GraphDriver"` SizeRw int64 `json:"SizeRw,omitempty"` SizeRootFs int64 `json:"SizeRootFs,omitempty"` Mounts []InspectMount `json:"Mounts"` Dependencies []string `json:"Dependencies"` NetworkSettings *InspectNetworkSettings `json:"NetworkSettings"` //TODO ExitCommand []string `json:"ExitCommand"` Namespace string `json:"Namespace"` IsInfra bool `json:"IsInfra"` Config *InspectContainerConfig `json:"Config"` } // InspectContainerConfig holds further data about how a container was initially // configured. type InspectContainerConfig struct { // Container hostname Hostname string `json:"Hostname"` // Container domain name - unused at present DomainName string `json:"Domainname"` // User the container was launched with User string `json:"User"` // Unused, at present AttachStdin bool `json:"AttachStdin"` // Unused, at present AttachStdout bool `json:"AttachStdout"` // Unused, at present AttachStderr bool `json:"AttachStderr"` // Whether the container creates a TTY Tty bool `json:"Tty"` // Whether the container leaves STDIN open OpenStdin bool `json:"OpenStdin"` // Whether STDIN is only left open once. // Presently not supported by Podman, unused. StdinOnce bool `json:"StdinOnce"` // Container environment variables Env []string `json:"Env"` // Container command Cmd []string `json:"Cmd"` // Container image Image string `json:"Image"` // Unused, at present. I've never seen this field populated. Volumes map[string]struct{} `json:"Volumes"` // Container working directory WorkingDir string `json:"WorkingDir"` // Container entrypoint Entrypoint string `json:"Entrypoint"` // On-build arguments - presently unused. More of Buildah's domain. OnBuild *string `json:"OnBuild"` // Container labels Labels map[string]string `json:"Labels"` // Container annotations Annotations map[string]string `json:"Annotations"` // Container stop signal StopSignal uint `json:"StopSignal"` // Configured healthcheck for the container Healthcheck *manifest.Schema2HealthConfig `json:"Healthcheck,omitempty"` } // InspectMount provides a record of a single mount in a container. It contains // fields for both named and normal volumes. Only user-specified volumes will be // included, and tmpfs volumes are not included even if the user specified them. type InspectMount struct { // Whether the mount is a volume or bind mount. Allowed values are // "volume" and "bind". Type string `json:"Type"` // The name of the volume. Empty for bind mounts. Name string `json:"Name,omptempty"` // The source directory for the volume. Source string `json:"Source"` // The destination directory for the volume. Specified as a path within // the container, as it would be passed into the OCI runtime. Destination string `json:"Destination"` // The driver used for the named volume. Empty for bind mounts. Driver string `json:"Driver"` // Contains SELinux :z/:Z mount options. Unclear what, if anything, else // goes in here. Mode string `json:"Mode"` // All remaining mount options. Additional data, not present in the // original output. Options []string `json:"Options"` // Whether the volume is read-write RW bool `json:"RW"` // Mount propagation for the mount. Can be empty if not specified, but // is always printed - no omitempty. Propagation string `json:"Propagation"` } // InspectContainerState provides a detailed record of a container's current // state. It is returned as part of InspectContainerData. // As with InspectContainerData, many portions of this struct are matched to // Docker, but here we see more fields that are unused (nonsensical in the // context of Libpod). type InspectContainerState struct { OciVersion string `json:"OciVersion"` Status string `json:"Status"` Running bool `json:"Running"` Paused bool `json:"Paused"` Restarting bool `json:"Restarting"` // TODO OOMKilled bool `json:"OOMKilled"` Dead bool `json:"Dead"` Pid int `json:"Pid"` ConmonPid int `json:"ConmonPid,omitempty"` ExitCode int32 `json:"ExitCode"` Error string `json:"Error"` // TODO StartedAt time.Time `json:"StartedAt"` FinishedAt time.Time `json:"FinishedAt"` Healthcheck HealthCheckResults `json:"Healthcheck,omitempty"` } // InspectNetworkSettings holds information about the network settings of the // container. // Many fields are maintained only for compatibility with `docker inspect` and // are unused within Libpod. type InspectNetworkSettings struct { Bridge string `json:"Bridge"` SandboxID string `json:"SandboxID"` HairpinMode bool `json:"HairpinMode"` LinkLocalIPv6Address string `json:"LinkLocalIPv6Address"` LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"` Ports []ocicni.PortMapping `json:"Ports"` SandboxKey string `json:"SandboxKey"` SecondaryIPAddresses []string `json:"SecondaryIPAddresses"` SecondaryIPv6Addresses []string `json:"SecondaryIPv6Addresses"` EndpointID string `json:"EndpointID"` Gateway string `json:"Gateway"` GlobalIPv6Address string `json:"GlobalIPv6Address"` GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` IPAddress string `json:"IPAddress"` IPPrefixLen int `json:"IPPrefixLen"` IPv6Gateway string `json:"IPv6Gateway"` MacAddress string `json:"MacAddress"` } // Inspect a container for low-level information func (c *Container) Inspect(size bool) (*InspectContainerData, error) { if !c.batched { c.lock.Lock() defer c.lock.Unlock() if err := c.syncContainer(); err != nil { return nil, err } } storeCtr, err := c.runtime.store.Container(c.ID()) if err != nil { return nil, errors.Wrapf(err, "error getting container from store %q", c.ID()) } layer, err := c.runtime.store.Layer(storeCtr.LayerID) if err != nil { return nil, errors.Wrapf(err, "error reading information about layer %q", storeCtr.LayerID) } driverData, err := driver.GetDriverData(c.runtime.store, layer.ID) if err != nil { return nil, errors.Wrapf(err, "error getting graph driver info %q", c.ID()) } return c.getContainerInspectData(size, driverData) } func (c *Container) getContainerInspectData(size bool, driverData *driver.Data) (*InspectContainerData, error) { config := c.config runtimeInfo := c.state stateSpec, err := c.specFromState() if err != nil { return nil, err } // Process is allowed to be nil in the stateSpec args := []string{} if config.Spec.Process != nil { args = config.Spec.Process.Args } var path string if len(args) > 0 { path = args[0] } if len(args) > 1 { args = args[1:] } execIDs := []string{} for id := range c.state.ExecSessions { execIDs = append(execIDs, id) } resolvPath := "" hostsPath := "" hostnamePath := "" if c.state.BindMounts != nil { if getPath, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { resolvPath = getPath } if getPath, ok := c.state.BindMounts["/etc/hosts"]; ok { hostsPath = getPath } if getPath, ok := c.state.BindMounts["/etc/hostname"]; ok { hostnamePath = getPath } } mounts, err := c.getInspectMounts(stateSpec) if err != nil { return nil, err } data := &InspectContainerData{ ID: config.ID, Created: config.CreatedTime, Path: path, Args: args, State: &InspectContainerState{ OciVersion: stateSpec.Version, Status: runtimeInfo.State.String(), Running: runtimeInfo.State == define.ContainerStateRunning, Paused: runtimeInfo.State == define.ContainerStatePaused, OOMKilled: runtimeInfo.OOMKilled, Dead: runtimeInfo.State.String() == "bad state", Pid: runtimeInfo.PID, ConmonPid: runtimeInfo.ConmonPID, ExitCode: runtimeInfo.ExitCode, Error: "", // can't get yet StartedAt: runtimeInfo.StartedTime, FinishedAt: runtimeInfo.FinishedTime, }, ImageID: config.RootfsImageID, ImageName: config.RootfsImageName, ExitCommand: config.ExitCommand, Namespace: config.Namespace, Rootfs: config.Rootfs, ResolvConfPath: resolvPath, HostnamePath: hostnamePath, HostsPath: hostsPath, StaticDir: config.StaticDir, LogPath: config.LogPath, OCIRuntime: config.OCIRuntime, ConmonPidFile: config.ConmonPidFile, Name: config.Name, RestartCount: int32(runtimeInfo.RestartCount), Driver: driverData.Name, MountLabel: config.MountLabel, ProcessLabel: config.ProcessLabel, EffectiveCaps: stateSpec.Process.Capabilities.Effective, BoundingCaps: stateSpec.Process.Capabilities.Bounding, AppArmorProfile: stateSpec.Process.ApparmorProfile, ExecIDs: execIDs, GraphDriver: driverData, Mounts: mounts, Dependencies: c.Dependencies(), NetworkSettings: &InspectNetworkSettings{ Bridge: "", // TODO SandboxID: "", // TODO - is this even relevant? HairpinMode: false, // TODO LinkLocalIPv6Address: "", // TODO - do we even support IPv6? LinkLocalIPv6PrefixLen: 0, // TODO - do we even support IPv6? Ports: []ocicni.PortMapping{}, // TODO - maybe worth it to put this in Docker format? SandboxKey: "", // Network namespace path SecondaryIPAddresses: nil, // TODO - do we support this? SecondaryIPv6Addresses: nil, // TODO - do we support this? EndpointID: "", // TODO - is this even relevant? Gateway: "", // TODO GlobalIPv6Address: "", GlobalIPv6PrefixLen: 0, IPAddress: "", IPPrefixLen: 0, IPv6Gateway: "", MacAddress: "", // TODO }, IsInfra: c.IsInfra(), } if c.state.ConfigPath != "" { data.OCIConfigPath = c.state.ConfigPath } if c.config.HealthCheckConfig != nil { // This container has a healthcheck defined in it; we need to add it's state healthCheckState, err := c.GetHealthCheckLog() if err != nil { // An error here is not considered fatal; no health state will be displayed logrus.Error(err) } else { data.State.Healthcheck = healthCheckState } } // Copy port mappings into network settings if config.PortMappings != nil { data.NetworkSettings.Ports = config.PortMappings } // Get information on the container's network namespace (if present) data = c.getContainerNetworkInfo(data) inspectConfig, err := c.generateInspectContainerConfig(stateSpec) if err != nil { return nil, err } data.Config = inspectConfig if size { rootFsSize, err := c.rootFsSize() if err != nil { logrus.Errorf("error getting rootfs size %q: %v", config.ID, err) } rwSize, err := c.rwSize() if err != nil { logrus.Errorf("error getting rw size %q: %v", config.ID, err) } data.SizeRootFs = rootFsSize data.SizeRw = rwSize } return data, nil } // Get inspect-formatted mounts list. // Only includes user-specified mounts. Only includes bind mounts and named // volumes, not tmpfs volumes. func (c *Container) getInspectMounts(ctrSpec *spec.Spec) ([]InspectMount, error) { inspectMounts := []InspectMount{} // No mounts, return early if len(c.config.UserVolumes) == 0 { return inspectMounts, nil } namedVolumes, mounts := c.sortUserVolumes(ctrSpec) for _, volume := range namedVolumes { mountStruct := InspectMount{} mountStruct.Type = "volume" mountStruct.Destination = volume.Dest mountStruct.Name = volume.Name // For src and driver, we need to look up the named // volume. volFromDB, err := c.runtime.state.Volume(volume.Name) if err != nil { return nil, errors.Wrapf(err, "error looking up volume %s in container %s config", volume.Name, c.ID()) } mountStruct.Driver = volFromDB.Driver() mountStruct.Source = volFromDB.MountPoint() parseMountOptionsForInspect(volume.Options, &mountStruct) inspectMounts = append(inspectMounts, mountStruct) } for _, mount := range mounts { // It's a mount. // Is it a tmpfs? If so, discard. if mount.Type == "tmpfs" { continue } mountStruct := InspectMount{} mountStruct.Type = "bind" mountStruct.Source = mount.Source mountStruct.Destination = mount.Destination parseMountOptionsForInspect(mount.Options, &mountStruct) inspectMounts = append(inspectMounts, mountStruct) } return inspectMounts, nil } // Parse mount options so we can populate them in the mount structure. // The mount passed in will be modified. func parseMountOptionsForInspect(options []string, mount *InspectMount) { isRW := true mountProp := "" zZ := "" otherOpts := []string{} // Some of these may be overwritten if the user passes us garbage opts // (for example, [ro,rw]) // We catch these on the Podman side, so not a problem there, but other // users of libpod who do not properly validate mount options may see // this. // Not really worth dealing with on our end - garbage in, garbage out. for _, opt := range options { switch opt { case "ro": isRW = false case "rw": // Do nothing, silently discard case "shared", "slave", "private", "rshared", "rslave", "rprivate": mountProp = opt case "z", "Z": zZ = opt default: otherOpts = append(otherOpts, opt) } } mount.RW = isRW mount.Propagation = mountProp mount.Mode = zZ mount.Options = otherOpts } // Generate the InspectContainerConfig struct for the Config field of Inspect. func (c *Container) generateInspectContainerConfig(spec *spec.Spec) (*InspectContainerConfig, error) { ctrConfig := new(InspectContainerConfig) ctrConfig.Hostname = c.Hostname() ctrConfig.User = c.config.User if spec.Process != nil { ctrConfig.Tty = spec.Process.Terminal ctrConfig.Env = []string{} for _, val := range spec.Process.Env { ctrConfig.Env = append(ctrConfig.Env, val) } ctrConfig.WorkingDir = spec.Process.Cwd } ctrConfig.OpenStdin = c.config.Stdin ctrConfig.Image = c.config.RootfsImageName // Leave empty is not explicitly overwritten by user if len(c.config.Command) != 0 { ctrConfig.Cmd = []string{} for _, val := range c.config.Command { ctrConfig.Cmd = append(ctrConfig.Cmd, val) } } // Leave empty if not explicitly overwritten by user if len(c.config.Entrypoint) != 0 { ctrConfig.Entrypoint = strings.Join(c.config.Entrypoint, " ") } if len(c.config.Labels) != 0 { ctrConfig.Labels = make(map[string]string) for k, v := range c.config.Labels { ctrConfig.Labels[k] = v } } if len(spec.Annotations) != 0 { ctrConfig.Annotations = make(map[string]string) for k, v := range spec.Annotations { ctrConfig.Annotations[k] = v } } ctrConfig.StopSignal = c.config.StopSignal // TODO: should JSON deep copy this to ensure internal pointers don't // leak. ctrConfig.Healthcheck = c.config.HealthCheckConfig return ctrConfig, nil }