Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions cmd/nerdctl/container/container_inspect_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,10 +391,16 @@ func TestContainerInspectHostConfig(t *testing.T) {
"--add-host", "host2:10.0.0.2",
"--ipc", "host",
"--memory", "512m",
"--memory-reservation", "200m",
"--memory-swappiness", "60",
"--pids-limit", "100",
"--ulimit", "nofile=1024:65536",
"--read-only",
"--shm-size", "256m",
"--uts", "host",
"--restart", "on-failure:3",
"--runtime", "io.containerd.runc.v2",
"--annotation", "com.example.key=test-val",
testutil.AlpineImage, "sleep", nerdtest.Infinity)
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
}
Expand Down Expand Up @@ -430,6 +436,21 @@ func TestContainerInspectHostConfig(t *testing.T) {
assert.Equal(tt, true, inspect.HostConfig.ReadonlyRootfs)
assert.Equal(tt, "host", inspect.HostConfig.UTSMode)
assert.Equal(tt, int64(268435456), inspect.HostConfig.ShmSize)
assert.Equal(tt, int64(209715200), inspect.HostConfig.MemoryReservation)
assert.Equal(tt, int64(100), inspect.HostConfig.PidsLimit)
assert.Equal(tt, 1, len(inspect.HostConfig.Ulimits))
assert.Equal(tt, "nofile", inspect.HostConfig.Ulimits[0].Name)
assert.Equal(tt, int64(65536), inspect.HostConfig.Ulimits[0].Hard)
assert.Equal(tt, int64(1024), inspect.HostConfig.Ulimits[0].Soft)
assert.Equal(tt, "on-failure", inspect.HostConfig.RestartPolicy.Name)
assert.Equal(tt, 3, inspect.HostConfig.RestartPolicy.MaximumRetryCount)
if !nerdtest.IsDocker() {
// The docker CI runner warns "Your kernel does not support memory
// swappiness capabilities or the cgroup is not mounted" and returns null.
assert.Assert(tt, inspect.HostConfig.MemorySwappiness != nil)
assert.Equal(tt, int64(60), *inspect.HostConfig.MemorySwappiness)
}
assert.Equal(tt, "test-val", inspect.HostConfig.Annotations["com.example.key"])
})

testCase.Run(t)
Expand Down Expand Up @@ -509,6 +530,14 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) {
assert.Equal(tt, hc.ShmSize, inspect.HostConfig.ShmSize)
assert.Equal(tt, hc.Runtime, inspect.HostConfig.Runtime)
assert.Equal(tt, 0, len(inspect.HostConfig.Devices))
assert.Equal(tt, false, inspect.HostConfig.Privileged)
assert.Equal(tt, false, inspect.HostConfig.AutoRemove)
assert.Equal(tt, int64(0), inspect.HostConfig.MemoryReservation)
assert.Equal(tt, int64(0), inspect.HostConfig.PidsLimit)
assert.Equal(tt, 0, len(inspect.HostConfig.CapAdd))
assert.Equal(tt, 0, len(inspect.HostConfig.CapDrop))
assert.Equal(tt, 0, len(inspect.HostConfig.Ulimits))
assert.Assert(tt, inspect.HostConfig.MemorySwappiness == nil)

// Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment.
got := len(inspect.HostConfig.Sysctls)
Expand Down Expand Up @@ -881,3 +910,71 @@ type hostConfigValues struct {
GroupAddSize int
Runtime string
}

func TestContainerInspectHostConfigPrivileged(t *testing.T) {
testCase := nerdtest.Setup()

testCase.Setup = func(data test.Data, helpers test.Helpers) {
helpers.Ensure("run", "-d", "--name", data.Identifier(),
"--privileged",
testutil.AlpineImage, "sleep", nerdtest.Infinity)
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
}

testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", data.Identifier())
}

testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("inspect", data.Identifier())
}

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) {
var dc []dockercompat.Container

err := json.Unmarshal([]byte(stdout), &dc)
assert.NilError(tt, err)
assert.Equal(tt, 1, len(dc))

inspect := dc[0]
assert.Equal(tt, true, inspect.HostConfig.Privileged)
})

