diff --git a/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go b/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go index bc9753c07f..210ca9d230 100644 --- a/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go +++ b/cmd/libs/go2idl/go-to-protobuf/protobuf/cmd.go @@ -73,6 +73,7 @@ func New() *Generator { `k8s.io/kubernetes/pkg/apis/extensions/v1beta1`, `k8s.io/kubernetes/pkg/apis/autoscaling/v1`, `k8s.io/kubernetes/pkg/apis/authorization/v1`, + `k8s.io/kubernetes/pkg/apis/autoscaling/v2alpha1`, `k8s.io/kubernetes/pkg/apis/authorization/v1beta1`, `k8s.io/kubernetes/pkg/apis/batch/v1`, `k8s.io/kubernetes/pkg/apis/batch/v2alpha1`, diff --git a/hack/.linted_packages b/hack/.linted_packages index c75df4cd23..7be28477cd 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -94,6 +94,7 @@ pkg/client/informers/informers_generated/apps/v1beta1 pkg/client/informers/informers_generated/autoscaling pkg/client/informers/informers_generated/autoscaling/internalversion pkg/client/informers/informers_generated/autoscaling/v1 +pkg/client/informers/informers_generated/autoscaling/v2alpha1 pkg/client/informers/informers_generated/batch pkg/client/informers/informers_generated/batch/internalversion pkg/client/informers/informers_generated/batch/v1 @@ -126,6 +127,7 @@ pkg/client/listers/authorization/v1 pkg/client/listers/authorization/v1beta1 pkg/client/listers/autoscaling/internalversion pkg/client/listers/autoscaling/v1 +pkg/client/listers/autoscaling/v2alpha1 pkg/client/listers/batch/internalversion pkg/client/listers/batch/v1 pkg/client/listers/batch/v2alpha1 diff --git a/hack/lib/init.sh b/hack/lib/init.sh index 2e909995a2..8f1cb305f9 100644 --- a/hack/lib/init.sh +++ b/hack/lib/init.sh @@ -58,6 +58,7 @@ authentication.k8s.io/v1beta1 \ authorization.k8s.io/v1 \ authorization.k8s.io/v1beta1 \ autoscaling/v1 \ +autoscaling/v2alpha1 \ batch/v1 \ batch/v2alpha1 \ certificates.k8s.io/v1beta1 \ diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 0b7495df05..b050ca525b 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -1612,8 +1612,8 @@ run_recursive_resources_tests() { output_message=$(! kubectl autoscale --min=1 --max=2 -f hack/testdata/recursive/rc --recursive 2>&1 "${kube_flags[@]}") # Post-condition: busybox0 & busybox replication controllers are autoscaled # with min. of 1 replica & max of 2 replicas, and since busybox2 is malformed, it should error - kube::test::get_object_assert 'hpa busybox0' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '1 2 ' - kube::test::get_object_assert 'hpa busybox1' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '1 2 ' + kube::test::get_object_assert 'hpa busybox0' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '1 2 80' + kube::test::get_object_assert 'hpa busybox1' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '1 2 80' kube::test::if_has_string "${output_message}" "Object 'Kind' is missing" kubectl delete hpa busybox0 "${kube_flags[@]}" kubectl delete hpa busybox1 "${kube_flags[@]}" @@ -2221,7 +2221,7 @@ run_rc_tests() { kubectl delete hpa frontend "${kube_flags[@]}" # autoscale 2~3 pods, no CPU utilization specified, rc specified by name kubectl autoscale rc frontend "${kube_flags[@]}" --min=2 --max=3 - kube::test::get_object_assert 'hpa frontend' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 ' + kube::test::get_object_assert 'hpa frontend' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 80' kubectl delete hpa frontend "${kube_flags[@]}" # autoscale without specifying --max should fail ! kubectl autoscale rc frontend "${kube_flags[@]}" @@ -2280,7 +2280,7 @@ run_deployment_tests() { kube::test::get_object_assert deployment "{{range.items}}{{$id_field}}:{{end}}" 'nginx-deployment:' # autoscale 2~3 pods, no CPU utilization specified kubectl-with-retry autoscale deployment nginx-deployment "${kube_flags[@]}" --min=2 --max=3 - kube::test::get_object_assert 'hpa nginx-deployment' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 ' + kube::test::get_object_assert 'hpa nginx-deployment' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 80' # Clean up # Note that we should delete hpa first, otherwise it may fight with the deployment reaper. kubectl delete hpa nginx-deployment "${kube_flags[@]}" @@ -2471,7 +2471,7 @@ run_rs_tests() { kubectl delete hpa frontend "${kube_flags[@]}" # autoscale 2~3 pods, no CPU utilization specified, replica set specified by name kubectl autoscale rs frontend "${kube_flags[@]}" --min=2 --max=3 - kube::test::get_object_assert 'hpa frontend' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 ' + kube::test::get_object_assert 'hpa frontend' "{{$hpa_min_field}} {{$hpa_max_field}} {{$hpa_cpu_field}}" '2 3 80' kubectl delete hpa frontend "${kube_flags[@]}" # autoscale without specifying --max should fail ! kubectl autoscale rs frontend "${kube_flags[@]}" diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 1cb6065945..a8680cb526 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -546,8 +546,36 @@ func autoscalingFuncs(t apitesting.TestingCommon) []interface{} { c.FuzzNoCustom(s) // fuzz self without calling this function again minReplicas := int32(c.Rand.Int31()) s.MinReplicas = &minReplicas - targetCpu := int32(c.RandUint64()) - s.TargetCPUUtilizationPercentage = &targetCpu + + // NB: since this is used for round-tripping, we can only fuzz + // fields that round-trip successfully, so only the resource source + // type is usable here + targetUtilization := int32(c.RandUint64()) + s.Metrics = []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: &targetUtilization, + }, + }, + } + }, + func(s *autoscaling.HorizontalPodAutoscalerStatus, c fuzz.Continue) { + c.FuzzNoCustom(s) // fuzz self without calling this function again + // NB: since this is used for round-tripping, we can only fuzz + // fields that round-trip successfully, so only the resource status + // type is usable here + currentUtilization := int32(c.RandUint64()) + s.CurrentMetrics = []autoscaling.MetricStatus{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricStatus{ + Name: api.ResourceCPU, + CurrentAverageUtilization: ¤tUtilization, + }, + }, + } }, } } diff --git a/pkg/apis/autoscaling/BUILD b/pkg/apis/autoscaling/BUILD index 7b622f2543..b3db9c986e 100644 --- a/pkg/apis/autoscaling/BUILD +++ b/pkg/apis/autoscaling/BUILD @@ -10,6 +10,7 @@ load( go_library( name = "go_default_library", srcs = [ + "annotations.go", "doc.go", "register.go", "types.go", @@ -17,6 +18,8 @@ go_library( ], tags = ["automanaged"], deps = [ + "//pkg/api:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/resource", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/conversion", "//vendor:k8s.io/apimachinery/pkg/runtime", @@ -37,6 +40,7 @@ filegroup( ":package-srcs", "//pkg/apis/autoscaling/install:all-srcs", "//pkg/apis/autoscaling/v1:all-srcs", + "//pkg/apis/autoscaling/v2alpha1:all-srcs", "//pkg/apis/autoscaling/validation:all-srcs", ], tags = ["automanaged"], diff --git a/pkg/apis/autoscaling/annotations.go b/pkg/apis/autoscaling/annotations.go new file mode 100644 index 0000000000..0acfb04ca8 --- /dev/null +++ b/pkg/apis/autoscaling/annotations.go @@ -0,0 +1,30 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 autoscaling + +// OtherMetricsAnnotation is the annotation which holds non-CPU-utilization HPA metric +// specs when converting the `Metrics` field from autoscaling/v2alpha1 +const OtherMetricsAnnotation = "autoscaling.alpha.kubernetes.io/metrics" + +// CurrentMetricsAnnotation is the annotation which holds non-CPU-utilization HPA metric +// statuses when converting the `CurrentMetrics` field from autoscaling/v2alpha1 +const CurrentMetricsAnnotation = "autoscaling.alpha.kubernetes.io/current-metrics" + +// DefaultCPUUtilization is the default value for CPU utilization, provided no other +// metrics are present. This is here because it's used by both the v2alpha1 defaulting +// logic, and the pseudo-defaulting done in v1 conversion. +const DefaultCPUUtilization = 80 diff --git a/pkg/apis/autoscaling/install/BUILD b/pkg/apis/autoscaling/install/BUILD index 42b2c1bf0e..cb6a73502d 100644 --- a/pkg/apis/autoscaling/install/BUILD +++ b/pkg/apis/autoscaling/install/BUILD @@ -15,6 +15,7 @@ go_library( "//pkg/api:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//pkg/apis/autoscaling/v1:go_default_library", + "//pkg/apis/autoscaling/v2alpha1:go_default_library", "//vendor:k8s.io/apimachinery/pkg/apimachinery/announced", "//vendor:k8s.io/apimachinery/pkg/apimachinery/registered", "//vendor:k8s.io/apimachinery/pkg/runtime", diff --git a/pkg/apis/autoscaling/install/install.go b/pkg/apis/autoscaling/install/install.go index a97c963b8c..3625e246c9 100644 --- a/pkg/apis/autoscaling/install/install.go +++ b/pkg/apis/autoscaling/install/install.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/autoscaling" "k8s.io/kubernetes/pkg/apis/autoscaling/v1" + "k8s.io/kubernetes/pkg/apis/autoscaling/v2alpha1" ) func init() { @@ -36,12 +37,13 @@ func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *r if err := announced.NewGroupMetaFactory( &announced.GroupMetaFactoryArgs{ GroupName: autoscaling.GroupName, - VersionPreferenceOrder: []string{v1.SchemeGroupVersion.Version}, + VersionPreferenceOrder: []string{v1.SchemeGroupVersion.Version, v2alpha1.SchemeGroupVersion.Version}, ImportPrefix: "k8s.io/kubernetes/pkg/apis/autoscaling", AddInternalObjectsToScheme: autoscaling.AddToScheme, }, announced.VersionToSchemeFunc{ - v1.SchemeGroupVersion.Version: v1.AddToScheme, + v1.SchemeGroupVersion.Version: v1.AddToScheme, + v2alpha1.SchemeGroupVersion.Version: v2alpha1.AddToScheme, }, ).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil { panic(err) diff --git a/pkg/apis/autoscaling/types.go b/pkg/apis/autoscaling/types.go index 55af1907b3..65cdb54beb 100644 --- a/pkg/apis/autoscaling/types.go +++ b/pkg/apis/autoscaling/types.go @@ -17,7 +17,9 @@ limitations under the License. package autoscaling import ( + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" ) // Scale represents a scaling request for a resource. @@ -67,68 +69,237 @@ type CrossVersionObjectReference struct { APIVersion string } -// specification of a horizontal pod autoscaler. +// HorizontalPodAutoscalerSpec describes the desired functionality of the HorizontalPodAutoscaler. type HorizontalPodAutoscalerSpec struct { - // reference to scaled resource; horizontal pod autoscaler will learn the current resource consumption - // and will set the desired number of pods by using its Scale subresource. + // ScaleTargetRef points to the target resource to scale, and is used to the pods for which metrics + // should be collected, as well as to actually change the replica count. ScaleTargetRef CrossVersionObjectReference - // lower limit for the number of pods that can be set by the autoscaler, default 1. + // MinReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. + // It defaults to 1 pod. // +optional MinReplicas *int32 - // upper limit for the number of pods that can be set by the autoscaler. It cannot be smaller than MinReplicas. + // MaxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. + // It cannot be less that minReplicas. MaxReplicas int32 - // target average CPU utilization (represented as a percentage of requested CPU) over all the pods; - // if not specified the default autoscaling policy will be used. + // Metrics contains the specifications for which to use to calculate the + // desired replica count (the maximum replica count across all metrics will + // be used). The desired replica count is calculated multiplying the + // ratio between the target value and the current value by the current + // number of pods. Ergo, metrics used must decrease as the pod count is + // increased, and vice-versa. See the individual metric source types for + // more information about how each type of metric must respond. // +optional - TargetCPUUtilizationPercentage *int32 + Metrics []MetricSpec } -// current status of a horizontal pod autoscaler +// MetricSourceType indicates the type of metric. +type MetricSourceType string + +var ( + // ObjectMetricSourceType is a metric describing a kubernetes object + // (for example, hits-per-second on an Ingress object). + ObjectMetricSourceType MetricSourceType = "Object" + // PodsMetricSourceType is a metric describing each pod in the current scale + // target (for example, transactions-processed-per-second). The values + // will be averaged together before being compared to the target value. + PodsMetricSourceType MetricSourceType = "Pods" + // ResourceMetricSourceType is a resource metric known to Kubernetes, as + // specified in requests and limits, describing each pod in the current + // scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics (the "pods" source). + ResourceMetricSourceType MetricSourceType = "Resource" +) + +// MetricSpec specifies how to scale based on a single metric +// (only `type` and one other matching field should be set at once). +type MetricSpec struct { + // Type is the type of metric source. It should match one of the fields below. + Type MetricSourceType + + // Object refers to a metric describing a single kubernetes object + // (for example, hits-per-second on an Ingress object). + // +optional + Object *ObjectMetricSource + // Pods refers to a metric describing each pod in the current scale target + // (for example, transactions-processed-per-second). The values will be + // averaged together before being compared to the target value. + // +optional + Pods *PodsMetricSource + // Resource refers to a resource metric (such as those specified in + // requests and limits) known to Kubernetes describing each pod in the + // current scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics using the "pods" source. + // +optional + Resource *ResourceMetricSource +} + +// ObjectMetricSource indicates how to scale on a metric describing a +// kubernetes object (for example, hits-per-second on an Ingress object). +type ObjectMetricSource struct { + // Target is the described Kubernetes object. + Target CrossVersionObjectReference + + // MetricName is the name of the metric in question. + MetricName string + // TargetValue is the target value of the metric (as a quantity). + TargetValue resource.Quantity +} + +// PodsMetricSource indicates how to scale on a metric describing each pod in +// the current scale target (for example, transactions-processed-per-second). +// The values will be averaged together before being compared to the target +// value. +type PodsMetricSource struct { + // MetricName is the name of the metric in question + MetricName string + // TargetAverageValue is the target value of the average of the + // metric across all relevant pods (as a quantity) + TargetAverageValue resource.Quantity +} + +// ResourceMetricSource indicates how to scale on a resource metric known to +// Kubernetes, as specified in requests and limits, describing each pod in the +// current scale target (e.g. CPU or memory). The values will be averaged +// together before being compared to the target. Such metrics are built in to +// Kubernetes, and have special scaling options on top of those available to +// normal per-pod metrics using the "pods" source. Only one "target" type +// should be set. +type ResourceMetricSource struct { + // Name is the name of the resource in question. + Name api.ResourceName + // TargetAverageUtilization is the target value of the average of the + // resource metric across all relevant pods, represented as a percentage of + // the requested value of the resource for the pods. + // +optional + TargetAverageUtilization *int32 + // TargetAverageValue is the the target value of the average of the + // resource metric across all relevant pods, as a raw value (instead of as + // a percentage of the request), similar to the "pods" metric source type. + // +optional + TargetAverageValue *resource.Quantity +} + +// HorizontalPodAutoscalerStatus describes the current status of a horizontal pod autoscaler. type HorizontalPodAutoscalerStatus struct { - // most recent generation observed by this autoscaler. + // ObservedGeneration is the most recent generation observed by this autoscaler. // +optional ObservedGeneration *int64 - // last time the HorizontalPodAutoscaler scaled the number of pods; + // LastScaleTime is the last time the HorizontalPodAutoscaler scaled the number of pods, // used by the autoscaler to control how often the number of pods is changed. // +optional LastScaleTime *metav1.Time - // current number of replicas of pods managed by this autoscaler. + // CurrentReplicas is current number of replicas of pods managed by this autoscaler, + // as last seen by the autoscaler. CurrentReplicas int32 - // desired number of replicas of pods managed by this autoscaler. + // DesiredReplicas is the desired number of replicas of pods managed by this autoscaler, + // as last calculated by the autoscaler. DesiredReplicas int32 - // current average CPU utilization over all pods, represented as a percentage of requested CPU, - // e.g. 70 means that an average pod is using now 70% of its requested CPU. + // CurrentMetrics is the last read state of the metrics used by this autoscaler. + CurrentMetrics []MetricStatus +} + +// MetricStatus describes the last-read state of a single metric. +type MetricStatus struct { + // Type is the type of metric source. It will match one of the fields below. + Type MetricSourceType + + // Object refers to a metric describing a single kubernetes object + // (for example, hits-per-second on an Ingress object). // +optional - CurrentCPUUtilizationPercentage *int32 + Object *ObjectMetricStatus + // Pods refers to a metric describing each pod in the current scale target + // (for example, transactions-processed-per-second). The values will be + // averaged together before being compared to the target value. + // +optional + Pods *PodsMetricStatus + // Resource refers to a resource metric (such as those specified in + // requests and limits) known to Kubernetes describing each pod in the + // current scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics using the "pods" source. + // +optional + Resource *ResourceMetricStatus +} + +// ObjectMetricStatus indicates the current value of a metric describing a +// kubernetes object (for example, hits-per-second on an Ingress object). +type ObjectMetricStatus struct { + // Target is the described Kubernetes object. + Target CrossVersionObjectReference + + // MetricName is the name of the metric in question. + MetricName string + // CurrentValue is the current value of the metric (as a quantity). + CurrentValue resource.Quantity +} + +// PodsMetricStatus indicates the current value of a metric describing each pod in +// the current scale target (for example, transactions-processed-per-second). +type PodsMetricStatus struct { + // MetricName is the name of the metric in question + MetricName string + // CurrentAverageValue is the current value of the average of the + // metric across all relevant pods (as a quantity) + CurrentAverageValue resource.Quantity +} + +// ResourceMetricStatus indicates the current value of a resource metric known to +// Kubernetes, as specified in requests and limits, describing each pod in the +// current scale target (e.g. CPU or memory). Such metrics are built in to +// Kubernetes, and have special scaling options on top of those available to +// normal per-pod metrics using the "pods" source. +type ResourceMetricStatus struct { + // Name is the name of the resource in question. + Name api.ResourceName + // CurrentAverageUtilization is the current value of the average of the + // resource metric across all relevant pods, represented as a percentage of + // the requested value of the resource for the pods. It will only be + // present if `targetAverageValue` was set in the corresponding metric + // specification. + // +optional + CurrentAverageUtilization *int32 + // CurrentAverageValue is the the current value of the average of the + // resource metric across all relevant pods, as a raw value (instead of as + // a percentage of the request), similar to the "pods" metric source type. + // It will always be set, regardless of the corresponding metric specification. + CurrentAverageValue resource.Quantity } // +genclient=true -// configuration of a horizontal pod autoscaler. +// HorizontalPodAutoscaler is the configuration for a horizontal pod +// autoscaler, which automatically manages the replica count of any resource +// implementing the scale subresource based on the metrics specified. type HorizontalPodAutoscaler struct { metav1.TypeMeta + // Metadata is the standard object metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata // +optional metav1.ObjectMeta - // behaviour of autoscaler. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status. + // Spec is the specification for the behaviour of the autoscaler. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status. // +optional Spec HorizontalPodAutoscalerSpec - // current information about the autoscaler. + // Status is the current information about the autoscaler. // +optional Status HorizontalPodAutoscalerStatus } -// list of horizontal pod autoscaler objects. +// HorizontalPodAutoscaler is a list of horizontal pod autoscaler objects. type HorizontalPodAutoscalerList struct { metav1.TypeMeta + // Metadata is the standard list metadata. // +optional metav1.ListMeta - // list of horizontal pod autoscaler objects. + // Items is the list of horizontal pod autoscaler objects. Items []HorizontalPodAutoscaler } diff --git a/pkg/apis/autoscaling/v1/BUILD b/pkg/apis/autoscaling/v1/BUILD index 297f4d3c43..2a35aede66 100644 --- a/pkg/apis/autoscaling/v1/BUILD +++ b/pkg/apis/autoscaling/v1/BUILD @@ -11,6 +11,7 @@ load( go_library( name = "go_default_library", srcs = [ + "conversion.go", "defaults.go", "doc.go", "generated.pb.go", @@ -24,6 +25,7 @@ go_library( ], tags = ["automanaged"], deps = [ + "//pkg/api:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//vendor:github.com/gogo/protobuf/proto", "//vendor:github.com/ugorji/go/codec", diff --git a/pkg/apis/autoscaling/v1/conversion.go b/pkg/apis/autoscaling/v1/conversion.go new file mode 100644 index 0000000000..5aeac55a04 --- /dev/null +++ b/pkg/apis/autoscaling/v1/conversion.go @@ -0,0 +1,216 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 v1 + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/autoscaling" +) + +func addConversionFuncs(scheme *runtime.Scheme) error { + // Add non-generated conversion functions + err := scheme.AddConversionFuncs( + Convert_autoscaling_HorizontalPodAutoscaler_To_v1_HorizontalPodAutoscaler, + Convert_v1_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler, + Convert_autoscaling_HorizontalPodAutoscalerSpec_To_v1_HorizontalPodAutoscalerSpec, + Convert_v1_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPodAutoscalerSpec, + Convert_autoscaling_HorizontalPodAutoscalerStatus_To_v1_HorizontalPodAutoscalerStatus, + Convert_v1_HorizontalPodAutoscalerStatus_To_autoscaling_HorizontalPodAutoscalerStatus, + ) + if err != nil { + return err + } + + return nil +} + +func Convert_autoscaling_HorizontalPodAutoscaler_To_v1_HorizontalPodAutoscaler(in *autoscaling.HorizontalPodAutoscaler, out *HorizontalPodAutoscaler, s conversion.Scope) error { + if err := autoConvert_autoscaling_HorizontalPodAutoscaler_To_v1_HorizontalPodAutoscaler(in, out, s); err != nil { + return err + } + + otherMetrics := make([]autoscaling.MetricSpec, 0, len(in.Spec.Metrics)) + for _, metric := range in.Spec.Metrics { + if metric.Type == autoscaling.ResourceMetricSourceType && metric.Resource != nil && metric.Resource.Name == api.ResourceCPU && metric.Resource.TargetAverageUtilization != nil { + continue + } + + otherMetrics = append(otherMetrics, metric) + } + + // NB: we need to save the status even if it maps to a CPU utilization status in order to save the raw value as well + if len(otherMetrics) > 0 || len(in.Status.CurrentMetrics) > 0 { + old := out.Annotations + out.Annotations = make(map[string]string, len(old)+2) + if old != nil { + for k, v := range old { + out.Annotations[k] = v + } + } + } + + if len(otherMetrics) > 0 { + otherMetricsEnc, err := json.Marshal(otherMetrics) + if err != nil { + return err + } + out.Annotations[autoscaling.OtherMetricsAnnotation] = string(otherMetricsEnc) + } + + if len(in.Status.CurrentMetrics) > 0 { + currentMetricsEnc, err := json.Marshal(in.Status.CurrentMetrics) + if err != nil { + return err + } + out.Annotations[autoscaling.CurrentMetricsAnnotation] = string(currentMetricsEnc) + } + + return nil +} + +func Convert_v1_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in *HorizontalPodAutoscaler, out *autoscaling.HorizontalPodAutoscaler, s conversion.Scope) error { + if err := autoConvert_v1_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in, out, s); err != nil { + return err + } + + if otherMetricsEnc, hasOtherMetrics := out.Annotations[autoscaling.OtherMetricsAnnotation]; hasOtherMetrics { + var otherMetrics []autoscaling.MetricSpec + if err := json.Unmarshal([]byte(otherMetricsEnc), &otherMetrics); err != nil { + return err + } + + for _, metric := range otherMetrics { + out.Spec.Metrics = append(out.Spec.Metrics, metric) + } + } + + if currentMetricsEnc, hasCurrentMetrics := out.Annotations[autoscaling.CurrentMetricsAnnotation]; hasCurrentMetrics { + // ignore any existing status values -- the ones here have more information + out.Status.CurrentMetrics = nil + if err := json.Unmarshal([]byte(currentMetricsEnc), &out.Status.CurrentMetrics); err != nil { + return err + } + } + + // autoscaling/v1 formerly had an implicit default applied in the controller. In v2alpha1, we apply it explicitly. + // We apply it here, explicitly, since we have access to the full set of metrics from the annotation. + if len(out.Spec.Metrics) == 0 { + // no other metrics, no explicit CPU value set + out.Spec.Metrics = []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + }, + }, + } + out.Spec.Metrics[0].Resource.TargetAverageUtilization = new(int32) + *out.Spec.Metrics[0].Resource.TargetAverageUtilization = autoscaling.DefaultCPUUtilization + } + + return nil +} + +func Convert_autoscaling_HorizontalPodAutoscalerSpec_To_v1_HorizontalPodAutoscalerSpec(in *autoscaling.HorizontalPodAutoscalerSpec, out *HorizontalPodAutoscalerSpec, s conversion.Scope) error { + if err := Convert_autoscaling_CrossVersionObjectReference_To_v1_CrossVersionObjectReference(&in.ScaleTargetRef, &out.ScaleTargetRef, s); err != nil { + return err + } + + out.MinReplicas = in.MinReplicas + out.MaxReplicas = in.MaxReplicas + + for _, metric := range in.Metrics { + if metric.Type == autoscaling.ResourceMetricSourceType && metric.Resource != nil && metric.Resource.Name == api.ResourceCPU { + if metric.Resource.TargetAverageUtilization != nil { + out.TargetCPUUtilizationPercentage = new(int32) + *out.TargetCPUUtilizationPercentage = *metric.Resource.TargetAverageUtilization + } + break + } + } + + return nil +} + +func Convert_v1_HorizontalPodAutoscalerSpec_To_autoscaling_HorizontalPodAutoscalerSpec(in *HorizontalPodAutoscalerSpec, out *autoscaling.HorizontalPodAutoscalerSpec, s conversion.Scope) error { + if err := Convert_v1_CrossVersionObjectReference_To_autoscaling_CrossVersionObjectReference(&in.ScaleTargetRef, &out.ScaleTargetRef, s); err != nil { + return err + } + + out.MinReplicas = in.MinReplicas + out.MaxReplicas = in.MaxReplicas + + if in.TargetCPUUtilizationPercentage != nil { + out.Metrics = []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + }, + }, + } + out.Metrics[0].Resource.TargetAverageUtilization = new(int32) + *out.Metrics[0].Resource.TargetAverageUtilization = *in.TargetCPUUtilizationPercentage + } + + return nil +} + +func Convert_autoscaling_HorizontalPodAutoscalerStatus_To_v1_HorizontalPodAutoscalerStatus(in *autoscaling.HorizontalPodAutoscalerStatus, out *HorizontalPodAutoscalerStatus, s conversion.Scope) error { + out.ObservedGeneration = in.ObservedGeneration + out.LastScaleTime = in.LastScaleTime + + out.CurrentReplicas = in.CurrentReplicas + out.DesiredReplicas = in.DesiredReplicas + + for _, metric := range in.CurrentMetrics { + if metric.Type == autoscaling.ResourceMetricSourceType && metric.Resource != nil && metric.Resource.Name == api.ResourceCPU { + if metric.Resource.CurrentAverageUtilization != nil { + + out.CurrentCPUUtilizationPercentage = new(int32) + *out.CurrentCPUUtilizationPercentage = *metric.Resource.CurrentAverageUtilization + } + } + } + return nil +} + +func Convert_v1_HorizontalPodAutoscalerStatus_To_autoscaling_HorizontalPodAutoscalerStatus(in *HorizontalPodAutoscalerStatus, out *autoscaling.HorizontalPodAutoscalerStatus, s conversion.Scope) error { + out.ObservedGeneration = in.ObservedGeneration + out.LastScaleTime = in.LastScaleTime + + out.CurrentReplicas = in.CurrentReplicas + out.DesiredReplicas = in.DesiredReplicas + + if in.CurrentCPUUtilizationPercentage != nil { + out.CurrentMetrics = []autoscaling.MetricStatus{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricStatus{ + Name: api.ResourceCPU, + }, + }, + } + out.CurrentMetrics[0].Resource.CurrentAverageUtilization = new(int32) + *out.CurrentMetrics[0].Resource.CurrentAverageUtilization = *in.CurrentCPUUtilizationPercentage + } + return nil +} diff --git a/pkg/apis/autoscaling/v1/defaults.go b/pkg/apis/autoscaling/v1/defaults.go index 0dde73214c..d423ad1253 100644 --- a/pkg/apis/autoscaling/v1/defaults.go +++ b/pkg/apis/autoscaling/v1/defaults.go @@ -32,4 +32,7 @@ func SetDefaults_HorizontalPodAutoscaler(obj *HorizontalPodAutoscaler) { minReplicas := int32(1) obj.Spec.MinReplicas = &minReplicas } + + // NB: we apply a default for CPU utilization in conversion because + // we need access to the annotations to properly apply the default. } diff --git a/pkg/apis/autoscaling/v1/register.go b/pkg/apis/autoscaling/v1/register.go index adb1d767f4..aaa225261c 100644 --- a/pkg/apis/autoscaling/v1/register.go +++ b/pkg/apis/autoscaling/v1/register.go @@ -34,7 +34,7 @@ func Resource(resource string) schema.GroupResource { } var ( - SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addDefaultingFuncs) + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addDefaultingFuncs, addConversionFuncs) AddToScheme = SchemeBuilder.AddToScheme ) diff --git a/pkg/apis/autoscaling/v2alpha1/BUILD b/pkg/apis/autoscaling/v2alpha1/BUILD new file mode 100644 index 0000000000..7caf0d9f24 --- /dev/null +++ b/pkg/apis/autoscaling/v2alpha1/BUILD @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", +) + +go_library( + name = "go_default_library", + srcs = [ + "defaults.go", + "doc.go", + "register.go", + "types.go", + ], + tags = ["automanaged"], + deps = [ + "//pkg/api/v1:go_default_library", + "//pkg/apis/autoscaling:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/resource", + "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", + "//vendor:k8s.io/apimachinery/pkg/runtime", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/pkg/apis/autoscaling/v2alpha1/defaults.go b/pkg/apis/autoscaling/v2alpha1/defaults.go new file mode 100644 index 0000000000..8764a9f42c --- /dev/null +++ b/pkg/apis/autoscaling/v2alpha1/defaults.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 v2alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/autoscaling" +) + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return scheme.AddDefaultingFuncs( + SetDefaults_HorizontalPodAutoscaler, + ) +} + +func SetDefaults_HorizontalPodAutoscaler(obj *HorizontalPodAutoscaler) { + if obj.Spec.MinReplicas == nil { + minReplicas := int32(1) + obj.Spec.MinReplicas = &minReplicas + } + + if len(obj.Spec.Metrics) == 0 { + utilizationDefaultVal := int32(autoscaling.DefaultCPUUtilization) + obj.Spec.Metrics = []MetricSpec{ + { + Type: ResourceMetricSourceType, + Resource: &ResourceMetricSource{ + Name: v1.ResourceCPU, + TargetAverageUtilization: &utilizationDefaultVal, + }, + }, + } + } +} diff --git a/pkg/apis/autoscaling/v2alpha1/doc.go b/pkg/apis/autoscaling/v2alpha1/doc.go new file mode 100644 index 0000000000..7c20d6dc11 --- /dev/null +++ b/pkg/apis/autoscaling/v2alpha1/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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. +*/ + +// +k8s:deepcopy-gen=package,register +// +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/autoscaling +// +k8s:openapi-gen=true + +package v2alpha1 // import "k8s.io/kubernetes/pkg/apis/autoscaling/v2alpha1" diff --git a/pkg/apis/autoscaling/v2alpha1/register.go b/pkg/apis/autoscaling/v2alpha1/register.go new file mode 100644 index 0000000000..cc2751bc07 --- /dev/null +++ b/pkg/apis/autoscaling/v2alpha1/register.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 v2alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/api/v1" +) + +// GroupName is the group name use in this package +const GroupName = "autoscaling" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v2alpha1"} + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addDefaultingFuncs) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &HorizontalPodAutoscaler{}, + &HorizontalPodAutoscalerList{}, + &v1.ListOptions{}, + &v1.DeleteOptions{}, + &metav1.GetOptions{}, + &metav1.ExportOptions{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/autoscaling/v2alpha1/types.go b/pkg/apis/autoscaling/v2alpha1/types.go new file mode 100644 index 0000000000..6f75afb189 --- /dev/null +++ b/pkg/apis/autoscaling/v2alpha1/types.go @@ -0,0 +1,269 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 v2alpha1 + +import ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api/v1" +) + +// CrossVersionObjectReference contains enough information to let you identify the referred resource. +type CrossVersionObjectReference struct { + // Kind of the referent; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds" + Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` + // Name of the referent; More info: http://kubernetes.io/docs/user-guide/identifiers#names + Name string `json:"name" protobuf:"bytes,2,opt,name=name"` + // API version of the referent + // +optional + APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,3,opt,name=apiVersion"` +} + +// HorizontalPodAutoscalerSpec describes the desired functionality of the HorizontalPodAutoscaler. +type HorizontalPodAutoscalerSpec struct { + // scaleTargetRef points to the target resource to scale, and is used to the pods for which metrics + // should be collected, as well as to actually change the replica count. + ScaleTargetRef CrossVersionObjectReference `json:"scaleTargetRef" protobuf:"bytes,1,opt,name=scaleTargetRef"` + // minReplicas is the lower limit for the number of replicas to which the autoscaler can scale down. + // It defaults to 1 pod. + // +optional + MinReplicas *int32 `json:"minReplicas,omitempty" protobuf:"varint,2,opt,name=minReplicas"` + // maxReplicas is the upper limit for the number of replicas to which the autoscaler can scale up. + // It cannot be less that minReplicas. + MaxReplicas int32 `json:"maxReplicas" protobuf:"varint,3,opt,name=maxReplicas"` + // metrics contains the specifications for which to use to calculate the + // desired replica count (the maximum replica count across all metrics will + // be used). The desired replica count is calculated multiplying the + // ratio between the target value and the current value by the current + // number of pods. Ergo, metrics used must decrease as the pod count is + // increased, and vice-versa. See the individual metric source types for + // more information about how each type of metric must respond. + // +optional + Metrics []MetricSpec `json:"metrics,omitempty" protobuf:"bytes,4,rep,name=metrics"` +} + +// MetricSourceType indicates the type of metric. +type MetricSourceType string + +var ( + // ObjectMetricSourceType is a metric describing a kubernetes object + // (for example, hits-per-second on an Ingress object). + ObjectMetricSourceType MetricSourceType = "Object" + // PodsMetricSourceType is a metric describing each pod in the current scale + // target (for example, transactions-processed-per-second). The values + // will be averaged together before being compared to the target value. + PodsMetricSourceType MetricSourceType = "Pods" + // ResourceMetricSourceType is a resource metric known to Kubernetes, as + // specified in requests and limits, describing each pod in the current + // scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics (the "pods" source). + ResourceMetricSourceType MetricSourceType = "Resource" +) + +// MetricSpec specifies how to scale based on a single metric +// (only `type` and one other matching field should be set at once). +type MetricSpec struct { + // type is the type of metric source. It should match one of the fields below. + Type MetricSourceType `json:"type" protobuf:"bytes,1,name=type"` + + // object refers to a metric describing a single kubernetes object + // (for example, hits-per-second on an Ingress object). + // +optional + Object *ObjectMetricSource `json:"object,omitempty" protobuf:"bytes,2,opt,name=object"` + // pods refers to a metric describing each pod in the current scale target + // (for example, transactions-processed-per-second). The values will be + // averaged together before being compared to the target value. + // +optional + Pods *PodsMetricSource `json:"pods,omitempty" protobuf:"bytes,3,opt,name=pods"` + // resource refers to a resource metric (such as those specified in + // requests and limits) known to Kubernetes describing each pod in the + // current scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics using the "pods" source. + // +optional + Resource *ResourceMetricSource `json:"resource,omitempty" protobuf:"bytes,4,opt,name=resource"` +} + +// ObjectMetricSource indicates how to scale on a metric describing a +// kubernetes object (for example, hits-per-second on an Ingress object). +type ObjectMetricSource struct { + // target is the described Kubernetes object. + Target CrossVersionObjectReference `json:"target" protobuf:"bytes,1,name=target"` + + // metricName is the name of the metric in question. + MetricName string `json:"metricName" protobuf:"bytes,2,name=metricName"` + // targetValue is the target value of the metric (as a quantity). + TargetValue resource.Quantity `json:"targetValue" protobuf:"bytes,3,name=targetValue"` +} + +// PodsMetricSource indicates how to scale on a metric describing each pod in +// the current scale target (for example, transactions-processed-per-second). +// The values will be averaged together before being compared to the target +// value. +type PodsMetricSource struct { + // metricName is the name of the metric in question + MetricName string `json:"metricName" protobuf:"bytes,1,name=metricName"` + // targetAverageValue is the target value of the average of the + // metric across all relevant pods (as a quantity) + TargetAverageValue resource.Quantity `json:"targetAverageValue" protobuf:"bytes,2,name=targetAverageValue"` +} + +// ResourceMetricSource indicates how to scale on a resource metric known to +// Kubernetes, as specified in requests and limits, describing each pod in the +// current scale target (e.g. CPU or memory). The values will be averaged +// together before being compared to the target. Such metrics are built in to +// Kubernetes, and have special scaling options on top of those available to +// normal per-pod metrics using the "pods" source. Only one "target" type +// should be set. +type ResourceMetricSource struct { + // name is the name of the resource in question. + Name v1.ResourceName `json:"name" protobuf:"bytes,1,name=name"` + // targetAverageUtilization is the target value of the average of the + // resource metric across all relevant pods, represented as a percentage of + // the requested value of the resource for the pods. + // +optional + TargetAverageUtilization *int32 `json:"targetAverageUtilization,omitempty" protobuf:"varint,2,opt,name=targetAverageUtilization"` + // targetAverageValue is the the target value of the average of the + // resource metric across all relevant pods, as a raw value (instead of as + // a percentage of the request), similar to the "pods" metric source type. + // +optional + TargetAverageValue *resource.Quantity `json:"targetAverageValue,omitempty" protobuf:"bytes,3,opt,name=targetAverageValue"` +} + +// HorizontalPodAutoscalerStatus describes the current status of a horizontal pod autoscaler. +type HorizontalPodAutoscalerStatus struct { + // observedGeneration is the most recent generation observed by this autoscaler. + // +optional + ObservedGeneration *int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + + // lastScaleTime is the last time the HorizontalPodAutoscaler scaled the number of pods, + // used by the autoscaler to control how often the number of pods is changed. + // +optional + LastScaleTime *metav1.Time `json:"lastScaleTime,omitempty" protobuf:"bytes,2,opt,name=lastScaleTime"` + + // currentReplicas is current number of replicas of pods managed by this autoscaler, + // as last seen by the autoscaler. + CurrentReplicas int32 `json:"currentReplicas" protobuf:"varint,3,opt,name=currentReplicas"` + + // desiredReplicas is the desired number of replicas of pods managed by this autoscaler, + // as last calculated by the autoscaler. + DesiredReplicas int32 `json:"desiredReplicas" protobuf:"varint,4,opt,name=desiredReplicas"` + + // currentMetrics is the last read state of the metrics used by this autoscaler. + CurrentMetrics []MetricStatus `json:"currentMetrics" protobuf:"bytes,5,rep,name=currentMetrics"` +} + +// MetricStatus describes the last-read state of a single metric. +type MetricStatus struct { + // type is the type of metric source. It will match one of the fields below. + Type MetricSourceType `json:"type" protobuf:"bytes,1,name=type"` + + // object refers to a metric describing a single kubernetes object + // (for example, hits-per-second on an Ingress object). + // +optional + Object *ObjectMetricStatus `json:"object,omitempty" protobuf:"bytes,2,opt,name=object"` + // pods refers to a metric describing each pod in the current scale target + // (for example, transactions-processed-per-second). The values will be + // averaged together before being compared to the target value. + // +optional + Pods *PodsMetricStatus `json:"pods,omitempty" protobuf:"bytes,3,opt,name=pods"` + // resource refers to a resource metric (such as those specified in + // requests and limits) known to Kubernetes describing each pod in the + // current scale target (e.g. CPU or memory). Such metrics are built in to + // Kubernetes, and have special scaling options on top of those available + // to normal per-pod metrics using the "pods" source. + // +optional + Resource *ResourceMetricStatus `json:"resource,omitempty" protobuf:"bytes,4,opt,name=resource"` +} + +// ObjectMetricStatus indicates the current value of a metric describing a +// kubernetes object (for example, hits-per-second on an Ingress object). +type ObjectMetricStatus struct { + // target is the described Kubernetes object. + Target CrossVersionObjectReference `json:"target" protobuf:"bytes,1,name=target"` + + // metricName is the name of the metric in question. + MetricName string `json:"metricName" protobuf:"bytes,2,name=metricName"` + // currentValue is the current value of the metric (as a quantity). + CurrentValue resource.Quantity `json:"currentValue" protobuf:"bytes,3,name=currentValue"` +} + +// PodsMetricStatus indicates the current value of a metric describing each pod in +// the current scale target (for example, transactions-processed-per-second). +type PodsMetricStatus struct { + // metricName is the name of the metric in question + MetricName string `json:"metricName" protobuf:"bytes,1,name=metricName"` + // currentAverageValue is the current value of the average of the + // metric across all relevant pods (as a quantity) + CurrentAverageValue resource.Quantity `json:"currentAverageValue" protobuf:"bytes,2,name=currentAverageValue"` +} + +// ResourceMetricStatus indicates the current value of a resource metric known to +// Kubernetes, as specified in requests and limits, describing each pod in the +// current scale target (e.g. CPU or memory). Such metrics are built in to +// Kubernetes, and have special scaling options on top of those available to +// normal per-pod metrics using the "pods" source. +type ResourceMetricStatus struct { + // name is the name of the resource in question. + Name v1.ResourceName `json:"name" protobuf:"bytes,1,name=name"` + // currentAverageUtilization is the current value of the average of the + // resource metric across all relevant pods, represented as a percentage of + // the requested value of the resource for the pods. It will only be + // present if `targetAverageValue` was set in the corresponding metric + // specification. + // +optional + CurrentAverageUtilization *int32 `json:"currentAverageUtilization,omitempty" protobuf:"bytes,2,opt,name=currentAverageUtilization"` + // currentAverageValue is the the current value of the average of the + // resource metric across all relevant pods, as a raw value (instead of as + // a percentage of the request), similar to the "pods" metric source type. + // It will always be set, regardless of the corresponding metric specification. + CurrentAverageValue resource.Quantity `json:"currentAverageValue" protobuf:"bytes,3,name=currentAverageValue"` +} + +// +genclient=true + +// HorizontalPodAutoscaler is the configuration for a horizontal pod +// autoscaler, which automatically manages the replica count of any resource +// implementing the scale subresource based on the metrics specified. +type HorizontalPodAutoscaler struct { + metav1.TypeMeta `json:",inline"` + // metadata is the standard object metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // spec is the specification for the behaviour of the autoscaler. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status. + // +optional + Spec HorizontalPodAutoscalerSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // status is the current information about the autoscaler. + // +optional + Status HorizontalPodAutoscalerStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// HorizontalPodAutoscaler is a list of horizontal pod autoscaler objects. +type HorizontalPodAutoscalerList struct { + metav1.TypeMeta `json:",inline"` + // metadata is the standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // items is the list of horizontal pod autoscaler objects. + Items []HorizontalPodAutoscaler `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/pkg/apis/autoscaling/validation/BUILD b/pkg/apis/autoscaling/validation/BUILD index aad269da05..b91ae1cad1 100644 --- a/pkg/apis/autoscaling/validation/BUILD +++ b/pkg/apis/autoscaling/validation/BUILD @@ -15,9 +15,8 @@ go_library( deps = [ "//pkg/api/validation:go_default_library", "//pkg/apis/autoscaling:go_default_library", - "//pkg/apis/extensions/v1beta1:go_default_library", - "//pkg/controller/podautoscaler:go_default_library", "//vendor:k8s.io/apimachinery/pkg/api/validation/path", + "//vendor:k8s.io/apimachinery/pkg/util/sets", "//vendor:k8s.io/apimachinery/pkg/util/validation/field", ], ) @@ -28,8 +27,9 @@ go_test( library = ":go_default_library", tags = ["automanaged"], deps = [ + "//pkg/api:go_default_library", "//pkg/apis/autoscaling:go_default_library", - "//pkg/controller/podautoscaler:go_default_library", + "//vendor:k8s.io/apimachinery/pkg/api/resource", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", ], ) diff --git a/pkg/apis/autoscaling/validation/validation.go b/pkg/apis/autoscaling/validation/validation.go index 526423aba7..e317fed2a5 100644 --- a/pkg/apis/autoscaling/validation/validation.go +++ b/pkg/apis/autoscaling/validation/validation.go @@ -17,14 +17,13 @@ limitations under the License. package validation import ( - "encoding/json" + "strings" pathvalidation "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" apivalidation "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/apis/autoscaling" - "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" - "k8s.io/kubernetes/pkg/controller/podautoscaler" ) func ValidateScale(scale *autoscaling.Scale) field.ErrorList { @@ -53,12 +52,12 @@ func validateHorizontalPodAutoscalerSpec(autoscaler autoscaling.HorizontalPodAut if autoscaler.MinReplicas != nil && autoscaler.MaxReplicas < *autoscaler.MinReplicas { allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), autoscaler.MaxReplicas, "must be greater than or equal to `minReplicas`")) } - if autoscaler.TargetCPUUtilizationPercentage != nil && *autoscaler.TargetCPUUtilizationPercentage < 1 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("targetCPUUtilizationPercentage"), autoscaler.TargetCPUUtilizationPercentage, "must be greater than 0")) - } if refErrs := ValidateCrossVersionObjectReference(autoscaler.ScaleTargetRef, fldPath.Child("scaleTargetRef")); len(refErrs) > 0 { allErrs = append(allErrs, refErrs...) } + if refErrs := validateMetrics(autoscaler.Metrics, fldPath.Child("metrics")); len(refErrs) > 0 { + allErrs = append(allErrs, refErrs...) + } return allErrs } @@ -83,34 +82,9 @@ func ValidateCrossVersionObjectReference(ref autoscaling.CrossVersionObjectRefer return allErrs } -func validateHorizontalPodAutoscalerAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - if annotationValue, found := annotations[podautoscaler.HpaCustomMetricsTargetAnnotationName]; found { - // Try to parse the annotation - var targetList v1beta1.CustomMetricTargetList - if err := json.Unmarshal([]byte(annotationValue), &targetList); err != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("annotations"), annotations, "failed to parse custom metrics target annotation")) - } else { - if len(targetList.Items) == 0 { - allErrs = append(allErrs, field.Required(fldPath.Child("annotations", "items"), "custom metrics target must not be empty")) - } - for _, target := range targetList.Items { - if target.Name == "" { - allErrs = append(allErrs, field.Required(fldPath.Child("annotations", "items", "name"), "missing custom metric target name")) - } - if target.TargetValue.MilliValue() <= 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("annotations", "items", "value"), target.TargetValue, "custom metric target value must be greater than 0")) - } - } - } - } - return allErrs -} - func ValidateHorizontalPodAutoscaler(autoscaler *autoscaling.HorizontalPodAutoscaler) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&autoscaler.ObjectMeta, true, ValidateHorizontalPodAutoscalerName, field.NewPath("metadata")) allErrs = append(allErrs, validateHorizontalPodAutoscalerSpec(autoscaler.Spec, field.NewPath("spec"))...) - allErrs = append(allErrs, validateHorizontalPodAutoscalerAnnotations(autoscaler.Annotations, field.NewPath("metadata"))...) return allErrs } @@ -127,3 +101,124 @@ func ValidateHorizontalPodAutoscalerStatusUpdate(newAutoscaler, oldAutoscaler *a allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredReplicas), field.NewPath("status", "desiredReplicasa"))...) return allErrs } + +func validateMetrics(metrics []autoscaling.MetricSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + for i, metricSpec := range metrics { + idxPath := fldPath.Index(i) + if targetErrs := validateMetricSpec(metricSpec, idxPath); len(targetErrs) > 0 { + allErrs = append(allErrs, targetErrs...) + } + } + + return allErrs +} + +var validMetricSourceTypes = sets.NewString(string(autoscaling.ObjectMetricSourceType), string(autoscaling.PodsMetricSourceType), string(autoscaling.ResourceMetricSourceType)) +var validMetricSourceTypesList = validMetricSourceTypes.List() + +func validateMetricSpec(spec autoscaling.MetricSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(string(spec.Type)) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must specify a metric source type")) + } + + if !validMetricSourceTypes.Has(string(spec.Type)) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), spec.Type, validMetricSourceTypesList)) + } + + typesPresent := sets.NewString() + if spec.Object != nil { + typesPresent.Insert("object") + if typesPresent.Len() == 1 { + allErrs = append(allErrs, validateObjectSource(spec.Object, fldPath.Child("object"))...) + } + } + + if spec.Pods != nil { + typesPresent.Insert("pods") + if typesPresent.Len() == 1 { + allErrs = append(allErrs, validatePodsSource(spec.Pods, fldPath.Child("pods"))...) + } + } + + if spec.Resource != nil { + typesPresent.Insert("resource") + if typesPresent.Len() == 1 { + allErrs = append(allErrs, validateResourceSource(spec.Resource, fldPath.Child("resource"))...) + } + } + + expectedField := strings.ToLower(string(spec.Type)) + + if !typesPresent.Has(expectedField) { + allErrs = append(allErrs, field.Required(fldPath.Child(expectedField), "must populate information for the given metric source")) + } + + if typesPresent.Len() != 1 { + typesPresent.Delete(expectedField) + for typ := range typesPresent { + allErrs = append(allErrs, field.Forbidden(fldPath.Child(typ), "must populate the given metric source only")) + } + } + + return allErrs +} + +func validateObjectSource(src *autoscaling.ObjectMetricSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, ValidateCrossVersionObjectReference(src.Target, fldPath.Child("target"))...) + + if len(src.MetricName) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("metricName"), "must specify a metric name")) + } + + if src.TargetValue.Sign() != 1 { + allErrs = append(allErrs, field.Required(fldPath.Child("targetValue"), "must specify a positive target value")) + } + + return allErrs +} + +func validatePodsSource(src *autoscaling.PodsMetricSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(src.MetricName) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("metricName"), "must specify a metric name")) + } + + if src.TargetAverageValue.Sign() != 1 { + allErrs = append(allErrs, field.Required(fldPath.Child("targetAverageValue"), "must specify a positive target value")) + } + + return allErrs +} + +func validateResourceSource(src *autoscaling.ResourceMetricSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(src.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "must specify a resource name")) + } + + if src.TargetAverageUtilization == nil && src.TargetAverageValue == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("targetAverageUtilization"), "must set either a target raw value or a target utilization")) + } + + if src.TargetAverageUtilization != nil && *src.TargetAverageUtilization < 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("targetAverageUtilization"), src.TargetAverageUtilization, "must be greater than 0")) + } + + if src.TargetAverageUtilization != nil && src.TargetAverageValue != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("targetAverageValue"), "may not set both a target raw value and a target utilization")) + } + + if src.TargetAverageValue != nil && src.TargetAverageValue.Sign() != 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("targetAverageValue"), src.TargetAverageValue, "must be positive")) + } + + return allErrs +} diff --git a/pkg/apis/autoscaling/validation/validation_test.go b/pkg/apis/autoscaling/validation/validation_test.go index 9c7acdf6a2..4a70459db7 100644 --- a/pkg/apis/autoscaling/validation/validation_test.go +++ b/pkg/apis/autoscaling/validation/validation_test.go @@ -20,9 +20,10 @@ import ( "strings" "testing" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/autoscaling" - "k8s.io/kubernetes/pkg/controller/podautoscaler" ) func TestValidateScale(t *testing.T) { @@ -101,9 +102,17 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { Kind: "ReplicationController", Name: "myrc", }, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(70), + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + }, + }, + }, }, }, { @@ -124,9 +133,6 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "myautoscaler", Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - podautoscaler.HpaCustomMetricsTargetAnnotationName: "{\"items\":[{\"name\":\"qps\",\"value\":\"20\"}]}", - }, }, Spec: autoscaling.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscaling.CrossVersionObjectReference{ @@ -135,6 +141,65 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { }, MinReplicas: newInt32(1), MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageValue: resource.NewMilliQuantity(300, resource.DecimalSI), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myautoscaler", + Namespace: metav1.NamespaceDefault, + }, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.PodsMetricSourceType, + Pods: &autoscaling.PodsMetricSource{ + MetricName: "some/metric", + TargetAverageValue: *resource.NewMilliQuantity(300, resource.DecimalSI), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "myautoscaler", + Namespace: metav1.NamespaceDefault, + }, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ObjectMetricSourceType, + Object: &autoscaling.ObjectMetricSource{ + Target: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MetricName: "some/metric", + TargetValue: *resource.NewMilliQuantity(300, resource.DecimalSI), + }, + }, + }, }, }, } @@ -152,10 +217,18 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc"}, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(70), + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + }, + }, + }, }, }, msg: "scaleTargetRef.kind: Required", @@ -164,10 +237,18 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "..", Name: "myrc"}, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(70), + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "..", Name: "myrc"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + }, + }, + }, }, }, msg: "scaleTargetRef.kind: Invalid", @@ -176,10 +257,18 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "ReplicationController"}, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(70), + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + }, + }, + }, }, }, msg: "scaleTargetRef.name: Required", @@ -188,10 +277,18 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "ReplicationController", Name: ".."}, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(70), + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Kind: "ReplicationController", Name: ".."}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + }, + }, + }, }, }, msg: "scaleTargetRef.name: Invalid", @@ -231,10 +328,21 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { Namespace: metav1.NamespaceDefault, }, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{}, - MinReplicas: newInt32(1), - MaxReplicas: 5, - TargetCPUUtilizationPercentage: newInt32(-70), + ScaleTargetRef: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(-70), + }, + }, + }, }, }, msg: "must be greater than 0", @@ -244,80 +352,220 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "myautoscaler", Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - podautoscaler.HpaCustomMetricsTargetAnnotationName: "broken", - }, }, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{ - Kind: "ReplicationController", - Name: "myrc", + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(70), + TargetAverageValue: resource.NewMilliQuantity(300, resource.DecimalSI), + }, + }, }, - MinReplicas: newInt32(1), - MaxReplicas: 5, }, }, - msg: "failed to parse custom metrics target annotation", + msg: "may not set both a target raw value and a target utilization", }, { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myautoscaler", - Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - podautoscaler.HpaCustomMetricsTargetAnnotationName: "{}", - }, - }, + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{ - Kind: "ReplicationController", - Name: "myrc", + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + TargetAverageUtilization: newInt32(70), + }, + }, }, - MinReplicas: newInt32(1), - MaxReplicas: 5, }, }, - msg: "custom metrics target must not be empty", + msg: "must specify a resource name", }, { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myautoscaler", - Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - podautoscaler.HpaCustomMetricsTargetAnnotationName: "{\"items\":[{\"value\":\"20\"}]}", - }, - }, + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{ - Kind: "ReplicationController", - Name: "myrc", + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: newInt32(-10), + }, + }, }, - MinReplicas: newInt32(1), - MaxReplicas: 5, }, }, - msg: "missing custom metric target name", + msg: "must be greater than 0", }, { horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myautoscaler", - Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - podautoscaler.HpaCustomMetricsTargetAnnotationName: "{\"items\":[{\"name\":\"qps\",\"value\":\"0\"}]}", - }, - }, + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, Spec: autoscaling.HorizontalPodAutoscalerSpec{ - ScaleTargetRef: autoscaling.CrossVersionObjectReference{ - Kind: "ReplicationController", - Name: "myrc", + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + }, + }, }, - MinReplicas: newInt32(1), - MaxReplicas: 5, }, }, - msg: "custom metric target value must be greater than 0", + msg: "must set either a target raw value or a target utilization", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.PodsMetricSourceType, + Pods: &autoscaling.PodsMetricSource{ + TargetAverageValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + msg: "must specify a metric name", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.PodsMetricSourceType, + Pods: &autoscaling.PodsMetricSource{ + MetricName: "some/metric", + }, + }, + }, + }, + }, + msg: "must specify a positive target value", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ObjectMetricSourceType, + Object: &autoscaling.ObjectMetricSource{ + Target: autoscaling.CrossVersionObjectReference{ + Name: "myrc", + }, + MetricName: "some/metric", + TargetValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + msg: "target.kind: Required", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ObjectMetricSourceType, + Object: &autoscaling.ObjectMetricSource{ + Target: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + TargetValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + msg: "must specify a metric name", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + {}, + }, + }, + }, + msg: "must specify a metric source type", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.MetricSourceType("InvalidType"), + }, + }, + }, + }, + msg: "type: Unsupported value", + }, + { + horizontalPodAutoscaler: autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), + }, + Pods: &autoscaling.PodsMetricSource{ + MetricName: "some/metric", + TargetAverageValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + }, + }, + }, + msg: "must populate the given metric source only", }, } @@ -329,6 +577,58 @@ func TestValidateHorizontalPodAutoscaler(t *testing.T) { t.Errorf("unexpected error: %q, expected: %q", errs[0], c.msg) } } + + sourceTypes := map[autoscaling.MetricSourceType]autoscaling.MetricSpec{ + autoscaling.ResourceMetricSourceType: { + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + autoscaling.PodsMetricSourceType: { + Pods: &autoscaling.PodsMetricSource{ + MetricName: "some/metric", + TargetAverageValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + autoscaling.ObjectMetricSourceType: { + Object: &autoscaling.ObjectMetricSource{ + Target: autoscaling.CrossVersionObjectReference{ + Kind: "ReplicationController", + Name: "myrc", + }, + MetricName: "some/metric", + TargetValue: *resource.NewMilliQuantity(100, resource.DecimalSI), + }, + }, + } + + for correctType, spec := range sourceTypes { + for incorrectType := range sourceTypes { + if correctType == incorrectType { + continue + } + + spec.Type = incorrectType + + errs := ValidateHorizontalPodAutoscaler(&autoscaling.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "myautoscaler", Namespace: metav1.NamespaceDefault}, + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{Name: "myrc", Kind: "ReplicationController"}, + MinReplicas: newInt32(1), + MaxReplicas: 5, Metrics: []autoscaling.MetricSpec{spec}, + }, + }) + + expectedMsg := "must populate information for the given metric source" + + if len(errs) == 0 { + t.Errorf("expected failure with type of %v and spec for %v", incorrectType, correctType) + } else if !strings.Contains(errs[0].Error(), expectedMsg) { + t.Errorf("unexpected error: %q, expected %q", errs[0], expectedMsg) + } + } + } } func newInt32(val int32) *int32 { diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/BUILD b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/BUILD index 2d9da84dfb..5c25a17e15 100644 --- a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/BUILD +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/BUILD @@ -14,6 +14,7 @@ go_test( library = ":go_default_library", tags = ["automanaged"], deps = [ + "//pkg/api:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//pkg/apis/autoscaling/v1:go_default_library", "//pkg/registry/registrytest:go_default_library", diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage_test.go b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage_test.go index 5bd93bae21..a3af118378 100644 --- a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage_test.go +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage_test.go @@ -20,6 +20,7 @@ import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/autoscaling" // Ensure that autoscaling/v1 package is initialized. "k8s.io/apimachinery/pkg/fields" @@ -55,8 +56,16 @@ func validNewHorizontalPodAutoscaler(name string) *autoscaling.HorizontalPodAuto Kind: "ReplicationController", Name: "myrc", }, - MaxReplicas: 5, - TargetCPUUtilizationPercentage: &cpu, + MaxReplicas: 5, + Metrics: []autoscaling.MetricSpec{ + { + Type: autoscaling.ResourceMetricSourceType, + Resource: &autoscaling.ResourceMetricSource{ + Name: api.ResourceCPU, + TargetAverageUtilization: &cpu, + }, + }, + }, }, } } diff --git a/pkg/registry/autoscaling/rest/BUILD b/pkg/registry/autoscaling/rest/BUILD index 488f898e20..6e3bc586b5 100644 --- a/pkg/registry/autoscaling/rest/BUILD +++ b/pkg/registry/autoscaling/rest/BUILD @@ -15,6 +15,7 @@ go_library( "//pkg/api:go_default_library", "//pkg/apis/autoscaling:go_default_library", "//pkg/apis/autoscaling/v1:go_default_library", + "//pkg/apis/autoscaling/v2alpha1:go_default_library", "//pkg/registry/autoscaling/horizontalpodautoscaler/storage:go_default_library", "//vendor:k8s.io/apiserver/pkg/registry/generic", "//vendor:k8s.io/apiserver/pkg/registry/rest", diff --git a/pkg/registry/autoscaling/rest/storage_autoscaling.go b/pkg/registry/autoscaling/rest/storage_autoscaling.go index e413ca20e5..74e5e778b9 100644 --- a/pkg/registry/autoscaling/rest/storage_autoscaling.go +++ b/pkg/registry/autoscaling/rest/storage_autoscaling.go @@ -23,6 +23,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/autoscaling" autoscalingapiv1 "k8s.io/kubernetes/pkg/apis/autoscaling/v1" + autoscalingapiv2alpha1 "k8s.io/kubernetes/pkg/apis/autoscaling/v2alpha1" horizontalpodautoscalerstore "k8s.io/kubernetes/pkg/registry/autoscaling/horizontalpodautoscaler/storage" ) @@ -35,6 +36,10 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource genericapise apiGroupInfo.VersionedResourcesStorageMap[autoscalingapiv1.SchemeGroupVersion.Version] = p.v1Storage(apiResourceConfigSource, restOptionsGetter) apiGroupInfo.GroupMeta.GroupVersion = autoscalingapiv1.SchemeGroupVersion } + if apiResourceConfigSource.AnyResourcesForVersionEnabled(autoscalingapiv2alpha1.SchemeGroupVersion) { + apiGroupInfo.VersionedResourcesStorageMap[autoscalingapiv2alpha1.SchemeGroupVersion.Version] = p.v2alpha1Storage(apiResourceConfigSource, restOptionsGetter) + apiGroupInfo.GroupMeta.GroupVersion = autoscalingapiv2alpha1.SchemeGroupVersion + } return apiGroupInfo, true } @@ -51,6 +56,18 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource genericapiserver. return storage } +func (p RESTStorageProvider) v2alpha1Storage(apiResourceConfigSource genericapiserver.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) map[string]rest.Storage { + version := autoscalingapiv2alpha1.SchemeGroupVersion + + storage := map[string]rest.Storage{} + if apiResourceConfigSource.ResourceEnabled(version.WithResource("horizontalpodautoscalers")) { + hpaStorage, hpaStatusStorage := horizontalpodautoscalerstore.NewREST(restOptionsGetter) + storage["horizontalpodautoscalers"] = hpaStorage + storage["horizontalpodautoscalers/status"] = hpaStatusStorage + } + return storage +} + func (p RESTStorageProvider) GroupName() string { return autoscaling.GroupName }