diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index d9036d450..61c631471 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -76,6 +76,8 @@ type ServiceConfig struct { CPUSet string `compose:"cpuset" bundle:""` CPUShares int64 `compose:"cpu_shares" bundle:""` CPUQuota int64 `compose:"cpu_quota" bundle:""` + CPULimit int64 `compose:"" bundle:""` + CPUReservation int64 `compose:"" bundle:""` CapAdd []string `compose:"cap_add" bundle:""` CapDrop []string `compose:"cap_drop" bundle:""` Expose []string `compose:"expose" bundle:""` @@ -92,6 +94,7 @@ type ServiceConfig struct { Stdin bool `compose:"stdin_open" bundle:""` Tty bool `compose:"tty" bundle:""` MemLimit yaml.MemStringorInt `compose:"mem_limit" bundle:""` + MemReservation yaml.MemStringorInt `compose:"" bundle:""` TmpFs []string `compose:"tmpfs" bundle:""` Dockerfile string `compose:"dockerfile" bundle:""` Replicas int `compose:"replicas" bundle:""` diff --git a/pkg/loader/compose/v3.go b/pkg/loader/compose/v3.go index 4b2a9a9fb..7b5278572 100644 --- a/pkg/loader/compose/v3.go +++ b/pkg/loader/compose/v3.go @@ -17,19 +17,22 @@ limitations under the License. package compose import ( - libcomposeyaml "github.com/docker/libcompose/yaml" "io/ioutil" + "strconv" "strings" + libcomposeyaml "github.com/docker/libcompose/yaml" + "k8s.io/kubernetes/pkg/api" "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/types" + "os" + log "github.com/Sirupsen/logrus" "github.com/kubernetes/kompose/pkg/kobject" "github.com/pkg/errors" - "os" ) // converts os.Environ() ([]string) to map[string]string @@ -191,25 +194,46 @@ func dockerComposeToKomposeMapping(composeObject *types.Config) (kobject.Kompose serviceConfig.Command = composeServiceConfig.Entrypoint serviceConfig.Args = composeServiceConfig.Command - //Handling replicas - if composeServiceConfig.Deploy.Replicas != nil { - serviceConfig.Replicas = int(*composeServiceConfig.Deploy.Replicas) - } + // + // Deploy keys + // - // 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{}) { + + // 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) + } - //Here we handle all Docker Compose Deploy keys - //Handling restart-policy + // restart-policy: if composeServiceConfig.Deploy.RestartPolicy != nil { 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: // https://github.com/docker/cli/blob/master/cli/compose/types/types.go#L9 diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index ee62a8b51..2ef5d8893 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -389,14 +389,39 @@ func (k *Kubernetes) UpdateKubernetesObjects(name string, service kobject.Servic } // Configure the resource limits - if service.MemLimit != 0 { - memoryResourceList := api.ResourceList{ - api.ResourceMemory: *resource.NewQuantity( - int64(service.MemLimit), "RandomStringForFormat")} - template.Spec.Containers[0].Resources.Limits = memoryResourceList + if service.MemLimit != 0 || service.CPULimit != 0 { + resourceLimit := api.ResourceList{} + + if service.MemLimit != 0 { + 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{} + //set pid namespace mode if service.Pid != "" { if service.Pid == "host" { diff --git a/pkg/transformer/kubernetes/k8sutils_test.go b/pkg/transformer/kubernetes/k8sutils_test.go index a756e5f6d..e586cfca4 100644 --- a/pkg/transformer/kubernetes/k8sutils_test.go +++ b/pkg/transformer/kubernetes/k8sutils_test.go @@ -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) { // 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", - MemLimit: 1337, + 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", + MemLimit: 1337, + MemReservation: 1338, } // 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")) } - // 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 { if deploy, ok := obj.(*extensions.Deployment); ok { - memTest, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().AsInt64() - if memTest != 1337 { - t.Errorf("Expected 1337 for mem_limit check, got %v", memTest) + memLimit, _ := deploy.Spec.Template.Spec.Containers[0].Resources.Limits.Memory().AsInt64() + if memLimit != 1337 { + 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) } } } diff --git a/script/test/cmd/tests.sh b/script/test/cmd/tests.sh index c68ba935b..8ca4efa0d 100755 --- a/script/test/cmd/tests.sh +++ b/script/test/cmd/tests.sh @@ -270,6 +270,9 @@ cd $CURRENT_DIR # 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 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" diff --git a/script/test/fixtures/v3/docker-compose-memcpu.yaml b/script/test/fixtures/v3/docker-compose-memcpu.yaml new file mode 100644 index 000000000..98632c2be --- /dev/null +++ b/script/test/fixtures/v3/docker-compose-memcpu.yaml @@ -0,0 +1,13 @@ +version: "3" + +services: + foo: + deploy: + resources: + limits: + cpus: '0.01' + memory: 50M + reservations: + cpus: '0.001' + memory: 20M + image: redis diff --git a/script/test/fixtures/v3/output-k8s-full-example.json b/script/test/fixtures/v3/output-k8s-full-example.json index ae16ef792..9e1f6f0ed 100644 --- a/script/test/fixtures/v3/output-k8s-full-example.json +++ b/script/test/fixtures/v3/output-k8s-full-example.json @@ -155,7 +155,11 @@ ], "resources": { "limits": { + "cpu": "1", "memory": "52428800" + }, + "requests": { + "memory": "20971520" } }, "volumeMounts": [ diff --git a/script/test/fixtures/v3/output-memcpu-k8s.json b/script/test/fixtures/v3/output-memcpu-k8s.json new file mode 100644 index 000000000..f09f15a28 --- /dev/null +++ b/script/test/fixtures/v3/output-memcpu-k8s.json @@ -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": {} + } + ] +} diff --git a/script/test/fixtures/v3/output-os-full-example.json b/script/test/fixtures/v3/output-os-full-example.json index ae16ef792..9e1f6f0ed 100644 --- a/script/test/fixtures/v3/output-os-full-example.json +++ b/script/test/fixtures/v3/output-os-full-example.json @@ -155,7 +155,11 @@ ], "resources": { "limits": { + "cpu": "1", "memory": "52428800" + }, + "requests": { + "memory": "20971520" } }, "volumeMounts": [