testCase.Run(t)
}

func TestContainerInspectHostConfigCapabilities(t *testing.T) {
testCase := nerdtest.Setup()

testCase.Setup = func(data test.Data, helpers test.Helpers) {
helpers.Ensure("run", "-d", "--name", data.Identifier(),
"--cap-add", "NET_ADMIN",
"--cap-drop", "CHOWN",
testutil.AlpineImage, "sleep", nerdtest.Infinity)
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
}

testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rm", "-f", data.Identifier())
}

testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("inspect", data.Identifier())
}

testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) {
var dc []dockercompat.Container

err := json.Unmarshal([]byte(stdout), &dc)
assert.NilError(tt, err)
assert.Equal(tt, 1, len(dc))

inspect := dc[0]
assert.Assert(tt, slices.Contains(inspect.HostConfig.CapAdd, "CAP_NET_ADMIN"),
"Expected CAP_NET_ADMIN in CapAdd")
assert.Assert(tt, slices.Contains(inspect.HostConfig.CapDrop, "CAP_CHOWN"),
"Expected CAP_CHOWN in CapDrop")
})

testCase.Run(t)
}
7 changes: 7 additions & 0 deletions pkg/cmd/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
internalLabels.extraHosts = extraHosts

internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm)
internalLabels.privileged = options.Privileged

// TODO: abolish internal labels and only use annotations
ilOpt, err := withInternalLabels(internalLabels)
Expand Down Expand Up @@ -765,6 +766,8 @@ type internalLabels struct {
user string

healthcheck string

privileged bool
}

// WithInternalLabels sets the internal labels for a container.
Expand Down Expand Up @@ -845,6 +848,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO
m[labels.ContainerAutoRemove] = internalLabels.rm
}

if internalLabels.privileged {
m[labels.Privileged] = "true"
}

