spiegel_podman/libpod/container_inspect_linux.go
Priyansh Sao 04250c6672 fix: implement console size for container inspect in linux
fix: return tty size in container_inspect_linux.go

This PR updates the console size field for containers with terminal enabled.

For cases where there is an issue with getting the size, suitable debug information is given, no update happens and default [0,0] is returned.

This implementation is only for Linux systems.

For FreeBSD, the console size is not implemented yet so, adds a todo for that.

Fixes: #28368

Signed-off-by: Priyansh Sao <saopriyansh06@gmail.com>

fix: add system test for podman inspect

Adds system test to verify console size is correctly updated, includes two cases:

When a tty is attached to the container - non-zero console size.

When a tty is not attached to the container - zero console size.

Fixes: #28368

Signed-off-by: Priyansh Sao <saopriyansh06@gmail.com>
2026-09-22 19:26:23 +05:30

376 lines
12 KiB
Go

//go:build !remote
package libpod
import (
"fmt"
"os"
"sort"
"strings"
"github.com/moby/sys/capability"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/config"
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/podman/v6/pkg/util"
"go.podman.io/storage/types"
"golang.org/x/term"
)
func (c *Container) platformInspectContainerHostConfig(ctrSpec *spec.Spec, hostConfig *define.InspectContainerHostConfig) error {
// This is very expensive to initialize.
// So we don't want to initialize it unless we absolutely have to - IE,
// there are things that require a major:minor to path translation.
var deviceNodes map[string]string
if ctrSpec.Linux != nil {
if ctrSpec.Linux.IntelRdt != nil {
if ctrSpec.Linux.IntelRdt.ClosID != "" {
// container is assigned to a ClosID
hostConfig.IntelRdtClosID = ctrSpec.Linux.IntelRdt.ClosID
}
}
// Resource limits
if ctrSpec.Linux.Resources != nil {
if ctrSpec.Linux.Resources.CPU != nil {
if ctrSpec.Linux.Resources.CPU.Shares != nil {
hostConfig.CpuShares = *ctrSpec.Linux.Resources.CPU.Shares
}
if ctrSpec.Linux.Resources.CPU.Period != nil {
hostConfig.CpuPeriod = *ctrSpec.Linux.Resources.CPU.Period
}
if ctrSpec.Linux.Resources.CPU.Quota != nil {
hostConfig.CpuQuota = *ctrSpec.Linux.Resources.CPU.Quota
}
if ctrSpec.Linux.Resources.CPU.RealtimePeriod != nil {
hostConfig.CpuRealtimePeriod = *ctrSpec.Linux.Resources.CPU.RealtimePeriod
}
if ctrSpec.Linux.Resources.CPU.RealtimeRuntime != nil {
hostConfig.CpuRealtimeRuntime = *ctrSpec.Linux.Resources.CPU.RealtimeRuntime
}
hostConfig.CpusetCpus = ctrSpec.Linux.Resources.CPU.Cpus
hostConfig.CpusetMems = ctrSpec.Linux.Resources.CPU.Mems
}
if ctrSpec.Linux.Resources.Memory != nil {
if ctrSpec.Linux.Resources.Memory.Limit != nil {
hostConfig.Memory = *ctrSpec.Linux.Resources.Memory.Limit
}
if ctrSpec.Linux.Resources.Memory.Reservation != nil {
hostConfig.MemoryReservation = *ctrSpec.Linux.Resources.Memory.Reservation
}
if ctrSpec.Linux.Resources.Memory.Swap != nil {
hostConfig.MemorySwap = *ctrSpec.Linux.Resources.Memory.Swap
}
if ctrSpec.Linux.Resources.Memory.Swappiness != nil {
swappiness := int64(*ctrSpec.Linux.Resources.Memory.Swappiness)
hostConfig.MemorySwappiness = &swappiness
}
if ctrSpec.Linux.Resources.Memory.DisableOOMKiller != nil {
hostConfig.OomKillDisable = *ctrSpec.Linux.Resources.Memory.DisableOOMKiller
}
}
if ctrSpec.Linux.Resources.Pids != nil && ctrSpec.Linux.Resources.Pids.Limit != nil {
hostConfig.PidsLimit = *ctrSpec.Linux.Resources.Pids.Limit
}
hostConfig.CgroupConf = ctrSpec.Linux.Resources.Unified
if ctrSpec.Linux.Resources.BlockIO != nil {
if ctrSpec.Linux.Resources.BlockIO.Weight != nil {
hostConfig.BlkioWeight = *ctrSpec.Linux.Resources.BlockIO.Weight
}
hostConfig.BlkioWeightDevice = []define.InspectBlkioWeightDevice{}
for _, dev := range ctrSpec.Linux.Resources.BlockIO.WeightDevice {
key := fmt.Sprintf("%d:%d", dev.Major, dev.Minor)
// TODO: how do we handle LeafWeight vs
// Weight? For now, ignore anything
// without Weight set.
if dev.Weight == nil {
logrus.Infof("Ignoring weight device %s as it lacks a weight", key)
continue
}
if deviceNodes == nil {
nodes, err := util.FindDeviceNodes(true)
if err != nil {
return err
}
deviceNodes = nodes
}
path, ok := deviceNodes[key]
if !ok {
logrus.Infof("Could not locate weight device %s in system devices", key)
continue
}
weightDev := define.InspectBlkioWeightDevice{}
weightDev.Path = path
weightDev.Weight = *dev.Weight
hostConfig.BlkioWeightDevice = append(hostConfig.BlkioWeightDevice, weightDev)
}
readBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadBpsDevice)
if err != nil {
return err
}
hostConfig.BlkioDeviceReadBps = readBps
writeBps, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteBpsDevice)
if err != nil {
return err
}
hostConfig.BlkioDeviceWriteBps = writeBps
readIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleReadIOPSDevice)
if err != nil {
return err
}
hostConfig.BlkioDeviceReadIOps = readIops
writeIops, err := blkioDeviceThrottle(deviceNodes, ctrSpec.Linux.Resources.BlockIO.ThrottleWriteIOPSDevice)
if err != nil {
return err
}
hostConfig.BlkioDeviceWriteIOps = writeIops
}
}
}
// Cap add and cap drop.
// We need a default set of capabilities to compare against.
// The OCI generate package has one, and is commonly used, so we'll
// use it.
// Problem: there are 5 sets of capabilities.
// Use the bounding set for this computation, it's the most encompassing
// (but still not perfect).
capAdd := []string{}
capDrop := []string{}
// No point in continuing if we got a spec without a Process block...
if ctrSpec.Process != nil {
// Max an O(1) lookup table for default bounding caps.
boundingCaps := make(map[string]bool)
if !hostConfig.Privileged {
for _, cap := range c.runtime.config.Containers.DefaultCapabilities.Get() {
boundingCaps[cap] = true
}
} else {
// If we are privileged, use all caps.
for _, cap := range capability.ListKnown() {
boundingCaps[fmt.Sprintf("CAP_%s", strings.ToUpper(cap.String()))] = true
}
}
// Iterate through default caps.
// If it's not in default bounding caps, it was added.
// If it is, delete from the default set. Whatever remains after
// we finish are the dropped caps.
for _, cap := range ctrSpec.Process.Capabilities.Bounding {
if _, ok := boundingCaps[cap]; ok {
delete(boundingCaps, cap)
} else {
capAdd = append(capAdd, cap)
}
}
for cap := range boundingCaps {
capDrop = append(capDrop, cap)
}
// Sort CapDrop so it displays in consistent order (GH #9490)
sort.Strings(capDrop)
}
hostConfig.CapAdd = capAdd
hostConfig.CapDrop = capDrop
switch {
case c.config.IPCNsCtr != "":
hostConfig.IpcMode = fmt.Sprintf("container:%s", c.config.IPCNsCtr)
case ctrSpec.Linux != nil:
// Locate the spec's IPC namespace.
// If there is none, it's ipc=host.
// If there is one and it has a path, it's "ns:".
// If no path, it's default - the empty string.
hostConfig.IpcMode = "host"
for _, ns := range ctrSpec.Linux.Namespaces {
if ns.Type == spec.IPCNamespace {
if ns.Path != "" {
hostConfig.IpcMode = fmt.Sprintf("ns:%s", ns.Path)
} else {
switch {
case c.config.NoShm:
hostConfig.IpcMode = "none"
case c.config.NoShmShare:
hostConfig.IpcMode = "private"
default:
hostConfig.IpcMode = "shareable"
}
}
break
}
}
case c.config.NoShm:
hostConfig.IpcMode = "none"
case c.config.NoShmShare:
hostConfig.IpcMode = "private"
}
// Cgroup namespace mode
cgroupMode := ""
if c.config.CgroupNsCtr != "" {
cgroupMode = fmt.Sprintf("container:%s", c.config.CgroupNsCtr)
} else if ctrSpec.Linux != nil {
// Locate the spec's cgroup namespace
// If there is none, it's cgroup=host.
// If there is one and it has a path, it's "ns:".
// If there is no path, it's private.
for _, ns := range ctrSpec.Linux.Namespaces {
if ns.Type == spec.CgroupNamespace {
if ns.Path != "" {
cgroupMode = fmt.Sprintf("ns:%s", ns.Path)
} else {
cgroupMode = "private"
}
}
}
if cgroupMode == "" {
cgroupMode = "host"
}
}
hostConfig.CgroupMode = cgroupMode
// Cgroup parent
// Need to check if it's the default, and not print if so.
defaultCgroupParent := ""
switch c.CgroupManager() {
case config.CgroupfsCgroupsManager:
defaultCgroupParent = CgroupfsDefaultCgroupParent
case config.SystemdCgroupsManager:
defaultCgroupParent = SystemdDefaultCgroupParent
}
if c.config.CgroupParent != defaultCgroupParent {
hostConfig.CgroupParent = c.config.CgroupParent
}
hostConfig.CgroupManager = c.CgroupManager()
// PID namespace mode
pidMode := ""
if c.config.PIDNsCtr != "" {
pidMode = fmt.Sprintf("container:%s", c.config.PIDNsCtr)
} else if ctrSpec.Linux != nil {
// Locate the spec's PID namespace.
// If there is none, it's pid=host.
// If there is one and it has a path, it's "ns:".
// If there is no path, it's default - the empty string.
for _, ns := range ctrSpec.Linux.Namespaces {
if ns.Type == spec.PIDNamespace {
if ns.Path != "" {
pidMode = fmt.Sprintf("ns:%s", ns.Path)
} else {
pidMode = "private"
}
break
}
}
if pidMode == "" {
pidMode = "host"
}
}
hostConfig.PidMode = pidMode
// UTS namespace mode
utsMode := c.NamespaceMode(spec.UTSNamespace, ctrSpec)
hostConfig.UTSMode = utsMode
// User namespace mode
usernsMode := ""
if c.config.UserNsCtr != "" {
usernsMode = fmt.Sprintf("container:%s", c.config.UserNsCtr)
} else if ctrSpec.Linux != nil {
// Locate the spec's user namespace.
// If there is none, it's default - the empty string.
// If there is one, it's "private" if no path, or "ns:" if
// there's a path.
for _, ns := range ctrSpec.Linux.Namespaces {
if ns.Type == spec.UserNamespace {
if ns.Path != "" {
usernsMode = fmt.Sprintf("ns:%s", ns.Path)
} else {
usernsMode = "private"
}
}
}
// If userns=auto, setting up the namespace is deferred until the container
// is created. If the container is configured, check if it is going to have a
// private userns and return accordingly
if c.state.State == define.ContainerStateConfigured && c.config.IDMappings.AutoUserNs {
usernsMode = "private"
}
}
hostConfig.UsernsMode = usernsMode
if c.config.IDMappings.UIDMap != nil && c.config.IDMappings.GIDMap != nil {
hostConfig.IDMappings = generateIDMappings(c.config.IDMappings)
}
// Devices
// Do not include if privileged - assumed that all devices will be
// included.
var err error
hostConfig.Devices, err = c.GetDevices(hostConfig.Privileged, *ctrSpec, deviceNodes)
if err != nil {
return err
}
// ConsoleSize.
// Default to [0,0] if we can't get it for some reason,
// or if the container doesn't have a TTY.
hostConfig.ConsoleSize = []uint{0, 0}
if c.Terminal() && c.ensureState(define.ContainerStateRunning, define.ContainerStatePaused, define.ContainerStateStopping) {
procPath := fmt.Sprintf("/proc/%d/fd/0", c.state.PID)
f, err := os.Open(procPath)
if err != nil {
logrus.Debugf("unable to get console size for container %s: %v", c.ID(), err)
} else {
defer f.Close()
width, height, err := term.GetSize(int(f.Fd()))
if err != nil {
logrus.Debugf("could not get terminal size: %v", err)
} else {
hostConfig.ConsoleSize = []uint{uint(height), uint(width)}
}
}
}
return nil
}
func generateIDMappings(idMappings types.IDMappingOptions) *define.InspectIDMappings {
var inspectMappings define.InspectIDMappings
for _, uid := range idMappings.UIDMap {
inspectMappings.UIDMap = append(inspectMappings.UIDMap, fmt.Sprintf("%d:%d:%d", uid.ContainerID, uid.HostID, uid.Size))
}
for _, gid := range idMappings.GIDMap {
inspectMappings.GIDMap = append(inspectMappings.GIDMap, fmt.Sprintf("%d:%d:%d", gid.ContainerID, gid.HostID, gid.Size))
}
return &inspectMappings
}
// Return true if the container is running in the host's PID NS.
func (c *Container) inHostPidNS() (bool, error) {
if c.config.PIDNsCtr != "" {
return false, nil
}
ctrSpec, err := c.specFromState()
if err != nil {
return false, err
}
if ctrSpec.Linux != nil {
// Locate the spec's PID namespace.
// If there is none, it's pid=host.
// If there is one and it has a path, it's "ns:".
// If there is no path, it's default - the empty string.
for _, ns := range ctrSpec.Linux.Namespaces {
if ns.Type == spec.PIDNamespace {
return false, nil
}
}
}
return true, nil
}