diff --git a/docs/user-guide.md b/docs/user-guide.md index be213f5b..4b97baa5 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -211,9 +211,13 @@ The currently supported options are: | kompose.cronjob.schedule | kubernetes cronjob schedule (for example: '1 * * * *') | | kompose.cronjob.concurrency_policy | 'Forbid' / 'Allow' / 'Never' / '' | | kompose.cronjob.backoff_limit | kubernetes cronjob backoff limit (for example: '6') | -| kompose.init.containers.name | kubernetes init container name | -| kompose.init.containers.image | kubernetes init container image | -| kompose.init.containers.command | kubernetes init container commands | +| kompose.init.containers.name | kubernetes init container name | +| kompose.init.containers.image | kubernetes init container image | +| kompose.init.containers.command | kubernetes init container commands | +| kompose.hpa.replicas.min | defines Horizontal Pod Autoscaler minimum number of pod replicas | +| kompose.hpa.replicas.max | defines Horizontal Pod Autoscaler maximum number of pod replicas | +| kompose.hpa.cpu | defines Horizontal Pod Autoscaler cpu utilization trigger | +| kompose.hpa.memory | defines Horizontal Pod Autoscaler memory utilization trigger | **Note**: `kompose.service.type` label should be defined with `ports` only (except for headless service), otherwise `kompose` will fail. @@ -512,6 +516,55 @@ services: kompose.init.containers.image: perl ``` + +- `kompose.hpa.replicas.min` defines the floor for the number of replicas that the HPA can scale down to during a scaling event. Default value is set to 1. This means that, regardless of the load on the system, the HPA will always maintain at least one replica. More info: [HPA Min Replicas](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.replicas.min: 1 +``` + +- `kompose.hpa.replicas.max` defines the upper limit for the number of replicas that the HPA can create during a scaling event. Default value is set to 3. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA Max Replicas](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.replicas.max: 10 +``` + +- `kompose.hpa.cpu` defines % cpu utilization that triggers to scale the number of pods. It is represented as a percentage of a resource. Default value is set to 50. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA CPU Utilization](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.cpu: 50 +``` + +- `kompose.hpa.memory` defines memory utilization that triggers to scale the number of pods. It is represented as a percentage of a resource. Default value is set to 70. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA Memory Utilization](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.memory: 50 +``` + ## Restart If you want to create normal pods without controller you can use `restart` construct of compose to define that. Follow table below to see what happens on the `restart` value. diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 2bdccac1..92b4062c 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -93,6 +93,14 @@ const ( LabelInitContainerImage = "kompose.init.containers.image" // LabelInitContainerCommand defines commands LabelInitContainerCommand = "kompose.init.containers.command" + // LabelHpaMinReplicas defines min pod replicas + LabelHpaMinReplicas = "kompose.hpa.replicas.min" + // LabelHpaMaxReplicas defines max pod replicas + LabelHpaMaxReplicas = "kompose.hpa.replicas.max" + // LabelHpaCpu defines scaling decisions based on CPU utilization + LabelHpaCPU = "kompose.hpa.cpu" + // LabelHpaMemory defines scaling decisions based on memory utilization + LabelHpaMemory = "kompose.hpa.memory" ) // load environment variables from compose file diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 8145080c..42e24c97 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -41,6 +41,7 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" appsv1 "k8s.io/api/apps/v1" + hpa "k8s.io/api/autoscaling/v2beta2" api "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,6 +49,29 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// Default values for Horizontal Pod Autoscaler (HPA) +const ( + DefaultMinReplicas = 1 + DefaultMaxReplicas = 3 + DefaultCPUUtilization = 50 + DefaultMemoryUtilization = 70 +) + +// LabelKeys are the keys for HPA related labels in the service +var LabelKeys = []string{ + compose.LabelHpaCPU, + compose.LabelHpaMemory, + compose.LabelHpaMinReplicas, + compose.LabelHpaMaxReplicas, +} + +type HpaValues struct { + MinReplicas int32 + MaxReplicas int32 + CPUtilization int32 + MemoryUtilization int32 +} + /** * Generate Helm Chart configuration */ @@ -1030,3 +1054,122 @@ func parseContainerCommandsFromStr(line string) []string { } return commands } + +// searchHPAValues is useful to check if labels +// contains any labels related to Horizontal Pod Autoscaler +func searchHPAValues(labels map[string]string) bool { + for _, value := range LabelKeys { + if _, ok := labels[value]; ok { + return true + } + } + return false +} + +// createHPAResources creates a HorizontalPodAutoscaler (HPA) resource +// It sets the number of replicas in the service to 0 because +// the number of replicas will be managed by the HPA +func createHPAResources(name string, service *kobject.ServiceConfig) hpa.HorizontalPodAutoscaler { + valuesHpa := getResourceHpaValues(service) + service.Replicas = 0 + metrics := getHpaMetricSpec(valuesHpa) + scalerSpecs := hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: name, + APIVersion: "apps/v1", + }, + MinReplicas: &valuesHpa.MinReplicas, + MaxReplicas: valuesHpa.MaxReplicas, + Metrics: metrics, + }, + } + + return scalerSpecs +} + +// getResourceHpaValues retrieves the min/max replicas and CPU/memory utilization values +// control if maxReplicas is less than minReplicas +func getResourceHpaValues(service *kobject.ServiceConfig) HpaValues { + minReplicas := getHpaValue(service, compose.LabelHpaMinReplicas, DefaultMinReplicas) + maxReplicas := getHpaValue(service, compose.LabelHpaMaxReplicas, DefaultMaxReplicas) + + if maxReplicas < minReplicas { + log.Warnf("maxReplicas %d is less than minReplicas %d. Using minReplicas value %d", maxReplicas, minReplicas, minReplicas) + maxReplicas = minReplicas + } + + cpuUtilization := validatePercentageMetric(service, compose.LabelHpaCPU, DefaultCPUUtilization) + memoryUtilization := validatePercentageMetric(service, compose.LabelHpaMemory, DefaultMemoryUtilization) + + return HpaValues{ + MinReplicas: minReplicas, + MaxReplicas: maxReplicas, + CPUtilization: cpuUtilization, + MemoryUtilization: memoryUtilization, + } +} + +// validatePercentageMetric validates the CPU or memory metrics value +// ensuring that it falls within the acceptable range [1, 100]. +func validatePercentageMetric(service *kobject.ServiceConfig, metricLabel string, defaultValue int32) int32 { + metricValue := getHpaValue(service, metricLabel, defaultValue) + if metricValue > 100 || metricValue < 1 { + log.Warnf("Metric value %d is not within the acceptable range [1, 100]. Using default value %d", metricValue, defaultValue) + return defaultValue + } + return metricValue +} + +// getHpaValue convert the label value to integer +// If the label is not present or the conversion fails +// it returns the provided default value +func getHpaValue(service *kobject.ServiceConfig, label string, defaultValue int32) int32 { + valueFromLabel, err := strconv.Atoi(service.Labels[label]) + if err != nil || valueFromLabel < 0 { + log.Warnf("Error converting label %s. Using default value %d", label, defaultValue) + return defaultValue + } + return int32(valueFromLabel) +} + +// getHpaMetricSpec returns a list of metric specs for the HPA resource +// Target type is hardcoded to hpa.UtilizationMetricType +// Each MetricSpec specifies the type metric CPU/memory and average utilization value +// to trigger scaling +func getHpaMetricSpec(hpaValues HpaValues) []hpa.MetricSpec { + var metrics []hpa.MetricSpec + if hpaValues.CPUtilization > 0 { + metrics = append(metrics, hpa.MetricSpec{ + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: api.ResourceCPU, + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &hpaValues.CPUtilization, + }, + }, + }) + } + if hpaValues.MemoryUtilization > 0 { + metrics = append(metrics, hpa.MetricSpec{ + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: api.ResourceMemory, + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &hpaValues.MemoryUtilization, + }, + }, + }) + } + return metrics +} diff --git a/pkg/transformer/kubernetes/k8sutils_test.go b/pkg/transformer/kubernetes/k8sutils_test.go index d70d06d9..a7a0f11e 100644 --- a/pkg/transformer/kubernetes/k8sutils_test.go +++ b/pkg/transformer/kubernetes/k8sutils_test.go @@ -29,8 +29,10 @@ import ( "github.com/kubernetes/kompose/pkg/testutils" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" + hpa "k8s.io/api/autoscaling/v2beta2" api "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) /* @@ -945,3 +947,1278 @@ func Test_fillInitContainers(t *testing.T) { }) } } + +func Test_getHpaValue(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + label string + defaultValue int32 + } + tests := []struct { + name string + args args + want int32 + }{ + // LabelHpaMinReplicas + { + name: "LabelHpaMinReplicas with 1 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 1, + }, + { + name: "LabelHpaMinReplicas with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "0", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 0, + }, + { + name: "LabelHpaMinReplicas with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 1, + }, + // LabelHpaMaxReplicas + { + name: "LabelHpaMaxReplicas with 10 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: 30, + }, + want: 10, + }, + { + name: "LabelHpaMaxReplicas with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "0", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: DefaultMaxReplicas, + }, + want: 0, + }, + { + name: "LabelHpaMaxReplicas with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: DefaultMaxReplicas, + }, + want: DefaultMaxReplicas, + }, + // LabelHpaCPU + { + name: "LabelHpaCPU with 50 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: 30, + }, + want: 50, + }, + { + name: "LabelHpaCPU with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "0", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 0, + }, + { + name: "LabelHpaCPU with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + // LabelHpaMemory + { + name: "LabelHpaMemory with 70 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: 30, + }, + want: 70, + }, + { + name: "LabelHpaMemory with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "0", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 0, + }, + { + name: "LabelHpaMemory with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "cannot transform", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getHpaValue(tt.args.service, tt.args.label, tt.args.defaultValue); got != tt.want { + t.Errorf("getHpaValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getResourceHpaValues(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + } + tests := []struct { + name string + args args + want HpaValues + }{ + { + name: "check default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 1, + MaxReplicas: 3, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "check if max replicas are less than min replicas, and max replicas set to min replicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "5", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 5, + MaxReplicas: 5, // same as min replicas + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "with error values and use default values from LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: 3, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "LabelHpaMaxReplicas is minor to LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, // set min replicas number + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "error label and LabelHpaMaxReplicas is minor to LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, // same as min replicas number + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: 70, + }, + }, + { + name: "error label and LabelHpaMaxReplicas is minor to LabelHpaMinReplicas and cannot transform hpa mmemor utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, + CPUtilization: 50, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "all error label, set all default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "error label without some labels, missing labels set to default", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "without labels, should return default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{}, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only min replicas label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "3", + }, + }, + }, + want: HpaValues{ + MinReplicas: 3, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only max replicas label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMaxReplicas: "5", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: 5, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when all labels contain invalid values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only cpu utilization label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "80", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: 80, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only memory utilization label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "90", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: 90, + }, + }, + { + name: "only cpu and memory utilization labels are provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "80", + compose.LabelHpaMemory: "90", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: 80, + MemoryUtilization: 90, + }, + }, + { + name: "check default values when labels are empty strings", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "", + compose.LabelHpaMaxReplicas: "", + compose.LabelHpaCPU: "", + compose.LabelHpaMemory: "", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels contain invalid characters", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "abc", + compose.LabelHpaMaxReplicas: "xyz", + compose.LabelHpaCPU: "-100", + compose.LabelHpaMemory: "invalid", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels are set to zero", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "0", + compose.LabelHpaMaxReplicas: "0", + compose.LabelHpaCPU: "0", + compose.LabelHpaMemory: "0", + }, + }, + }, + want: HpaValues{ + MinReplicas: 0, + MaxReplicas: 0, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "check default values when all labels are negative", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "-5", + compose.LabelHpaMaxReplicas: "-10", + compose.LabelHpaCPU: "-20", + compose.LabelHpaMemory: "-30", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels cpu and memory are over", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "-2", + compose.LabelHpaMaxReplicas: "-2", + compose.LabelHpaCPU: "120", + compose.LabelHpaMemory: "120", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getResourceHpaValues(tt.args.service); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getResourceHpaValues() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_validatePercentageMetric(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + metricLabel string + defaultValue int32 + } + tests := []struct { + name string + args args + want int32 + }{ + { + name: "0 cpu utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "0", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 50, + }, + { + name: "default cpu valid range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "120", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cpu invalid range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "-120", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cpu utilization set to 100", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "100", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 100, + }, + { + name: "cpu utlization set to 101", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "101", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cannot convert value in cpu label", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "not converted", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "0 memory utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "0", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 70, + }, + { + name: "memory over 100 utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "120", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "-120 utilization memory wrong range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "-120", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "memory 100 usage", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "100", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 100, + }, + { + name: "101 memory utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "101", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "cannot convert memory from label", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "not converted", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := validatePercentageMetric(tt.args.service, tt.args.metricLabel, tt.args.defaultValue); got != tt.want { + t.Errorf("validatePercentageMetric() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getHpaMetricSpec(t *testing.T) { + valueCPUFixed := int32(50) + valueMemoryFixed := int32(70) + valueOver100 := int32(120) + valueUnderZero := int32(-120) + // valueZero := int32(0) + type args struct { + hpaValues HpaValues + } + tests := []struct { + name string + args args + want []hpa.MetricSpec + }{ + { + name: "no values", + args: args{ + hpaValues: HpaValues{}, // set all values to 0 + }, + want: nil, + }, + { + name: "only cpu", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueCPUFixed, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + }, + }, + { + name: "only memory", + args: args{ + hpaValues: HpaValues{ + MemoryUtilization: 70, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + { + name: "cpu and memory", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueCPUFixed, + MemoryUtilization: valueMemoryFixed, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + { + name: "memory over 100", + args: args{ + hpaValues: HpaValues{ + MemoryUtilization: valueOver100, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + }, + }, + { + name: "cpu and memory over 100", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueOver100, + MemoryUtilization: valueOver100, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + }, + }, + { + name: "cpu and memory under 0", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueUnderZero, + MemoryUtilization: valueUnderZero, + }, + }, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getHpaMetricSpec(tt.args.hpaValues); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getHpaMetricSpec() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createHPAResources(t *testing.T) { + valueCPUFixed := int32(50) + valueMemoryFixed := int32(70) + fixedMinReplicas := int32(1) + type args struct { + name string + service *kobject.ServiceConfig + } + tests := []struct { + name string + args args + want hpa.HorizontalPodAutoscaler + }{ + { + name: "all labels", + args: args{ + name: "web", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "web", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "web", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 10, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "minimum labels", + args: args{ + name: "api", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaCPU: "50", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "api", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "api", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: DefaultMaxReplicas, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing CPU utilization label", + args: args{ + name: "app", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "app", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "app", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 5, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing memory utilization label", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "8", + compose.LabelHpaCPU: "50", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 8, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong labels", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "not converted", + compose.LabelHpaMaxReplicas: "not converted", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: DefaultMaxReplicas, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing both CPU and memory utilization labels", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "5", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 5, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createHPAResources(tt.args.name, tt.args.service); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createHPAResources() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index a51a930c..f3ff6c70 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -1654,6 +1654,10 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. return nil, err } } + err = k.configHorizontalPodScaler(name, service, opt, &objects) + if err != nil { + return nil, errors.Wrap(err, "Error creating Kubernetes HPA") + } allobjects = append(allobjects, objects...) } @@ -1718,3 +1722,16 @@ func (k *Kubernetes) UpdateController(obj runtime.Object, updateTemplate func(*a } return nil } + +// configHorizontalPodScaler create Hpa resource also append to the objects +// first checks if the service labels contain any HPA labels using the searchHPAValues +func (k *Kubernetes) configHorizontalPodScaler(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) (err error) { + found := searchHPAValues(service.Labels) + if !found { + return nil + } + + hpa := createHPAResources(name, &service) + *objects = append(*objects, &hpa) + return nil +} diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 715ad237..baeab0e9 100755 --- a/script/test/cmd/tests_new.sh +++ b/script/test/cmd/tests_new.sh @@ -344,3 +344,8 @@ convert::expect_success "$os_cmd" "$os_output" || exit 1 k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/initcontainer/compose.yaml convert --stdout --with-kompose-annotation=false" k8s_output="$KOMPOSE_ROOT/script/test/fixtures/initcontainer/output-k8s.yaml" convert::expect_success_and_warning "$k8s_cmd" "$k8s_output" || exit 1 + +# Test HPA +k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/hpa/compose.yaml convert --stdout --with-kompose-annotation=false" +k8s_output="$KOMPOSE_ROOT/script/test/fixtures/hpa/output-k8s.yaml" +convert::expect_success "$k8s_cmd" "$k8s_output" || exit 1 diff --git a/script/test/fixtures/hpa/compose.yaml b/script/test/fixtures/hpa/compose.yaml new file mode 100644 index 00000000..80a2c432 --- /dev/null +++ b/script/test/fixtures/hpa/compose.yaml @@ -0,0 +1,10 @@ +version: "3.8" +services: + web: + image: nginx + labels: + kompose.hpa.cpu: 50 + kompose.hpa.memory: 70 + kompose.hpa.replicas.min: 1 + kompose.hpa.replicas.max: 10 + diff --git a/script/test/fixtures/hpa/output-k8s.yaml b/script/test/fixtures/hpa/output-k8s.yaml new file mode 100644 index 00000000..6f249d29 --- /dev/null +++ b/script/test/fixtures/hpa/output-k8s.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + io.kompose.service: web + name: web +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: web + template: + metadata: + labels: + io.kompose.network/hpa-default: "true" + io.kompose.service: web + spec: + containers: + - image: nginx + name: web + restartPolicy: Always + +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: web +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + type: Resource + - resource: + name: memory + target: + averageUtilization: 70 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: web +