From 8058d2c0185193a8dae0bc73ffe5b64ff16e105e Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 4 Dec 2024 23:24:12 +0000 Subject: [PATCH 01/22] feat: add support for network disabled flag Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 5 ++++- api/types/container_types.go | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index b4221735..ac0eb83d 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -210,13 +210,16 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { if networkMode == "" || networkMode == "default" { networkMode = "bridge" } + if req.NetworkDisabled { + networkMode = "none" + } dnsOpt := []string{} if req.HostConfig.DNSOptions != nil { dnsOpt = req.HostConfig.DNSOptions } netOpt := ncTypes.NetworkOptions{ Hostname: req.Hostname, - NetworkSlice: []string{networkMode}, // TODO: Set to none if "NetworkDisabled" is true in request + NetworkSlice: []string{networkMode}, DNSServers: req.HostConfig.DNS, // Custom DNS lookup servers. DNSResolvConfOptions: dnsOpt, // DNS options. DNSSearchDomains: req.HostConfig.DNSSearch, // Custom DNS search domains. diff --git a/api/types/container_types.go b/api/types/container_types.go index 800431ef..d52d7cce 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -41,11 +41,11 @@ type ContainerConfig struct { Cmd []string `json:",omitempty"` // Command to run when starting the container // TODO Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy // TODO: ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific). - Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) - Volumes map[string]struct{} `json:",omitempty"` // List of volumes (mounts) used for the container - WorkingDir string `json:",omitempty"` // Current directory (PWD) in the command will be launched - Entrypoint []string `json:",omitempty"` // Entrypoint to run when starting the container - // TODO: NetworkDisabled bool `json:",omitempty"` // Is network disabled + Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) + Volumes map[string]struct{} `json:",omitempty"` // List of volumes (mounts) used for the container + WorkingDir string `json:",omitempty"` // Current directory (PWD) in the command will be launched + Entrypoint []string `json:",omitempty"` // Entrypoint to run when starting the container + NetworkDisabled bool `json:",omitempty"` // Is network disabled // TODO: MacAddress string `json:",omitempty"` // Mac Address of the container // TODO: OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile Labels map[string]string `json:",omitempty"` // List of labels set to this container From 0fc77203171bbba6e840a725bc403a3a4b851f4f Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 5 Dec 2024 00:43:31 +0000 Subject: [PATCH 02/22] chore: add support for OomKillDisable flag Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 17 +++++++++-------- api/types/container_types.go | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index ac0eb83d..cc481136 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -126,14 +126,15 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { GOptions: globalOpt, // #region for basic flags - Interactive: false, // TODO: update this after attach supports STDIN - TTY: false, // TODO: update this after attach supports STDIN - Detach: true, // TODO: current implementation of create does not support AttachStdin, AttachStdout, and AttachStderr flags - Restart: restart, // Restart policy to apply when a container exits. - Rm: req.HostConfig.AutoRemove, // Automatically remove container upon exit. - Pull: "missing", // nerdctl default. - StopSignal: stopSignal, - StopTimeout: stopTimeout, + Interactive: false, // TODO: update this after attach supports STDIN + TTY: false, // TODO: update this after attach supports STDIN + Detach: true, // TODO: current implementation of create does not support AttachStdin, AttachStdout, and AttachStderr flags + Restart: restart, // Restart policy to apply when a container exits. + Rm: req.HostConfig.AutoRemove, // Automatically remove container upon exit. + Pull: "missing", // nerdctl default. + StopSignal: stopSignal, + StopTimeout: stopTimeout, + OomKillDisable: req.HostConfig.OomKillDisable, // #endregion // #region for platform flags diff --git a/api/types/container_types.go b/api/types/container_types.go index d52d7cce..13410780 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -81,6 +81,7 @@ type ContainerHostConfig struct { // 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) + OomKillDisable bool //specifies whether to disable OOM Killer // 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 From 79ad22afcac902b5ee721d15ea811a41dc7698d2 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 5 Dec 2024 16:46:47 +0000 Subject: [PATCH 03/22] chore: add support for MACAddress option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 1 + api/types/container_types.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index cc481136..c19107d7 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -226,6 +226,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { DNSSearchDomains: req.HostConfig.DNSSearch, // Custom DNS search domains. PortMappings: portMappings, AddHost: req.HostConfig.ExtraHosts, // Extra hosts. + MACAddress: req.MacAddress, } ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace) diff --git a/api/types/container_types.go b/api/types/container_types.go index 13410780..ad139cb7 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -46,7 +46,7 @@ type ContainerConfig struct { WorkingDir string `json:",omitempty"` // Current directory (PWD) in the command will be launched Entrypoint []string `json:",omitempty"` // Entrypoint to run when starting the container NetworkDisabled bool `json:",omitempty"` // Is network disabled - // TODO: MacAddress string `json:",omitempty"` // Mac Address of the container + MacAddress string `json:",omitempty"` // Mac Address of the container // TODO: OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile Labels map[string]string `json:",omitempty"` // List of labels set to this container StopSignal string `json:",omitempty"` // Signal to stop a container From 1ab260c54f98abee567aec63c77d2d4a5bcb6304 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 6 Dec 2024 01:14:19 +0000 Subject: [PATCH 04/22] chore: add unit tests for OomKillDisable, MACAddress and NetworkDisabled Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create_test.go | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 9c5475cb..c150b707 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -357,6 +357,65 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set specified NetworkDisabled setting", func() { + body := []byte(`{ + "Image": "test-image", + "NetworkDisabled": true + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + netOpt.NetworkSlice = []string{"none"} + + 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 the OomKillDisable option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "OomKillDisable": true + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + createOpt.OomKillDisable = 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 the MACAddress to a user specified value", func() { + body := []byte(`{ + "Image": "test-image", + "MacAddress": "12:34:56:78:9a:bc" + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + netOpt.MACAddress = "12:34:56:78:9a:bc" + + 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)) From 4a8774d225a4293ccabfc618473f0ba982a1d581 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 6 Dec 2024 01:39:04 +0000 Subject: [PATCH 05/22] chore: add support for BlkioWeight Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 1 + api/handlers/container/create_test.go | 21 +++++++++++++++++++++ api/types/container_types.go | 1 + 3 files changed, 23 insertions(+) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index c19107d7..6f68ee13 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -152,6 +152,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { MemorySwappiness64: -1, // nerdctl default. PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + BlkioWeight: req.HostConfig.BlkioWeight, // block IO weight (relative) // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index c150b707..cdcb3b02 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -416,6 +416,27 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set the BlkioWeight to a user specified value", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "BlkioWeight": 300 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + createOpt.BlkioWeight = 300 + + 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)) diff --git a/api/types/container_types.go b/api/types/container_types.go index ad139cb7..15f3d536 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -104,6 +104,7 @@ type ContainerHostConfig struct { Memory int64 // Memory limit (in bytes) // TODO: Resources + BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) // Mounts specs used by the container // TODO: Mounts []mount.Mount `json:",omitempty"` From e251f271b7a09c6a830e30551a7c9fe0a5ab98d0 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 6 Dec 2024 21:55:25 +0000 Subject: [PATCH 06/22] chore: add cpushares option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 1 + api/handlers/container/create_test.go | 21 +++++++++++++++++++++ api/types/container_types.go | 1 + 3 files changed, 23 insertions(+) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 6f68ee13..26cd6c9b 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -153,6 +153,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. BlkioWeight: req.HostConfig.BlkioWeight, // block IO weight (relative) + CPUPeriod: uint64(req.HostConfig.CPUPeriod), // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index cdcb3b02..5cb63dbb 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -437,6 +437,27 @@ 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 15f3d536..a0f3c515 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -102,6 +102,7 @@ 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 // TODO: Resources BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) From 3d4c1efaead1143cff614cbcf13ee435f39cd803 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Sat, 7 Dec 2024 01:25:05 +0000 Subject: [PATCH 07/22] chore: add CPUQuota option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 7 ++++- api/handlers/container/create_test.go | 37 +++++++++++++++++++++++++++ api/types/container_types.go | 1 + 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 26cd6c9b..e88f50ad 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -119,6 +119,11 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { capAdd = req.HostConfig.CapAdd } + CpuQuota := int64(-1) + if req.HostConfig.CPUQuota != 0 { + CpuQuota = int64(req.HostConfig.CPUQuota) + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -148,7 +153,7 @@ 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. + CPUQuota: CpuQuota, // nerdctl default. MemorySwappiness64: -1, // nerdctl default. PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 5cb63dbb..be56659e 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -458,6 +458,43 @@ var _ = Describe("Container Create API ", func() { 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) diff --git a/api/types/container_types.go b/api/types/container_types.go index a0f3c515..26354eb0 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -103,6 +103,7 @@ type ContainerHostConfig struct { 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: Resources BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) From 51cb9011cc626b8f22d1bea3c9c66a2382955a14 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Sun, 8 Dec 2024 16:06:54 +0000 Subject: [PATCH 08/22] chore: add Memory options Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 23 ++++++++++++-- api/handlers/container/create_test.go | 46 +++++++++++++++++++++++++++ api/types/container_types.go | 13 +++++--- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index e88f50ad..17a4cdab 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -124,6 +124,21 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { CpuQuota = int64(req.HostConfig.CPUQuota) } + memoryReservation := "" + if req.HostConfig.MemoryReservation != 0 { + memoryReservation = strconv.FormatInt(req.HostConfig.MemoryReservation, 10) + } + + memorySwap := "" + if req.HostConfig.MemorySwap != 0 { + memorySwap = strconv.FormatInt(req.HostConfig.MemorySwap, 10) + } + + memorySwappiness := int64(-1) + if req.HostConfig.MemorySwappiness != 0 && req.HostConfig.MemorySwappiness > -1 { + memorySwappiness = req.HostConfig.MemorySwappiness + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -154,11 +169,15 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { CPUShares: uint64(req.HostConfig.CPUShares), // CPU shares (relative weight) Memory: memory, // memory limit (in bytes) CPUQuota: CpuQuota, // nerdctl default. - MemorySwappiness64: -1, // nerdctl default. + MemorySwappiness64: memorySwappiness, // Tuning container memory swappiness behaviour PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. BlkioWeight: req.HostConfig.BlkioWeight, // block IO weight (relative) - CPUPeriod: uint64(req.HostConfig.CPUPeriod), + CPUPeriod: uint64(req.HostConfig.CPUPeriod), // CPU CFS (Completely Fair Scheduler) period + CPUSetCPUs: req.HostConfig.CPUSetCPUs, // CpusetCpus 0-2, 0,1 + CPUSetMems: req.HostConfig.CPUSetMems, // CpusetMems 0-2, 0,1 + MemoryReservation: memoryReservation, // Memory soft limit (in bytes) + MemorySwap: memorySwap, // Total memory usage (memory + swap); set `-1` to enable unlimited swap // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index be56659e..da4d1f72 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -495,6 +495,52 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set CpuSet create options for resources", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "CpusetCpus": "0,1", + "CpusetMems": "0,3" + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CPUSetCPUs = "0,1" + createOpt.CPUSetMems = "0,3" + 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": 209715200, + "MemorySwap": 514288000, + "MemorySwappiness": 25 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.MemoryReservation = "209715200" + 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 26354eb0..101e0003 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -100,10 +100,15 @@ type ContainerHostConfig struct { // TODO: Isolation Isolation // Isolation technology of the container (e.g. default, hyperv) // 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 + 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 + CPUSetCPUs string `json:"CpusetCpus"` // CPUSetCPUs specifies the CPUs in which to allow execution (0-3, 0,1) + 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 BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) From 40b964ec5c225ed7d9985c0c3e8fef30d95ce318 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 17:27:36 +0000 Subject: [PATCH 09/22] chore: add ContainerIDFile options Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 1 + api/handlers/container/create_test.go | 21 +++++++++++++++++++++ api/types/container_types.go | 14 +++++++------- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 17a4cdab..02c63f15 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -155,6 +155,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { StopSignal: stopSignal, StopTimeout: stopTimeout, OomKillDisable: req.HostConfig.OomKillDisable, + CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file // #endregion // #region for platform flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index da4d1f72..9f590f45 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -541,6 +541,27 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set ContainerIdFile option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "ContainerIDFile": "/lib/example.txt" + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.CidFile = "/lib/example.txt" + + 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)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 101e0003..b0cbcf8a 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -57,13 +57,13 @@ 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: ConsoleSize [2]uint // Initial console size (height,width) From a95843d863abd3bde150eb99655bdb22e76e67ee Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 18:00:51 +0000 Subject: [PATCH 10/22] chore: add VolumesFrom option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 10 ++++++++-- api/handlers/container/create_test.go | 24 +++++++++++++++++++++++- api/types/container_types.go | 2 +- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 02c63f15..67b173d8 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -121,7 +121,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { CpuQuota := int64(-1) if req.HostConfig.CPUQuota != 0 { - CpuQuota = int64(req.HostConfig.CPUQuota) + CpuQuota = req.HostConfig.CPUQuota } memoryReservation := "" @@ -139,6 +139,11 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { memorySwappiness = req.HostConfig.MemorySwappiness } + volumesFrom := []string{} + if req.HostConfig.VolumesFrom != nil { + volumesFrom = req.HostConfig.VolumesFrom + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -197,7 +202,8 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for volume flags - Volume: volumes, + Volume: volumes, + VolumesFrom: volumesFrom, // #endregion // #region for env flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 9f590f45..18bd3242 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -562,6 +562,27 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set VolumesFrom option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "VolumesFrom": [ "parent", "other:ro"] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.VolumesFrom = []string{"parent", "other:ro"} + + 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)) @@ -765,7 +786,8 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { // #endregion // #region for volume flags - Volume: nil, + Volume: nil, + VolumesFrom: []string{}, // nerdctl default. // #endregion // #region for env flags diff --git a/api/types/container_types.go b/api/types/container_types.go index b0cbcf8a..0b160b8a 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -64,8 +64,8 @@ type ContainerHostConfig struct { 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 + VolumesFrom []string // List of volumes to take from other container // TODO: VolumeDriver string // Name of the volume driver used to mount volumes - // 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 From c9f437522e5a55e9e22c3dd7585be738cbd48957 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 19:39:05 +0000 Subject: [PATCH 11/22] chore: add CapAdd option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 7 ++++++- api/handlers/container/create_test.go | 21 +++++++++++++++++++++ api/types/container_types.go | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 67b173d8..eb17ce8c 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -119,6 +119,11 @@ 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 + } + CpuQuota := int64(-1) if req.HostConfig.CPUQuota != 0 { CpuQuota = req.HostConfig.CPUQuota @@ -194,7 +199,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for security flags SecurityOpt: []string{}, // nerdctl default. CapAdd: capAdd, - CapDrop: []string{}, // nerdctl default. + CapDrop: capDrop, // #endregion // #region for runtime flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 18bd3242..83e5c1ec 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -583,6 +583,27 @@ var _ = Describe("Container Create API ", func() { 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 0b160b8a..7fa39d94 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -70,8 +70,8 @@ type ContainerHostConfig struct { // 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 + 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 From dab12fa85c69b8b6082595b3e0733a9ca35ec045 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 20:25:51 +0000 Subject: [PATCH 12/22] chore: add GroupAdd option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 7 ++++++- api/handlers/container/create_test.go | 6 ++++-- api/types/container_types.go | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index eb17ce8c..8f25c83c 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -149,6 +149,11 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { volumesFrom = req.HostConfig.VolumesFrom } + groupAdd := []string{} + if req.HostConfig.GroupAdd != nil { + groupAdd = req.HostConfig.GroupAdd + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -193,7 +198,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for user flags User: req.User, - GroupAdd: []string{}, // nerdctl default. + GroupAdd: groupAdd, // #endregion // #region for security flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 83e5c1ec..cb73c709 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -583,17 +583,19 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) - It("should set CapDrop option", func() { + It("should set CapDrop and GroupAdd option", func() { body := []byte(`{ "Image": "test-image", "HostConfig": { - "CapDrop": ["MKNOD"] + "CapDrop": ["MKNOD"], + "GroupAdd": ["someGroup"] } }`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) // expected create options createOpt.CapDrop = []string{"MKNOD"} + createOpt.GroupAdd = []string{"someGroup"} service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( cid, nil) diff --git a/api/types/container_types.go b/api/types/container_types.go index 7fa39d94..464d8c20 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -77,7 +77,7 @@ type ContainerHostConfig struct { 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 + 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) From 59e99f3d3de455f56a50bc57b5961453e0cd5986 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 23:15:00 +0000 Subject: [PATCH 13/22] chore: add IPC and OomScoreAdj option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 2 ++ api/handlers/container/create_test.go | 23 +++++++++++++++++++++++ api/types/container_types.go | 6 +++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 8f25c83c..45443951 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -171,6 +171,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { StopTimeout: stopTimeout, OomKillDisable: req.HostConfig.OomKillDisable, CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file + OomScoreAdj: req.HostConfig.OomScoreAdj, // #endregion // #region for platform flags @@ -194,6 +195,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { CPUSetMems: req.HostConfig.CPUSetMems, // CpusetMems 0-2, 0,1 MemoryReservation: memoryReservation, // Memory soft limit (in bytes) MemorySwap: memorySwap, // Total memory usage (memory + swap); set `-1` to enable unlimited swap + IPC: req.HostConfig.IpcMode, // IPC namespace to use // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index cb73c709..a845547a 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -606,6 +606,29 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set IPC and OomScoreAdj option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "IpcMode": "host", + "OomScoreAdj": 200 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.IPC = "host" + createOpt.OomScoreAdj = 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)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 464d8c20..0ee5cee1 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -78,11 +78,11 @@ type ContainerHostConfig struct { DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for ExtraHosts []string // List of extra hosts GroupAdd []string // List of additional groups that the container process will run as - // TODO: IpcMode IpcMode // IPC namespace to use for the container + IpcMode string // 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) - OomKillDisable bool //specifies whether to disable OOM Killer - // TODO: OomScoreAdj int // Container preference for OOM-killing + OomKillDisable bool // specifies whether to disable OOM Killer + OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) // TODO: PidMode PidMode // PID namespace to use for the container // TODO: Privileged bool // Is the container in privileged mode // TODO: PublishAllPorts bool // Should docker publish all exposed port for the container From 6d565bb9ed2ad0d3f745dfbefe496a5bef872776 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Mon, 9 Dec 2024 23:38:33 +0000 Subject: [PATCH 14/22] chore: add PidMode and Priviledged option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 2 ++ api/handlers/container/create_test.go | 24 ++++++++++++++++++++++++ api/types/container_types.go | 8 ++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 45443951..382a2dfb 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -172,6 +172,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { OomKillDisable: req.HostConfig.OomKillDisable, CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file OomScoreAdj: req.HostConfig.OomScoreAdj, + Pid: req.HostConfig.PidMode, // Pid namespace to use // #endregion // #region for platform flags @@ -207,6 +208,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { SecurityOpt: []string{}, // nerdctl default. CapAdd: capAdd, CapDrop: capDrop, + Privileged: req.HostConfig.Privileged, // #endregion // #region for runtime flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index a845547a..fa191c56 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -629,6 +629,29 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set PidMode and Privileged option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "PidMode": "host", + "Privileged": true + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Pid = "host" + 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) @@ -825,6 +848,7 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { 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 0ee5cee1..9a02c47d 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -81,10 +81,10 @@ type ContainerHostConfig struct { IpcMode string // 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) - OomKillDisable bool // specifies whether to disable OOM Killer - OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) - // TODO: PidMode PidMode // PID namespace to use for the container - // TODO: Privileged bool // Is the container in privileged mode + OomKillDisable bool // specifies whether to disable OOM Killer + 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 // 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. From 972465872959f443f2b1110e4c187f9e53f7418c Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 10 Dec 2024 00:04:00 +0000 Subject: [PATCH 15/22] chore: add ReadonlyRootfs and SecurityOpt option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 10 +++++++++- api/handlers/container/create_test.go | 23 +++++++++++++++++++++++ api/types/container_types.go | 12 ++++++------ 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 382a2dfb..bad9c9ab 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -154,6 +154,11 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { groupAdd = req.HostConfig.GroupAdd } + securityOpt := []string{} + if req.HostConfig.SecurityOpt != nil { + securityOpt = req.HostConfig.SecurityOpt + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -205,7 +210,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for security flags - SecurityOpt: []string{}, // nerdctl default. + SecurityOpt: securityOpt, // nerdctl default. CapAdd: capAdd, CapDrop: capDrop, Privileged: req.HostConfig.Privileged, @@ -246,6 +251,9 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { Stderr: nil, }, // #endregion + + // #region for rootfs flags + ReadOnly: req.HostConfig.ReadonlyRootfs, // Is the container root filesystem in read-only } portMappings, err := translatePortMappings(req.HostConfig.PortBindings) diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index fa191c56..eb6e4f3e 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -652,6 +652,29 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set ReadonlyRootfs and SecurityOpt option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "ReadonlyRootfs": true, + "SecurityOpt": [ "seccomp=/path/to/custom_seccomp.json", "apparmor=unconfined"] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.ReadOnly = true + createOpt.SecurityOpt = []string{"seccomp=/path/to/custom_seccomp.json", "apparmor=unconfined"} + + 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)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 9a02c47d..766d4e5c 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -81,13 +81,13 @@ type ContainerHostConfig struct { IpcMode string // 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) - OomKillDisable bool // specifies whether to disable OOM Killer - 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 + OomKillDisable bool // specifies whether to disable OOM Killer + 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 + 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. (["key=value"]) // 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 From 7019bbb9d9dd825bfd13a09cd9498107ec68e37e Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 11 Dec 2024 19:36:50 +0000 Subject: [PATCH 16/22] chore: add Tmpfs and UTSMode option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 12 ++++++++++++ api/handlers/container/create_test.go | 24 ++++++++++++++++++++++++ api/types/container_types.go | 16 ++++++++-------- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index bad9c9ab..d19a7742 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -107,6 +107,16 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } } + // Tmpfs: + // Tmpfs are passed in as a map of strings, + // but nerdctl expects an array of strings with format [TMPFS1:VALUE1, TMPFS2:VALUE2, ...]. + tmpfs := []string{} + if req.HostConfig.Tmpfs != nil { + for key, val := range req.HostConfig.Tmpfs { + tmpfs = append(tmpfs, fmt.Sprintf("%s:%s", key, val)) + } + } + // Environment vars: env := []string{} if req.Env != nil { @@ -223,6 +233,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for volume flags Volume: volumes, VolumesFrom: volumesFrom, + Tmpfs: tmpfs, // #endregion // #region for env flags @@ -282,6 +293,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { PortMappings: portMappings, AddHost: req.HostConfig.ExtraHosts, // Extra hosts. MACAddress: req.MacAddress, + UTSNamespace: req.HostConfig.UTSMode, } ctx := namespaces.WithNamespace(r.Context(), h.Config.Namespace) diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index eb6e4f3e..daff5369 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -675,6 +675,29 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set Tmpfs and UTSMode option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Tmpfs": { "/run": "rw,noexec,nosuid,size=65536k" }, + "UTSMode": "host" + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Tmpfs = []string{"/run:rw,noexec,nosuid,size=65536k"} + netOpt.UTSNamespace = "host" + + 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)) @@ -881,6 +904,7 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { // #region for volume flags Volume: nil, VolumesFrom: []string{}, // nerdctl default. + Tmpfs: []string{}, // #endregion // #region for env flags diff --git a/api/types/container_types.go b/api/types/container_types.go index 766d4e5c..9062f822 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -81,16 +81,16 @@ type ContainerHostConfig struct { IpcMode string // 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) - OomKillDisable bool // specifies whether to disable OOM Killer - 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 - 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. (["key=value"]) + OomKillDisable bool // specifies whether to disable OOM Killer + 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 + 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. (["key=value"]) + Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container + UTSMode string // UTS namespace to use for the container // TODO: PublishAllPorts bool // Should docker publish all exposed port for the container // 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 From baf89dcea3a9a5221c5a4e20332e4d7a4bf93b93 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 11 Dec 2024 22:30:31 +0000 Subject: [PATCH 17/22] chore: add ShmSize, Sysctl and Runtime option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 31 +++++++++++++++++++++++---- api/handlers/container/create_test.go | 26 ++++++++++++++++++++++ api/types/container_types.go | 6 +++--- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index d19a7742..0bbfcccb 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -117,6 +117,16 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } } + // Sysctls: + // Sysctls are passed in as a map of strings, + // but nerdctl expects an array of strings with format [Sysctls1=VALUE1, Sysctls2=VALUE2, ...]. + sysctls := []string{} + if req.HostConfig.Sysctls != nil { + for key, val := range req.HostConfig.Sysctls { + sysctls = append(sysctls, fmt.Sprintf("%s=%s", key, val)) + } + } + // Environment vars: env := []string{} if req.Env != nil { @@ -141,19 +151,29 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { memoryReservation := "" if req.HostConfig.MemoryReservation != 0 { - memoryReservation = strconv.FormatInt(req.HostConfig.MemoryReservation, 10) + memoryReservation = fmt.Sprint(req.HostConfig.MemoryReservation) } memorySwap := "" if req.HostConfig.MemorySwap != 0 { - memorySwap = strconv.FormatInt(req.HostConfig.MemorySwap, 10) + memorySwap = fmt.Sprint(req.HostConfig.MemorySwap) } memorySwappiness := int64(-1) - if req.HostConfig.MemorySwappiness != 0 && req.HostConfig.MemorySwappiness > -1 { + if req.HostConfig.MemorySwappiness > 0 { memorySwappiness = req.HostConfig.MemorySwappiness } + shmSize := "" + if req.HostConfig.ShmSize > 0 { + shmSize = fmt.Sprint(req.HostConfig.ShmSize) + } + + runtime := defaults.Runtime + if req.HostConfig.Runtime != "" { + runtime = req.HostConfig.Runtime + } + volumesFrom := []string{} if req.HostConfig.VolumesFrom != nil { volumesFrom = req.HostConfig.VolumesFrom @@ -212,6 +232,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { MemoryReservation: memoryReservation, // Memory soft limit (in bytes) MemorySwap: memorySwap, // Total memory usage (memory + swap); set `-1` to enable unlimited swap IPC: req.HostConfig.IpcMode, // IPC namespace to use + ShmSize: shmSize, // ShmSize set the size of /dev/shm // #endregion // #region for user flags @@ -227,7 +248,8 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for runtime flags - Runtime: defaults.Runtime, // nerdctl default. + Runtime: runtime, // Runtime to use for this container, e.g. "crun", or "io.containerd.runc.v2". + Sysctl: sysctls, // Sysctl set sysctl options, e.g "net.ipv4.ip_forward=1" // #endregion // #region for volume flags @@ -265,6 +287,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for rootfs flags ReadOnly: req.HostConfig.ReadonlyRootfs, // Is the container root filesystem in read-only + // #endregion } portMappings, err := translatePortMappings(req.HostConfig.PortBindings) diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index daff5369..c280abca 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -698,6 +698,31 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set ShmSize, Sysctl and Runtime option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Sysctls": { "net.ipv4.ip_forward": "1" }, + "ShmSize": 302348, + "Runtime": "crun" + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.ShmSize = "302348" + createOpt.Sysctl = []string{"net.ipv4.ip_forward=1"} + createOpt.Runtime = "crun" + + 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)) @@ -899,6 +924,7 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { // #region for runtime flags Runtime: defaults.Runtime, // nerdctl default. + Sysctl: []string{}, // #endregion // #region for volume flags diff --git a/api/types/container_types.go b/api/types/container_types.go index 9062f822..ac0d7d2d 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -89,12 +89,12 @@ type ContainerHostConfig struct { SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. (["key=value"]) Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container UTSMode string // UTS namespace to use for the container + ShmSize int64 // Size of /dev/shm in bytes. The size must be greater than 0. + Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container + Runtime string `json:",omitempty"` // Runtime to use with this container // TODO: PublishAllPorts bool // Should docker publish all exposed port for the container // TODO: StorageOpt map[string]string `json:",omitempty"` // Storage driver options per 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) From 588fb0387a6c9d7fb3ae846e1362099e426ab4c1 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 11 Dec 2024 23:48:48 +0000 Subject: [PATCH 18/22] chore: add Ulimits option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 8 ++++++++ api/handlers/container/create_test.go | 22 ++++++++++++++++++++++ api/types/container_types.go | 6 +++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 0bbfcccb..a6f8b4cf 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -127,6 +127,13 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } } + 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 { @@ -233,6 +240,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { MemorySwap: memorySwap, // Total memory usage (memory + swap); set `-1` to enable unlimited swap IPC: req.HostConfig.IpcMode, // IPC namespace to use ShmSize: shmSize, // ShmSize set the size of /dev/shm + Ulimit: ulimits, // List of ulimits to be set in the container // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index c280abca..b0c30b4f 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -723,6 +723,27 @@ var _ = Describe("Container Create API ", func() { 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 return 404 if the image was not found", func() { body := []byte(`{"Image": "test-image"}`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) @@ -908,6 +929,7 @@ 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 diff --git a/api/types/container_types.go b/api/types/container_types.go index ac0d7d2d..9312c306 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. @@ -111,7 +112,8 @@ type ContainerHostConfig struct { MemorySwappiness int64 // MemorySwappiness64 specifies the tune container memory swappiness (0 to 100) (default -1) // TODO: Resources - BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) + Ulimits []*Ulimit // List of ulimits to be set in the container + BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) // Mounts specs used by the container // TODO: Mounts []mount.Mount `json:",omitempty"` @@ -258,3 +260,5 @@ type StatsJSON struct { // Networks request version >=1.21 Networks map[string]dockertypes.NetworkStats `json:"networks,omitempty"` } + +type Ulimit = units.Ulimit From 9bdd9b80d42bb92e0d14a5cb95380febd6bb139c Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 12 Dec 2024 18:40:01 +0000 Subject: [PATCH 19/22] chore: add Device option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 25 +++++++++++++++++++++++++ api/handlers/container/create_test.go | 22 ++++++++++++++++++++++ api/types/container_types.go | 11 +++++++++-- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index a6f8b4cf..0d3f0116 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -134,6 +134,30 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } } + // devices: + // devices are passed in as a map of DeviceMapping, + // but nerdctl expects an array of strings with format [devices1:VALUE1, devices2:VALUE2, ...]. + devices := []string{} + if req.HostConfig.Devices != nil { + for _, deviceMap := range req.HostConfig.Devices { + deviceString := "" + if deviceMap.PathOnHost != "" { + deviceString += deviceMap.PathOnHost + } + + if deviceMap.PathInContainer != "" { + deviceString += ":" + deviceString += deviceMap.PathInContainer + } + + if deviceMap.CgroupPermissions != "" { + deviceString += ":" + deviceString += deviceMap.CgroupPermissions + } + devices = append(devices, deviceString) + } + } + // Environment vars: env := []string{} if req.Env != nil { @@ -241,6 +265,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { IPC: req.HostConfig.IpcMode, // IPC namespace to use ShmSize: shmSize, // ShmSize set the size of /dev/shm Ulimit: ulimits, // List of ulimits to be set in the container + Device: devices, // Device specifies add a host device to the container // #endregion // #region for user flags diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index b0c30b4f..2267b516 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -744,6 +744,27 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set Devices option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Devices": [{"PathOnHost": "/dev/null", "PathInContainer": "/dev/null", "CgroupPermissions": "rwm"},{"PathOnHost": "/var/lib", "CgroupPermissions": "ro"}] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Device = []string{"/dev/null:/dev/null:rwm", "/var/lib:ro"} + + 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)) @@ -930,6 +951,7 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. Ulimit: []string{}, + Device: []string{}, // #endregion // #region for user flags diff --git a/api/types/container_types.go b/api/types/container_types.go index 9312c306..ba63321f 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -112,8 +112,9 @@ type ContainerHostConfig struct { 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 - BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) + Ulimits []*Ulimit // List of ulimits to be set in the container + BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) + Devices []DeviceMapping // List of devices to map inside the container // Mounts specs used by the container // TODO: Mounts []mount.Mount `json:",omitempty"` @@ -262,3 +263,9 @@ type StatsJSON struct { } type Ulimit = units.Ulimit + +type DeviceMapping struct { + PathOnHost string + PathInContainer string + CgroupPermissions string +} From f07830686a99e3da51e3006b23c769ac42d63cac Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Thu, 12 Dec 2024 19:46:21 +0000 Subject: [PATCH 20/22] chore: add PidLimit option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 8 ++++++-- api/handlers/container/create_test.go | 6 ++++-- api/types/container_types.go | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 0d3f0116..3de6e8a6 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -136,7 +136,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // devices: // devices are passed in as a map of DeviceMapping, - // but nerdctl expects an array of strings with format [devices1:VALUE1, devices2:VALUE2, ...]. + // but nerdctl expects an array of strings with format [PathOnHost1:PathInContainer1:CgroupPermissions1, PathOnHost2:PathInContainer2:CgroupPermissions2, ...]. devices := []string{} if req.HostConfig.Devices != nil { for _, deviceMap := range req.HostConfig.Devices { @@ -220,6 +220,10 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { securityOpt = req.HostConfig.SecurityOpt } + pidLimit := int64(-1) + if req.HostConfig.PidsLimit > 0 { + pidLimit = req.HostConfig.PidsLimit + } globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -254,7 +258,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { Memory: memory, // memory limit (in bytes) CPUQuota: CpuQuota, // nerdctl default. MemorySwappiness64: memorySwappiness, // Tuning container memory swappiness behaviour - PidsLimit: -1, // nerdctl default. + PidsLimit: pidLimit, // PidsLimit specifies the tune container pids limit Cgroupns: defaults.CgroupnsMode(), // nerdctl default. BlkioWeight: req.HostConfig.BlkioWeight, // block IO weight (relative) CPUPeriod: uint64(req.HostConfig.CPUPeriod), // CPU CFS (Completely Fair Scheduler) period diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 2267b516..5080b68f 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -744,17 +744,19 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) - It("should set Devices option", func() { + It("should set Devices and PidLimit option", func() { body := []byte(`{ "Image": "test-image", "HostConfig": { - "Devices": [{"PathOnHost": "/dev/null", "PathInContainer": "/dev/null", "CgroupPermissions": "rwm"},{"PathOnHost": "/var/lib", "CgroupPermissions": "ro"}] + "Devices": [{"PathOnHost": "/dev/null", "PathInContainer": "/dev/null", "CgroupPermissions": "rwm"},{"PathOnHost": "/var/lib", "CgroupPermissions": "ro"}], + "PidsLimit": 200 } }`) req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) // expected create options createOpt.Device = []string{"/dev/null:/dev/null:rwm", "/var/lib:ro"} + createOpt.PidsLimit = 200 service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( cid, nil) diff --git a/api/types/container_types.go b/api/types/container_types.go index ba63321f..167b5f34 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -115,6 +115,7 @@ type ContainerHostConfig struct { Ulimits []*Ulimit // List of ulimits to be set in the container BlkioWeight uint16 // Block IO weight (relative weight vs. other containers) 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"` From cd4599b669d7f88b493cc1e201af379b24bd7efd Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Fri, 13 Dec 2024 22:53:27 +0000 Subject: [PATCH 21/22] chore: add CgroupnsMode option Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create.go | 18 ++++++++++++++- api/handlers/container/create_test.go | 21 +++++++++++++++++ api/types/container_types.go | 33 +++++++++++++++++++-------- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 3de6e8a6..3480e4d6 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -127,6 +127,16 @@ 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 { @@ -224,6 +234,12 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { if req.HostConfig.PidsLimit > 0 { pidLimit = req.HostConfig.PidsLimit } + + cgroupnsMode := defaults.CgroupnsMode() + if req.HostConfig.CgroupnsMode.Valid() { + cgroupnsMode = string(req.HostConfig.CgroupnsMode) + } + globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ Stdout: nil, @@ -259,7 +275,7 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { CPUQuota: CpuQuota, // nerdctl default. MemorySwappiness64: memorySwappiness, // Tuning container memory swappiness behaviour PidsLimit: pidLimit, // PidsLimit specifies the tune container pids limit - Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + Cgroupns: cgroupnsMode, // Cgroupns specifies the cgroup namespace to use BlkioWeight: req.HostConfig.BlkioWeight, // block IO weight (relative) CPUPeriod: uint64(req.HostConfig.CPUPeriod), // CPU CFS (Completely Fair Scheduler) period CPUSetCPUs: req.HostConfig.CPUSetCPUs, // CpusetCpus 0-2, 0,1 diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 5080b68f..8b32424d 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -767,6 +767,27 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set CgroupnsMode option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "CgroupnsMode": "host" + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Cgroupns = "host" + + 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)) diff --git a/api/types/container_types.go b/api/types/container_types.go index 167b5f34..295620f6 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -71,15 +71,15 @@ type ContainerHostConfig struct { // 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 - 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 - GroupAdd []string // List of additional groups that the container process will run as - IpcMode string // 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 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 + GroupAdd []string // List of additional groups that the container process will run as + IpcMode string // 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) OomKillDisable bool // specifies whether to disable OOM Killer @@ -270,3 +270,18 @@ type DeviceMapping struct { PathInContainer string CgroupPermissions string } + +// CgroupnsMode represents the cgroup namespace mode of the container +type CgroupnsMode string + +// cgroup namespace modes for containers +const ( + CgroupnsModeEmpty CgroupnsMode = "" + CgroupnsModePrivate CgroupnsMode = "private" + CgroupnsModeHost CgroupnsMode = "host" +) + +// Valid indicates whether the cgroup namespace mode is valid +func (c CgroupnsMode) Valid() bool { + return c == CgroupnsModePrivate || c == CgroupnsModeHost +} From 88fa8819c44ddf93450dfc962dc40e2c46b8acee Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Tue, 17 Dec 2024 03:39:46 +0000 Subject: [PATCH 22/22] chore: add e2e tests Signed-off-by: Arjun Raja Yogidas --- api/handlers/container/create_test.go | 4 +- api/types/container_types.go | 6 +- e2e/tests/container_create.go | 165 ++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 8b32424d..42d4e1f4 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -521,7 +521,7 @@ var _ = Describe("Container Create API ", func() { body := []byte(`{ "Image": "test-image", "HostConfig": { - "MemoryReservation": 209715200, + "MemoryReservation": 209710, "MemorySwap": 514288000, "MemorySwappiness": 25 } @@ -529,7 +529,7 @@ var _ = Describe("Container Create API ", func() { req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) // expected create options - createOpt.MemoryReservation = "209715200" + createOpt.MemoryReservation = "209710" createOpt.MemorySwap = "514288000" createOpt.MemorySwappiness64 = 25 service.EXPECT().Create(gomock.Any(), "test-image", nil, equalTo(createOpt), equalTo(netOpt)).Return( diff --git a/api/types/container_types.go b/api/types/container_types.go index 295620f6..5570bf79 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -271,17 +271,17 @@ type DeviceMapping struct { CgroupPermissions string } -// CgroupnsMode represents the cgroup namespace mode of the container +// CgroupnsMode represents the cgroup namespace mode of the container. type CgroupnsMode string -// cgroup namespace modes for containers +// cgroup namespace modes for containers. const ( CgroupnsModeEmpty CgroupnsMode = "" CgroupnsModePrivate CgroupnsMode = "private" CgroupnsModeHost CgroupnsMode = "host" ) -// Valid indicates whether the cgroup namespace mode is valid +// Valid indicates whether the cgroup namespace mode is valid. func (c CgroupnsMode) Valid() bool { return c == CgroupnsModePrivate || c == CgroupnsModeHost } 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"}