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")}}, }, }, },