Add CPU limit, CPU Reservation and Memory Reservation

This adds support for CPU limit, CPU reservation as well as memory
reservation.

Specifically, when using the `deploy` key in Docker Compose.
This commit is contained in:
Charlie Drage 2017-08-03 09:41:14 -04:00
parent 3b2ef30afc
commit af26b797a2
9 changed files with 254 additions and 43 deletions

View File

@ -76,6 +76,8 @@ type ServiceConfig struct {
CPUSet string `compose:"cpuset" bundle:""` CPUSet string `compose:"cpuset" bundle:""`
CPUShares int64 `compose:"cpu_shares" bundle:""` CPUShares int64 `compose:"cpu_shares" bundle:""`
CPUQuota int64 `compose:"cpu_quota" bundle:""` CPUQuota int64 `compose:"cpu_quota" bundle:""`
CPULimit int64 `compose:"" bundle:""`
CPUReservation int64 `compose:"" bundle:""`
CapAdd []string `compose:"cap_add" bundle:""` CapAdd []string `compose:"cap_add" bundle:""`
CapDrop []string `compose:"cap_drop" bundle:""` CapDrop []string `compose:"cap_drop" bundle:""`
Expose []string `compose:"expose" bundle:""` Expose []string `compose:"expose" bundle:""`
@ -92,6 +94,7 @@ type ServiceConfig struct {
Stdin bool `compose:"stdin_open" bundle:""` Stdin bool `compose:"stdin_open" bundle:""`
Tty bool `compose:"tty" bundle:""` Tty bool `compose:"tty" bundle:""`
MemLimit yaml.MemStringorInt `compose:"mem_limit" bundle:""` MemLimit yaml.MemStringorInt `compose:"mem_limit" bundle:""`
MemReservation yaml.MemStringorInt `compose:"" bundle:""`
TmpFs []string `compose:"tmpfs" bundle:""` TmpFs []string `compose:"tmpfs" bundle:""`
Dockerfile string `compose:"dockerfile" bundle:""` Dockerfile string `compose:"dockerfile" bundle:""`
Replicas int `compose:"replicas" bundle:""` Replicas int `compose:"replicas" bundle:""`

View File

@ -17,19 +17,22 @@ limitations under the License.
package compose package compose
import ( import (
libcomposeyaml "github.com/docker/libcompose/yaml"
"io/ioutil" "io/ioutil"
"strconv"
"strings" "strings"
libcomposeyaml "github.com/docker/libcompose/yaml"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types" "github.com/docker/cli/cli/compose/types"
"os"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/kubernetes/kompose/pkg/kobject" "github.com/kubernetes/kompose/pkg/kobject"
"github.com/pkg/errors" "github.com/pkg/errors"
"os"
) )
// converts os.Environ() ([]string) to map[string]string // converts os.Environ() ([]string) to map[string]string
@ -191,25 +194,46 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose
serviceConfig.Command = composeServiceConfig.Entrypoint serviceConfig.Command = composeServiceConfig.Entrypoint
serviceConfig.Args = composeServiceConfig.Command serviceConfig.Args = composeServiceConfig.Command
//Handling replicas //
if composeServiceConfig.Deploy.Replicas != nil { // Deploy keys
serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas) //
}
// This is a bit messy since we use yaml.MemStringorInt
// TODO: Refactor yaml.MemStringorInt in kobject.go to int64
// Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
if (composeServiceConfig.Deploy.Resources != types.Resources{}) { if (composeServiceConfig.Deploy.Resources != types.Resources{}) {
serviceConfig.MemLimit = libcomposeyaml.MemStringorInt(composeServiceConfig.Deploy.Resources.Limits.MemoryBytes)
}
//Here we handle all Docker Compose Deploy keys
//Handling restart-policy // memory:
// TODO: Refactor yaml.MemStringorInt in kobject.go to int64
// Since Deploy.Resources.Limits does not initialize, we must check type Resources before continuing
serviceConfig.MemLimit = libcomposeyaml.MemStringorInt(composeServiceConfig.Deploy.Resources.Limits.MemoryBytes)
serviceConfig.MemReservation = libcomposeyaml.MemStringorInt(composeServiceConfig.Deploy.Resources.Reservations.MemoryBytes)
// cpu:
// convert to k8s format, for example: 0.5 = 500m
// See: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
// "The expression 0.1 is equivalent to the expression 100m, which can be read as “one hundred millicpu”."
cpuLimit, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Limits.NanoCPUs, 64)
if err != nil {
return kobject.KomposeObject{}, errors.Wrap(err, "Unable to convert cpu limits resources value")
}
serviceConfig.CPULimit = int64(cpuLimit * 1000)
cpuReservation, err := strconv.ParseFloat(composeServiceConfig.Deploy.Resources.Reservations.NanoCPUs, 64)
if err != nil {
return kobject.KomposeObject{}, errors.Wrap(err, "Unable to convert cpu limits reservation value")
}
serviceConfig.CPUReservation = int64(cpuReservation * 1000)
}
// restart-policy:
if composeServiceConfig.Deploy.RestartPolicy != nil { if composeServiceConfig.Deploy.RestartPolicy != nil {
serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition serviceConfig.Restart = composeServiceConfig.Deploy.RestartPolicy.Condition
} }
// POOF. volumes_From is gone in v3. docker/cli will error out of volumes_from is added in v3
// serviceConfig.VolumesFrom = composeServiceConfig.VolumesFrom // replicas:
if composeServiceConfig.Deploy.Replicas != nil {
serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas)
}
// TODO: Build is not yet supported, see: // TODO: Build is not yet supported, see:
// https://github.com/docker/cli/blob/master/cli/compose/types/types.go#L9 // https://github.com/docker/cli/blob/master/cli/compose/types/types.go#L9

