Skip to content

Commit eda92da

Browse files
committed
feat: add missing HostConfig fields to dockercompat inspect response
Signed-off-by: ayush-panta <ayushkp@amazon.com>
1 parent 37740a3 commit eda92da

5 files changed

Lines changed: 504 additions & 29 deletions

File tree

cmd/nerdctl/container/container_inspect_linux_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,16 @@ func TestContainerInspectHostConfig(t *testing.T) {
391391
"--add-host", "host2:10.0.0.2",
392392
"--ipc", "host",
393393
"--memory", "512m",
394+
"--memory-reservation", "200m",
395+
"--memory-swappiness", "60",
396+
"--pids-limit", "100",
397+
"--ulimit", "nofile=1024:65536",
394398
"--read-only",
395399
"--shm-size", "256m",
396400
"--uts", "host",
397401
"--runtime", "io.containerd.runc.v2",
402+
"--restart", "on-failure:3",
403+
"--annotation", "com.example.key=test-val",
398404
testutil.AlpineImage, "sleep", nerdtest.Infinity)
399405
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
400406
}
@@ -430,6 +436,17 @@ func TestContainerInspectHostConfig(t *testing.T) {
430436
assert.Equal(tt, true, inspect.HostConfig.ReadonlyRootfs)
431437
assert.Equal(tt, "host", inspect.HostConfig.UTSMode)
432438
assert.Equal(tt, int64(268435456), inspect.HostConfig.ShmSize)
439+
assert.Equal(tt, int64(209715200), inspect.HostConfig.MemoryReservation)
440+
assert.Assert(tt, inspect.HostConfig.MemorySwappiness != nil)
441+
assert.Equal(tt, int64(60), *inspect.HostConfig.MemorySwappiness)
442+
assert.Equal(tt, int64(100), inspect.HostConfig.PidsLimit)
443+
assert.Equal(tt, 1, len(inspect.HostConfig.Ulimits))
444+
assert.Equal(tt, "nofile", inspect.HostConfig.Ulimits[0].Name)
445+
assert.Equal(tt, int64(65536), inspect.HostConfig.Ulimits[0].Hard)
446+
assert.Equal(tt, int64(1024), inspect.HostConfig.Ulimits[0].Soft)
447+
assert.Equal(tt, "on-failure", inspect.HostConfig.RestartPolicy.Name)
448+
assert.Equal(tt, 3, inspect.HostConfig.RestartPolicy.MaximumRetryCount)
449+
assert.Equal(tt, "test-val", inspect.HostConfig.Annotations["com.example.key"])
433450
})
434451

435452
testCase.Run(t)
@@ -509,6 +526,14 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) {
509526
assert.Equal(tt, hc.ShmSize, inspect.HostConfig.ShmSize)
510527
assert.Equal(tt, hc.Runtime, inspect.HostConfig.Runtime)
511528
assert.Equal(tt, 0, len(inspect.HostConfig.Devices))
529+
assert.Equal(tt, false, inspect.HostConfig.Privileged)
530+
assert.Equal(tt, false, inspect.HostConfig.AutoRemove)
531+
assert.Assert(tt, inspect.HostConfig.CapAdd == nil)
532+
assert.Assert(tt, inspect.HostConfig.CapDrop == nil)
533+
assert.Equal(tt, int64(0), inspect.HostConfig.PidsLimit)
534+
assert.Assert(tt, inspect.HostConfig.Ulimits == nil)
535+
assert.Equal(tt, int64(0), inspect.HostConfig.MemoryReservation)
536+
assert.Assert(tt, inspect.HostConfig.MemorySwappiness == nil)
512537

513538
// Sysctls can be empty or contain "net.ipv4.ip_unprivileged_port_start" depending on the environment.
514539
got := len(inspect.HostConfig.Sysctls)
@@ -881,3 +906,71 @@ type hostConfigValues struct {
881906
GroupAddSize int
882907
Runtime string
883908
}
909+
910+
func TestContainerInspectHostConfigPrivileged(t *testing.T) {
911+
testCase := nerdtest.Setup()
912+
913+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
914+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
915+
"--privileged",
916+
testutil.AlpineImage, "sleep", nerdtest.Infinity)
917+
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
918+
}
919+
920+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
921+
helpers.Anyhow("rm", "-f", data.Identifier())
922+
}
923+
924+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
925+
return helpers.Command("inspect", data.Identifier())
926+
}
927+
928+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) {
929+
var dc []dockercompat.Container
930+
931+
err := json.Unmarshal([]byte(stdout), &dc)
932+
assert.NilError(tt, err)
933+
assert.Equal(tt, 1, len(dc))
934+
935+
inspect := dc[0]
936+
assert.Equal(tt, true, inspect.HostConfig.Privileged)
937+
})
938+
939+
testCase.Run(t)
940+
}
941+
942+
func TestContainerInspectHostConfigCapabilities(t *testing.T) {
943+
testCase := nerdtest.Setup()
944+
945+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
946+
helpers.Ensure("run", "-d", "--name", data.Identifier(),
947+
"--cap-add", "NET_ADMIN",
948+
"--cap-drop", "CHOWN",
949+
testutil.AlpineImage, "sleep", nerdtest.Infinity)
950+
nerdtest.EnsureContainerStarted(helpers, data.Identifier())
951+
}
952+
953+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
954+
helpers.Anyhow("rm", "-f", data.Identifier())
955+
}
956+
957+
testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
958+
return helpers.Command("inspect", data.Identifier())
959+
}
960+
961+
testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, tt tig.T) {
962+
var dc []dockercompat.Container
963+
964+
err := json.Unmarshal([]byte(stdout), &dc)
965+
assert.NilError(tt, err)
966+
assert.Equal(tt, 1, len(dc))
967+
968+
inspect := dc[0]
969+
assert.Assert(tt, slices.Contains(inspect.HostConfig.CapAdd, "CAP_NET_ADMIN"),
970+
"Expected CAP_NET_ADMIN in CapAdd")
971+
assert.Assert(tt, slices.Contains(inspect.HostConfig.CapDrop, "CAP_CHOWN"),
972+
"Expected CAP_CHOWN in CapDrop")
973+
})
974+
975+
testCase.Run(t)
976+
}

