Skip to content

Commit

Permalink
feat: add container create options
Browse files Browse the repository at this point in the history
Signed-off-by: Cezar Rata <[email protected]>
  • Loading branch information
cezar-r committed Sep 13, 2024
1 parent 26d1c7d commit e7bb22f
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 21 deletions.
46 changes: 36 additions & 10 deletions api/handlers/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
}

Expand Down
85 changes: 84 additions & 1 deletion api/handlers/container/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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"
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down
33 changes: 24 additions & 9 deletions api/types/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
64 changes: 63 additions & 1 deletion e2e/tests/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
}

Expand Down

0 comments on commit e7bb22f

Please sign in to comment.