View File

@ -389,14 +389,39 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic
} }
// Configure the resource limits // Configure the resource limits
if service.MemLimit != 0 { if service.MemLimit != 0 || service.CPULimit != 0 {
memoryResourceList := api.ResourceList{ resourceLimit := api.ResourceList{}
api.ResourceMemory: *resource.NewQuantity(
int64(service.MemLimit), "RandomStringForFormat")} if service.MemLimit != 0 {
template.Spec.Containers[0].Resources.Limits = memoryResourceList resourceLimit[api.ResourceMemory] = *resource.NewQuantity(int64(service.MemLimit), "RandomStringForFormat")
}
if service.CPULimit != 0 {
resourceLimit[api.ResourceCPU] = *resource.NewQuantity(service.CPULimit, "RandomStringForFormat")
}
template.Spec.Containers[0].Resources.Limits = resourceLimit
} }
// Configure the resource requests
if service.MemReservation != 0 || service.CPUReservation != 0 {
resourceRequests := api.ResourceList{}
if service.MemReservation != 0 {
resourceRequests[api.ResourceMemory] = *resource.NewQuantity(int64(service.MemReservation), "RandomStringForFormat")
}
if service.CPUReservation != 0 {
resourceRequests[api.ResourceCPU] = *resource.NewQuantity(service.CPUReservation, "RandomStringForFormat")
}
template.Spec.Containers[0].Resources.Requests = resourceRequests
}
// Configure resource reservations
podSecurityContext := &api.PodSecurityContext{} podSecurityContext := &api.PodSecurityContext{}
//set pid namespace mode //set pid namespace mode
if service.Pid != "" { if service.Pid != "" {
if service.Pid == "host" { if service.Pid == "host" {

View File

@ -78,30 +78,31 @@ func TestCreateService(t *testing.T) {
} }
/* /*
Test the creation of a service with a memory limit Test the creation of a service with a memory limit and reservation
*/ */
func TestCreateServiceWithMemLimit(t *testing.T) { func TestCreateServiceWithMemLimit(t *testing.T) {
// An example service // An example service
service := kobject.ServiceConfig{ service := kobject.ServiceConfig{
ContainerName: "name", ContainerName: "name",
Image: "image", Image: "image",
Environment: []kobject.EnvVar{kobject.EnvVar{Name: "env", Value: "value"}}, Environment: []kobject.EnvVar{kobject.EnvVar{Name: "env", Value: "value"}},
Port: []kobject.Ports{kobject.Ports{HostPort: 123, ContainerPort: 456, Protocol: api.ProtocolTCP}}, Port: []kobject.Ports{kobject.Ports{HostPort: 123, ContainerPort: 456, Protocol: api.ProtocolTCP}},
Command: []string{"cmd"}, Command: []string{"cmd"},
WorkingDir: "dir", WorkingDir: "dir",
Args: []string{"arg1", "arg2"}, Args: []string{"arg1", "arg2"},
VolList: []string{"/tmp/volume"}, VolList: []string{"/tmp/volume"},
Network: []string{"network1", "network2"}, // not supported Network: []string{"network1", "network2"}, // not supported
Labels: nil, Labels: nil,
Annotations: map[string]string{"abc": "def"}, Annotations: map[string]string{"abc": "def"},
CPUQuota: 1, // not supported CPUQuota: 1, // not supported
CapAdd: []string{"cap_add"}, // not supported CapAdd: []string{"cap_add"}, // not supported
CapDrop: []string{"cap_drop"}, // not supported CapDrop: []string{"cap_drop"}, // not supported
Expose: []string{"expose"}, // not supported Expose: []string{"expose"}, // not supported
Privileged: true, Privileged: true,
Restart: "always", Restart: "always",
MemLimit: 1337, MemLimit: 1337,
MemReservation: 1338,
} }
// An example object generated via k8s runtime.Objects() // An example object generated via k8s runtime.Objects()
@ -114,12 +115,69 @@ func TestCreateServiceWithMemLimit(t *testing.T) {
t.Error(errors.Wrap(err, "k.Transform failed")) t.Error(errors.Wrap(err, "k.Transform failed"))
} }
// Retrieve the deployment object and test that it matches the MemLimit value // Retrieve the deployment object and test that it matches the mem value
for _, obj := range objects { for _, obj := range objects {
if deploy, ok := obj.(*extensions.Deployment); ok { if deploy, ok := obj.(*extensions.Deployment); ok {
memTest, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().AsInt64() memLimit, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().AsInt64()
if memTest != 1337 { if memLimit != 1337 {
t.Errorf("Expected 1337 for mem_limit check, got %v", memTest) t.Errorf("Expected 1337 for memory limit check, got %v", memLimit)
}
memReservation, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Requests.Memory().AsInt64()
if memReservation != 1338 {
t.Errorf("Expected 1338 for memory reservation check, got %v", memReservation)
}
}
}
}
/*
Test the creation of a service with a cpu limit and reservation
*/
func TestCreateServiceWithCPULimit(t *testing.T) {
// An example service
service := kobject.ServiceConfig{
ContainerName: "name",
Image: "image",
Environment: []kobject.EnvVar{kobject.EnvVar{Name: "env", Value: "value"}},
Port: []kobject.Ports{kobject.Ports{HostPort: 123, ContainerPort: 456, Protocol: api.ProtocolTCP}},
Command: []string{"cmd"},
WorkingDir: "dir",
Args: []string{"arg1", "arg2"},
VolList: []string{"/tmp/volume"},
Network: []string{"network1", "network2"}, // not supported
Labels: nil,
Annotations: map[string]string{"abc": "def"},
CPUQuota: 1, // not supported
CapAdd: []string{"cap_add"}, // not supported
CapDrop: []string{"cap_drop"}, // not supported
Expose: []string{"expose"}, // not supported
Privileged: true,
Restart: "always",
CPULimit: 10,
CPUReservation: 1,
}
// An example object generated via k8s runtime.Objects()
komposeObject := kobject.KomposeObject{
ServiceConfigs: map[string]kobject.ServiceConfig{"app": service},
}
k := Kubernetes{}
objects, err := k.Transform(komposeObject, kobject.ConvertOptions{CreateD: true, Replicas: 3})
if err != nil {
t.Error(errors.Wrap(err, "k.Transform failed"))
}
// Retrieve the deployment object and test that it matches the cpu value
for _, obj := range objects {
if deploy, ok := obj.(*extensions.Deployment); ok {
cpuLimit, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Cpu().AsInt64()
if cpuLimit != 10 {
t.Errorf("Expected 10 for cpu limit check, got %v", cpuLimit)
}
cpuReservation, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Requests.Cpu().AsInt64()
if cpuReservation != 1 {
t.Errorf("Expected 1 for cpu reservation check, got %v", cpuReservation)
} }
} }
} }

View File

@ -270,6 +270,9 @@ cd $CURRENT_DIR
# Test V3 Support of Docker Compose # Test V3 Support of Docker Compose
# Test support for cpu and memory limits + reservations
convert::expect_success "kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/v3/docker-compose-memcpu.yaml" "$KOMPOSE_ROOT/script/test/fixtures/v3/output-memcpu-k8s.json"
# Test volumes are passed correctly # Test volumes are passed correctly
convert::expect_success "kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/v3/docker-compose-volumes.yaml" "$KOMPOSE_ROOT/script/test/fixtures/v3/output-volumes-k8s.json" convert::expect_success "kompose convert --stdout -j -f $KOMPOSE_ROOT/script/test/fixtures/v3/docker-compose-volumes.yaml" "$KOMPOSE_ROOT/script/test/fixtures/v3/output-volumes-k8s.json"

View File

@ -0,0 +1,13 @@
version: "3"
services:
foo:
deploy:
resources:
limits:
cpus: '0.01'
memory: 50M
reservations:
cpus: '0.001'
memory: 20M
image: redis

View File

@ -155,7 +155,11 @@
], ],
"resources": { "resources": {
"limits": { "limits": {
"cpu": "1",
"memory": "52428800" "memory": "52428800"
},
"requests": {
"memory": "20971520"
} }
}, },
"volumeMounts": [ "volumeMounts": [

View File

@ -0,0 +1,77 @@
{
"kind": "List",
"apiVersion": "v1",
"metadata": {},
"items": [
{
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "foo",
"creationTimestamp": null,
"labels": {
"io.kompose.service": "foo"
}
},
"spec": {
"ports": [
{
"name": "headless",
"port": 55555,
"targetPort": 0
}
],
"selector": {
"io.kompose.service": "foo"
},
"clusterIP": "None"
},
"status": {
"loadBalancer": {}
}
},
{
"kind": "Deployment",
"apiVersion": "extensions/v1beta1",
"metadata": {
"name": "foo",
"creationTimestamp": null,
"labels": {
"io.kompose.service": "foo"
}
},
"spec": {
"replicas": 1,
"template": {
"metadata": {
"creationTimestamp": null,
"labels": {
"io.kompose.service": "foo"
}
},
"spec": {
"containers": [
{
"name": "foo",
"image": "redis",
"resources": {
"limits": {
"cpu": "10",
"memory": "52428800"
},
"requests": {
"cpu": "1",
"memory": "20971520"
}
}
}
],
"restartPolicy": "Always"
}
},
"strategy": {}
},
"status": {}
}
]
}

View File

@ -155,7 +155,11 @@
], ],
"resources": { "resources": {
"limits": { "limits": {
"cpu": "1",
"memory": "52428800" "memory": "52428800"
},
"requests": {
"memory": "20971520"
} }
}, },
"volumeMounts": [ "volumeMounts": [