From 894a3e6d3ff68b8686297e601f81d5fdff311e35 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Fri, 2 Jan 2015 19:10:03 -0800 Subject: [PATCH] Wire resource.Quantity into api --- pkg/api/serialization_test.go | 28 ++++++++++++++-- pkg/api/types.go | 33 +++++++++++++++---- pkg/api/v1beta1/conversion.go | 61 ++++++++++++++++++++++++++++++++++- pkg/api/v1beta1/types.go | 9 +++++- pkg/api/v1beta2/conversion.go | 61 ++++++++++++++++++++++++++++++++++- pkg/api/v1beta2/types.go | 9 +++++- pkg/api/v1beta3/types.go | 26 ++++++++++----- pkg/util/util.go | 8 +++++ 8 files changed, 214 insertions(+), 21 deletions(-) diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index 5c79980e154..156e0119eaf 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -27,16 +27,28 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + docker "github.com/fsouza/go-dockerclient" fuzz "github.com/google/gofuzz" + "speter.net/go/exp/math/dec/inf" ) var fuzzIters = flag.Int("fuzz_iters", 40, "How many fuzzing iterations to do.") +// apiObjectComparer can do semantic deep equality checks for api objects. +var apiObjectComparer = conversion.EqualitiesOrDie( + func(a, b resource.Quantity) bool { + // Ignore formatting, only care that numeric value stayed the same. + return a.Amount.Cmp(b.Amount) == 0 + }, +) + // apiObjectFuzzer can randomly populate api objects. var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( func(j *runtime.PluginBase, c fuzz.Continue) { @@ -136,6 +148,16 @@ var apiObjectFuzzer = fuzz.New().NilChance(.5).NumElements(1, 1).Funcs( c.RandString(): c.RandString(), } }, + + func(q *resource.Quantity, c fuzz.Continue) { + // Real Quantity fuzz testing is done elsewhere; + // this limited subset of functionality survives + // round-tripping to v1beta1/2. + q.Amount = &inf.Dec{} + q.Format = resource.DecimalExponent + //q.Amount.SetScale(inf.Scale(-c.Intn(12))) + q.Amount.SetUnscaled(c.Int63n(1000)) + }, ) func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { @@ -159,7 +181,7 @@ func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { t.Errorf("0: %v: %v\nCodec: %v\nData: %s\nSource: %#v", name, err, codec, string(data), source) return } - if !reflect.DeepEqual(source, obj2) { + if !apiObjectComparer.DeepEqual(source, obj2) { t.Errorf("1: %v: diff: %v\nCodec: %v\nData: %s\nSource: %#v", name, util.ObjectGoPrintDiff(source, obj2), codec, string(data), source) return } @@ -170,7 +192,7 @@ func runTest(t *testing.T, codec runtime.Codec, source runtime.Object) { t.Errorf("2: %v: %v", name, err) return } - if !reflect.DeepEqual(source, obj3) { + if !apiObjectComparer.DeepEqual(source, obj3) { t.Errorf("3: %v: diff: %v\nCodec: %v", name, util.ObjectDiff(source, obj3), codec) return } @@ -244,7 +266,7 @@ func TestEncode_Ptr(t *testing.T) { if _, ok := obj2.(*api.Pod); !ok { t.Fatalf("Got wrong type") } - if !reflect.DeepEqual(obj2, pod) { + if !apiObjectComparer.DeepEqual(obj2, pod) { t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2) } } diff --git a/pkg/api/types.go b/pkg/api/types.go index d7c0e6e3cf6..ebb32586ede 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -17,6 +17,7 @@ limitations under the License. package api import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -309,12 +310,12 @@ type Container struct { Ports []Port `json:"ports,omitempty"` Env []EnvVar `json:"env,omitempty"` // Optional: Defaults to unlimited. - Memory int `json:"memory,omitempty"` + Memory resource.Quantity `json:"memory,omitempty"` // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` - LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty"` + CPU resource.Quantity `json:"cpu,omitempty"` + VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` + LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty"` // Optional: Default to false. @@ -747,9 +748,29 @@ type NodeResources struct { Capacity ResourceList `json:"capacity,omitempty"` } +// ResourceName is the name identifying various resources in a ResourceList. type ResourceName string -type ResourceList map[ResourceName]util.IntOrString +const ( + // CPU, in cores. (500m = .5 cores) + ResourceCPU ResourceName = "cpu" + // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) + ResourceMemory ResourceName = "memory" +) + +// ResourceList is a set of (resource name, quantity) pairs. +type ResourceList map[ResourceName]resource.Quantity + +// Get is a convenience function, which returns a 0 quantity if the +// resource list is nil, empty, or lacks a value for the requested resource. +// Treat as read only! +func (rl ResourceList) Get(name ResourceName) *resource.Quantity { + if rl == nil { + return &resource.Quantity{} + } + q := rl[name] + return &q +} // Node is a worker node in Kubernetenes // The name of the node according to etcd is in ObjectMeta.Name. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index f4ff1f5a854..3248d31d4d8 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -18,10 +18,13 @@ package v1beta1 import ( "errors" + "fmt" "strconv" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func init() { @@ -38,7 +41,7 @@ func init() { // newer.Scheme.AddStructFieldConversion(string(""), "Status", string(""), "Condition") // newer.Scheme.AddStructFieldConversion(string(""), "Condition", string(""), "Status") - newer.Scheme.AddConversionFuncs( + err := newer.Scheme.AddConversionFuncs( // TypeMeta must be split into two objects func(in *newer.TypeMeta, out *TypeMeta, s conversion.Scope) error { out.Kind = in.Kind @@ -582,5 +585,61 @@ func init() { out.Timestamp = in.Timestamp return s.Convert(&in.InvolvedObject, &out.InvolvedObject, 0) }, + + // This is triggered for the Memory field of Container. + func(in *int64, out *resource.Quantity, s conversion.Scope) error { + out.Set(*in) + out.Format = resource.BinarySI + return nil + }, + func(in *resource.Quantity, out *int64, s conversion.Scope) error { + *out = in.Value() + return nil + }, + + // This is triggered by the CPU field of Container. + // Note that if we add other int/Quantity conversions my + // simple hack (int64=Value(), int=MilliValue()) here won't work. + func(in *int, out *resource.Quantity, s conversion.Scope) error { + out.SetMilli(int64(*in)) + out.Format = resource.DecimalSI + return nil + }, + func(in *resource.Quantity, out *int, s conversion.Scope) error { + *out = int(in.MilliValue()) + return nil + }, + + // Convert resource lists. + func(in *ResourceList, out *newer.ResourceList, s conversion.Scope) error { + *out = newer.ResourceList{} + for k, v := range *in { + fv, err := strconv.ParseFloat(v.String(), 64) + if err != nil { + return fmt.Errorf("value '%v' of '%v': %v", v, k, err) + } + if k == ResourceCPU { + (*out)[newer.ResourceCPU] = *resource.NewMilliQuantity(int64(fv*1000), resource.DecimalSI) + } else { + (*out)[newer.ResourceName(k)] = *resource.NewQuantity(int64(fv), resource.BinarySI) + } + } + return nil + }, + func(in *newer.ResourceList, out *ResourceList, s conversion.Scope) error { + *out = ResourceList{} + for k, v := range *in { + if k == newer.ResourceCPU { + (*out)[ResourceCPU] = util.NewIntOrStringFromString(fmt.Sprintf("%v", float64(v.MilliValue())/1000)) + } else { + (*out)[ResourceName(k)] = util.NewIntOrStringFromInt(int(v.Value())) + } + } + return nil + }, ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index fa4eebac2f3..77e19b6b9b5 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -254,7 +254,7 @@ type Container struct { 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"` // Optional: Defaults to unlimited. - Memory int `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` // Optional: Defaults to unlimited. CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` @@ -583,6 +583,13 @@ type NodeResources struct { type ResourceName string +const ( + // CPU, in cores. (floating point w/ 3 decimal places) + ResourceCPU ResourceName = "cpu" + // Memory, in bytes. + ResourceMemory ResourceName = "memory" +) + type ResourceList map[ResourceName]util.IntOrString // Minion is a worker node in Kubernetenes. diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 10e4ad98b0c..00d8cfb313a 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -18,10 +18,13 @@ package v1beta2 import ( "errors" + "fmt" "strconv" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) func init() { @@ -38,7 +41,7 @@ func init() { // newer.Scheme.AddStructFieldConversion(string(""), "Status", string(""), "Condition") // newer.Scheme.AddStructFieldConversion(string(""), "Condition", string(""), "Status") - newer.Scheme.AddConversionFuncs( + err := newer.Scheme.AddConversionFuncs( // TypeMeta must be split into two objects func(in *newer.TypeMeta, out *TypeMeta, s conversion.Scope) error { out.Kind = in.Kind @@ -498,5 +501,61 @@ func init() { out.Timestamp = in.Timestamp return s.Convert(&in.InvolvedObject, &out.InvolvedObject, 0) }, + + // This is triggered for the Memory field of Container. + func(in *int64, out *resource.Quantity, s conversion.Scope) error { + out.Set(*in) + out.Format = resource.BinarySI + return nil + }, + func(in *resource.Quantity, out *int64, s conversion.Scope) error { + *out = in.Value() + return nil + }, + + // This is triggered by the CPU field of Container. + // Note that if we add other int/Quantity conversions my + // simple hack (int64=Value(), int=MilliValue()) here won't work. + func(in *int, out *resource.Quantity, s conversion.Scope) error { + out.SetMilli(int64(*in)) + out.Format = resource.DecimalSI + return nil + }, + func(in *resource.Quantity, out *int, s conversion.Scope) error { + *out = int(in.MilliValue()) + return nil + }, + + // Convert resource lists. + func(in *ResourceList, out *newer.ResourceList, s conversion.Scope) error { + *out = newer.ResourceList{} + for k, v := range *in { + fv, err := strconv.ParseFloat(v.String(), 64) + if err != nil { + return fmt.Errorf("value '%v' of '%v': %v", v, k, err) + } + if k == ResourceCPU { + (*out)[newer.ResourceCPU] = *resource.NewMilliQuantity(int64(fv*1000), resource.DecimalSI) + } else { + (*out)[newer.ResourceName(k)] = *resource.NewQuantity(int64(fv), resource.BinarySI) + } + } + return nil + }, + func(in *newer.ResourceList, out *ResourceList, s conversion.Scope) error { + *out = ResourceList{} + for k, v := range *in { + if k == newer.ResourceCPU { + (*out)[ResourceCPU] = util.NewIntOrStringFromString(fmt.Sprintf("%v", float64(v.MilliValue())/1000)) + } else { + (*out)[ResourceName(k)] = util.NewIntOrStringFromInt(int(v.Value())) + } + } + return nil + }, ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } } diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 7438f264fa8..e8c26dab202 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -218,7 +218,7 @@ type Container struct { 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"` // Optional: Defaults to unlimited. - Memory int `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` + Memory int64 `json:"memory,omitempty" description:"memory limit in bytes; defaults to unlimited"` // Optional: Defaults to unlimited. CPU int `json:"cpu,omitempty" description:"CPU share in thousandths of a core"` VolumeMounts []VolumeMount `json:"volumeMounts,omitempty" description:"pod volumes to mount into the container's filesystem"` @@ -546,6 +546,13 @@ type NodeResources struct { type ResourceName string +const ( + // CPU, in cores. (500m = .5 cores) + ResourceCPU ResourceName = "cpu" + // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) + ResourceMemory ResourceName = "memory" +) + type ResourceList map[ResourceName]util.IntOrString // Minion is a worker node in Kubernetenes. diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index daf7b01b42d..cca9fed96d3 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta3 import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -326,13 +327,13 @@ type Container struct { WorkingDir string `json:"workingDir,omitempty"` Ports []Port `json:"ports,omitempty"` Env []EnvVar `json:"env,omitempty"` - // Optional: Defaults to unlimited. - Memory int `json:"memory,omitempty"` - // Optional: Defaults to unlimited. - CPU int `json:"cpu,omitempty"` - VolumeMounts []VolumeMount `json:"volumeMounts,omitempty"` - LivenessProbe *LivenessProbe `json:"livenessProbe,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,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 *LivenessProbe `json:"livenessProbe,omitempty"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty"` // Optional: Default to false. @@ -776,9 +777,18 @@ type NodeCondition struct { Message string `json:"message,omitempty"` } +// ResourceName is the name identifying various resources in a ResourceList. type ResourceName string -type ResourceList map[ResourceName]util.IntOrString +const ( + // CPU, in cores. (500m = .5 cores) + ResourceCPU ResourceName = "cpu" + // Memory, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) + ResourceMemory ResourceName = "memory" +) + +// ResourceList is a set of (resource name, quantity) pairs. +type ResourceList map[ResourceName]resource.Quantity // Node is a worker node in Kubernetes. // The name of the node according to etcd is in ID. diff --git a/pkg/util/util.go b/pkg/util/util.go index f7ee32f44d7..a84356888ad 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -113,6 +113,14 @@ func (intstr *IntOrString) UnmarshalJSON(value []byte) error { return json.Unmarshal(value, &intstr.IntVal) } +// String returns the string value, or Itoa's the int value. +func (intstr *IntOrString) String() string { + if intstr.Kind == IntstrString { + return intstr.StrVal + } + return strconv.Itoa(intstr.IntVal) +} + // MarshalJSON implements the json.Marshaller interface. func (intstr IntOrString) MarshalJSON() ([]byte, error) { switch intstr.Kind {