pkg/cmd/container/create.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
395395
internalLabels.extraHosts = extraHosts
396396

397397
internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm)
398+
internalLabels.privileged = options.Privileged
398399

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

767768
healthcheck string
769+
770+
privileged bool
768771
}
769772

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

851+
if internalLabels.privileged {
852+
m[labels.Privileged] = "true"
853+
}
854+
848855
if internalLabels.cidFile != "" {
849856
hostConfigLabel.CidFile = internalLabels.cidFile
850857
}

pkg/inspecttypes/dockercompat/dockercompat.go

Lines changed: 157 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -142,26 +142,27 @@ type HostConfig struct {
142142
// Binds []string // List of volume bindings for this container
143143
ContainerIDFile string // File (path) where the containerId is written
144144
LogConfig loggerLogConfig // Configuration of the logs for this container
145-
// NetworkMode NetworkMode // Network mode to use for the container
146-
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
147-
// RestartPolicy RestartPolicy // Restart policy to be used for the container
148-
// AutoRemove bool // Automatically remove container when it exits
145+
NetworkMode string // Network mode to use for the container
146+
PortBindings nat.PortMap // Port mapping between the exposed port (container) and the host
147+
RestartPolicy RestartPolicy // Restart policy to be used for the container
148+
AutoRemove bool // Automatically remove container when it exits
149149
// VolumeDriver string // Name of the volume driver used to mount volumes
150150
// VolumesFrom []string // List of volumes to take from other container
151-
// CapAdd strslice.StrSlice // List of kernel capabilities to add to the container
152-
// CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container
153-
154-
CgroupnsMode string // Cgroup namespace mode to use for the container
155-
DNS []string `json:"Dns"` // List of DNS server to lookup
156-
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
157-
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
158-
ExtraHosts []string // List of extra hosts
159-
GroupAdd []string // GroupAdd specifies additional groups to join
160-
IpcMode string `json:"IpcMode"` // IPC namespace to use for the container
151+
CapAdd []string // List of kernel capabilities to add to the container
152+
CapDrop []string // List of kernel capabilities to remove from the container
153+
154+
CgroupnsMode string // Cgroup namespace mode to use for the container
155+
DNS []string `json:"Dns"` // List of DNS server to lookup
156+
DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for
157+
DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for
158+
ExtraHosts []string // List of extra hosts
159+
GroupAdd []string // GroupAdd specifies additional groups to join
160+
IpcMode string `json:"IpcMode"` // IPC namespace to use for the container
161+
Annotations map[string]string `json:",omitempty"` // Arbitrary non-identifying metadata attached to container and provided to the runtime
161162
// Cgroup CgroupSpec // Cgroup to use for the container
162163
OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)
163164
PidMode string // PID namespace to use for the container
164-
// Privileged bool // Is the container in privileged mode
165+
Privileged bool // Is the container in privileged mode
165166
// PublishAllPorts bool // Should docker publish all exposed port for the container
166167
ReadonlyRootfs bool // Is the container root filesystem in read-only
167168
// SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux.
@@ -180,6 +181,10 @@ type HostConfig struct {
180181
CPURealtimeRuntime int64 `json:"CpuRealtimeRuntime"` // Limits the CPU real-time runtime in microseconds
181182
Memory int64 // Memory limit (in bytes)
182183
MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap
184+
MemoryReservation int64 // Memory soft limit (in bytes)
185+
MemorySwappiness *int64 // Tuning container memory swappiness (0 to 100); nil means not set
186+
PidsLimit int64 // Setting PIDs limit for a container; 0 or -1 for unlimited
187+
Ulimits []*units.Ulimit // List of ulimits to be set in the container
183188
OomKillDisable bool // specifies whether to disable OOM Killer
184189
Devices []DeviceMapping // List of devices to map inside the container
185190
BlkioSettings
@@ -268,6 +273,12 @@ type DeviceMapping struct {
268273
CgroupPermissions string
269274
}
270275

276+
// RestartPolicy represents the restart policies of the container.
277+
type RestartPolicy struct {
278+
Name string
279+
MaximumRetryCount int
280+
}
281+
271282
type CPUSettings struct {
272283
CPUSetCpus string
273284
CPUSetMems string
@@ -309,6 +320,26 @@ type NetworkEndpointSettings struct {
309320
// TODO DriverOpts map[string]string
310321
}
311322

323+
// defaultCaps mirrors containerd's defaultUnixCaps() — the 14 capabilities
324+
// granted to non-privileged containers by default. Used as the baseline for
325+
// reconstructing CapAdd/CapDrop from the OCI spec's bounding set.
326+
var defaultCaps = map[string]struct{}{
327+
"CAP_CHOWN": {},
328+
"CAP_DAC_OVERRIDE": {},
329+
"CAP_FSETID": {},
330+
"CAP_FOWNER": {},
331+
"CAP_MKNOD": {},
332+
"CAP_NET_RAW": {},
333+
"CAP_SETGID": {},
334+
"CAP_SETUID": {},
335+
"CAP_SETFCAP": {},
336+
"CAP_SETPCAP": {},
337+
"CAP_NET_BIND_SERVICE": {},
338+
"CAP_SYS_CHROOT": {},
339+
"CAP_KILL": {},
340+
"CAP_AUDIT_WRITE": {},
341+
}
342+
312343
// ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container.
313344
func ContainerFromNative(n *native.Container) (*Container, error) {
314345
var hostname string
@@ -500,6 +531,11 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
500531
c.HostConfig.OomKillDisable = memorySettings.DisableOOMKiller
501532
c.HostConfig.Memory = memorySettings.Limit
502533
c.HostConfig.MemorySwap = memorySettings.Swap
534+
c.HostConfig.MemoryReservation = memorySettings.Reservation
535+
if memorySettings.Swappiness != nil {
536+
swappiness := int64(*memorySettings.Swappiness)
537+
c.HostConfig.MemorySwappiness = &swappiness
538+
}
503539

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

612+
capAdd, capDrop, err := getCapabilitiesFromNative(n.Spec.(*specs.Spec))
613+
if err != nil {
614+
return nil, fmt.Errorf("failed to get capabilities: %w", err)
615+
}
616+
c.HostConfig.CapAdd = capAdd
617+
c.HostConfig.CapDrop = capDrop
618+
619+
ulimits, err := getUlimitsFromNative(n.Spec.(*specs.Spec))
620+
if err != nil {
621+
return nil, fmt.Errorf("failed to get ulimits: %w", err)
622+
}
623+
c.HostConfig.Ulimits = ulimits
624+
625+
if policyStr := n.Labels[restart.PolicyLabel]; policyStr != "" {
626+
rp, err := restart.NewPolicy(policyStr)
627+
if err != nil {
628+
return nil, fmt.Errorf("failed to parse restart policy: %w", err)
629+
}
630+
c.HostConfig.RestartPolicy = RestartPolicy{
631+
Name: rp.Name(),
632+
MaximumRetryCount: rp.MaximumRetryCount(),
633+
}
634+
}
635+
636+
if len(containerAnnotations) > 0 {
637+
userAnnotations := make(map[string]string)
638+
for k, v := range containerAnnotations {
639+
if !strings.HasPrefix(k, labels.Prefix) {
640+
userAnnotations[k] = v
641+
}
642+
}
643+
if len(userAnnotations) > 0 {
644+
c.HostConfig.Annotations = userAnnotations
645+
}
646+
}
647+
648+
if sp, ok := n.Spec.(*specs.Spec); ok {
649+
if sp.Linux != nil && sp.Linux.Resources != nil &&
650+
sp.Linux.Resources.Pids != nil && sp.Linux.Resources.Pids.Limit != nil {
651+
c.HostConfig.PidsLimit = *sp.Linux.Resources.Pids.Limit
652+
}
653+
}
654+
655+
if networksJSON := n.Labels[labels.Networks]; networksJSON != "" {
656+
var networks []string
657+
if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil {
658+
return nil, fmt.Errorf("failed to parse networks label: %v", err)
659+
}
660+
if len(networks) > 0 {
661+
c.HostConfig.NetworkMode = networks[0]
662+
}
663+
}
664+
665+
c.HostConfig.Privileged = n.Labels[labels.Privileged] == "true"
666+
667+
c.HostConfig.AutoRemove = n.Labels[labels.ContainerAutoRemove] == "true"
668+
576669
// Add health check config if present in labels
577670
if hConfig, ok := n.Labels[labels.HealthCheck]; ok && hConfig != "" {
578671
healthCheckConfig, err := healthcheck.HealthCheckFromJSON(hConfig)
@@ -850,6 +943,15 @@ func getMemorySettingsFromNative(sp *specs.Spec) (*MemorySetting, error) {
850943
if sp.Linux.Resources.Memory.Swap != nil {
851944
res.Swap = *sp.Linux.Resources.Memory.Swap
852945
}
946+
947+
if sp.Linux.Resources.Memory.Reservation != nil {
948+
res.Reservation = *sp.Linux.Resources.Memory.Reservation
949+
}
950+
951+
if sp.Linux.Resources.Memory.Swappiness != nil {
952+
v := *sp.Linux.Resources.Memory.Swappiness
953+
res.Swappiness = &v
954+
}
853955
}
854956
return res, nil
855957
}
@@ -904,6 +1006,41 @@ func getSysctlFromNative(sp *specs.Spec) (map[string]string, error) {
9041006
return res, nil
9051007
}
9061008

1009+
func getCapabilitiesFromNative(sp *specs.Spec) (capAdd, capDrop []string, err error) {
1010+
if sp.Process == nil || sp.Process.Capabilities == nil {
1011+
return nil, nil, nil
1012+
}
1013+
boundingSet := make(map[string]struct{}, len(sp.Process.Capabilities.Bounding))
1014+
for _, cap := range sp.Process.Capabilities.Bounding {
1015+
boundingSet[cap] = struct{}{}
1016+
if _, isDefault := defaultCaps[cap]; !isDefault {
1017+
capAdd = append(capAdd, cap)
1018+
}
1019+
}
1020+
for cap := range defaultCaps {
1021+
if _, present := boundingSet[cap]; !present {
1022+
capDrop = append(capDrop, cap)
1023+
}
1024+
}
1025+
return capAdd, capDrop, nil
1026+
}
1027+
1028+
func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) {
1029+
if sp.Process == nil || len(sp.Process.Rlimits) == 0 {
1030+
return nil, nil
1031+
}
1032+
var ulimits []*units.Ulimit
1033+
for _, rl := range sp.Process.Rlimits {
1034+
name := strings.ToLower(strings.TrimPrefix(rl.Type, "RLIMIT_"))
1035+
ulimits = append(ulimits, &units.Ulimit{
1036+
Name: name,
1037+
Hard: int64(rl.Hard),
1038+
Soft: int64(rl.Soft),
1039+
})
1040+
}
1041+
return ulimits, nil
1042+
}
1043+
9071044
type IPAMConfig struct {
9081045
Subnet string `json:"Subnet,omitempty"`
9091046
Gateway string `json:"Gateway,omitempty"`
@@ -944,9 +1081,11 @@ type structuredCNI struct {
9441081
}
9451082

9461083
type MemorySetting struct {
947-
Limit int64 `json:"limit"`
948-
Swap int64 `json:"swap"`
949-
DisableOOMKiller bool `json:"disableOOMKiller"`
1084+
Limit int64 `json:"limit"`
1085+
Swap int64 `json:"swap"`
1086+
Reservation int64 `json:"reservation"`
1087+
Swappiness *uint64 `json:"swappiness"`
1088+
DisableOOMKiller bool `json:"disableOOMKiller"`
9501089
}
9511090

9521091
// parseNetworkSubnets extracts and parses subnet configurations from IPAM config

0 commit comments

Comments
 (0)