if internalLabels.cidFile != "" {
hostConfigLabel.CidFile = internalLabels.cidFile
}
Expand Down
177 changes: 159 additions & 18 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,26 +142,27 @@ type HostConfig struct {
// Binds []string // List of volume bindings for this container
ContainerIDFile string // File (path) where the containerId is written
LogConfig loggerLogConfig // Configuration of the logs for this container
// NetworkMode NetworkMode // Network mode to use for the container
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
// RestartPolicy RestartPolicy // Restart policy to be used for the container
// AutoRemove bool // Automatically remove container when it exits
NetworkMode string // Network mode to use for the container
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
RestartPolicy RestartPolicy // Restart policy to be used for the container
AutoRemove bool // Automatically remove container when it exits
// VolumeDriver string // Name of the volume driver used to mount volumes
// VolumesFrom []string // List of volumes to take from other container
// CapAdd strslice.StrSlice // List of kernel capabilities to add to the container
// CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container

CgroupnsMode string // Cgroup namespace mode to use for the container
DNS []string `json:"Dns"` // List of DNS server to lookup
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
ExtraHosts []string // List of extra hosts
GroupAdd []string // GroupAdd specifies additional groups to join
IpcMode string `json:"IpcMode"` // IPC namespace to use for the container
CapAdd []string // List of kernel capabilities to add to the container
CapDrop []string // List of kernel capabilities to remove from the container

CgroupnsMode string // Cgroup namespace mode to use for the container
DNS []string `json:"Dns"` // List of DNS server to lookup
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
ExtraHosts []string // List of extra hosts
GroupAdd []string // GroupAdd specifies additional groups to join
IpcMode string `json:"IpcMode"` // IPC namespace to use for the container
Annotations map[string]string `json:",omitempty"` // Arbitrary non-identifying metadata attached to container and provided to the runtime
// Cgroup CgroupSpec // Cgroup to use for the container
OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)
PidMode string // PID namespace to use for the container
// Privileged bool // Is the container in privileged mode
Privileged bool // Is the container in privileged mode
// PublishAllPorts bool // Should docker publish all exposed port for the container
ReadonlyRootfs bool // Is the container root filesystem in read-only
// SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux.
Expand All @@ -180,6 +181,10 @@ type HostConfig struct {
CPURealtimeRuntime int64 `json:"CpuRealtimeRuntime"` // Limits the CPU real-time runtime in microseconds
Memory int64 // Memory limit (in bytes)
MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap
MemoryReservation int64 // Memory soft limit (in bytes)
MemorySwappiness *int64 // Tuning container memory swappiness (0 to 100); nil means not set
PidsLimit int64 // Setting PIDs limit for a container; 0 or -1 for unlimited
Ulimits []*units.Ulimit // List of ulimits to be set in the container
OomKillDisable bool // specifies whether to disable OOM Killer
Devices []DeviceMapping // List of devices to map inside the container
BlkioSettings
Expand Down Expand Up @@ -268,6 +273,12 @@ type DeviceMapping struct {
CgroupPermissions string
}

// RestartPolicy represents the restart policies of the container.
type RestartPolicy struct {
Name string
MaximumRetryCount int
}

type CPUSettings struct {
CPUSetCpus string
CPUSetMems string
Expand Down Expand Up @@ -309,6 +320,26 @@ type NetworkEndpointSettings struct {
// TODO DriverOpts map[string]string
}

// defaultCaps mirrors containerd's defaultUnixCaps() — the 14 capabilities
// granted to non-privileged containers by default. Used as the baseline for
// reconstructing CapAdd/CapDrop from the OCI spec's bounding set.
var defaultCaps = map[string]struct{}{
"CAP_CHOWN": {},
"CAP_DAC_OVERRIDE": {},
"CAP_FSETID": {},
"CAP_FOWNER": {},
"CAP_MKNOD": {},
"CAP_NET_RAW": {},
"CAP_SETGID": {},
"CAP_SETUID": {},
"CAP_SETFCAP": {},
"CAP_SETPCAP": {},
"CAP_NET_BIND_SERVICE": {},
"CAP_SYS_CHROOT": {},
"CAP_KILL": {},
"CAP_AUDIT_WRITE": {},
}

// ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container.
func ContainerFromNative(n *native.Container) (*Container, error) {
var hostname string
Expand Down Expand Up @@ -500,6 +531,11 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
c.HostConfig.OomKillDisable = memorySettings.DisableOOMKiller
c.HostConfig.Memory = memorySettings.Limit
c.HostConfig.MemorySwap = memorySettings.Swap
c.HostConfig.MemoryReservation = memorySettings.Reservation
if memorySettings.Swappiness != nil {
swappiness := int64(*memorySettings.Swappiness)
c.HostConfig.MemorySwappiness = &swappiness
}

dnsSettings, err := getDNSFromNative(n.Labels)
if err != nil {
Expand Down Expand Up @@ -573,6 +609,63 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
c.Config.User = n.Labels[labels.User]
}

capAdd, capDrop, err := getCapabilitiesFromNative(n.Spec.(*specs.Spec))
if err != nil {
return nil, fmt.Errorf("failed to get capabilities: %w", err)
}
c.HostConfig.CapAdd = capAdd
c.HostConfig.CapDrop = capDrop

ulimits, err := getUlimitsFromNative(n.Spec.(*specs.Spec))
if err != nil {
return nil, fmt.Errorf("failed to get ulimits: %w", err)
}
c.HostConfig.Ulimits = ulimits

if policyStr := n.Labels[restart.PolicyLabel]; policyStr != "" {
rp, err := restart.NewPolicy(policyStr)
if err != nil {
return nil, fmt.Errorf("failed to parse restart policy: %w", err)
}
c.HostConfig.RestartPolicy = RestartPolicy{
Name: rp.Name(),
MaximumRetryCount: rp.MaximumRetryCount(),
}
}

if len(containerAnnotations) > 0 {
userAnnotations := make(map[string]string)
for k, v := range containerAnnotations {
if !strings.HasPrefix(k, labels.Prefix) {
userAnnotations[k] = v
}
}
if len(userAnnotations) > 0 {
c.HostConfig.Annotations = userAnnotations
}
}

if sp, ok := n.Spec.(*specs.Spec); ok {
if sp.Linux != nil && sp.Linux.Resources != nil &&
sp.Linux.Resources.Pids != nil && sp.Linux.Resources.Pids.Limit != nil {
c.HostConfig.PidsLimit = *sp.Linux.Resources.Pids.Limit
}
}

if networksJSON := n.Labels[labels.Networks]; networksJSON != "" {
var networks []string
if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil {
return nil, fmt.Errorf("failed to parse networks label: %v", err)
}
if len(networks) > 0 {
c.HostConfig.NetworkMode = networks[0]
}
}

c.HostConfig.Privileged = n.Labels[labels.Privileged] == "true"

c.HostConfig.AutoRemove = n.Labels[labels.ContainerAutoRemove] == "true"

// Add health check config if present in labels
if hConfig, ok := n.Labels[labels.HealthCheck]; ok && hConfig != "" {
healthCheckConfig, err := healthcheck.HealthCheckFromJSON(hConfig)
Expand Down Expand Up @@ -850,6 +943,15 @@ func getMemorySettingsFromNative(sp *specs.Spec) (*MemorySetting, error) {
if sp.Linux.Resources.Memory.Swap != nil {
res.Swap = *sp.Linux.Resources.Memory.Swap
}

if sp.Linux.Resources.Memory.Reservation != nil {
res.Reservation = *sp.Linux.Resources.Memory.Reservation
}

if sp.Linux.Resources.Memory.Swappiness != nil {
v := *sp.Linux.Resources.Memory.Swappiness
res.Swappiness = &v
}
}
return res, nil
}
Expand Down Expand Up @@ -904,6 +1006,43 @@ func getSysctlFromNative(sp *specs.Spec) (map[string]string, error) {
return res, nil
}

func getCapabilitiesFromNative(sp *specs.Spec) (capAdd, capDrop []string, err error) {
if sp.Process == nil || sp.Process.Capabilities == nil {
return nil, nil, nil
}
capAdd = []string{}
capDrop = []string{}
boundingSet := make(map[string]struct{}, len(sp.Process.Capabilities.Bounding))
for _, cap := range sp.Process.Capabilities.Bounding {
boundingSet[cap] = struct{}{}
if _, isDefault := defaultCaps[cap]; !isDefault {
capAdd = append(capAdd, cap)
}
}
for cap := range defaultCaps {
if _, present := boundingSet[cap]; !present {
capDrop = append(capDrop, cap)
}
}
return capAdd, capDrop, nil
}

func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) {
if sp.Process == nil || len(sp.Process.Rlimits) == 0 {
return nil, nil
}
ulimits := make([]*units.Ulimit, 0, len(sp.Process.Rlimits))
for _, rl := range sp.Process.Rlimits {
name := strings.ToLower(strings.TrimPrefix(rl.Type, "RLIMIT_"))
ulimits = append(ulimits, &units.Ulimit{
Name: name,
Hard: int64(rl.Hard),
Soft: int64(rl.Soft),
})
}
return ulimits, nil
}

type IPAMConfig struct {
Subnet string `json:"Subnet,omitempty"`
Gateway string `json:"Gateway,omitempty"`
Expand Down Expand Up @@ -944,9 +1083,11 @@ type structuredCNI struct {
}

type MemorySetting struct {
Limit int64 `json:"limit"`
Swap int64 `json:"swap"`
DisableOOMKiller bool `json:"disableOOMKiller"`
Limit int64 `json:"limit"`
Swap int64 `json:"swap"`
Reservation int64 `json:"reservation"`
Swappiness *uint64 `json:"swappiness"`
DisableOOMKiller bool `json:"disableOOMKiller"`
}

// parseNetworkSubnets extracts and parses subnet configurations from IPAM config
Expand Down
Loading
Loading