diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index b4221735..3480e4d6 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -107,6 +107,67 @@ 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)) + } + } + + // 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)) + } + } + + // Annotations: TODO - available in nerdctl 2.0 + // Annotations are passed in as a map of strings, + // but nerdctl expects an array of strings with format [annotations1=VALUE1, annotations2=VALUE2, ...]. + // annotations := []string{} + // if req.HostConfig.Annotations != nil { + // for key, val := range req.HostConfig.Annotations { + // annotations = append(annotations, fmt.Sprintf("%s=%s", key, val)) + // } + // } + + ulimits := []string{} + if req.HostConfig.Ulimits != nil { + for _, ulimit := range req.HostConfig.Ulimits { + ulimits = append(ulimits, ulimit.String()) + } + } + + // 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 { @@ -119,6 +180,66 @@ 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 + } + + memoryReservation := "" + if req.HostConfig.MemoryReservation != 0 { + memoryReservation = fmt.Sprint(req.HostConfig.MemoryReservation) + } + + memorySwap := "" + if req.HostConfig.MemorySwap != 0 { + memorySwap = fmt.Sprint(req.HostConfig.MemorySwap) + } + + memorySwappiness := int64(-1) + if req.HostConfig.MemorySwappiness > 0 { + memorySwappiness = req.HostConfig.MemorySwappiness + } + + 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 + } + + pidLimit := int64(-1) + 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, @@ -126,14 +247,18 @@ 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, + 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 @@ -147,29 +272,43 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #region for resource flags CPUShares: uint64(req.HostConfig.CPUShares), // CPU shares (relative weight) Memory: memory, // memory limit (in bytes) - CPUQuota: -1, // nerdctl default. - MemorySwappiness64: -1, // nerdctl default. - PidsLimit: -1, // nerdctl default. - Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + CPUQuota: CpuQuota, // nerdctl default. + MemorySwappiness64: memorySwappiness, // Tuning container memory swappiness behaviour + PidsLimit: pidLimit, // PidsLimit specifies the tune container pids limit + 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 + 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 + 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 User: req.User, - GroupAdd: []string{}, // nerdctl default. + GroupAdd: groupAdd, // #endregion // #region for security flags - SecurityOpt: []string{}, // nerdctl default. + SecurityOpt: securityOpt, // nerdctl default. CapAdd: capAdd, - CapDrop: []string{}, // nerdctl default. + 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 @@ -198,6 +337,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) @@ -210,18 +353,23 @@ 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. 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 9c5475cb..42d4e1f4 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -357,6 +357,437 @@ 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 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 + + 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)) @@ -542,6 +973,8 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { MemorySwappiness64: -1, // nerdctl default. PidsLimit: -1, // nerdctl default. Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + Ulimit: []string{}, + Device: []string{}, // #endregion // #region for user flags @@ -553,14 +986,18 @@ 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 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 800431ef..5570bf79 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. @@ -41,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 @@ -57,52 +58,64 @@ 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 + 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 // Applicable to UNIX platforms - CapAdd []string // List of kernel capabilities to add to the container - // TODO: CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container - // TODO: CgroupnsMode CgroupnsMode // Cgroup namespace mode to use for the container - 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: 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 + 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 + 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: ReadonlyRootfs bool // Is the container root filesystem in read-only - // TODO: SecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux. // TODO: StorageOpt map[string]string `json:",omitempty"` // Storage driver options per container. - // TODO: Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the container - // TODO: UTSMode UTSMode // UTS namespace to use for the container // TODO: UsernsMode UsernsMode // The user namespace to use for the container - // TODO: ShmSize int64 // Total shm memory usage - // TODO: Sysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the container - // TODO: Runtime string `json:",omitempty"` // Runtime to use with this container // Applicable to Windows // TODO: Isolation Isolation // Isolation technology of the container (e.g. default, hyperv) // Contains container's resources (cgroups, ulimits) - CPUShares int64 `json:"CpuShares"` // CPU shares (relative weight vs. other containers) - Memory int64 // Memory limit (in bytes) + 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 + 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"` @@ -249,3 +262,26 @@ type StatsJSON struct { // Networks request version >=1.21 Networks map[string]dockertypes.NetworkStats `json:"networks,omitempty"` } + +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 +} 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"}