diff --git a/api/handlers/container/create.go b/api/handlers/container/create.go index de87e7e..c59ae84 100644 --- a/api/handlers/container/create.go +++ b/api/handlers/container/create.go @@ -20,6 +20,7 @@ import ( "github.com/runfinch/finch-daemon/api/response" "github.com/runfinch/finch-daemon/api/types" "github.com/runfinch/finch-daemon/pkg/errdefs" + "github.com/runfinch/finch-daemon/pkg/utility/maputility" ) type containerCreateResponse struct { @@ -44,6 +45,14 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { } // defaults + rp := req.HostConfig.RestartPolicy + restart := "no" // Docker API default. + if rp.Name != "" { + restart = rp.Name + if rp.MaximumRetryCount > 0 { + restart = fmt.Sprintf("%s:%d", restart, rp.MaximumRetryCount) + } + } stopSignal := "SIGTERM" // nerdctl default. if req.StopSignal != "" { stopSignal = req.StopSignal @@ -56,6 +65,15 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { if req.HostConfig.Memory > 0 { memory = fmt.Sprint(req.HostConfig.Memory) } + lc := req.HostConfig.LogConfig + logDriver := "json-file" // Docker API default + if lc.Type != "" { + logDriver = lc.Type + } + logOpt := []string{} + if len(lc.Config) > 0 { + logOpt = maputility.Flatten(lc.Config, maputility.KeyEqualsValueFormat) + } // Volumes: // nerdctl expects volumes to be a list of bind mounts or individual user created volumes. @@ -111,8 +129,8 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { 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: "no", // Docker API default. - Rm: req.HostConfig.AutoRemove, // Automatically remove container upon exit + 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, @@ -127,11 +145,12 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for resource flags - Memory: memory, // memory limit (in bytes) - CPUQuota: -1, // nerdctl default. - MemorySwappiness64: -1, // nerdctl default. - PidsLimit: -1, // nerdctl default. - Cgroupns: defaults.CgroupnsMode(), // nerdctl default. + 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. // #endregion // #region for user flags @@ -166,7 +185,8 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { // #endregion // #region for logging flags - LogDriver: "json-file", // nerdctl default. + LogDriver: logDriver, // logging driver for the container + LogOpt: logOpt, // logging driver specific options // #endregion // #region for image pull and verify options @@ -190,10 +210,16 @@ func (h *handler) create(w http.ResponseWriter, r *http.Request) { if networkMode == "" || networkMode == "default" { networkMode = "bridge" } + 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 - DNSResolvConfOptions: []string{}, // nerdctl default. + NetworkSlice: []string{networkMode}, // TODO: Set to none if "NetworkDisabled" is true in request + DNSServers: req.HostConfig.DNS, // Custom DNS lookup servers. + DNSResolvConfOptions: dnsOpt, // DNS options. + DNSSearchDomains: req.HostConfig.DNSSearch, // Custom DNS search domains. PortMappings: portMappings, } diff --git a/api/handlers/container/create_test.go b/api/handlers/container/create_test.go index 5a09f5a..4a90600 100644 --- a/api/handlers/container/create_test.go +++ b/api/handlers/container/create_test.go @@ -212,7 +212,11 @@ var _ = Describe("Container Create API ", func() { "Image": "test-image", "HostConfig": { "AutoRemove": true, - "Memory": 209715200 + "Memory": 209715200, + "RestartPolicy": { + "Name": "on-failure", + "MaximumRetryCount": 0 + } }, "User": "test-user", "Env": ["VARIABLE1=1", "VAR2=var2"], @@ -225,6 +229,7 @@ var _ = Describe("Container Create API ", func() { // expected create options createOpt.Rm = true + createOpt.Restart = "on-failure" createOpt.User = "test-user" createOpt.Env = []string{"VARIABLE1=1", "VAR2=var2"} createOpt.Workdir = "/test-dir" @@ -243,6 +248,83 @@ var _ = Describe("Container Create API ", func() { Expect(rr.Body).Should(MatchJSON(jsonResponse)) }) + It("should set specified create options for resources", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "Memory": 209715200, + "CPUShares": 1 + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.Memory = "209715200" + createOpt.CPUShares = 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 create options for logging", func() { + body := []byte(`{ + "Image": "test-image", + "HostConfig": { + "LogConfig": { + "Type": "json-file", + "Config": { + "key": "value" + } + } + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected create options + createOpt.LogDriver = "json-file" + createOpt.LogOpt = []string{"key=value"} + + 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 network options", func() { + body := []byte(`{ + "Image": "test-image", + "Hostname": "test-host", + "HostConfig": { + "DNS": ["8.8.8.8"], + "DNSOptions": ["test-opt"], + "DNSSearch": ["test.com"] + } + }`) + req, _ := http.NewRequest(http.MethodPost, "/containers/create", bytes.NewReader(body)) + + // expected network options + netOpt.Hostname = "test-host" + netOpt.DNSServers = []string{"8.8.8.8"} + netOpt.DNSResolvConfOptions = []string{"test-opt"} + netOpt.DNSSearchDomains = []string{"test.com"} + + 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 volume mounts", func() { body := []byte(`{ "Image": "test-image", @@ -493,6 +575,7 @@ func getDefaultCreateOpt(conf config.Config) types.ContainerCreateOptions { // #region for logging flags LogDriver: "json-file", // nerdctl default. + LogOpt: []string{}, // #endregion // #region for image pull and verify types diff --git a/api/types/container_types.go b/api/types/container_types.go index 4c8beb7..7090117 100644 --- a/api/types/container_types.go +++ b/api/types/container_types.go @@ -59,11 +59,11 @@ 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 - // TODO: 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 - // TODO: RestartPolicy RestartPolicy // Restart policy to be used for the container - AutoRemove bool // Automatically remove container when it exits + 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) @@ -73,9 +73,9 @@ type ContainerHostConfig struct { 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 - // TODO: DNS []string `json:"Dns"` // List of DNS server to lookup - // TODO: DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for - // TODO: DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for + 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 // TODO: 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 @@ -99,7 +99,8 @@ type ContainerHostConfig struct { // TODO: Isolation Isolation // Isolation technology of the container (e.g. default, hyperv) // Contains container's resources (cgroups, ulimits) - Memory int64 // Memory limit (in bytes) + CPUShares int64 `json:"CpuShares"` // CPU shares (relative weight vs. other containers) + Memory int64 // Memory limit (in bytes) // TODO: Resources // Mounts specs used by the container @@ -115,6 +116,20 @@ type ContainerHostConfig struct { // TODO: Init *bool `json:",omitempty"` } +// LogConfig represents the logging configuration of the container. +// From https://github.com/moby/moby/blob/v24.0.2/api/types/container/hostconfig.go#L319-L323 +type LogConfig struct { + Type string + Config map[string]string +} + +// RestartPolicy represents the restart policies of the container. +// From https://github.com/moby/moby/blob/v24.0.2/api/types/container/hostconfig.go#L272-L276 +type RestartPolicy struct { + Name string + MaximumRetryCount int +} + type ContainerCreateRequest struct { ContainerConfig HostConfig ContainerHostConfig diff --git a/e2e/tests/container_create.go b/e2e/tests/container_create.go index dac7992..2e5afa3 100644 --- a/e2e/tests/container_create.go +++ b/e2e/tests/container_create.go @@ -382,9 +382,71 @@ func ContainerCreate(opt *option.Option) { Expect(ctr.ID).ShouldNot(BeEmpty()) // start container and verify current directory - out := command.StdoutStr(opt, "start", "-a", testContainerName) + out := command.StdoutStr(opt, "start", testContainerName) Expect(out).Should(Equal(workdir)) }) + It("should create a container with specified memory allocation", func() { + // define options + options.HostConfig.Memory = 209715200 // 200 MiB + options.Cmd = []string{"sleep", "Infinity"} + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + // start container + command.Run(opt, "start", testContainerName) + + // verify memory allocation from stats command + resp := command.StdoutStr(opt, "stats", "--no-stream", "--format", "'{{ json .}}'", testContainerName) + var stats map[string]string + err := json.Unmarshal([]byte(strings.Trim(resp, "'")), &stats) + Expect(err).Should(BeNil()) + Expect(stats).Should(HaveKey("MemUsage")) + + memAloc := strings.Split(stats["MemUsage"], " / ")[1] + Expect(memAloc).Should(Equal("200MiB")) + }) + It("should create a container with specified logging options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.LogConfig = types.LogConfig{ + Type: "json-file", + Config: map[string]string{"key": "value"}, + } + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + // inspect container + resp := command.Stdout(opt, "inspect", testContainerName) + var inspect []*dockercompat.Container + err := json.Unmarshal(resp, &inspect) + Expect(err).Should(BeNil()) + Expect(inspect).Should(HaveLen(1)) + + // check options + Expect(inspect[0].LogPath).ShouldNot(BeNil()) + }) + It("should create a container with specified network options", func() { + // define options + options.Cmd = []string{"sleep", "Infinity"} + options.HostConfig.DNS = []string{"8.8.8.8"} + options.HostConfig.DNSOptions = []string{"test-opt"} + options.HostConfig.DNSSearch = []string{"test.com"} + + // create container + statusCode, ctr := createContainer(uClient, url, testContainerName, options) + Expect(statusCode).Should(Equal(http.StatusCreated)) + Expect(ctr.ID).ShouldNot(BeEmpty()) + + // start a container and verify network settings + command.Run(opt, "start", testContainerName) + verifyNetworkSettings(opt, testContainerName, "bridge") + }) }) }