diff --git a/pkg/api/types.go b/pkg/api/types.go index 47513640f8..0753e86dd3 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -467,12 +467,26 @@ type EndpointsList struct { func (*EndpointsList) IsAnAPIObject() {} +// NodeResources represents resources on a Kubernetes system node +// see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. +type NodeResources struct { + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" yaml:"capacity,omitempty"` +} + +type ResourceName string + +// TODO Replace this with a more complete "Quantity" struct +type ResourceList map[ResourceName]util.IntOrString + // Minion is a worker node in Kubernetenes. // The name of the minion according to etcd is in JSONBase.ID. type Minion struct { JSONBase `json:",inline" yaml:",inline"` // Queried from cloud provider, if available. HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` + // Resources available on the node + NodeResources NodeResources `json:"resources,omitempty" yaml:"resources,omitempty"` } func (*Minion) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 50654307ab..a9cbefe0fd 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -451,12 +451,25 @@ type EndpointsList struct { func (*EndpointsList) IsAnAPIObject() {} +// NodeResources represents resources on a Kubernetes system node +// see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. +type NodeResources struct { + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" yaml:"capacity,omitempty"` +} + +type ResourceName string + +type ResourceList map[ResourceName]util.IntOrString + // Minion is a worker node in Kubernetenes. // The name of the minion according to etcd is in JSONBase.ID. type Minion struct { JSONBase `json:",inline" yaml:",inline"` // Queried from cloud provider, if available. HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` + // Resources available on the node + NodeResources NodeResources `json:"resources,omitempty" yaml:"resources,omitempty"` } func (*Minion) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 837dfcd585..b90a896718 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -448,12 +448,25 @@ type EndpointsList struct { func (*EndpointsList) IsAnAPIObject() {} +// NodeResources represents resources on a Kubernetes system node +// see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. +type NodeResources struct { + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" yaml:"capacity,omitempty"` +} + +type ResourceName string + +type ResourceList map[ResourceName]util.IntOrString + // Minion is a worker node in Kubernetenes. // The name of the minion according to etcd is in JSONBase.ID. type Minion struct { JSONBase `json:",inline" yaml:",inline"` // Queried from cloud provider, if available. HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` + // Resources available on the node + NodeResources NodeResources `json:"resources,omitempty" yaml:"resources,omitempty"` } func (*Minion) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 26a0f1f587..9702a09841 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -564,10 +564,19 @@ type NodeSpec struct { // NodeStatus is information about the current status of a node. type NodeStatus struct { - // Queried from cloud provider, if available. - HostIP string `json:"hostIP,omitempty" yaml:"hostIP,omitempty"` } +// NodeResources represents resources on a Kubernetes system node +// see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/resources.md for more details. +type NodeResources struct { + // Capacity represents the available resources. + Capacity ResourceList `json:"capacity,omitempty" yaml:"capacity,omitempty"` +} + +type ResourceName string + +type ResourceList map[ResourceName]util.IntOrString + // Node is a worker node in Kubernetenes. // The name of the node according to etcd is in JSONBase.ID. type Node struct { @@ -579,6 +588,9 @@ type Node struct { // Status describes the current status of a Node Status NodeStatus `json:"status,omitempty" yaml:"status,omitempty"` + + // NodeResources describe the resoruces available on the node. + NodeResources NodeResources `json:"resources,omitempty" yaml:"resources,omitempty"` } // NodeList is a list of minions. diff --git a/pkg/resources/doc.go b/pkg/resources/doc.go new file mode 100644 index 0000000000..1c42e5b38b --- /dev/null +++ b/pkg/resources/doc.go @@ -0,0 +1,18 @@ +/* +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 resources has constants and utilities for dealing with resources +package resources diff --git a/pkg/resources/resources.go b/pkg/resources/resources.go new file mode 100644 index 0000000000..4647b367b5 --- /dev/null +++ b/pkg/resources/resources.go @@ -0,0 +1,75 @@ +/* +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 resources + +import ( + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" +) + +const ( + CPU api.ResourceName = "cpu" + Memory api.ResourceName = "memory" +) + +// TODO: None of these currently handle SI units + +func GetFloatResource(resources api.ResourceList, name api.ResourceName, def float64) float64 { + value, found := resources[name] + if !found { + return def + } + if value.Kind == util.IntstrInt { + return float64(value.IntVal) + } + result, err := strconv.ParseFloat(value.StrVal, 64) + if err != nil { + glog.Errorf("parsing failed for %s: %s", name, value.StrVal) + return def + } + return result +} + +func GetIntegerResource(resources api.ResourceList, name api.ResourceName, def int) int { + value, found := resources[name] + if !found { + return def + } + if value.Kind == util.IntstrInt { + return value.IntVal + } + result, err := strconv.Atoi(value.StrVal) + if err != nil { + glog.Errorf("parsing failed for %s: %s", name, value.StrVal) + return def + } + return result +} + +func GetStringResource(resources api.ResourceList, name api.ResourceName, def string) string { + value, found := resources[name] + if !found { + return def + } + if value.Kind == util.IntstrInt { + return strconv.Itoa(value.IntVal) + } + return value.StrVal +} diff --git a/pkg/resources/resources_test.go b/pkg/resources/resources_test.go new file mode 100644 index 0000000000..27f097ab26 --- /dev/null +++ b/pkg/resources/resources_test.go @@ -0,0 +1,169 @@ +/* +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 resources + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestGetInteger(t *testing.T) { + tests := []struct { + res api.ResourceList + name api.ResourceName + expected int + def int + test string + }{ + { + res: api.ResourceList{}, + name: CPU, + expected: 1, + def: 1, + test: "nothing present", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromInt(2), + }, + name: CPU, + expected: 2, + def: 1, + test: "present", + }, + { + res: api.ResourceList{ + Memory: util.NewIntOrStringFromInt(2), + }, + name: CPU, + expected: 1, + def: 1, + test: "not-present", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromString("2"), + }, + name: CPU, + expected: 2, + def: 1, + test: "present-string", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromString("foo"), + }, + name: CPU, + expected: 1, + def: 1, + test: "present-invalid", + }, + } + + for _, test := range tests { + val := GetIntegerResource(test.res, test.name, test.def) + if val != test.expected { + t.Errorf("%s: expected: %d found %d", test.expected, val) + } + } +} +func TestGetFloat(t *testing.T) { + tests := []struct { + res api.ResourceList + name api.ResourceName + expected float64 + def float64 + test string + }{ + { + res: api.ResourceList{}, + name: CPU, + expected: 1.5, + def: 1.5, + test: "nothing present", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromInt(2), + }, + name: CPU, + expected: 2.0, + def: 1.5, + test: "present", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromString("2.5"), + }, + name: CPU, + expected: 2.5, + def: 1, + test: "present-string", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromString("foo"), + }, + name: CPU, + expected: 1, + def: 1, + test: "present-invalid", + }, + } + + for _, test := range tests { + val := GetFloatResource(test.res, test.name, test.def) + if val != test.expected { + t.Errorf("%s: expected: %d found %d", test.expected, val) + } + } +} +func TestGetString(t *testing.T) { + tests := []struct { + res api.ResourceList + name api.ResourceName + expected string + def string + test string + }{ + { + res: api.ResourceList{}, + name: CPU, + expected: "foo", + def: "foo", + test: "nothing present", + }, + { + res: api.ResourceList{ + CPU: util.NewIntOrStringFromString("bar"), + }, + name: CPU, + expected: "bar", + def: "foo", + test: "present", + }, + } + + for _, test := range tests { + val := GetStringResource(test.res, test.name, test.def) + if val != test.expected { + t.Errorf("%s: expected: %d found %d", test.expected, val) + } + } +} diff --git a/pkg/scheduler/predicates.go b/pkg/scheduler/predicates.go index 348345b8a2..1ab2ac786d 100644 --- a/pkg/scheduler/predicates.go +++ b/pkg/scheduler/predicates.go @@ -19,8 +19,62 @@ package scheduler import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resources" + "github.com/golang/glog" ) +type NodeInfo interface { + GetNodeInfo(nodeName string) (api.Minion, error) +} + +type ResourceFit struct { + info NodeInfo +} + +type resourceRequest struct { + milliCPU int + memory int +} + +func getResourceRequest(pod *api.Pod) resourceRequest { + result := resourceRequest{} + for ix := range pod.DesiredState.Manifest.Containers { + result.memory += pod.DesiredState.Manifest.Containers[ix].Memory + result.milliCPU += pod.DesiredState.Manifest.Containers[ix].CPU + } + return result +} + +// PodFitsResources calculates fit based on requested, rather than used resources +func (r *ResourceFit) PodFitsResources(pod api.Pod, existingPods []api.Pod, node string) (bool, error) { + podRequest := getResourceRequest(&pod) + if podRequest.milliCPU == 0 && podRequest.memory == 0 { + // no resources requested always fits. + return true, nil + } + info, err := r.info.GetNodeInfo(node) + if err != nil { + return false, err + } + milliCPURequested := 0 + memoryRequested := 0 + for ix := range existingPods { + existingRequest := getResourceRequest(&existingPods[ix]) + milliCPURequested += existingRequest.milliCPU + memoryRequested += existingRequest.memory + } + + // TODO: convert to general purpose resource matching, when pods ask for resources + totalMilliCPU := int(resources.GetFloatResource(info.NodeResources.Capacity, resources.CPU, 0) * 1000) + totalMemory := resources.GetIntegerResource(info.NodeResources.Capacity, resources.Memory, 0) + + fitsCPU := totalMilliCPU == 0 || (totalMilliCPU-milliCPURequested) >= podRequest.milliCPU + fitsMemory := totalMemory == 0 || (totalMemory-memoryRequested) >= podRequest.memory + glog.V(3).Infof("Calculated fit: cpu: %s, memory %s", fitsCPU, fitsMemory) + + return fitsCPU && fitsMemory, nil +} + func PodFitsPorts(pod api.Pod, existingPods []api.Pod, node string) (bool, error) { for _, scheduledPod := range existingPods { for _, container := range pod.DesiredState.Manifest.Containers { diff --git a/pkg/scheduler/predicates_test.go b/pkg/scheduler/predicates_test.go index 89539f7d79..388b611ee1 100644 --- a/pkg/scheduler/predicates_test.go +++ b/pkg/scheduler/predicates_test.go @@ -20,8 +20,110 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resources" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) +type FakeNodeInfo api.Minion + +func (n FakeNodeInfo) GetNodeInfo(nodeName string) (api.Minion, error) { + return api.Minion(n), nil +} + +func makeResources(milliCPU int, memory int) api.NodeResources { + return api.NodeResources{ + Capacity: api.ResourceList{ + resources.CPU: util.IntOrString{ + IntVal: milliCPU, + Kind: util.IntstrInt, + }, + resources.Memory: util.IntOrString{ + IntVal: memory, + Kind: util.IntstrInt, + }, + }, + } +} + +func newResourcePod(usage ...resourceRequest) api.Pod { + containers := []api.Container{} + for _, req := range usage { + containers = append(containers, api.Container{ + Memory: req.memory, + CPU: req.milliCPU, + }) + } + return api.Pod{ + DesiredState: api.PodState{ + Manifest: api.ContainerManifest{ + Containers: containers, + }, + }, + } +} + +func TestPodFitsResources(t *testing.T) { + tests := []struct { + pod api.Pod + existingPods []api.Pod + fits bool + test string + }{ + { + pod: api.Pod{}, + existingPods: []api.Pod{ + newResourcePod(resourceRequest{milliCPU: 10, memory: 20}), + }, + fits: true, + test: "no resources requested always fits", + }, + { + pod: newResourcePod(resourceRequest{milliCPU: 1, memory: 1}), + existingPods: []api.Pod{ + newResourcePod(resourceRequest{milliCPU: 10, memory: 20}), + }, + fits: false, + test: "too many resources fails", + }, + { + pod: newResourcePod(resourceRequest{milliCPU: 1, memory: 1}), + existingPods: []api.Pod{ + newResourcePod(resourceRequest{milliCPU: 5, memory: 5}), + }, + fits: true, + test: "both resources fit", + }, + { + pod: newResourcePod(resourceRequest{milliCPU: 1, memory: 2}), + existingPods: []api.Pod{ + newResourcePod(resourceRequest{milliCPU: 5, memory: 19}), + }, + fits: false, + test: "one resources fits", + }, + { + pod: newResourcePod(resourceRequest{milliCPU: 5, memory: 1}), + existingPods: []api.Pod{ + newResourcePod(resourceRequest{milliCPU: 5, memory: 19}), + }, + fits: true, + test: "equal edge case", + }, + } + for _, test := range tests { + node := api.Minion{NodeResources: makeResources(10, 20)} + + fit := ResourceFit{FakeNodeInfo(node)} + fits, err := fit.PodFitsResources(test.pod, test.existingPods, "machine") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if fits != test.fits { + t.Errorf("%s: expected: %v got %v", test.test, test.fits, fits) + } + } +} + func TestPodFitsPorts(t *testing.T) { tests := []struct { pod api.Pod