From 93b36e631d29db9a128c54391e93cae3f8e7d0df Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 18 Dec 2024 06:41:12 +0000 Subject: [PATCH] chore: add api options to container create Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 64 ++++++++-- api/handlers/container/create_test.go | 171 +++++++++++++++++++++++++- api/types/container_types.go | 61 +++++---- e2e/tests/container_create.go | 165 +++++++++++++++++++++++++ 4 files changed, 429 insertions(+), 32 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index b4221735..5e1b012f 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -107,6 +107,22 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } } + // Annotations: TODO - available in nerdctl 2.0 + // Annotations are passed in as a map of strings, + // but nerdctl expects an array of strings with format [annotations1=VALUE1, annotations2=VALUE2, ...]. + // annotations := []string{} + // if req.HostConfig.Annotations != nil { + // for key, val := range req.HostConfig.Annotations { + // annotations = append(annotations, fmt.Sprintf("%s=%s", key, val)) + // } + // } + + ulimits := []string{} + if req.HostConfig.Ulimits != nil { + for _, ulimit := range req.HostConfig.Ulimits { + ulimits = append(ulimits, ulimit.String()) + } + } // Environment vars: env := []string{} if req.Env != nil { @@ -119,6 +135,36 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { capAdd = req.HostConfig.CapAdd } + capDrop := []string{} + if req.HostConfig.CapDrop != nil { + capDrop = req.HostConfig.CapDrop + } + + memoryReservation := "" + if req.HostConfig.MemoryReservation != 0 { + memoryReservation = fmt.Sprint(req.HostConfig.MemoryReservation) + } + + memorySwap := "" + if req.HostConfig.MemorySwap != 0 { + memorySwap = fmt.Sprint(req.HostConfig.MemorySwap) + } + + memorySwappiness := int64(-1) + if req.HostConfig.MemorySwappiness > 0 { + memorySwappiness = req.HostConfig.MemorySwappiness + } + + pidLimit := int64(-1) + if req.HostConfig.PidsLimit > 0 { + pidLimit = req.HostConfig.PidsLimit + } + + CpuQuota := int64(-1) + if req.HostConfig.CPUQuota != 0 { + CpuQuota = req.HostConfig.CPUQuota + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -134,6 +180,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { Pull: "missing", // nerdctl default. StopSignal: stopSignal, StopTimeout: stopTimeout, + CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file // #endregion // #region for platform flags @@ -147,23 +194,26 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for resource flags CPUShares: uint64(req.HostConfig.CPUShares), // CPU shares (relative weight) Memory: memory, // memory limit (in bytes) - CPUQuota: -1, // nerdctl default. - MemorySwappiness64: -1, // nerdctl default. - PidsLimit: -1, // nerdctl default. + CPUQuota: CpuQuota, // CPUQuota limits the CPU CFS (Completely Fair Scheduler) quota + MemorySwappiness64: memorySwappiness, // Tuning container memory swappiness behaviour + PidsLimit: pidLimit, // PidsLimit specifies the tune container pids limit Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + MemoryReservation: memoryReservation, // Memory soft limit (in bytes) + MemorySwap: memorySwap, // Total memory usage (memory + swap); set `-1` to enable unlimited swap + Ulimit: ulimits, // List of ulimits to be set in the container + CPUPeriod: uint64(req.HostConfig.CPUPeriod), // #endregion // #region for user flags - User: req.User, - GroupAdd: []string{}, // nerdctl default. + User: req.User, // #endregion // #region for security flags SecurityOpt: []string{}, // nerdctl default. CapAdd: capAdd, - CapDrop: []string{}, // nerdctl default. + CapDrop: capDrop, + Privileged: req.HostConfig.Privileged, // #endregion - // #region for runtime flags Runtime: defaults.Runtime, // nerdctl default. // #endregion diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 9c5475cb..34bd4b27 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -357,6 +357,172 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set CPUPeriod create options for resources", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "CpuPeriod": 100000 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CPUPeriod = 100000 + + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set CpuQuota create options for resources", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "CpuQuota": 50000 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CPUQuota = 50000 + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set CpuQuota to -1 by default", func() { + body := []byte(`{ + "Image": "test-image" + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CPUQuota = -1 + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set MemoryReservation, MemorySwap and MemorySwappiness create options for resources", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "MemoryReservation": 209710, + "MemorySwap": 514288000, + "MemorySwappiness": 25 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.MemoryReservation = "209710" + createOpt.MemorySwap = "514288000" + createOpt.MemorySwappiness64 = 25 + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set CapDrop option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "CapDrop": ["MKNOD"] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CapDrop = []string{"MKNOD"} + + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set Privileged option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Privileged": true + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Privileged = true + + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set Ulimit option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Ulimits": [{"Name": "nofile", "Soft": 1024, "Hard": 2048},{"Name": "nproc", "Soft": 1024, "Hard": 4048}] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Ulimit = []string{"nofile=1024:2048", "nproc=1024:4048"} + + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + + It("should set PidLimit option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "PidsLimit": 200 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.PidsLimit = 200 + + service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( + cid, nil) + + // handler should return success message with 201 status code. + h.create(rr, req) + Expect(rr).Should(HaveHTTPStatus(http.StatusCreated)) + Expect(rr.Body).Should(MatchJSON(jsonResponse)) + }) + It("should return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) @@ -542,17 +708,18 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { MemorySwappiness64: -1, // nerdctl default. PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + Ulimit: []string{}, // #endregion // #region for user flags - User: "", - GroupAdd: []string{}, // nerdctl default. + User: "", // #endregion // #region for security flags SecurityOpt: []string{}, // nerdctl default. CapAdd: []string{}, // nerdctl default. CapDrop: []string{}, // nerdctl default. + Privileged: false, // #endregion // #region for runtime flags diff --git a/api/types/container_types.go b/api/types/container_types.go index 800431ef..e29cbefd 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -11,6 +11,7 @@ import ( "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" dockertypes "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" + "github.com/docker/go-units" ) // AttachOptions defines the available options for the container attach call. @@ -57,43 +58,44 @@ type ContainerConfig struct { // HostConfig is from https://github.com/moby/moby/blob/v24.0.2/api/types/container/hostconfig.go#L376-L436 type ContainerHostConfig struct { // Applicable to all platforms - Binds []string // List of volume bindings for this container - // TODO: ContainerIDFile string // File (path) where the containerId is written - LogConfig LogConfig // Configuration of the logs for this container - 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 + Binds []string // List of volume bindings for this container + ContainerIDFile string // File (path) where the containerId is written + LogConfig LogConfig // Configuration of the logs for this container + 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 // TODO: VolumeDriver string // Name of the volume driver used to mount volumes - // TODO: VolumesFrom []string // List of volumes to take from other container + // TODO: VolumesFrom []string // List of volumes to take from other container // TODO: ConsoleSize [2]uint // Initial console size (height,width) // TODO: Annotations map[string]string `json:",omitempty"` // Arbitrary non-identifying metadata attached to container and provided to the runtime // Applicable to UNIX platforms - CapAdd []string // List of kernel capabilities to add to the container - // TODO: CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container - // TODO: CgroupnsMode CgroupnsMode // Cgroup namespace mode 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 + // TODO: CgroupnsMode CgroupnsMode // 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 - // TODO: GroupAdd []string // List of additional groups that the container process will run as - // TODO: IpcMode IpcMode // IPC namespace to use for the container + // TODO: GroupAdd []string // List of additional groups that the container process will run as + // TODO: IpcMode IpcMode // IPC namespace to use for the container // TODO: Cgroup CgroupSpec // Cgroup to use for the container // TODO: Links []string // List of links (in the name:alias form) - // TODO: OomScoreAdj int // Container preference for OOM-killing - // TODO: PidMode PidMode // PID namespace to use for the container - // TODO: Privileged bool // Is the container in privileged mode + // TODO: OomKillDisable bool // specifies whether to disable OOM Killer + // TODO: OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) + // TODO: PidMode string // PID namespace to use for the container + Privileged bool // Is the container in privileged mode + // TODO: ReadonlyRootfs bool // Is the container root filesystem in read-only + // TODO: SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. (["key=value"]) + // TODO: Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container + // TODO: UTSMode string // UTS namespace to use for the container + // TODO: ShmSize int64 // Size of /dev/shm in bytes. The size must be greater than 0. + // TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container + // TODO: Runtime string `json:",omitempty"` // Runtime to use with this container // TODO: PublishAllPorts bool // Should docker publish all exposed port for the container - // TODO: ReadonlyRootfs bool // Is the container root filesystem in read-only - // TODO: SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. // TODO: StorageOpt map[string]string `json:",omitempty"` // Storage driver options per container. - // TODO: Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container - // TODO: UTSMode UTSMode // UTS namespace to use for the container // TODO: UsernsMode UsernsMode // The user namespace to use for the container - // TODO: ShmSize int64 // Total shm memory usage - // TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container - // TODO: Runtime string `json:",omitempty"` // Runtime to use with this container // Applicable to Windows // TODO: Isolation Isolation // Isolation technology of the container (e.g. default, hyperv) @@ -101,8 +103,19 @@ type ContainerHostConfig struct { // Contains container's resources (cgroups, ulimits) CPUShares int64 `json:"CpuShares"` // CPU shares (relative weight vs. other containers) Memory int64 // Memory limit (in bytes) + CPUPeriod int64 `json:"CpuPeriod"` // CPU CFS (Completely Fair Scheduler) period + CPUQuota int64 `json:"CpuQuota"` // CPU CFS (Completely Fair Scheduler) quota + // TODO: CPUSetCPUs string `json:"CpusetCpus"` // CPUSetCPUs specifies the CPUs in which to allow execution (0-3, 0,1) + // TODO: CPUSetMems string `json:"CpusetMems"` // CPUSetMems specifies the memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + MemoryReservation int64 // MemoryReservation specifies the memory soft limit (in bytes) + MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap + MemorySwappiness int64 // MemorySwappiness64 specifies the tune container memory swappiness (0 to 100) (default -1) // TODO: Resources + Ulimits []*Ulimit // List of ulimits to be set in the container + // TODO: BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) + // TODO: Devices []DeviceMapping // List of devices to map inside the container + PidsLimit int64 // Setting PIDs limit for a container; Set `0` or `-1` for unlimited, or `null` to not change. // Mounts specs used by the container // TODO: Mounts []mount.Mount `json:",omitempty"` @@ -249,3 +262,5 @@ type StatsJSON struct { // Networks request version >=1.21 Networks map[string]dockertypes.NetworkStats `json:"networks,omitempty"` } + +type Ulimit = units.Ulimit diff --git a/e2e/tests/container_create.go b/e2e/tests/container_create.go index 957ed215..8af83aec 100644 --- a/e2e/tests/container_create.go +++ b/e2e/tests/container_create.go @@ -432,6 +432,171 @@ func ContainerCreate(opt *option.Option) { // verify log path exists Expect(inspect[0].LogPath).ShouldNot(BeNil()) }) + + It("should create a container with specified CPU qouta and period options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.CPUQuota = 11111 + options.HostConfig.CPUShares = 2048 + options.HostConfig.CPUPeriod = 100000 + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName) + var nativeInspect []map[string]interface{} + err := json.Unmarshal(nativeResp, &nativeInspect) + Expect(err).Should(BeNil()) + Expect(nativeInspect).Should(HaveLen(1)) + + // Navigate to the CPU quota value + spec, ok := nativeInspect[0]["Spec"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + linux, ok := spec["linux"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + resources, ok := linux["resources"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + cpu, ok := resources["cpu"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + quota, ok := cpu["quota"].(float64) + Expect(ok).Should(BeTrue()) + period, ok := cpu["period"].(float64) + Expect(ok).Should(BeTrue()) + shares, ok := cpu["shares"].(float64) + Expect(ok).Should(BeTrue()) + + // Verify the CPU quota + Expect(int64(quota)).Should(Equal(int64(11111))) + Expect(int64(shares)).Should(Equal(int64(2048))) + Expect(int64(period)).Should(Equal(int64(100000))) + }) + + It("should create a container with specified Memory qouta and PidLimits options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.Memory = 4048 + options.HostConfig.PidsLimit = 200 + options.HostConfig.MemoryReservation = 28 + options.HostConfig.MemorySwap = 514288000 + options.HostConfig.MemorySwappiness = 25 + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName) + var nativeInspect []map[string]interface{} + err := json.Unmarshal(nativeResp, &nativeInspect) + Expect(err).Should(BeNil()) + Expect(nativeInspect).Should(HaveLen(1)) + + // Navigate to the CPU quota value + spec, ok := nativeInspect[0]["Spec"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + linux, ok := spec["linux"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + resources, ok := linux["resources"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + memory, _ := resources["memory"].(map[string]interface{}) + + pids, _ := resources["pids"].(map[string]interface{}) + + Expect(int64(pids["limit"].(float64))).Should(Equal(options.HostConfig.PidsLimit)) + Expect(int64(memory["limit"].(float64))).Should(Equal(options.HostConfig.Memory)) + }) + + It("should create a container with specified Ulimit options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + + options.HostConfig.Ulimits = []*types.Ulimit{ + { + Name: "nofile", + Soft: 1024, + Hard: 2048, + }, + } + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName) + var nativeInspect []map[string]interface{} + err := json.Unmarshal(nativeResp, &nativeInspect) + Expect(err).Should(BeNil()) + Expect(nativeInspect).Should(HaveLen(1)) + + // Navigate to the CPU quota value + spec, _ := nativeInspect[0]["Spec"].(map[string]interface{}) + rlimits := spec["process"].(map[string]interface{})["rlimits"].([]interface{}) + for _, ulimit := range options.HostConfig.Ulimits { + found := false + for _, rlimit := range rlimits { + r := rlimit.(map[string]interface{}) + if r["type"] == "RLIMIT_NOFILE" { + Expect(r["hard"]).To(Equal(float64(ulimit.Hard))) + Expect(r["soft"]).To(Equal(float64(ulimit.Soft))) + found = true + break + } + } + Expect(found).To(BeTrue()) + } + }) + + It("should create a container with Priviledged options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.Privileged = true + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName) + var nativeInspect []map[string]interface{} + err := json.Unmarshal(nativeResp, &nativeInspect) + Expect(err).Should(BeNil()) + Expect(nativeInspect).Should(HaveLen(1)) + + // Navigate to the CPU quota value + spec, ok := nativeInspect[0]["Spec"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + capabilities := spec["process"].(map[string]interface{})["capabilities"].(map[string]interface{}) + Expect(capabilities["bounding"]).To(ContainElements("CAP_SYS_ADMIN", "CAP_NET_ADMIN", "CAP_SYS_MODULE")) + }) + + It("should correctly apply CapAdd and CapDrop", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.CapAdd = []string{"SYS_TIME", "NET_ADMIN"} + options.HostConfig.CapDrop = []string{"CHOWN", "NET_RAW"} + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + nativeResp := command.Stdout(opt, "inspect", "--mode=native", testContainerName) + var nativeInspect []map[string]interface{} + err := json.Unmarshal(nativeResp, &nativeInspect) + Expect(err).Should(BeNil()) + Expect(nativeInspect).Should(HaveLen(1)) + + // Navigate to the CPU quota value + spec, ok := nativeInspect[0]["Spec"].(map[string]interface{}) + Expect(ok).Should(BeTrue()) + capabilities := spec["process"].(map[string]interface{})["capabilities"].(map[string]interface{}) + Expect(capabilities["bounding"]).To(ContainElements("CAP_SYS_TIME", "CAP_NET_ADMIN")) + Expect(capabilities["bounding"]).NotTo(ContainElements("CAP_CHOWN", "CAP_NET_RAW")) + }) + It("should create a container with specified network options", func() { // define options options.Cmd = []string{"sleep", "Infinity"}