From 38eba42581ab3d693bd453822c6da96046f1140f Mon Sep 17 00:00:00 2001 From: wangxye Date: Tue, 21 May 2024 16:35:42 +0800 Subject: [PATCH] feat: support service webhook to add service topology annotation for vip-loadbalance Signed-off-by: wangxye --- .../v1alpha1/poolservice_default.go | 61 ----- .../v1alpha1/poolservice_default_test.go | 144 ------------ pkg/yurtmanager/webhook/server.go | 4 +- .../webhook/service/corev1/service_default.go | 47 ++++ .../service/corev1/service_default_test.go | 216 ++++++++++++++++++ .../corev1/service_handler.go} | 31 +-- 6 files changed, 276 insertions(+), 227 deletions(-) delete mode 100644 pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default.go delete mode 100644 pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default_test.go create mode 100644 pkg/yurtmanager/webhook/service/corev1/service_default.go create mode 100644 pkg/yurtmanager/webhook/service/corev1/service_default_test.go rename pkg/yurtmanager/webhook/{poolservice/v1alpha1/poolservice_handler.go => service/corev1/service_handler.go} (62%) diff --git a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default.go b/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default.go deleted file mode 100644 index bfc136dfec3..00000000000 --- a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright 2024 The OpenYurt 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 v1alpha1 - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - - "github.com/openyurtio/openyurt/pkg/apis/network" - "github.com/openyurtio/openyurt/pkg/apis/network/v1alpha1" - viplb "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/loadbalancerset/viploadbalancer" -) - -// Default satisfies the defaulting webhook interface. -func (webhook *PoolServiceHandler) Default(ctx context.Context, obj runtime.Object) error { - ps, ok := obj.(*v1alpha1.PoolService) - if !ok { - return apierrors.NewBadRequest(fmt.Sprintf("expected a PoolService but got a %T", obj)) - } - - v1alpha1.SetDefaultsPoolService(ps) - - if ps.Spec.LoadBalancerClass != nil { - if *ps.Spec.LoadBalancerClass == viplb.VipLoadBalancerClass { - service := &corev1.Service{} - svcName := ps.Labels[network.LabelServiceName] - if err := webhook.Client.Get(ctx, types.NamespacedName{Name: svcName, Namespace: ps.Namespace}, service); err != nil { - return apierrors.NewBadRequest(fmt.Sprintf("failed to get service %s corresponding to poolservice %s: %v", svcName, ps.Name, err)) - } - - if service.Annotations == nil { - service.Annotations = make(map[string]string) - } - - service.Annotations[viplb.AnnotationServiceTopologyKey] = viplb.AnnotationServiceTopologyValueNodePool - - if err := webhook.Client.Update(ctx, service); err != nil { - return err - } - } - - } - - return nil -} diff --git a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default_test.go b/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default_test.go deleted file mode 100644 index e10a7e6be6f..00000000000 --- a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_default_test.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright 2024 The OpenYurt 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 v1alpha1 - -import ( - "context" - "reflect" - "testing" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "github.com/openyurtio/openyurt/pkg/apis" - "github.com/openyurtio/openyurt/pkg/apis/network" - "github.com/openyurtio/openyurt/pkg/apis/network/v1alpha1" - viplb "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/loadbalancerset/viploadbalancer" -) - -func TestDefault(t *testing.T) { - var ( - elbClass = "service.openyurt.io/elb" - vipClass = "service.openyurt.io/viplb" - ) - - testcases := map[string]struct { - poolservice runtime.Object - errHappened bool - service *corev1.Service - wantedService *corev1.Service - }{ - "pod with specified loadBalancerClass but not viplb": { - poolservice: &v1alpha1.PoolService{ - Spec: v1alpha1.PoolServiceSpec{ - LoadBalancerClass: &elbClass, - }, - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - network.LabelServiceName: "nginx", - }, - }, - }, - service: &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "nginx", - ResourceVersion: "1", - }, - }, - wantedService: &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "nginx", - ResourceVersion: "1", - }, - }, - }, - "pod with specified loadBalancerClass: viplb": { - poolservice: &v1alpha1.PoolService{ - Spec: v1alpha1.PoolServiceSpec{ - LoadBalancerClass: &vipClass, - }, - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - network.LabelServiceName: "nginx", - }, - }, - }, - service: &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "nginx", - ResourceVersion: "1", - }, - }, - wantedService: &corev1.Service{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Service", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "nginx", - ResourceVersion: "2", - Annotations: map[string]string{ - viplb.AnnotationServiceTopologyKey: viplb.AnnotationServiceTopologyValueNodePool, - }, - }, - }, - }, - } - - for k, tc := range testcases { - t.Run(k, func(t *testing.T) { - scheme := runtime.NewScheme() - if err := clientgoscheme.AddToScheme(scheme); err != nil { - t.Fatal("Fail to add kubernetes clint-go custom resource") - } - apis.AddToScheme(scheme) - - var c client.Client - if tc.service != nil { - c = fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(tc.service).Build() - } else { - c = fakeclient.NewClientBuilder().WithScheme(scheme).Build() - } - - h := &PoolServiceHandler{Client: c} - err := h.Default(context.TODO(), tc.poolservice) - if tc.errHappened { - if err == nil { - t.Errorf("expect error, got nil") - } - } else if err != nil { - t.Errorf("expect no error, but got %v", err) - } else { - ps := tc.poolservice.(*v1alpha1.PoolService) - serviceName := ps.Labels[network.LabelServiceName] - currentServices := &corev1.Service{} - if err := c.Get(context.TODO(), client.ObjectKey{Name: serviceName}, currentServices); err != nil { - t.Errorf("failed to get service %s: %v", serviceName, err) - } - - if !reflect.DeepEqual(currentServices, tc.wantedService) { - t.Errorf("expect %#+v, got %#+v", tc.wantedService, currentServices) - } - } - }) - } -} diff --git a/pkg/yurtmanager/webhook/server.go b/pkg/yurtmanager/webhook/server.go index 675f2d09a36..2dd86051aa3 100644 --- a/pkg/yurtmanager/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -37,7 +37,7 @@ import ( v1alpha1platformadmin "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/platformadmin/v1alpha1" v1alpha2platformadmin "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/platformadmin/v1alpha2" v1alpha1pod "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/pod/v1alpha1" - v1alpha1poolservice "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/poolservice/v1alpha1" + corev1service "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/service/corev1" "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" webhookcontroller "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util/controller" v1alpha1yurtappdaemon "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/yurtappdaemon/v1alpha1" @@ -81,10 +81,10 @@ func init() { addControllerWebhook(names.PlatformAdminController, &v1alpha2platformadmin.PlatformAdminHandler{}) addControllerWebhook(names.YurtAppOverriderController, &v1alpha1yurtappoverrider.YurtAppOverriderHandler{}) addControllerWebhook(names.YurtAppOverriderController, &v1alpha1deploymentrender.DeploymentRenderHandler{}) - addControllerWebhook(names.VipLoadBalancerController, &v1alpha1poolservice.PoolServiceHandler{}) independentWebhooks[v1node.WebhookName] = &v1node.NodeHandler{} independentWebhooks[v1alpha1pod.WebhookName] = &v1alpha1pod.PodHandler{} + independentWebhooks[corev1service.WebhookName] = &corev1service.ServiceHandler{} } // Note !!! @kadisi diff --git a/pkg/yurtmanager/webhook/service/corev1/service_default.go b/pkg/yurtmanager/webhook/service/corev1/service_default.go new file mode 100644 index 00000000000..8d636ffa3c9 --- /dev/null +++ b/pkg/yurtmanager/webhook/service/corev1/service_default.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 The OpenYurt 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 corev1 + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + + viplb "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/loadbalancerset/viploadbalancer" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *ServiceHandler) Default(ctx context.Context, obj runtime.Object) error { + svc, ok := obj.(*corev1.Service) + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("expected a Service but got a %T", obj)) + } + + if svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + if svc.Spec.LoadBalancerClass != nil && *svc.Spec.LoadBalancerClass == viplb.VipLoadBalancerClass { + if svc.Annotations == nil { + svc.Annotations = make(map[string]string) + } + + if _, ok := svc.Annotations[viplb.AnnotationServiceTopologyKey]; !ok { + svc.Annotations[viplb.AnnotationServiceTopologyKey] = viplb.AnnotationServiceTopologyValueNodePool + } + } + } + + return nil +} diff --git a/pkg/yurtmanager/webhook/service/corev1/service_default_test.go b/pkg/yurtmanager/webhook/service/corev1/service_default_test.go new file mode 100644 index 00000000000..7a17172aaf0 --- /dev/null +++ b/pkg/yurtmanager/webhook/service/corev1/service_default_test.go @@ -0,0 +1,216 @@ +/* +Copyright 2024 The OpenYurt 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 corev1 + +import ( + "context" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openyurtio/openyurt/pkg/apis" + viplb "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/loadbalancerset/viploadbalancer" +) + +func TestDefault(t *testing.T) { + var ( + elbClass = "service.openyurt.io/elb" + vipClass = "service.openyurt.io/viplb" + ) + + testcases := map[string]struct { + errHappened bool + service runtime.Object + wantedService *corev1.Service + }{ + "service is not loadBalance type": { + service: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 80, + TargetPort: intstr.FromInt(80), + }, + }, + }, + }, + wantedService: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + Port: 80, + TargetPort: intstr.FromInt(80), + }, + }, + }, + }, + }, + "service with specified loadBalancer type but not viplb": { + service: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &elbClass, + }, + }, + wantedService: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &elbClass, + }, + }, + }, + "service with specified loadBalancer type: viplb": { + service: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &vipClass, + }, + }, + wantedService: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + Annotations: map[string]string{ + viplb.AnnotationServiceTopologyKey: viplb.AnnotationServiceTopologyValueNodePool, + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &vipClass, + }, + }, + }, + "service with specified loadBalancer type: viplb, but has specified AnnotationServiceTopologyKey": { + service: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + Annotations: map[string]string{ + viplb.AnnotationServiceTopologyKey: "service/nodepool", + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &vipClass, + }, + }, + wantedService: &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx", + ResourceVersion: "1", + Annotations: map[string]string{ + viplb.AnnotationServiceTopologyKey: "service/nodepool", + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerClass: &vipClass, + }, + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatal("Fail to add kubernetes clint-go custom resource") + } + apis.AddToScheme(scheme) + + var c client.Client + c = fakeclient.NewClientBuilder().WithScheme(scheme).Build() + + h := &ServiceHandler{Client: c} + err := h.Default(context.TODO(), tc.service) + if tc.errHappened { + if err == nil { + t.Errorf("expect error, got nil") + } + } else if err != nil { + t.Errorf("expect no error, but got %v", err) + } else { + currentServices := tc.service.(*corev1.Service) + if !reflect.DeepEqual(currentServices, tc.wantedService) { + t.Errorf("expect %#+v, \n got %#+v", tc.wantedService, currentServices) + } + } + }) + } +} diff --git a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_handler.go b/pkg/yurtmanager/webhook/service/corev1/service_handler.go similarity index 62% rename from pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_handler.go rename to pkg/yurtmanager/webhook/service/corev1/service_handler.go index b97dc9a6649..fb0b6a910a3 100644 --- a/pkg/yurtmanager/webhook/poolservice/v1alpha1/poolservice_handler.go +++ b/pkg/yurtmanager/webhook/service/corev1/service_handler.go @@ -11,44 +11,35 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* -Copyright 2024 The OpenYurt 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 v1alpha1 +package corev1 import ( + corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/webhook" - "github.com/openyurtio/openyurt/pkg/apis/network/v1alpha1" "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) +const ( + WebhookName = "service" +) + // SetupWebhookWithManager sets up Cluster webhooks. mutate path, validatepath, error -func (webhook *PoolServiceHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { +func (webhook *ServiceHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { // init webhook.Client = mgr.GetClient() - gvk, err := apiutil.GVKForObject(&v1alpha1.PoolService{}, mgr.GetScheme()) + gvk, err := apiutil.GVKForObject(&corev1.Service{}, mgr.GetScheme()) if err != nil { return "", "", err } return util.GenerateMutatePath(gvk), util.GenerateValidatePath(gvk), ctrl.NewWebhookManagedBy(mgr). - For(&v1alpha1.PoolService{}). + For(&corev1.Service{}). WithDefaulter(webhook). Complete() } @@ -56,8 +47,8 @@ func (webhook *PoolServiceHandler) SetupWebhookWithManager(mgr ctrl.Manager) (st // +kubebuilder:webhook:path=/mutate-network-openyurt-io-poolservice,mutating=true,failurePolicy=fail,sideEffects=None,admissionReviewVersions=v1;v1beta1,groups=network.openyurt.io,resources=poolservices,verbs=create;update,versions=v1alpha1,name=mutate.network.v1alpha1.poolservice.openyurt.io // Cluster implements a validating and defaulting webhook for Cluster. -type PoolServiceHandler struct { +type ServiceHandler struct { Client client.Client } -var _ webhook.CustomDefaulter = &PoolServiceHandler{} +var _ webhook.CustomDefaulter = &ServiceHandler{}