diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index 3820e78b..5948279e 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -107,15 +107,14 @@ 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)) - // } - // } + 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 { @@ -123,6 +122,50 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { ulimits = append(ulimits, ulimit.String()) } } + // 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)) + } + } + + // 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)) + } + } + + // devices: + // devices are passed in as a map of DeviceMapping, + // 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 { + 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 { @@ -164,6 +207,40 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { if req.HostConfig.CPUQuota != 0 { CpuQuota = req.HostConfig.CPUQuota } + 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 + } + + groupAdd := []string{} + if req.HostConfig.GroupAdd != nil { + groupAdd = req.HostConfig.GroupAdd + } + + securityOpt := []string{} + if req.HostConfig.SecurityOpt != nil { + securityOpt = req.HostConfig.SecurityOpt + } + + cgroupnsMode := defaults.CgroupnsMode() + if req.HostConfig.CgroupnsMode.Valid() { + cgroupnsMode = string(req.HostConfig.CgroupnsMode) + } + + var oomScoreAdjChanged bool + if req.HostConfig.OomScoreAdj != 0 || req.HostConfig.OomScoreAdjChanged { + oomScoreAdjChanged = req.HostConfig.OomScoreAdjChanged + } globalOpt := ncTypes.GlobalCommandOptions(*h.Config) createOpt := ncTypes.ContainerCreateOptions{ @@ -172,15 +249,19 @@ 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, - CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file + 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, + CidFile: req.HostConfig.ContainerIDFile, // CidFile write the container ID to the file + OomKillDisable: req.HostConfig.OomKillDisable, + OomScoreAdj: req.HostConfig.OomScoreAdj, + OomScoreAdjChanged: oomScoreAdjChanged, + Pid: req.HostConfig.PidMode, // Pid namespace to use // #endregion // #region for platform flags @@ -197,29 +278,39 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { 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. + Cgroupns: cgroupnsMode, // Cgroupns specifies the cgroup namespace to use 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), + 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 + CPUSetMems: req.HostConfig.CPUSetMems, // CpusetMems 0-2, 0,1 + IPC: req.HostConfig.IpcMode, // IPC namespace to use + ShmSize: shmSize, // ShmSize set the size of /dev/shm + Device: devices, // Device specifies add a host device to the container // #endregion // #region for user flags - User: req.User, + User: req.User, + GroupAdd: groupAdd, // #endregion // #region for security flags - SecurityOpt: []string{}, // nerdctl default. + SecurityOpt: securityOpt, // nerdctl default. CapAdd: capAdd, CapDrop: capDrop, Privileged: req.HostConfig.Privileged, // #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 - Volume: volumes, + Volume: volumes, + VolumesFrom: volumesFrom, + Tmpfs: tmpfs, // #endregion // #region for env flags @@ -230,8 +321,9 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for metadata flags - Name: name, // container name - Label: labels, // container labels + Name: name, // container name + Label: labels, // container labels + Annotations: annotations, // #endregion // #region for logging flags @@ -248,6 +340,10 @@ 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 + // #endregion } portMappings, err := translatePortMappings(req.HostConfig.PortBindings) @@ -260,18 +356,24 @@ 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 + Domainname: req.Domainname, + NetworkSlice: []string{networkMode}, DNSServers: req.HostConfig.DNS, // Custom DNS lookup servers. DNSResolvConfOptions: dnsOpt, // DNS options. DNSSearchDomains: req.HostConfig.DNSSearch, // Custom DNS search domains. 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 78320bb0..0b1ff4c6 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -302,6 +302,7 @@ var _ = Describe("Container Create API ", func() { body := []byte(`{ "Image": "test-image", "Hostname": "test-host", + "Domainname": "", "HostConfig": { "DNS": ["8.8.8.8"], "DNSOptions": ["test-opt"], @@ -313,6 +314,38 @@ var _ = Describe("Container Create API ", func() { // expected network options netOpt.Hostname = "test-host" + netOpt.Domainname = "" + netOpt.DNSServers = []string{"8.8.8.8"} + netOpt.DNSResolvConfOptions = []string{"test-opt"} + netOpt.DNSSearchDomains = []string{"test.com"} + netOpt.AddHost = []string{"test-host:127.0.0.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 specified domainname", func() { + body := []byte(`{ + "Image": "test-image", + "Hostname": "test-host", + "Domainname": "test.com", + "HostConfig": { + "DNS": ["8.8.8.8"], + "DNSOptions": ["test-opt"], + "DNSSearch": ["test.com"], + "ExtraHosts": ["test-host:127.0.0.1"] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + netOpt.Hostname = "test-host" + netOpt.Domainname = "test.com" netOpt.DNSServers = []string{"8.8.8.8"} netOpt.DNSResolvConfOptions = []string{"test-opt"} netOpt.DNSSearchDomains = []string{"test.com"} @@ -357,6 +390,43 @@ 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 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 set CPUPeriod create options for resources", func() { body := []byte(`{ "Image": "test-image", @@ -378,6 +448,26 @@ var _ = Describe("Container Create API ", func() { 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 CpuQuota create options for resources", func() { body := []byte(`{ "Image": "test-image", @@ -523,6 +613,379 @@ 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 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 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": 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 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 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 set CapDrop and GroupAdd option", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "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) + + // 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 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 + createOpt.OomScoreAdjChanged = 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 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 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 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 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 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 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"}], + "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) + + // 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 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)) @@ -709,6 +1172,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 @@ -720,14 +1184,18 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { CapAdd: []string{}, // nerdctl default. CapDrop: []string{}, // nerdctl default. Privileged: false, + GroupAdd: []string{}, // nerdctl default. // #endregion // #region for runtime flags Runtime: defaults.Runtime, // nerdctl default. + Sysctl: []string{}, // #endregion // #region for volume flags - Volume: nil, + 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 346e8b84..65f36e48 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -28,8 +28,8 @@ type AttachOptions struct { // ContainerConfig is from https://github.com/moby/moby/blob/v24.0.2/api/types/container/config.go#L64-L96 type ContainerConfig struct { - Hostname string `json:",omitempty"` // Hostname - // TODO: Domainname string // Domainname + Hostname string `json:",omitempty"` // Hostname + Domainname string `json:",omitempty"` // Domainname User string `json:",omitempty"` // User that will run the command(s) inside the container, also support user:group AttachStdin bool // Attach the standard input, makes possible user interaction // TODO: AttachStdout bool // Attach the standard output @@ -42,12 +42,12 @@ 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 - // TODO: MacAddress string `json:",omitempty"` // Mac Address of the container + 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 + 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 @@ -65,34 +65,35 @@ 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 + 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 - // TODO: GroupAdd []string // List of additional groups that the container process will run as - // TODO: IpcMode IpcMode // IPC namespace to use for the container + CapAdd []string // List of kernel capabilities to add to the container + CapDrop []string // List of kernel capabilities to remove from the container + CgroupnsMode 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) - // 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 + OomKillDisable bool // specifies whether to disable OOM Killer + OomScoreAdj int // specifies the tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000) + OomScoreAdjChanged bool // OomScoreAdjChanged specifies whether the OOM preferences has been changed + 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 + 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 @@ -101,21 +102,21 @@ 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 - // 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) + 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 - 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. + 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"` @@ -264,3 +265,24 @@ type StatsJSON struct { } type Ulimit = units.Ulimit + +type DeviceMapping struct { + PathOnHost string + 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 +}