github.com/kubewharf/katalyst-core@v0.5.3/pkg/webhook/mutating/pod/pod_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 pod
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"testing"
    23  
    24  	whcontext "github.com/slok/kubewebhook/pkg/webhook/context"
    25  	"github.com/stretchr/testify/assert"
    26  	admissionv1beta1 "k8s.io/api/admission/v1beta1"
    27  	appsv1 "k8s.io/api/apps/v1"
    28  	v1 "k8s.io/api/core/v1"
    29  	"k8s.io/apimachinery/pkg/api/resource"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/utils/pointer"
    34  
    35  	apis "github.com/kubewharf/katalyst-api/pkg/apis/autoscaling/v1alpha1"
    36  	workloadapis "github.com/kubewharf/katalyst-api/pkg/apis/workload/v1alpha1"
    37  	apiconsts "github.com/kubewharf/katalyst-api/pkg/consts"
    38  	katalystbase "github.com/kubewharf/katalyst-core/cmd/base"
    39  	"github.com/kubewharf/katalyst-core/pkg/config/generic"
    40  	webhookconfig "github.com/kubewharf/katalyst-core/pkg/config/webhook"
    41  	"github.com/kubewharf/katalyst-core/pkg/util/native"
    42  )
    43  
    44  var stsGVK = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "StatefulSet"}
    45  
    46  func getPodJSON(pod *v1.Pod) []byte {
    47  	bs, _ := json.Marshal(pod)
    48  	return bs
    49  }
    50  
    51  func TestMutatePod(t *testing.T) {
    52  	t.Parallel()
    53  
    54  	container1 := &v1.Container{
    55  		Name: "c1",
    56  		Resources: v1.ResourceRequirements{
    57  			Limits: map[v1.ResourceName]resource.Quantity{
    58  				v1.ResourceMemory: resource.MustParse("4Gi"),
    59  				v1.ResourceCPU:    resource.MustParse("4"),
    60  			},
    61  			Requests: map[v1.ResourceName]resource.Quantity{
    62  				v1.ResourceMemory: resource.MustParse("2Gi"),
    63  				v1.ResourceCPU:    resource.MustParse("2"),
    64  			},
    65  		},
    66  	}
    67  	sts1 := &appsv1.StatefulSet{
    68  		TypeMeta: metav1.TypeMeta{
    69  			Kind:       stsGVK.Kind,
    70  			APIVersion: stsGVK.GroupVersion().String(),
    71  		},
    72  		ObjectMeta: metav1.ObjectMeta{
    73  			Namespace: "default",
    74  			Name:      "sts1",
    75  			Annotations: map[string]string{
    76  				apiconsts.WorkloadAnnotationVPAEnabledKey: apiconsts.WorkloadAnnotationVPAEnabled,
    77  			},
    78  		},
    79  		Spec: appsv1.StatefulSetSpec{
    80  			Replicas: pointer.Int32(1),
    81  			Template: v1.PodTemplateSpec{
    82  				Spec: v1.PodSpec{
    83  					Containers: []v1.Container{*container1},
    84  				},
    85  			},
    86  			Selector: &metav1.LabelSelector{
    87  				MatchLabels: map[string]string{
    88  					"workload": "sts",
    89  				},
    90  			},
    91  		},
    92  		Status: appsv1.StatefulSetStatus{},
    93  	}
    94  
    95  	vpa1 := &apis.KatalystVerticalPodAutoscaler{
    96  		ObjectMeta: metav1.ObjectMeta{
    97  			Namespace: "default",
    98  			Name:      "vpa1",
    99  		},
   100  		Spec: apis.KatalystVerticalPodAutoscalerSpec{
   101  			TargetRef: apis.CrossVersionObjectReference{
   102  				Kind:       stsGVK.Kind,
   103  				Name:       "sts1",
   104  				APIVersion: stsGVK.GroupVersion().String(),
   105  			},
   106  			UpdatePolicy: apis.PodUpdatePolicy{
   107  				PodUpdatingStrategy: "",
   108  				PodMatchingStrategy: apis.PodMatchingStrategyAll,
   109  				PodApplyStrategy:    "",
   110  			},
   111  		},
   112  		Status: apis.KatalystVerticalPodAutoscalerStatus{
   113  			ContainerResources: []apis.ContainerResources{
   114  				{
   115  					ContainerName: pointer.String("c1"),
   116  					Requests: &apis.ContainerResourceList{
   117  						Target: map[v1.ResourceName]resource.Quantity{
   118  							v1.ResourceMemory: resource.MustParse("1Gi"),
   119  							v1.ResourceCPU:    resource.MustParse("1"),
   120  						},
   121  						UncappedTarget: map[v1.ResourceName]resource.Quantity{
   122  							v1.ResourceMemory: resource.MustParse("1Gi"),
   123  							v1.ResourceCPU:    resource.MustParse("1"),
   124  						},
   125  					},
   126  				},
   127  			},
   128  		},
   129  	}
   130  
   131  	spd1 := &workloadapis.ServiceProfileDescriptor{
   132  		TypeMeta: metav1.TypeMeta{},
   133  		ObjectMeta: metav1.ObjectMeta{
   134  			Namespace: "default",
   135  			Name:      "spd1",
   136  		},
   137  		Spec: workloadapis.ServiceProfileDescriptorSpec{
   138  			TargetRef: apis.CrossVersionObjectReference{
   139  				Kind:       stsGVK.Kind,
   140  				Name:       "sts1",
   141  				APIVersion: stsGVK.GroupVersion().String(),
   142  			},
   143  		},
   144  		Status: workloadapis.ServiceProfileDescriptorStatus{},
   145  	}
   146  
   147  	pod1 := &v1.Pod{
   148  		TypeMeta: metav1.TypeMeta{},
   149  		ObjectMeta: metav1.ObjectMeta{
   150  			Namespace: "default",
   151  			Name:      "pod1",
   152  			Annotations: map[string]string{
   153  				apiconsts.WorkloadAnnotationVPAEnabledKey: apiconsts.WorkloadAnnotationVPAEnabled,
   154  			},
   155  			OwnerReferences: []metav1.OwnerReference{
   156  				{
   157  					Name:       "sts1",
   158  					Kind:       stsGVK.Kind,
   159  					APIVersion: stsGVK.GroupVersion().String(),
   160  				},
   161  			},
   162  		},
   163  		Spec: v1.PodSpec{
   164  			Containers: []v1.Container{
   165  				*container1,
   166  			},
   167  		},
   168  		Status: v1.PodStatus{},
   169  	}
   170  	pod2 := &v1.Pod{
   171  		TypeMeta: metav1.TypeMeta{},
   172  		ObjectMeta: metav1.ObjectMeta{
   173  			Namespace: "default",
   174  			Name:      "pod2",
   175  			Annotations: map[string]string{
   176  				"k1": "v1",
   177  			},
   178  			OwnerReferences: []metav1.OwnerReference{
   179  				{
   180  					Name:       "sts1",
   181  					Kind:       stsGVK.Kind,
   182  					APIVersion: stsGVK.GroupVersion().String(),
   183  				},
   184  			},
   185  		},
   186  		Spec: v1.PodSpec{
   187  			Containers: []v1.Container{
   188  				*container1,
   189  			},
   190  		},
   191  		Status: v1.PodStatus{},
   192  	}
   193  
   194  	for _, tc := range []struct {
   195  		name     string
   196  		object   runtime.Object
   197  		gvr      string
   198  		vpa      *apis.KatalystVerticalPodAutoscaler
   199  		spd      *workloadapis.ServiceProfileDescriptor
   200  		pod      *v1.Pod
   201  		review   *admissionv1beta1.AdmissionReview
   202  		expPatch []string
   203  	}{
   204  		{
   205  			name:   "mutate pod resource and annotation",
   206  			object: sts1,
   207  			gvr:    "statefulsets.v1.apps",
   208  			vpa:    vpa1,
   209  			spd:    spd1,
   210  			pod:    pod1,
   211  			review: &admissionv1beta1.AdmissionReview{
   212  				Request: &admissionv1beta1.AdmissionRequest{
   213  					UID: "test",
   214  					Object: runtime.RawExtension{
   215  						Raw: getPodJSON(pod1),
   216  					},
   217  				},
   218  			},
   219  			expPatch: []string{
   220  				`{"op":"replace","path":"/spec/containers/0/resources/requests/cpu","value":"1"}`,
   221  				`{"op":"replace","path":"/spec/containers/0/resources/requests/memory","value":"1Gi"}`,
   222  			},
   223  		},
   224  		{
   225  			name:   "mutate pod(vpa not enabled) resource and annotation",
   226  			object: sts1,
   227  			gvr:    "statefulsets.v1.apps",
   228  			vpa:    vpa1,
   229  			spd:    spd1,
   230  			pod:    pod2,
   231  			review: &admissionv1beta1.AdmissionReview{
   232  				Request: &admissionv1beta1.AdmissionRequest{
   233  					UID: "test",
   234  					Object: runtime.RawExtension{
   235  						Raw: getPodJSON(pod2),
   236  					},
   237  				},
   238  			},
   239  			expPatch: []string{
   240  				`{"op":"replace","path":"/spec/containers/0/resources/requests/cpu","value":"1"}`,
   241  				`{"op":"replace","path":"/spec/containers/0/resources/requests/memory","value":"1Gi"}`,
   242  			},
   243  		},
   244  	} {
   245  		tc := tc
   246  		t.Run(tc.name, func(t *testing.T) {
   247  			t.Parallel()
   248  
   249  			genericConf := &generic.GenericConfiguration{}
   250  			webhookGenericConf := webhookconfig.NewGenericWebhookConfiguration()
   251  			webhookGenericConf.DynamicGVResources = []string{
   252  				"statefulsets.v1.apps",
   253  				"replicasets.v1.apps",
   254  				"deployments.v1.apps",
   255  			}
   256  
   257  			controlCtx, err := katalystbase.GenerateFakeGenericContext(nil, nil, []runtime.Object{tc.object})
   258  			assert.NoError(t, err)
   259  
   260  			workloadInformers := controlCtx.DynamicResourcesManager.GetDynamicInformers()
   261  
   262  			u, err := native.ToUnstructured(tc.object)
   263  			assert.NoError(t, err)
   264  			err = workloadInformers[tc.gvr].Informer.Informer().GetStore().Add(u)
   265  			assert.NoError(t, err)
   266  
   267  			wh, _, err := NewWebhookPod(context.TODO(), controlCtx, genericConf, webhookGenericConf, nil)
   268  			assert.NoError(t, err)
   269  
   270  			vpaInformer := controlCtx.InternalInformerFactory.Autoscaling().V1alpha1().KatalystVerticalPodAutoscalers()
   271  			err = vpaInformer.Informer().GetStore().Add(tc.vpa)
   272  			assert.NoError(t, err)
   273  
   274  			spdInformer := controlCtx.InternalInformerFactory.Workload().V1alpha1().ServiceProfileDescriptors()
   275  			err = spdInformer.Informer().GetStore().Add(tc.spd)
   276  			assert.NoError(t, err)
   277  
   278  			podInformer := controlCtx.KubeInformerFactory.Core().V1().Pods()
   279  			err = podInformer.Informer().GetStore().Add(tc.pod)
   280  			assert.NoError(t, err)
   281  
   282  			ar := &admissionv1beta1.AdmissionRequest{
   283  				Name:      tc.pod.Name,
   284  				Namespace: "default",
   285  			}
   286  
   287  			ctx := context.TODO()
   288  			ctx = whcontext.SetAdmissionRequest(ctx, ar)
   289  
   290  			gotResponse := wh.Review(ctx, tc.review)
   291  
   292  			// Check uid, allowed and patch
   293  			assert.True(t, gotResponse.Allowed)
   294  			assert.Equal(t, tc.review.Request.UID, gotResponse.UID)
   295  			gotPatch := string(gotResponse.Patch)
   296  			for _, expPatchOp := range tc.expPatch {
   297  				assert.Contains(t, gotPatch, expPatchOp)
   298  			}
   299  		})
   300  	}
   301  }