github.com/kubewharf/katalyst-core@v0.5.3/pkg/controller/vpa/vparec_test.go (about)

     1  /*
     2  Copyright 2022 The Katalyst Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package vpa
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/alecthomas/units"
    24  	"github.com/stretchr/testify/assert"
    25  	appsv1 "k8s.io/api/apps/v1"
    26  	v1 "k8s.io/api/core/v1"
    27  	"k8s.io/apimachinery/pkg/api/resource"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/client-go/tools/cache"
    31  	cliflag "k8s.io/component-base/cli/flag"
    32  	"k8s.io/utils/pointer"
    33  
    34  	apis "github.com/kubewharf/katalyst-api/pkg/apis/autoscaling/v1alpha1"
    35  	apiconsts "github.com/kubewharf/katalyst-api/pkg/consts"
    36  	katalystbase "github.com/kubewharf/katalyst-core/cmd/base"
    37  	"github.com/kubewharf/katalyst-core/cmd/katalyst-controller/app/options"
    38  	"github.com/kubewharf/katalyst-core/pkg/config/controller"
    39  	"github.com/kubewharf/katalyst-core/pkg/config/generic"
    40  	"github.com/kubewharf/katalyst-core/pkg/controller/vpa/util"
    41  	"github.com/kubewharf/katalyst-core/pkg/util/native"
    42  )
    43  
    44  func TestVPARecControllerSyncVPA(t *testing.T) {
    45  	t.Parallel()
    46  
    47  	pod1 := makePod("pod1",
    48  		map[string]string{apiconsts.WorkloadAnnotationVPAEnabledKey: apiconsts.WorkloadAnnotationVPAEnabled},
    49  		map[string]string{"workload": "sts1"},
    50  		[]metav1.OwnerReference{
    51  			{
    52  				Kind:       "StatefulSet",
    53  				APIVersion: "apps/v1",
    54  				Name:       "sts1",
    55  			},
    56  		},
    57  	)
    58  	pod1.Spec.Containers = []v1.Container{
    59  		{
    60  			Name: "c1",
    61  			Resources: v1.ResourceRequirements{
    62  				Limits: map[v1.ResourceName]resource.Quantity{
    63  					v1.ResourceCPU:    *resource.NewQuantity(4, resource.DecimalSI),
    64  					v1.ResourceMemory: *resource.NewQuantity(4*int64(units.GiB), resource.BinarySI),
    65  				},
    66  				Requests: map[v1.ResourceName]resource.Quantity{
    67  					v1.ResourceCPU:    *resource.NewQuantity(2, resource.DecimalSI),
    68  					v1.ResourceMemory: *resource.NewQuantity(2*int64(units.GiB), resource.BinarySI),
    69  				},
    70  			},
    71  		},
    72  	}
    73  	pod1.Status.QOSClass = v1.PodQOSBurstable
    74  	for _, tc := range []struct {
    75  		name    string
    76  		vpaOld  *apis.KatalystVerticalPodAutoscaler
    77  		vparecs []*apis.VerticalPodAutoscalerRecommendation
    78  		object  runtime.Object
    79  		pods    []*v1.Pod
    80  		vpaNew  *apis.KatalystVerticalPodAutoscaler
    81  	}{
    82  		{
    83  			name: "set vpa without resource policy",
    84  			vpaOld: &apis.KatalystVerticalPodAutoscaler{
    85  				ObjectMeta: metav1.ObjectMeta{
    86  					Name:      "vpa1",
    87  					Namespace: "default",
    88  					UID:       "vpauid1",
    89  				},
    90  				Spec: apis.KatalystVerticalPodAutoscalerSpec{
    91  					TargetRef: apis.CrossVersionObjectReference{
    92  						Kind:       "StatefulSet",
    93  						APIVersion: "apps/v1",
    94  						Name:       "sts1",
    95  					},
    96  				},
    97  			},
    98  			vpaNew: &apis.KatalystVerticalPodAutoscaler{
    99  				ObjectMeta: metav1.ObjectMeta{
   100  					Name:      "vpa1",
   101  					Namespace: "default",
   102  					UID:       "vpauid1",
   103  				},
   104  				Spec: apis.KatalystVerticalPodAutoscalerSpec{
   105  					TargetRef: apis.CrossVersionObjectReference{
   106  						Kind:       "StatefulSet",
   107  						APIVersion: "apps/v1",
   108  						Name:       "sts1",
   109  					},
   110  				},
   111  				Status: apis.KatalystVerticalPodAutoscalerStatus{
   112  					ContainerResources: []apis.ContainerResources{
   113  						{
   114  							ContainerName: pointer.String("c1"),
   115  							Requests: &apis.ContainerResourceList{
   116  								Target: map[v1.ResourceName]resource.Quantity{
   117  									v1.ResourceCPU:    resource.MustParse("1"),
   118  									v1.ResourceMemory: resource.MustParse("1Gi"),
   119  								},
   120  								UncappedTarget: map[v1.ResourceName]resource.Quantity{
   121  									v1.ResourceCPU:    resource.MustParse("1"),
   122  									v1.ResourceMemory: resource.MustParse("1Gi"),
   123  								},
   124  							},
   125  						},
   126  					},
   127  				},
   128  			},
   129  			vparecs: []*apis.VerticalPodAutoscalerRecommendation{
   130  				{
   131  					ObjectMeta: metav1.ObjectMeta{
   132  						Name:      "vparec1",
   133  						Namespace: "default",
   134  						UID:       "vparecuid1",
   135  						OwnerReferences: []metav1.OwnerReference{
   136  							{
   137  								Name: "vpa1",
   138  								UID:  "vpauid1",
   139  							},
   140  						},
   141  					},
   142  					Spec: apis.VerticalPodAutoscalerRecommendationSpec{
   143  						ContainerRecommendations: []apis.RecommendedContainerResources{
   144  							{
   145  								ContainerName: pointer.String("c1"),
   146  								Requests: &apis.RecommendedRequestResources{
   147  									Resources: map[v1.ResourceName]resource.Quantity{
   148  										v1.ResourceCPU:    *resource.NewQuantity(1, resource.DecimalSI),
   149  										v1.ResourceMemory: *resource.NewQuantity(1*int64(units.GiB), resource.BinarySI),
   150  									},
   151  								},
   152  							},
   153  						},
   154  					},
   155  				},
   156  			},
   157  			object: &appsv1.StatefulSet{
   158  				TypeMeta: metav1.TypeMeta{
   159  					Kind:       "StatefulSet",
   160  					APIVersion: "apps/v1",
   161  				},
   162  				ObjectMeta: metav1.ObjectMeta{
   163  					Name:      "sts1",
   164  					Namespace: "default",
   165  				},
   166  				Spec: appsv1.StatefulSetSpec{
   167  					Selector: &metav1.LabelSelector{
   168  						MatchLabels: map[string]string{
   169  							"workload": "sts1",
   170  						},
   171  					},
   172  				},
   173  			},
   174  			pods: []*v1.Pod{
   175  				pod1,
   176  			},
   177  		},
   178  		{
   179  			name: "set vpa with resource policy",
   180  			vpaOld: &apis.KatalystVerticalPodAutoscaler{
   181  				ObjectMeta: metav1.ObjectMeta{
   182  					Name:      "vpa1",
   183  					Namespace: "default",
   184  					UID:       "vpauid1",
   185  				},
   186  				Spec: apis.KatalystVerticalPodAutoscalerSpec{
   187  					TargetRef: apis.CrossVersionObjectReference{
   188  						Kind:       "StatefulSet",
   189  						APIVersion: "apps/v1",
   190  						Name:       "sts1",
   191  					},
   192  					ResourcePolicy: apis.PodResourcePolicy{
   193  						ContainerPolicies: []apis.ContainerResourcePolicy{
   194  							{
   195  								ContainerName: pointer.String("c1"),
   196  								MinAllowed: v1.ResourceList{
   197  									v1.ResourceCPU:    resource.MustParse("2"),
   198  									v1.ResourceMemory: resource.MustParse("2Gi"),
   199  								},
   200  								ControlledResources: []v1.ResourceName{v1.ResourceCPU, v1.ResourceMemory},
   201  								ControlledValues:    apis.ContainerControlledValuesRequestsAndLimits,
   202  							},
   203  						},
   204  					},
   205  				},
   206  			},
   207  			vpaNew: &apis.KatalystVerticalPodAutoscaler{
   208  				ObjectMeta: metav1.ObjectMeta{
   209  					Name:      "vpa1",
   210  					Namespace: "default",
   211  					UID:       "vpauid1",
   212  				},
   213  				Spec: apis.KatalystVerticalPodAutoscalerSpec{
   214  					TargetRef: apis.CrossVersionObjectReference{
   215  						Kind:       "StatefulSet",
   216  						APIVersion: "apps/v1",
   217  						Name:       "sts1",
   218  					},
   219  					ResourcePolicy: apis.PodResourcePolicy{
   220  						ContainerPolicies: []apis.ContainerResourcePolicy{
   221  							{
   222  								ContainerName: pointer.String("c1"),
   223  								MinAllowed: v1.ResourceList{
   224  									v1.ResourceCPU:    resource.MustParse("2"),
   225  									v1.ResourceMemory: resource.MustParse("2Gi"),
   226  								},
   227  								ControlledResources: []v1.ResourceName{v1.ResourceCPU, v1.ResourceMemory},
   228  								ControlledValues:    apis.ContainerControlledValuesRequestsAndLimits,
   229  							},
   230  						},
   231  					},
   232  				},
   233  				Status: apis.KatalystVerticalPodAutoscalerStatus{
   234  					ContainerResources: []apis.ContainerResources{
   235  						{
   236  							ContainerName: pointer.String("c1"),
   237  							Requests: &apis.ContainerResourceList{
   238  								Target: map[v1.ResourceName]resource.Quantity{
   239  									v1.ResourceCPU:    resource.MustParse("2"),
   240  									v1.ResourceMemory: resource.MustParse("2Gi"),
   241  								},
   242  								UncappedTarget: map[v1.ResourceName]resource.Quantity{
   243  									v1.ResourceCPU:    resource.MustParse("1"),
   244  									v1.ResourceMemory: resource.MustParse("1Gi"),
   245  								},
   246  								LowerBound: map[v1.ResourceName]resource.Quantity{
   247  									v1.ResourceCPU:    resource.MustParse("2"),
   248  									v1.ResourceMemory: resource.MustParse("2Gi"),
   249  								},
   250  							},
   251  						},
   252  					},
   253  				},
   254  			},
   255  			vparecs: []*apis.VerticalPodAutoscalerRecommendation{
   256  				{
   257  					ObjectMeta: metav1.ObjectMeta{
   258  						Name:      "vparec1",
   259  						Namespace: "default",
   260  						UID:       "vparecuid1",
   261  						OwnerReferences: []metav1.OwnerReference{
   262  							{
   263  								Name: "vpa1",
   264  								UID:  "vpauid1",
   265  							},
   266  						},
   267  					},
   268  					Spec: apis.VerticalPodAutoscalerRecommendationSpec{
   269  						ContainerRecommendations: []apis.RecommendedContainerResources{
   270  							{
   271  								ContainerName: pointer.String("c1"),
   272  								Requests: &apis.RecommendedRequestResources{
   273  									Resources: map[v1.ResourceName]resource.Quantity{
   274  										v1.ResourceCPU:    *resource.NewQuantity(1, resource.DecimalSI),
   275  										v1.ResourceMemory: *resource.NewQuantity(1*int64(units.GiB), resource.BinarySI),
   276  									},
   277  								},
   278  							},
   279  						},
   280  					},
   281  				},
   282  			},
   283  			object: &appsv1.StatefulSet{
   284  				TypeMeta: metav1.TypeMeta{
   285  					Kind:       "StatefulSet",
   286  					APIVersion: "apps/v1",
   287  				},
   288  				ObjectMeta: metav1.ObjectMeta{
   289  					Name:      "sts1",
   290  					Namespace: "default",
   291  				},
   292  				Spec: appsv1.StatefulSetSpec{
   293  					Selector: &metav1.LabelSelector{
   294  						MatchLabels: map[string]string{
   295  							"workload": "sts1",
   296  						},
   297  					},
   298  				},
   299  			},
   300  			pods: []*v1.Pod{
   301  				pod1,
   302  			},
   303  		},
   304  	} {
   305  		tc := tc
   306  		t.Run(tc.name, func(t *testing.T) {
   307  			t.Parallel()
   308  
   309  			genericConfig := &generic.GenericConfiguration{}
   310  			controllerConf := &controller.GenericControllerConfiguration{
   311  				DynamicGVResources: []string{"statefulsets.v1.apps"},
   312  			}
   313  			fss := &cliflag.NamedFlagSets{}
   314  			vpaOptions := options.NewVPAOptions()
   315  			vpaOptions.AddFlags(fss)
   316  			vpaConf := controller.NewVPAConfig()
   317  			vpaOptions.ApplyTo(vpaConf)
   318  			controlCtx, err := katalystbase.GenerateFakeGenericContext(nil, []runtime.Object{tc.vpaOld}, []runtime.Object{tc.object})
   319  			assert.NoError(t, err)
   320  
   321  			vparec, err := NewVPARecommendationController(context.TODO(), controlCtx, genericConfig, controllerConf, vpaConf)
   322  			assert.NoError(t, err)
   323  
   324  			workloadInformers := controlCtx.DynamicResourcesManager.GetDynamicInformers()
   325  			u, err := native.ToUnstructured(tc.object)
   326  			assert.NoError(t, err)
   327  			err = workloadInformers["statefulsets.v1.apps"].Informer.Informer().GetStore().Add(u)
   328  			assert.NoError(t, err)
   329  
   330  			vpaInformer := controlCtx.InternalInformerFactory.Autoscaling().V1alpha1().KatalystVerticalPodAutoscalers()
   331  			err = vpaInformer.Informer().GetStore().Add(tc.vpaOld)
   332  			assert.NoError(t, err)
   333  
   334  			vparecInformer := controlCtx.InternalInformerFactory.Autoscaling().V1alpha1().VerticalPodAutoscalerRecommendations()
   335  			for _, rec := range tc.vparecs {
   336  				err = vparecInformer.Informer().GetStore().Add(rec)
   337  				assert.NoError(t, err)
   338  			}
   339  			for _, pod := range tc.pods {
   340  				err = controlCtx.KubeInformerFactory.Core().V1().Pods().Informer().GetStore().Add(pod)
   341  				assert.NoError(t, err)
   342  			}
   343  
   344  			for _, vpaRec := range tc.vparecs {
   345  				key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(vpaRec)
   346  				assert.NoError(t, err)
   347  
   348  				err = vparec.syncVPARec(key)
   349  				assert.NoError(t, err)
   350  			}
   351  
   352  			vpa, err := controlCtx.Client.InternalClient.AutoscalingV1alpha1().
   353  				KatalystVerticalPodAutoscalers(tc.vpaNew.Namespace).Get(context.TODO(), tc.vpaNew.Name, metav1.GetOptions{})
   354  			assert.NoError(t, err)
   355  			assert.Equal(t, tc.vpaNew, vpa)
   356  		})
   357  	}
   358  }
   359  
   360  func TestVPARecControllerSyncVPARec(t *testing.T) {
   361  	t.Parallel()
   362  
   363  	for _, tc := range []struct {
   364  		name      string
   365  		vparecOld *apis.VerticalPodAutoscalerRecommendation
   366  		vpa       *apis.KatalystVerticalPodAutoscaler
   367  		vparecNew *apis.VerticalPodAutoscalerRecommendation
   368  	}{
   369  		{
   370  			name: "set vpa with resource policy",
   371  			vpa: &apis.KatalystVerticalPodAutoscaler{
   372  				ObjectMeta: metav1.ObjectMeta{
   373  					Name:      "vpa1",
   374  					Namespace: "default",
   375  					UID:       "vpauid1",
   376  				},
   377  				Spec: apis.KatalystVerticalPodAutoscalerSpec{
   378  					TargetRef: apis.CrossVersionObjectReference{
   379  						Kind:       "StatefulSet",
   380  						APIVersion: "apps/v1",
   381  						Name:       "sts1",
   382  					},
   383  					ResourcePolicy: apis.PodResourcePolicy{
   384  						ContainerPolicies: []apis.ContainerResourcePolicy{
   385  							{
   386  								ContainerName: pointer.String("c1"),
   387  								MinAllowed: v1.ResourceList{
   388  									v1.ResourceCPU:    resource.MustParse("2"),
   389  									v1.ResourceMemory: resource.MustParse("2Gi"),
   390  								},
   391  								ControlledResources: []v1.ResourceName{v1.ResourceCPU, v1.ResourceMemory},
   392  								ControlledValues:    apis.ContainerControlledValuesRequestsAndLimits,
   393  							},
   394  						},
   395  					},
   396  				},
   397  				Status: apis.KatalystVerticalPodAutoscalerStatus{
   398  					ContainerResources: []apis.ContainerResources{
   399  						{
   400  							ContainerName: pointer.String("c1"),
   401  							Requests: &apis.ContainerResourceList{
   402  								Target: map[v1.ResourceName]resource.Quantity{
   403  									v1.ResourceCPU:    resource.MustParse("2"),
   404  									v1.ResourceMemory: resource.MustParse("2Gi"),
   405  								},
   406  								UncappedTarget: map[v1.ResourceName]resource.Quantity{
   407  									v1.ResourceCPU:    resource.MustParse("1"),
   408  									v1.ResourceMemory: resource.MustParse("1Gi"),
   409  								},
   410  							},
   411  						},
   412  					},
   413  					Conditions: []apis.VerticalPodAutoscalerCondition{
   414  						{
   415  							Type:   apis.RecommendationUpdated,
   416  							Status: v1.ConditionTrue,
   417  							Reason: util.VPAConditionReasonUpdated,
   418  						},
   419  					},
   420  				},
   421  			},
   422  			vparecOld: &apis.VerticalPodAutoscalerRecommendation{
   423  				ObjectMeta: metav1.ObjectMeta{
   424  					Name:      "vparec1",
   425  					Namespace: "default",
   426  					UID:       "vparecuid1",
   427  					OwnerReferences: []metav1.OwnerReference{
   428  						{
   429  							Name: "vpa1",
   430  							UID:  "vpauid1",
   431  						},
   432  					},
   433  				},
   434  				Spec: apis.VerticalPodAutoscalerRecommendationSpec{
   435  					ContainerRecommendations: []apis.RecommendedContainerResources{
   436  						{
   437  							ContainerName: pointer.String("c1"),
   438  							Requests: &apis.RecommendedRequestResources{
   439  								Resources: map[v1.ResourceName]resource.Quantity{
   440  									v1.ResourceCPU:    *resource.NewQuantity(1, resource.DecimalSI),
   441  									v1.ResourceMemory: *resource.NewQuantity(1*int64(units.GiB), resource.BinarySI),
   442  								},
   443  							},
   444  						},
   445  					},
   446  				},
   447  			},
   448  			vparecNew: &apis.VerticalPodAutoscalerRecommendation{
   449  				ObjectMeta: metav1.ObjectMeta{
   450  					Name:      "vparec1",
   451  					Namespace: "default",
   452  					UID:       "vparecuid1",
   453  					OwnerReferences: []metav1.OwnerReference{
   454  						{
   455  							Name: "vpa1",
   456  							UID:  "vpauid1",
   457  						},
   458  					},
   459  				},
   460  				Spec: apis.VerticalPodAutoscalerRecommendationSpec{
   461  					ContainerRecommendations: []apis.RecommendedContainerResources{
   462  						{
   463  							ContainerName: pointer.String("c1"),
   464  							Requests: &apis.RecommendedRequestResources{
   465  								Resources: map[v1.ResourceName]resource.Quantity{
   466  									v1.ResourceCPU:    resource.MustParse("1"),
   467  									v1.ResourceMemory: resource.MustParse("1Gi"),
   468  								},
   469  							},
   470  						},
   471  					},
   472  				},
   473  				Status: apis.VerticalPodAutoscalerRecommendationStatus{
   474  					ContainerRecommendations: []apis.RecommendedContainerResources{
   475  						{
   476  							ContainerName: pointer.String("c1"),
   477  							Requests: &apis.RecommendedRequestResources{
   478  								Resources: v1.ResourceList{
   479  									v1.ResourceCPU:    resource.MustParse("2"),
   480  									v1.ResourceMemory: resource.MustParse("2Gi"),
   481  								},
   482  							},
   483  						},
   484  					},
   485  					Conditions: []apis.VerticalPodAutoscalerRecommendationCondition{
   486  						{
   487  							Type:    apis.RecommendationUpdatedToVPA,
   488  							Status:  v1.ConditionTrue,
   489  							Reason:  util.VPARecConditionReasonUpdated,
   490  							Message: "",
   491  						},
   492  					},
   493  				},
   494  			},
   495  		},
   496  	} {
   497  		tc := tc
   498  		t.Run(tc.name, func(t *testing.T) {
   499  			t.Parallel()
   500  
   501  			genericConfig := &generic.GenericConfiguration{}
   502  			controllerConf := &controller.GenericControllerConfiguration{
   503  				DynamicGVResources: []string{"statefulsets.v1.apps"},
   504  			}
   505  
   506  			fss := &cliflag.NamedFlagSets{}
   507  			vpaOptions := options.NewVPAOptions()
   508  			vpaOptions.AddFlags(fss)
   509  			vpaConf := controller.NewVPAConfig()
   510  			vpaOptions.ApplyTo(vpaConf)
   511  
   512  			controlCtx, err := katalystbase.GenerateFakeGenericContext(nil, nil, nil)
   513  			assert.NoError(t, err)
   514  
   515  			vparecController, err := NewVPARecommendationController(context.TODO(), controlCtx, genericConfig, controllerConf, vpaConf)
   516  			assert.NoError(t, err)
   517  
   518  			vpaInformer := controlCtx.InternalInformerFactory.Autoscaling().V1alpha1().KatalystVerticalPodAutoscalers()
   519  			err = vpaInformer.Informer().GetStore().Add(tc.vpa)
   520  			assert.NoError(t, err)
   521  			_, err = controlCtx.Client.InternalClient.AutoscalingV1alpha1().
   522  				KatalystVerticalPodAutoscalers(tc.vpa.Namespace).
   523  				Create(context.TODO(), tc.vpa, metav1.CreateOptions{})
   524  			assert.NoError(t, err)
   525  
   526  			vparecInformer := controlCtx.InternalInformerFactory.Autoscaling().V1alpha1().VerticalPodAutoscalerRecommendations()
   527  			err = vparecInformer.Informer().GetStore().Add(tc.vparecOld)
   528  			assert.NoError(t, err)
   529  			_, err = controlCtx.Client.InternalClient.AutoscalingV1alpha1().
   530  				VerticalPodAutoscalerRecommendations(tc.vparecOld.Namespace).
   531  				Create(context.TODO(), tc.vparecOld, metav1.CreateOptions{})
   532  			assert.NoError(t, err)
   533  
   534  			controlCtx.StartInformer(vparecController.ctx)
   535  			synced := cache.WaitForCacheSync(vparecController.ctx.Done(), vparecController.syncedFunc...)
   536  			assert.True(t, synced)
   537  
   538  			key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(tc.vpa)
   539  			assert.NoError(t, err)
   540  
   541  			err = vparecController.syncVPA(key)
   542  			assert.NoError(t, err)
   543  
   544  			vparec, err := controlCtx.Client.InternalClient.AutoscalingV1alpha1().
   545  				VerticalPodAutoscalerRecommendations(tc.vparecNew.Namespace).
   546  				Get(context.TODO(), tc.vparecNew.Name, metav1.GetOptions{})
   547  			assert.NoError(t, err)
   548  			vparec.Status.Conditions[0].LastTransitionTime = metav1.Time{}
   549  			assert.Equal(t, tc.vparecNew, vparec)
   550  		})
   551  	}
   552  }