added this labels:

kompose.hpa.minreplicas
kompose.hpa.maxreplicas
kompose.hpa.cpu
kompose.hpa.memory

documentated labels user_guide.md

generate custom metrics from cpu, memory and set min/max replicas

Signed-off-by: jose luis <2064537+sosan@users.noreply.github.com>
This commit is contained in:
jose luis
2024-03-26 20:58:57 +01:00
parent 14152d4a81
commit a38f7f9ea3
5 changed files with 792 additions and 0 deletions
+128
View File
@@ -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 = 10
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
*/
@@ -985,3 +1009,107 @@ func reformatSecretConfigUnderscoreWithDash(secretConfig types.ServiceSecretConf
return newSecretConfig
}
// searchHPAValues is useful to check if labels
// contains any labels related to Horizontal Pod Autoscaler
func searchHPAValues(labels map[string]string) bool {
found := true
for _, value := range LabelKeys {
if _, ok := labels[value]; ok {
return found
}
}
return !found
}
// 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 {
maxReplicas = minReplicas
}
return HpaValues{
MinReplicas: minReplicas,
MaxReplicas: maxReplicas,
CPUtilization: getHpaValue(service, compose.LabelHpaCPU, DefaultCpuUtilization),
MemoryUtilization: getHpaValue(service, compose.LabelHpaMemory, DefaultMemoryUtilization),
}
}
// 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 {
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
}
+578
View File
@@ -29,7 +29,9 @@ import (
"github.com/kubernetes/kompose/pkg/testutils"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
hpa "k8s.io/api/autoscaling/v2beta2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
/*
@@ -738,3 +740,579 @@ func TestRemoveEmptyInterfaces(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: 1,
},
{
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: 10,
},
want: 10,
},
{
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: 10,
},
want: 10,
},
// 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: 50,
},
want: 50,
},
{
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: 50,
},
want: 50,
},
// 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: 70,
},
want: 70,
},
{
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: 70,
},
want: 70,
},
}
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 same values",
args: args{
service: &kobject.ServiceConfig{
Labels: map[string]string{
compose.LabelHpaMinReplicas: "1",
compose.LabelHpaMaxReplicas: "10",
compose.LabelHpaCPU: "50",
compose.LabelHpaMemory: "70",
},
},
},
want: HpaValues{
MinReplicas: 1,
MaxReplicas: 10,
CPUtilization: 50,
MemoryUtilization: 70,
},
},
{
name: "check same values",
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,
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: 1,
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,
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,
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: "50",
compose.LabelHpaMemory: "cannot transform",
},
},
},
want: HpaValues{
MinReplicas: 6,
MaxReplicas: 6,
CPUtilization: 50,
MemoryUtilization: 70,
},
},
{
name: "all error label",
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: 1,
MaxReplicas: 10,
CPUtilization: 50,
MemoryUtilization: 70,
},
},
{
name: "error label without some labels",
args: args{
service: &kobject.ServiceConfig{
Labels: map[string]string{
compose.LabelHpaMinReplicas: "cannot transform",
compose.LabelHpaMaxReplicas: "cannot transform",
},
},
},
want: HpaValues{
MinReplicas: 1,
MaxReplicas: 10,
CPUtilization: 50,
MemoryUtilization: 70,
},
},
{
name: "without labels, should return default values",
args: args{
service: &kobject.ServiceConfig{
Labels: map[string]string{},
},
},
want: HpaValues{
MinReplicas: 1,
MaxReplicas: 10,
CPUtilization: 50,
MemoryUtilization: 70,
},
},
}
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_getHpaMetricSpec(t *testing.T) {
valueCPUFixed := int32(50)
valueMemoryFixed := int32(70)
type args struct {
hpaValues HpaValues
}
tests := []struct {
name string
args args
want []hpa.MetricSpec
}{
{
name: "no values",
args: args{
hpaValues: HpaValues{},
},
want: nil,
},
{
name: "only cpu",
args: args{
hpaValues: HpaValues{
CPUtilization: 50,
},
},
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: 50,
MemoryUtilization: 70,
},
},
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,
},
},
},
},
},
}
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,
},
},
},
},
},
},
},
}
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)
}
})
}
}
+17
View File
@@ -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 transforming Kubernetes objects")
}
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
}