From 5e36f63f8b909618281e0301d7eff7280e4556d7 Mon Sep 17 00:00:00 2001 From: Vishnu Kannan Date: Sun, 25 Jan 2015 04:19:36 +0000 Subject: [PATCH] Adding ResourceRequirementSpec to v1beta1, v1beta2, and v1beta3 APIs. The old resource quantities 'CPU' and 'Memory' will be preserved until support for v1beta1 and v1beta2 APIs are dropped. Improved resource validation in the process. --- pkg/api/resource_helpers.go | 42 ++++++ pkg/api/resource_helpers_test.go | 53 +++++++ pkg/api/types.go | 20 +-- pkg/api/v1beta1/conversion.go | 135 ++++++++++++++++- pkg/api/v1beta1/conversion_test.go | 66 +++++++++ pkg/api/v1beta1/types.go | 16 ++- pkg/api/v1beta2/conversion.go | 136 ++++++++++++++++++ pkg/api/v1beta2/conversion_test.go | 66 +++++++++ pkg/api/v1beta2/types.go | 16 ++- pkg/api/v1beta3/types.go | 25 ++-- pkg/api/validation/validation.go | 53 ++++--- pkg/api/validation/validation_test.go | 67 ++++++++- pkg/kubelet/kubelet.go | 4 +- .../resource_quota_controller.go | 4 +- pkg/scheduler/predicates.go | 9 +- pkg/scheduler/predicates_test.go | 12 +- pkg/scheduler/priorities.go | 12 +- pkg/scheduler/priorities_test.go | 38 ++++- plugin/pkg/admission/limitranger/admission.go | 8 +- .../admission/limitranger/admission_test.go | 123 +++++++--------- .../admission/resourcedefaults/admission.go | 9 +- .../resourcedefaults/admission_test.go | 10 +- .../admission/resourcequota/admission_test.go | 33 +++-- 23 files changed, 782 insertions(+), 175 deletions(-) create mode 100644 pkg/api/resource_helpers.go create mode 100644 pkg/api/resource_helpers_test.go diff --git a/pkg/api/resource_helpers.go b/pkg/api/resource_helpers.go new file mode 100644 index 00000000000..e487d895459 --- /dev/null +++ b/pkg/api/resource_helpers.go @@ -0,0 +1,42 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +// Returns string version of ResourceName. +func (self ResourceName) String() string { + return string(self) +} + +// Returns the CPU limit if specified. +func (self *ResourceList) Cpu() *resource.Quantity { + if val, ok := (*self)[ResourceCPU]; ok { + return &val + } + return &resource.Quantity{} +} + +// Returns the Memory limit if specified. +func (self *ResourceList) Memory() *resource.Quantity { + if val, ok := (*self)[ResourceMemory]; ok { + return &val + } + return &resource.Quantity{} +} diff --git a/pkg/api/resource_helpers_test.go b/pkg/api/resource_helpers_test.go new file mode 100644 index 00000000000..2f59b73f970 --- /dev/null +++ b/pkg/api/resource_helpers_test.go @@ -0,0 +1,53 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func TestResourceHelpers(t *testing.T) { + cpuLimit := resource.MustParse("10") + memoryLimit := resource.MustParse("10G") + resourceSpec := ResourceRequirementSpec{ + Limits: ResourceList{ + "cpu": cpuLimit, + "memory": memoryLimit, + "kube.io/storage": memoryLimit, + }, + } + if res := resourceSpec.Limits.Cpu(); *res != cpuLimit { + t.Errorf("expected cpulimit %d, got %d", cpuLimit, res) + } + if res := resourceSpec.Limits.Memory(); *res != memoryLimit { + t.Errorf("expected memorylimit %d, got %d", memoryLimit, res) + } + resourceSpec = ResourceRequirementSpec{ + Limits: ResourceList{ + "memory": memoryLimit, + "kube.io/storage": memoryLimit, + }, + } + if res := resourceSpec.Limits.Cpu(); res.Value() != 0 { + t.Errorf("expected cpulimit %d, got %d", 0, res) + } + if res := resourceSpec.Limits.Memory(); *res != memoryLimit { + t.Errorf("expected memorylimit %d, got %d", memoryLimit, res) + } +} diff --git a/pkg/api/types.go b/pkg/api/types.go index a9179a16b23..3d72b98ed9e 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -304,6 +304,12 @@ type Capabilities struct { Drop []CapabilityType `json:"drop,omitempty"` } +// ResourceRequirementSpec describes the compute resource requirements. +type ResourceRequirementSpec struct { + // Limits describes the maximum amount of compute resources required. + Limits ResourceList `json:"limits,omitempty"` +} + // Container represents a single container that is expected to be run on the host. type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must @@ -317,13 +323,11 @@ type Container struct { WorkingDir string `json:"workingDir,omitempty"` Ports []Port `json:"ports,omitempty"` Env []EnvVar `json:"env,omitempty"` - // Optional: Defaults to unlimited. - Memory resource.Quantity `json:"memory,omitempty"` - // Optional: Defaults to unlimited. - CPU resource.Quantity `json:"cpu,omitempty"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` - LivenessProbe *Probe `json:"livenessProbe,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty"` + // Compute resource requirements. + Resources ResourceRequirementSpec `json:"resources,omitempty"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` + LivenessProbe *Probe `json:"livenessProbe,omitempty"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty"` // Optional: Default to false. @@ -775,8 +779,6 @@ type NodeResources struct { type ResourceName string const ( - // The default compute resource namespace for all standard resource types. - DefaultResourceNamespace = "kubernetes.io" // CPU, in cores. (500m = .5 cores) ResourceCPU ResourceName = "cpu" // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index a205d054fec..34432b08c3b 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -444,7 +444,140 @@ func init() { } return nil }, - + // Converts internal Container to v1beta1.Container. + // Fields 'CPU' and 'Memory' are not present in the internal Container object. + // Hence the need for a custom conversion function. + func(in *newer.Container, out *Container, s conversion.Scope) error { + if err := s.Convert(&in.Name, &out.Name, 0); err != nil { + return err + } + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + if err := s.Convert(&in.Command, &out.Command, 0); err != nil { + return err + } + if err := s.Convert(&in.WorkingDir, &out.WorkingDir, 0); err != nil { + return err + } + if err := s.Convert(&in.Ports, &out.Ports, 0); err != nil { + return err + } + if err := s.Convert(&in.Env, &out.Env, 0); err != nil { + return err + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if err := s.Convert(in.Resources.Limits.Cpu(), &out.CPU, 0); err != nil { + return err + } + if err := s.Convert(in.Resources.Limits.Memory(), &out.Memory, 0); err != nil { + return err + } + if err := s.Convert(&in.VolumeMounts, &out.VolumeMounts, 0); err != nil { + return err + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { + return err + } + if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { + return err + } + if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { + return err + } + if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + return err + } + return nil + }, + // Internal API does not support CPU to be specified via an explicit field. + // Hence it must be stored in Container.Resources. + func(in *int, out *newer.ResourceList, s conversion.Scope) error { + if *in <= 0 { + return nil + } + quantity := resource.Quantity{} + if err := s.Convert(in, &quantity, 0); err != nil { + return err + } + (*out)[newer.ResourceCPU] = quantity + return nil + }, + // Internal API does not support Memory to be specified via an explicit field. + // Hence it must be stored in Container.Resources. + func(in *int64, out *newer.ResourceList, s conversion.Scope) error { + if *in <= 0 { + return nil + } + quantity := resource.Quantity{} + if err := s.Convert(in, &quantity, 0); err != nil { + return err + } + (*out)[newer.ResourceMemory] = quantity + return nil + }, + // Converts v1beta1.Container to internal Container. + // Fields 'CPU' and 'Memory' are not present in the internal Container object. + // Hence the need for a custom conversion function. + func(in *Container, out *newer.Container, s conversion.Scope) error { + if err := s.Convert(&in.Name, &out.Name, 0); err != nil { + return err + } + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + if err := s.Convert(&in.Command, &out.Command, 0); err != nil { + return err + } + if err := s.Convert(&in.WorkingDir, &out.WorkingDir, 0); err != nil { + return err + } + if err := s.Convert(&in.Ports, &out.Ports, 0); err != nil { + return err + } + if err := s.Convert(&in.Env, &out.Env, 0); err != nil { + return err + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if err := s.Convert(&in.CPU, &out.Resources.Limits, 0); err != nil { + return err + } + if err := s.Convert(&in.Memory, &out.Resources.Limits, 0); err != nil { + return err + } + if err := s.Convert(&in.VolumeMounts, &out.VolumeMounts, 0); err != nil { + return err + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { + return err + } + if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { + return err + } + if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { + return err + } + if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + return err + } + return nil + }, func(in *newer.PodSpec, out *ContainerManifest, s conversion.Scope) error { if err := s.Convert(&in.Volumes, &out.Volumes, 0); err != nil { return err diff --git a/pkg/api/v1beta1/conversion_test.go b/pkg/api/v1beta1/conversion_test.go index 17a120d526b..4b2ade31da0 100644 --- a/pkg/api/v1beta1/conversion_test.go +++ b/pkg/api/v1beta1/conversion_test.go @@ -22,7 +22,9 @@ import ( "testing" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) var Convert = newer.Scheme.Convert @@ -316,3 +318,67 @@ func TestPullPolicyConversion(t *testing.T) { } } } + +func getResourceRequirements(cpu, memory resource.Quantity) current.ResourceRequirementSpec { + res := current.ResourceRequirementSpec{} + res.Limits = current.ResourceList{} + if cpu.Value() > 0 { + res.Limits[current.ResourceCPU] = util.NewIntOrStringFromInt(int(cpu.Value())) + } + if memory.Value() > 0 { + res.Limits[current.ResourceMemory] = util.NewIntOrStringFromInt(int(memory.Value())) + } + + return res +} + +func TestContainerConversion(t *testing.T) { + cpuLimit := resource.MustParse("10") + memoryLimit := resource.MustParse("10M") + null := resource.Quantity{} + testCases := []current.Container{ + { + Name: "container", + Resources: getResourceRequirements(cpuLimit, memoryLimit), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Resources: getResourceRequirements(null, memoryLimit), + }, + { + Name: "container", + Memory: memoryLimit.Value(), + Resources: getResourceRequirements(cpuLimit, null), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Memory: memoryLimit.Value(), + }, + { + Name: "container", + Memory: memoryLimit.Value(), + Resources: getResourceRequirements(cpuLimit, resource.MustParse("100M")), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Resources: getResourceRequirements(resource.MustParse("500"), memoryLimit), + }, + } + + for i, tc := range testCases { + got := newer.Container{} + if err := Convert(&tc, &got); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if cpu := got.Resources.Limits.Cpu(); cpu.Value() != cpuLimit.Value() { + t.Errorf("[Case: %d] Expected cpu: %v, got: %v", i, cpuLimit, *cpu) + } + if memory := got.Resources.Limits.Memory(); memory.Value() != memoryLimit.Value() { + t.Errorf("[Case: %d] Expected memory: %v, got: %v", i, memoryLimit, *memory) + } + } +} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 6940734702a..652080d8bc4 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -252,6 +252,11 @@ type Capabilities struct { Drop []CapabilityType `json:"drop,omitempty" description:"droped capabilities"` } +type ResourceRequirementSpec struct { + // Limits describes the maximum amount of compute resources required. + Limits ResourceList `json:"limits,omitempty" description:"Maximum amount of compute resources allowed"` +} + // Container represents a single container that is expected to be run on the host. type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must @@ -262,13 +267,14 @@ type Container struct { // Optional: Defaults to whatever is defined in the image. Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` - Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` - Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` + WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` + Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` + Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` + Resources ResourceRequirementSpec `json:"resources,omitempty" description:"Compute Resources required by this container"` // Optional: Defaults to unlimited. - Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` + CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails"` Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 4ae43951a2c..ccec2ad9e46 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -295,7 +295,143 @@ func init() { } return nil }, + // Converts internal Container to v1beta1.Container. + // Fields 'CPU' and 'Memory' are not present in the internal Container object. + // Hence the need for a custom conversion function. + func(in *newer.Container, out *Container, s conversion.Scope) error { + if err := s.Convert(&in.Name, &out.Name, 0); err != nil { + return err + } + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + if err := s.Convert(&in.Command, &out.Command, 0); err != nil { + return err + } + if err := s.Convert(&in.WorkingDir, &out.WorkingDir, 0); err != nil { + return err + } + if err := s.Convert(&in.Ports, &out.Ports, 0); err != nil { + return err + } + if err := s.Convert(&in.Env, &out.Env, 0); err != nil { + return err + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if err := s.Convert(in.Resources.Limits.Cpu(), &out.CPU, 0); err != nil { + return err + } + if err := s.Convert(in.Resources.Limits.Memory(), &out.Memory, 0); err != nil { + return err + } + if err := s.Convert(&in.VolumeMounts, &out.VolumeMounts, 0); err != nil { + return err + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { + return err + } + if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { + return err + } + if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { + return err + } + if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + return err + } + return nil + }, + // Internal API does not support CPU to be specified via an explicit field. + // Hence it must be stored in Container.Resources. + func(in *int, out *newer.ResourceList, s conversion.Scope) error { + if *in == 0 { + return nil + } + quantity := resource.Quantity{} + if err := s.Convert(in, &quantity, 0); err != nil { + return err + } + (*out)[newer.ResourceCPU] = quantity + + return nil + }, + // Internal API does not support Memory to be specified via an explicit field. + // Hence it must be stored in Container.Resources. + func(in *int64, out *newer.ResourceList, s conversion.Scope) error { + if *in == 0 { + return nil + } + quantity := resource.Quantity{} + if err := s.Convert(in, &quantity, 0); err != nil { + return err + } + (*out)[newer.ResourceMemory] = quantity + + return nil + }, + // Converts v1beta1.Container to internal newer.Container. + // Fields 'CPU' and 'Memory' are not present in the internal newer.Container object. + // Hence the need for a custom conversion function. + func(in *Container, out *newer.Container, s conversion.Scope) error { + if err := s.Convert(&in.Name, &out.Name, 0); err != nil { + return err + } + if err := s.Convert(&in.Image, &out.Image, 0); err != nil { + return err + } + if err := s.Convert(&in.Command, &out.Command, 0); err != nil { + return err + } + if err := s.Convert(&in.WorkingDir, &out.WorkingDir, 0); err != nil { + return err + } + if err := s.Convert(&in.Ports, &out.Ports, 0); err != nil { + return err + } + if err := s.Convert(&in.Env, &out.Env, 0); err != nil { + return err + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if err := s.Convert(&in.CPU, &out.Resources.Limits, 0); err != nil { + return err + } + if err := s.Convert(&in.Memory, &out.Resources.Limits, 0); err != nil { + return err + } + if err := s.Convert(&in.VolumeMounts, &out.VolumeMounts, 0); err != nil { + return err + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { + return err + } + if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { + return err + } + if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { + return err + } + if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + return err + } + return nil + }, func(in *newer.PodSpec, out *ContainerManifest, s conversion.Scope) error { if err := s.Convert(&in.Volumes, &out.Volumes, 0); err != nil { return err diff --git a/pkg/api/v1beta2/conversion_test.go b/pkg/api/v1beta2/conversion_test.go index 2b04256d9bf..e6411d95cd0 100644 --- a/pkg/api/v1beta2/conversion_test.go +++ b/pkg/api/v1beta2/conversion_test.go @@ -21,7 +21,9 @@ import ( "testing" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func TestServiceEmptySelector(t *testing.T) { @@ -146,3 +148,67 @@ func TestPullPolicyConversion(t *testing.T) { } } } + +func getResourceRequirements(cpu, memory resource.Quantity) current.ResourceRequirementSpec { + res := current.ResourceRequirementSpec{} + res.Limits = current.ResourceList{} + if cpu.Value() > 0 { + res.Limits[current.ResourceCPU] = util.NewIntOrStringFromInt(int(cpu.Value())) + } + if memory.Value() > 0 { + res.Limits[current.ResourceMemory] = util.NewIntOrStringFromInt(int(memory.Value())) + } + + return res +} + +func TestContainerConversion(t *testing.T) { + cpuLimit := resource.MustParse("10") + memoryLimit := resource.MustParse("10M") + null := resource.Quantity{} + testCases := []current.Container{ + { + Name: "container", + Resources: getResourceRequirements(cpuLimit, memoryLimit), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Resources: getResourceRequirements(null, memoryLimit), + }, + { + Name: "container", + Memory: memoryLimit.Value(), + Resources: getResourceRequirements(cpuLimit, null), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Memory: memoryLimit.Value(), + }, + { + Name: "container", + Memory: memoryLimit.Value(), + Resources: getResourceRequirements(cpuLimit, resource.MustParse("100M")), + }, + { + Name: "container", + CPU: int(cpuLimit.MilliValue()), + Resources: getResourceRequirements(resource.MustParse("500"), memoryLimit), + }, + } + + for i, tc := range testCases { + got := newer.Container{} + if err := newer.Scheme.Convert(&tc, &got); err != nil { + t.Errorf("[Case: %d] Unexpected error: %v", i, err) + continue + } + if cpu := got.Resources.Limits.Cpu(); cpu.Value() != cpuLimit.Value() { + t.Errorf("[Case: %d] Expected cpu: %v, got: %v", i, cpuLimit, *cpu) + } + if memory := got.Resources.Limits.Memory(); memory.Value() != memoryLimit.Value() { + t.Errorf("[Case: %d] Expected memory: %v, got: %v", i, memoryLimit, *memory) + } + } +} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 052b4ee777e..d2c52c4b3eb 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -216,6 +216,11 @@ type Capabilities struct { Drop []CapabilityType `json:"drop,omitempty" description:"droped capabilities"` } +type ResourceRequirementSpec struct { + // Limits describes the maximum amount of compute resources required. + Limits ResourceList `json:"limits,omitempty" description:"Maximum amount of compute resources allowed"` +} + // Container represents a single container that is expected to be run on the host. type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must @@ -226,13 +231,14 @@ type Container struct { // Optional: Defaults to whatever is defined in the image. Command []string `json:"command,omitempty" description:"command argv array; not executed within a shell; defaults to entrypoint or command in the image"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` - Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` - Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` + WorkingDir string `json:"workingDir,omitempty" description:"container's working directory; defaults to image's default"` + Ports []Port `json:"ports,omitempty" description:"list of ports to expose from the container"` + Env []EnvVar `json:"env,omitempty" description:"list of environment variables to set in the container"` + Resources ResourceRequirementSpec `json:"resources,omitempty" description:"Compute Resources required by this container"` // Optional: Defaults to unlimited. - Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` + CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty" description:"periodic probe of container liveness; container will be restarted if the probe fails"` Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events"` diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 26c7a07478d..1088d68a956 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -322,6 +322,12 @@ type Capabilities struct { Drop []CapabilityType `json:"drop,omitempty"` } +// ResourceRequirementSpec describes the compute resource requirements. +type ResourceRequirementSpec struct { + // Limits describes the maximum amount of compute resources required. + Limits ResourceList `json:"limits,omitempty" description:"Maximum amount of compute resources allowed"` +} + // Container represents a single container that is expected to be run on the host. type Container struct { // Required: This must be a DNS_LABEL. Each container in a pod must @@ -332,16 +338,13 @@ type Container struct { // Optional: Defaults to whatever is defined in the image. Command []string `json:"command,omitempty"` // Optional: Defaults to Docker's default. - WorkingDir string `json:"workingDir,omitempty"` - Ports []Port `json:"ports,omitempty"` - Env []EnvVar `json:"env,omitempty"` - // Optional: Defaults to unlimited. Units: bytes. - Memory resource.Quantity `json:"memory,omitempty"` - // Optional: Defaults to unlimited. Units: Cores. (500m == 1/2 core) - CPU resource.Quantity `json:"cpu,omitempty"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` - LivenessProbe *Probe `json:"livenessProbe,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty"` + WorkingDir string `json:"workingDir,omitempty"` + Ports []Port `json:"ports,omitempty"` + Env []EnvVar `json:"env,omitempty"` + Resources ResourceRequirementSpec `json:"resources,omitempty" description:"Compute Resources required by this container"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` + LivenessProbe *Probe `json:"livenessProbe,omitempty"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty"` // Optional: Default to false. @@ -796,8 +799,6 @@ type NodeCondition struct { type ResourceName string const ( - // The default compute resource namespace for all standard resource types. - DefaultResourceNamespace = "kubernetes.io" // CPU, in cores. (500m = .5 cores) ResourceCPU ResourceName = "cpu" // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 2271f8e20ea..e9a31d9e45d 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -22,6 +22,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" errs "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -395,6 +396,7 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs cErrs = append(cErrs, validateEnv(ctr.Env).Prefix("env")...) cErrs = append(cErrs, validateVolumeMounts(ctr.VolumeMounts, volumes).Prefix("volumeMounts")...) cErrs = append(cErrs, validatePullPolicyWithDefault(ctr).Prefix("pullPolicy")...) + cErrs = append(cErrs, validateResourceRequirements(ctr).Prefix("resources")...) allErrs = append(allErrs, cErrs.PrefixIndex(i)...) } // Check for colliding ports across all containers. @@ -696,28 +698,17 @@ func ValidateMinionUpdate(oldMinion *api.Node, minion *api.Node) errs.Validation return allErrs } -// Typename is a generic representation for all compute resource typenames. +// Validate compute resource typename. // Refer to docs/resources.md for more details. -func ValidateResourceName(str string) errs.ValidationErrorList { +func validateResourceName(str string) errs.ValidationErrorList { if !util.IsQualifiedName(str) { return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename format %q", str)} } - parts := strings.Split(str, "/") - switch len(parts) { - case 1: - if !api.IsStandardResourceName(parts[0]) { + if len(strings.Split(str, "/")) == 1 { + if !api.IsStandardResourceName(str) { return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename. %q is neither a standard resource type nor is fully qualified", str)} } - break - case 2: - if parts[0] == api.DefaultResourceNamespace { - if !api.IsStandardResourceName(parts[1]) { - return errs.ValidationErrorList{fmt.Errorf("invalid compute resource typename. %q contains a compute resource type not supported", str)} - - } - } - break } return errs.ValidationErrorList{} @@ -740,15 +731,37 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { for i := range limitRange.Spec.Limits { limit := limitRange.Spec.Limits[i] for k := range limit.Max { - allErrs = append(allErrs, ValidateResourceName(string(k))...) + allErrs = append(allErrs, validateResourceName(string(k))...) } for k := range limit.Min { - allErrs = append(allErrs, ValidateResourceName(string(k))...) + allErrs = append(allErrs, validateResourceName(string(k))...) } } return allErrs } +func validateBasicResource(quantity resource.Quantity) errs.ValidationErrorList { + if quantity.Value() < 0 { + return errs.ValidationErrorList{fmt.Errorf("%v is not a valid resource quantity", quantity.Value())} + } + return errs.ValidationErrorList{} +} + +// Validates resource requirement spec. +func validateResourceRequirements(container *api.Container) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + for resourceName, quantity := range container.Resources.Limits { + // Validate resource name. + errs := validateResourceName(resourceName.String()) + if api.IsStandardResourceName(resourceName.String()) { + errs = append(errs, validateBasicResource(quantity).Prefix(fmt.Sprintf("Resource %s: ", resourceName))...) + } + allErrs = append(allErrs, errs...) + } + + return allErrs +} + // ValidateResourceQuota tests if required fields in the ResourceQuota are set. func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} @@ -763,13 +776,13 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro allErrs = append(allErrs, errs.NewFieldInvalid("namespace", resourceQuota.Namespace, "")) } for k := range resourceQuota.Spec.Hard { - allErrs = append(allErrs, ValidateResourceName(string(k))...) + allErrs = append(allErrs, validateResourceName(string(k))...) } for k := range resourceQuota.Status.Hard { - allErrs = append(allErrs, ValidateResourceName(string(k))...) + allErrs = append(allErrs, validateResourceName(string(k))...) } for k := range resourceQuota.Status.Used { - allErrs = append(allErrs, ValidateResourceName(string(k))...) + allErrs = append(allErrs, validateResourceName(string(k))...) } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index d91db387e92..47fa9d2b88c 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -268,6 +268,13 @@ func TestValidatePullPolicy(t *testing.T) { } +func getResourceLimits(cpu, memory string) api.ResourceList { + res := api.ResourceList{} + res[api.ResourceCPU] = resource.MustParse(cpu) + res[api.ResourceMemory] = resource.MustParse(memory) + return res +} + func TestValidateContainers(t *testing.T) { volumes := util.StringSet{} capabilities.SetForTests(capabilities.Capabilities{ @@ -287,6 +294,17 @@ func TestValidateContainers(t *testing.T) { }, }, }, + { + Name: "resources-test", + Image: "image", + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + api.ResourceName(api.ResourceCPU): resource.MustParse("10"), + api.ResourceName(api.ResourceMemory): resource.MustParse("10G"), + api.ResourceName("my.org/resource"): resource.MustParse("10m"), + }, + }, + }, {Name: "abc-1234", Image: "image", Privileged: true}, } if errs := validateContainers(successCase, volumes); len(errs) != 0 { @@ -349,6 +367,35 @@ func TestValidateContainers(t *testing.T) { "privilege disabled": { {Name: "abc", Image: "image", Privileged: true}, }, + "invalid compute resource": { + { + Name: "abc-123", + Image: "image", + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "disk": resource.MustParse("10G"), + }, + }, + }, + }, + "Resource CPU invalid": { + { + Name: "abc-123", + Image: "image", + Resources: api.ResourceRequirementSpec{ + Limits: getResourceLimits("-10", "0"), + }, + }, + }, + "Resource Memory invalid": { + { + Name: "abc-123", + Image: "image", + Resources: api.ResourceRequirementSpec{ + Limits: getResourceLimits("0", "-10"), + }, + }, + }, } for k, v := range errorCases { if errs := validateContainers(v, volumes); len(errs) == 0 { @@ -422,8 +469,12 @@ func TestValidateManifest(t *testing.T) { Image: "image", Command: []string{"foo", "bar"}, WorkingDir: "/tmp", - Memory: resource.MustParse("1"), - CPU: resource.MustParse("1"), + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": resource.MustParse("1"), + "memory": resource.MustParse("1"), + }, + }, Ports: []api.Port{ {Name: "p1", ContainerPort: 80, HostPort: 8080}, {Name: "p2", ContainerPort: 81}, @@ -711,7 +762,9 @@ func TestValidatePodUpdate(t *testing.T) { Containers: []api.Container{ { Image: "foo:V1", - CPU: resource.MustParse("100m"), + Resources: api.ResourceRequirementSpec{ + Limits: getResourceLimits("100m", "0"), + }, }, }, }, @@ -722,7 +775,9 @@ func TestValidatePodUpdate(t *testing.T) { Containers: []api.Container{ { Image: "foo:V2", - CPU: resource.MustParse("1000m"), + Resources: api.ResourceRequirementSpec{ + Limits: getResourceLimits("1000m", "0"), + }, }, }, }, @@ -1675,8 +1730,6 @@ func TestValidateResourceNames(t *testing.T) { {"", false}, {".", false}, {"..", false}, - {"kubernetes.io/cpu", true}, - {"kubernetes.io/disk", false}, {"my.favorite.app.co/12345", true}, {"my.favorite.app.co/_12345", false}, {"my.favorite.app.co/12345_", false}, @@ -1687,7 +1740,7 @@ func TestValidateResourceNames(t *testing.T) { {"kubernetes.io/will/not/work/", false}, } for _, item := range table { - err := ValidateResourceName(item.input) + err := validateResourceName(item.input) if len(err) != 0 && item.success { t.Errorf("expected no failure for input %q", item.input) } else if len(err) == 0 && !item.success { diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index a2a1f113d4b..bd3cd98f46b 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -649,8 +649,8 @@ func (kl *Kubelet) runContainer(pod *api.BoundPod, container *api.Container, pod ExposedPorts: exposedPorts, Hostname: pod.Name, Image: container.Image, - Memory: container.Memory.Value(), - CPUShares: milliCPUToShares(container.CPU.MilliValue()), + Memory: container.Resources.Limits.Memory().Value(), + CPUShares: milliCPUToShares(container.Resources.Limits.Cpu().MilliValue()), WorkingDir: container.WorkingDir, }, } diff --git a/pkg/resourcequota/resource_quota_controller.go b/pkg/resourcequota/resource_quota_controller.go index 74075945d80..1b2d83f5d18 100644 --- a/pkg/resourcequota/resource_quota_controller.go +++ b/pkg/resourcequota/resource_quota_controller.go @@ -179,7 +179,7 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err func PodCPU(pod *api.Pod) *resource.Quantity { val := int64(0) for j := range pod.Spec.Containers { - val = val + pod.Spec.Containers[j].CPU.MilliValue() + val = val + pod.Spec.Containers[j].Resources.Limits.Cpu().MilliValue() } return resource.NewMilliQuantity(int64(val), resource.DecimalSI) } @@ -188,7 +188,7 @@ func PodCPU(pod *api.Pod) *resource.Quantity { func PodMemory(pod *api.Pod) *resource.Quantity { val := int64(0) for j := range pod.Spec.Containers { - val = val + pod.Spec.Containers[j].Memory.Value() + val = val + pod.Spec.Containers[j].Resources.Limits.Memory().Value() } return resource.NewQuantity(int64(val), resource.DecimalSI) } diff --git a/pkg/scheduler/predicates.go b/pkg/scheduler/predicates.go index 65698c6ced6..6572fbeae67 100644 --- a/pkg/scheduler/predicates.go +++ b/pkg/scheduler/predicates.go @@ -95,8 +95,9 @@ type resourceRequest struct { func getResourceRequest(pod *api.Pod) resourceRequest { result := resourceRequest{} for ix := range pod.Spec.Containers { - result.memory += pod.Spec.Containers[ix].Memory.Value() - result.milliCPU += pod.Spec.Containers[ix].CPU.MilliValue() + limits := pod.Spec.Containers[ix].Resources.Limits + result.memory += limits.Memory().Value() + result.milliCPU += limits.Cpu().MilliValue() } return result } @@ -120,8 +121,8 @@ func (r *ResourceFit) PodFitsResources(pod api.Pod, existingPods []api.Pod, node memoryRequested += existingRequest.memory } - totalMilliCPU := info.Spec.Capacity.Get(api.ResourceCPU).MilliValue() - totalMemory := info.Spec.Capacity.Get(api.ResourceMemory).Value() + totalMilliCPU := info.Spec.Capacity.Cpu().MilliValue() + totalMemory := info.Spec.Capacity.Memory().Value() fitsCPU := totalMilliCPU == 0 || (totalMilliCPU-milliCPURequested) >= podRequest.milliCPU fitsMemory := totalMemory == 0 || (totalMemory-memoryRequested) >= podRequest.memory diff --git a/pkg/scheduler/predicates_test.go b/pkg/scheduler/predicates_test.go index 9ef6c11b5b8..ad3856fd9d6 100644 --- a/pkg/scheduler/predicates_test.go +++ b/pkg/scheduler/predicates_test.go @@ -46,8 +46,8 @@ func (nodes FakeNodeListInfo) GetNodeInfo(nodeName string) (*api.Node, error) { func makeResources(milliCPU int64, memory int64) api.NodeResources { return api.NodeResources{ Capacity: api.ResourceList{ - api.ResourceCPU: *resource.NewMilliQuantity(milliCPU, resource.DecimalSI), - api.ResourceMemory: *resource.NewQuantity(memory, resource.BinarySI), + "cpu": *resource.NewMilliQuantity(milliCPU, resource.DecimalSI), + "memory": *resource.NewQuantity(memory, resource.BinarySI), }, } } @@ -56,8 +56,12 @@ func newResourcePod(usage ...resourceRequest) api.Pod { containers := []api.Container{} for _, req := range usage { containers = append(containers, api.Container{ - Memory: *resource.NewQuantity(req.memory, resource.BinarySI), - CPU: *resource.NewMilliQuantity(req.milliCPU, resource.DecimalSI), + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": *resource.NewMilliQuantity(req.milliCPU, resource.DecimalSI), + "memory": *resource.NewQuantity(req.memory, resource.BinarySI), + }, + }, }) } return api.Pod{ diff --git a/pkg/scheduler/priorities.go b/pkg/scheduler/priorities.go index 6685358c949..ee3d8729ea5 100644 --- a/pkg/scheduler/priorities.go +++ b/pkg/scheduler/priorities.go @@ -42,19 +42,19 @@ func calculateOccupancy(pod api.Pod, node api.Node, pods []api.Pod) HostPriority totalMemory := int64(0) for _, existingPod := range pods { for _, container := range existingPod.Spec.Containers { - totalMilliCPU += container.CPU.MilliValue() - totalMemory += container.Memory.Value() + totalMilliCPU += container.Resources.Limits.Cpu().MilliValue() + totalMemory += container.Resources.Limits.Memory().Value() } } // Add the resources requested by the current pod being scheduled. // This also helps differentiate between differently sized, but empty, minions. for _, container := range pod.Spec.Containers { - totalMilliCPU += container.CPU.MilliValue() - totalMemory += container.Memory.Value() + totalMilliCPU += container.Resources.Limits.Cpu().MilliValue() + totalMemory += container.Resources.Limits.Memory().Value() } - capacityMilliCPU := node.Spec.Capacity.Get(api.ResourceCPU).MilliValue() - capacityMemory := node.Spec.Capacity.Get(api.ResourceMemory).Value() + capacityMilliCPU := node.Spec.Capacity.Cpu().MilliValue() + capacityMemory := node.Spec.Capacity.Memory().Value() cpuScore := calculateScore(totalMilliCPU, capacityMilliCPU, node.Name) memoryScore := calculateScore(totalMemory, capacityMemory, node.Name) diff --git a/pkg/scheduler/priorities_test.go b/pkg/scheduler/priorities_test.go index 8aa878586b4..3e9ecbe5f16 100644 --- a/pkg/scheduler/priorities_test.go +++ b/pkg/scheduler/priorities_test.go @@ -30,8 +30,8 @@ func makeMinion(node string, milliCPU, memory int64) api.Node { ObjectMeta: api.ObjectMeta{Name: node}, Spec: api.NodeSpec{ Capacity: api.ResourceList{ - api.ResourceCPU: *resource.NewMilliQuantity(milliCPU, resource.DecimalSI), - api.ResourceMemory: *resource.NewQuantity(memory, resource.BinarySI), + "cpu": *resource.NewMilliQuantity(milliCPU, resource.DecimalSI), + "memory": *resource.NewQuantity(memory, resource.BinarySI), }, }, } @@ -57,14 +57,40 @@ func TestLeastRequested(t *testing.T) { } cpuOnly := api.PodSpec{ Containers: []api.Container{ - {CPU: resource.MustParse("1000m")}, - {CPU: resource.MustParse("2000m")}, + { + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": resource.MustParse("1000m"), + }, + }, + }, + { + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": resource.MustParse("2000m"), + }, + }, + }, }, } cpuAndMemory := api.PodSpec{ Containers: []api.Container{ - {CPU: resource.MustParse("1000m"), Memory: resource.MustParse("2000")}, - {CPU: resource.MustParse("2000m"), Memory: resource.MustParse("3000")}, + { + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": resource.MustParse("1000m"), + "memory": resource.MustParse("2000"), + }, + }, + }, + { + Resources: api.ResourceRequirementSpec{ + Limits: api.ResourceList{ + "cpu": resource.MustParse("2000m"), + "memory": resource.MustParse("3000"), + }, + }, + }, }, } tests := []struct { diff --git a/plugin/pkg/admission/limitranger/admission.go b/plugin/pkg/admission/limitranger/admission.go index 5af414cd5a8..f13c2294d1d 100644 --- a/plugin/pkg/admission/limitranger/admission.go +++ b/plugin/pkg/admission/limitranger/admission.go @@ -103,8 +103,8 @@ func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) e for i := range pod.Spec.Containers { container := pod.Spec.Containers[i] - containerCPU := container.CPU.MilliValue() - containerMem := container.Memory.Value() + containerCPU := container.Resources.Limits.Cpu().MilliValue() + containerMem := container.Resources.Limits.Memory().Value() if i == 0 { minContainerCPU = containerCPU @@ -113,8 +113,8 @@ func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) e maxContainerMem = containerMem } - podCPU = podCPU + container.CPU.MilliValue() - podMem = podMem + container.Memory.Value() + podCPU = podCPU + container.Resources.Limits.Cpu().MilliValue() + podMem = podMem + container.Resources.Limits.Memory().Value() minContainerCPU = Min(containerCPU, minContainerCPU) minContainerMem = Min(containerMem, minContainerMem) diff --git a/plugin/pkg/admission/limitranger/admission_test.go b/plugin/pkg/admission/limitranger/admission_test.go index 570cdcacc52..11713432b34 100644 --- a/plugin/pkg/admission/limitranger/admission_test.go +++ b/plugin/pkg/admission/limitranger/admission_test.go @@ -23,6 +23,19 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" ) +func getResourceRequirements(cpu, memory string) api.ResourceRequirementSpec { + res := api.ResourceRequirementSpec{} + res.Limits = api.ResourceList{} + if cpu != "" { + res.Limits[api.ResourceCPU] = resource.MustParse(cpu) + } + if memory != "" { + res.Limits[api.ResourceMemory] = resource.MustParse(memory) + } + + return res +} + func TestPodLimitFunc(t *testing.T) { limitRange := &api.LimitRange{ ObjectMeta: api.ObjectMeta{ @@ -32,25 +45,13 @@ func TestPodLimitFunc(t *testing.T) { Limits: []api.LimitRangeItem{ { Type: api.LimitTypePod, - Max: api.ResourceList{ - api.ResourceCPU: resource.MustParse("200m"), - api.ResourceMemory: resource.MustParse("4Gi"), - }, - Min: api.ResourceList{ - api.ResourceCPU: resource.MustParse("50m"), - api.ResourceMemory: resource.MustParse("2Mi"), - }, + Max: getResourceRequirements("200m", "4Gi").Limits, + Min: getResourceRequirements("50m", "2Mi").Limits, }, { Type: api.LimitTypeContainer, - Max: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100m"), - api.ResourceMemory: resource.MustParse("2Gi"), - }, - Min: api.ResourceList{ - api.ResourceCPU: resource.MustParse("25m"), - api.ResourceMemory: resource.MustParse("1Mi"), - }, + Max: getResourceRequirements("100m", "2Gi").Limits, + Min: getResourceRequirements("25m", "1Mi").Limits, }, }, }, @@ -62,14 +63,12 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "foo:V1", - CPU: resource.MustParse("100m"), - Memory: resource.MustParse("2Gi"), + Image: "foo:V1", + Resources: getResourceRequirements("100m", "2Gi"), }, { - Image: "boo:V1", - CPU: resource.MustParse("100m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("100m", "2Gi"), }, }, }, @@ -79,9 +78,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("100m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("100m", "2Gi"), }, }, }, @@ -94,9 +92,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("25m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("25m", "2Gi"), }, }, }, @@ -106,9 +103,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("110m"), - Memory: resource.MustParse("1Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("110m", "1Gi"), }, }, }, @@ -118,9 +114,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("30m"), - Memory: resource.MustParse("0"), + Image: "boo:V1", + Resources: getResourceRequirements("30m", "0"), }, }, }, @@ -130,9 +125,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("30m"), - Memory: resource.MustParse("3Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("30m", "3Gi"), }, }, }, @@ -142,9 +136,8 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("40m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("40m", "2Gi"), }, }, }, @@ -154,24 +147,20 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("1Mi"), + Image: "boo:V1", + Resources: getResourceRequirements("60m", "1Mi"), }, { - Image: "boo:V2", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("1Mi"), + Image: "boo:V2", + Resources: getResourceRequirements("60m", "1Mi"), }, { - Image: "boo:V3", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("1Mi"), + Image: "boo:V3", + Resources: getResourceRequirements("60m", "1Mi"), }, { - Image: "boo:V4", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("1Mi"), + Image: "boo:V4", + Resources: getResourceRequirements("60m", "1Mi"), }, }, }, @@ -181,19 +170,16 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V1", + Resources: getResourceRequirements("60m", "2Gi"), }, { - Image: "boo:V2", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V2", + Resources: getResourceRequirements("60m", "2Gi"), }, { - Image: "boo:V3", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("2Gi"), + Image: "boo:V3", + Resources: getResourceRequirements("60m", "2Gi"), }, }, }, @@ -203,19 +189,16 @@ func TestPodLimitFunc(t *testing.T) { Spec: api.PodSpec{ Containers: []api.Container{ { - Image: "boo:V1", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("0"), + Image: "boo:V1", + Resources: getResourceRequirements("60m", "0"), }, { - Image: "boo:V2", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("0"), + Image: "boo:V2", + Resources: getResourceRequirements("60m", "0"), }, { - Image: "boo:V3", - CPU: resource.MustParse("60m"), - Memory: resource.MustParse("0"), + Image: "boo:V3", + Resources: getResourceRequirements("60m", "0"), }, }, }, diff --git a/plugin/pkg/admission/resourcedefaults/admission.go b/plugin/pkg/admission/resourcedefaults/admission.go index e17feb1ccb0..11ea55a59ad 100644 --- a/plugin/pkg/admission/resourcedefaults/admission.go +++ b/plugin/pkg/admission/resourcedefaults/admission.go @@ -55,11 +55,12 @@ func (resourceDefaults) Admit(a admission.Attributes) (err error) { obj := a.GetObject() pod := obj.(*api.Pod) for index := range pod.Spec.Containers { - if pod.Spec.Containers[index].Memory.Value() == 0 { - pod.Spec.Containers[index].Memory = resource.MustParse(defaultMemory) + pod.Spec.Containers[index].Resources.Limits = api.ResourceList{} + if pod.Spec.Containers[index].Resources.Limits.Memory().Value() == 0 { + pod.Spec.Containers[index].Resources.Limits[api.ResourceMemory] = resource.MustParse(defaultMemory) } - if pod.Spec.Containers[index].CPU.Value() == 0 { - pod.Spec.Containers[index].CPU = resource.MustParse(defaultCPU) + if pod.Spec.Containers[index].Resources.Limits.Cpu().Value() == 0 { + pod.Spec.Containers[index].Resources.Limits[api.ResourceCPU] = resource.MustParse(defaultCPU) } } return nil diff --git a/plugin/pkg/admission/resourcedefaults/admission_test.go b/plugin/pkg/admission/resourcedefaults/admission_test.go index c901232864e..9e0792c51e1 100644 --- a/plugin/pkg/admission/resourcedefaults/admission_test.go +++ b/plugin/pkg/admission/resourcedefaults/admission_test.go @@ -41,11 +41,13 @@ func TestAdmission(t *testing.T) { } for i := range pod.Spec.Containers { - if pod.Spec.Containers[i].Memory.String() != "512Mi" { - t.Errorf("Unexpected memory value %s", pod.Spec.Containers[i].Memory.String()) + memory := pod.Spec.Containers[i].Resources.Limits.Memory().String() + cpu := pod.Spec.Containers[i].Resources.Limits.Cpu().String() + if memory != "512Mi" { + t.Errorf("Unexpected memory value %s", memory) } - if pod.Spec.Containers[i].CPU.String() != "1" { - t.Errorf("Unexpected cpu value %s", pod.Spec.Containers[i].CPU.String()) + if cpu != "1" { + t.Errorf("Unexpected cpu value %s", cpu) } } } diff --git a/plugin/pkg/admission/resourcequota/admission_test.go b/plugin/pkg/admission/resourcequota/admission_test.go index 77b06ce4377..5f6ef977253 100644 --- a/plugin/pkg/admission/resourcequota/admission_test.go +++ b/plugin/pkg/admission/resourcequota/admission_test.go @@ -25,6 +25,19 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) +func getResourceRequirements(cpu, memory string) api.ResourceRequirementSpec { + res := api.ResourceRequirementSpec{} + res.Limits = api.ResourceList{} + if cpu != "" { + res.Limits[api.ResourceCPU] = resource.MustParse(cpu) + } + if memory != "" { + res.Limits[api.ResourceMemory] = resource.MustParse(memory) + } + + return res +} + func TestAdmissionIgnoresDelete(t *testing.T) { namespace := "default" handler := NewResourceQuota(&client.Fake{}) @@ -43,7 +56,7 @@ func TestIncrementUsagePods(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, }, @@ -78,7 +91,7 @@ func TestIncrementUsageMemory(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, }, @@ -96,7 +109,7 @@ func TestIncrementUsageMemory(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }} dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) if err != nil { @@ -121,7 +134,7 @@ func TestExceedUsageMemory(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, }, @@ -139,7 +152,7 @@ func TestExceedUsageMemory(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("3Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "3Gi")}}, }} _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) if err == nil { @@ -156,7 +169,7 @@ func TestIncrementUsageCPU(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, }, @@ -174,7 +187,7 @@ func TestIncrementUsageCPU(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }} dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) if err != nil { @@ -199,7 +212,7 @@ func TestExceedUsageCPU(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, }, @@ -217,7 +230,7 @@ func TestExceedUsageCPU(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("500m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("500m", "1Gi")}}, }} _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) if err == nil { @@ -234,7 +247,7 @@ func TestExceedUsagePods(t *testing.T) { ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "vol"}}, - Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Resources: getResourceRequirements("100m", "1Gi")}}, }, }, },