From 08e2b1aaab7bdf12701af524b7ada7f7019d40d5 Mon Sep 17 00:00:00 2001 From: tnsimon Date: Tue, 26 Nov 2024 22:20:28 +1100 Subject: [PATCH 1/4] feat: add endpoints webhook for node autonomy --- .../yurt-manager-auto-generated.yaml | 19 + .../controller/util/pod/pod_util.go | 10 + .../controller/util/pod/pod_util_test.go | 47 ++ .../webhook/endpoints/v1/endpoints_default.go | 113 ++++ .../endpoints/v1/endpoints_default_test.go | 568 ++++++++++++++++++ .../webhook/endpoints/v1/endpoints_handler.go | 32 + pkg/yurtmanager/webhook/server.go | 3 + 7 files changed, 792 insertions(+) create mode 100644 pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go create mode 100644 pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go create mode 100644 pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go diff --git a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml index 01b0e219a63..6cb167d5c95 100644 --- a/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml +++ b/charts/yurt-manager/templates/yurt-manager-auto-generated.yaml @@ -1419,6 +1419,25 @@ webhooks: resources: - deployments sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: yurt-manager-webhook-service + namespace: {{ .Release.Namespace }} + path: /mutate-core-openyurt-io-v1-endpoints + failurePolicy: Ignore + name: mutate.core.v1.endpoints.openyurt.io + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - UPDATE + resources: + - endpoints + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/pkg/yurtmanager/controller/util/pod/pod_util.go b/pkg/yurtmanager/controller/util/pod/pod_util.go index 9a6da0359ea..81da8707f73 100644 --- a/pkg/yurtmanager/controller/util/pod/pod_util.go +++ b/pkg/yurtmanager/controller/util/pod/pod_util.go @@ -58,6 +58,16 @@ func IsPodReadyConditionTrue(status v1.PodStatus) bool { return condition != nil && condition.Status == v1.ConditionTrue } +// IsPodCrashLoopBackOff returns true if a pod is in CrashLoopBackOff state; false otherwise. +func IsPodCrashLoopBackOff(status v1.PodStatus) bool { + for _, c := range status.ContainerStatuses { + if c.State.Waiting != nil && c.State.Waiting.Reason == "CrashLoopBackOff" { + return true + } + } + return false +} + // GetPodReadyCondition extracts the pod ready condition from the given status and returns that. // Returns nil if the condition is not present. func GetPodReadyCondition(status v1.PodStatus) *v1.PodCondition { diff --git a/pkg/yurtmanager/controller/util/pod/pod_util_test.go b/pkg/yurtmanager/controller/util/pod/pod_util_test.go index 077c3d0e9bd..4e6351de7c8 100644 --- a/pkg/yurtmanager/controller/util/pod/pod_util_test.go +++ b/pkg/yurtmanager/controller/util/pod/pod_util_test.go @@ -20,6 +20,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" @@ -168,3 +169,49 @@ func TestUpdatePodCondition(t *testing.T) { }) } } + +func TestIsPodCrashLoopBackoff(t *testing.T) { + testCases := []struct { + name string + status v1.PodStatus + expect bool + }{ + { + name: "yes", + status: v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + }, + }, + }, + }, + }, + expect: true, + }, + { + name: "no", + status: v1.PodStatus{ + ContainerStatuses: []v1.ContainerStatus{ + { + State: v1.ContainerState{}, + }, + }, + }, + expect: false, + }, + { + name: "empty", + status: v1.PodStatus{}, + expect: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expect, IsPodCrashLoopBackOff(tc.status)) + }) + } +} diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go new file mode 100644 index 00000000000..b2c7e16fe89 --- /dev/null +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go @@ -0,0 +1,113 @@ +package v1 + +import ( + "context" + "fmt" + + nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" + podutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/pod" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Default satisfies the defaulting webhook interface. +func (webhook *EndpointsHandler) Default(ctx context.Context, obj runtime.Object) error { + endpoints, ok := obj.(*corev1.Endpoints) + if !ok { + return fmt.Errorf("expected an Endpoints object but got %T", obj) + } + + return remapAutonomyEndpoints(ctx, webhook.Client, endpoints) +} + +// isNodeAutonomous checks if the node has autonomy annotations +// and returns true if it does, false otherwise. +func isNodeAutonomous(ctx context.Context, c client.Client, nodeName string) (bool, error) { + node := &corev1.Node{} + err := c.Get(ctx, client.ObjectKey{Name: nodeName}, node) + if err != nil { + // If node doesn't exist, it doesn't have autonomy + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + return nodeutil.IsPodBoundenToNode(node), nil +} + +// isPodCrashLoopBackOff checks if the pod is crashloopbackoff +// and returns true if it is, false otherwise. +func isPodCrashLoopBackOff(ctx context.Context, c client.Client, podName, namespace string) (bool, error) { + pod := &corev1.Pod{} + err := c.Get(ctx, client.ObjectKey{Name: podName, Namespace: namespace}, pod) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + return podutil.IsPodCrashLoopBackOff(pod.Status), nil +} + +// remapAutonomyEndpoints remaps the notReadyAddresses to the readyAddresses +// for the subsets scheduled to nodes that have autonomy annotations. +// The function checks the pod status and if the pod is not in crashloopbackoff, +// it remaps the address to readyAddresses. +func remapAutonomyEndpoints(ctx context.Context, client client.Client, endpoints *corev1.Endpoints) error { + // Track nodes with autonomy to avoid repeated checks + nodesWithAutonomy := make(map[string]bool) + + // Get all the notReadyAddresses for subsets + for i, s := range endpoints.Subsets { + // Create a zero-length slice with the same underlying array + newNotReadyAddresses := s.NotReadyAddresses[:0] + + for _, a := range s.NotReadyAddresses { + if a.NodeName == nil || a.TargetRef == nil { + newNotReadyAddresses = append(newNotReadyAddresses, a) + continue + } + + // Get the node and check autonomy annotations + hasAutonomy, ok := nodesWithAutonomy[*a.NodeName] + if !ok { + isAutonomous, err := isNodeAutonomous(ctx, client, *a.NodeName) + if err != nil { + return err + } + // Store autonomy status for future checks + nodesWithAutonomy[*a.NodeName] = isAutonomous + hasAutonomy = isAutonomous + } + + // If the node doesn't have autonomy, skip + if !hasAutonomy { + newNotReadyAddresses = append(newNotReadyAddresses, a) + continue + } + + // Get the pod + isPodCrashLoopBackOff, err := isPodCrashLoopBackOff(ctx, client, a.TargetRef.Name, a.TargetRef.Namespace) + if err != nil { + return err + } + + if isPodCrashLoopBackOff { + newNotReadyAddresses = append(newNotReadyAddresses, a) + continue + } + + // Move the address to the ready addresses in the subset + endpoints.Subsets[i].Addresses = append(endpoints.Subsets[i].Addresses, *a.DeepCopy()) + } + + // Update the subset with the new notReadyAddresses + endpoints.Subsets[i].NotReadyAddresses = newNotReadyAddresses + } + + return nil +} diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go new file mode 100644 index 00000000000..8340db1b02e --- /dev/null +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go @@ -0,0 +1,568 @@ +package v1_test + +import ( + "context" + "testing" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/projectinfo" + nodeutils "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" + v1 "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/endpoints/v1" + "github.com/stretchr/testify/require" + 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" + "k8s.io/utils/ptr" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestDefault_AutonomyAnnotations(t *testing.T) { + endpoint1 := corev1.EndpointAddress{ + IP: "10.0.0.1", + NodeName: ptr.To("node1"), + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: "default", + }, + } + + // Endpoint2 is mapped to pod2 which is always ready + endpoint2 := corev1.EndpointAddress{ + IP: "10.0.0.2", + NodeName: ptr.To("node1"), + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod2", + Namespace: "default", + }, + } + + // Fix the pod to ready for the test + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, + }, + } + + // Test cases for Default + // endpoint2 should either be remapped or not + tests := []struct { + name string + endpoints *corev1.Endpoints + node *corev1.Node + expectedEndpoints *corev1.Endpoints + expectErr bool + }{ + { + name: "Node autonomy duration annotation", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + projectinfo.GetNodeAutonomyDurationAnnotation(): "10m", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + endpoint1, + endpoint2, // endpoint2 moved to readyAddresses + }, + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + { + name: "Node autonomy duration annotation empty", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + projectinfo.GetNodeAutonomyDurationAnnotation(): "", // empty + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, // not moved to ready + }, + }, + }, + expectErr: false, + }, + { + name: "Autonomy annotation true", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + projectinfo.GetAutonomyAnnotation(): "true", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + endpoint1, + endpoint2, + }, // endpoint2 moved to readyAddresses + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + { + name: "Autonomy annotation false", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + projectinfo.GetAutonomyAnnotation(): "false", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, // not moved to ready + }, + }, + }, + expectErr: false, + }, + { + name: "Pod binding annotation true", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + nodeutils.PodBindingAnnotation: "true", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + endpoint1, + endpoint2, + }, // endpoint2 moved to readyAddresses + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + { + name: "Pod binding annotation false", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + nodeutils.PodBindingAnnotation: "false", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, // not moved to ready + }, + }, + }, + expectErr: false, + }, + { + name: "Node has no annotations", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{}, // Nothing + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + expectErr: false, + }, + { + name: "Other node", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + node: &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node3", // Other + Annotations: map[string]string{ + projectinfo.GetNodeAutonomyDurationAnnotation(): "10m", + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + expectErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + scheme := runtime.NewScheme() + err := clientgoscheme.AddToScheme(scheme) + require.NoError(t, err, "Fail to add kubernetes clint-go custom resource") + + apis.AddToScheme(scheme) + + // Build client + clientBuilder := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(pod) + if tc.node != nil { + clientBuilder = clientBuilder.WithObjects(tc.node) + } + + // Invoke Default + w := &v1.EndpointsHandler{Client: clientBuilder.Build()} + err = w.Default(context.TODO(), tc.endpoints) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + // Check the result + require.Equal(t, tc.expectedEndpoints, tc.endpoints) + }) + } +} + +func TestDefault_PodCrashLoopBack(t *testing.T) { + endpoint1 := corev1.EndpointAddress{ + IP: "10.0.0.1", + NodeName: ptr.To("node1"), + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod1", + Namespace: "default", + }, + } + + endpoint2 := corev1.EndpointAddress{ + IP: "10.0.0.2", + NodeName: ptr.To("node1"), + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod2", + Namespace: "default", + }, + } + + // Fix the node annotation to autonomy duration + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + Annotations: map[string]string{ + projectinfo.GetNodeAutonomyDurationAnnotation(): "10m", + }, + }, + } + + // Test cases for Default + // endpoint2 should either be remapped or not + tests := []struct { + name string + endpoints *corev1.Endpoints + pod *corev1.Pod + expectedEndpoints *corev1.Endpoints + expectErr bool + }{ + { + name: "Pod not crashloopback", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + endpoint1, + endpoint2, // endpoint2 moved to readyAddresses + }, + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + { + name: "Pod is crashloopbackoff", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + }, + }, + }, + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, // not moved to ready + }, + }, + }, + expectErr: false, + }, + { + name: "Pod no container states", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + Status: corev1.PodStatus{}, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1, endpoint2}, + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + { + name: "Pod multiple container statuses", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{}, + }, + }, + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "CrashLoopBackOff", + }, + }, + }, + }, + }, + }, + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, // not moved to ready + }, + }, + }, + expectErr: false, + }, + { + name: "Pod is empty", + endpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{endpoint1}, + NotReadyAddresses: []corev1.EndpointAddress{endpoint2}, + }, + }, + }, + pod: &corev1.Pod{}, // Empty pod + expectedEndpoints: &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + endpoint1, + endpoint2, + }, // endpoint2 moved to readyAddresses + NotReadyAddresses: []corev1.EndpointAddress{}, + }, + }, + }, + expectErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + scheme := runtime.NewScheme() + err := clientgoscheme.AddToScheme(scheme) + require.NoError(t, err, "Fail to add kubernetes clint-go custom resource") + + apis.AddToScheme(scheme) + + // Build client + clientBuilder := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(node) + + // Pod + if tc.pod != nil { + clientBuilder = clientBuilder.WithObjects(tc.pod) + } + + // Invoke Default + w := &v1.EndpointsHandler{Client: clientBuilder.Build()} + err = w.Default(context.TODO(), tc.endpoints) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + // Check the result + require.Equal(t, tc.expectedEndpoints, tc.endpoints) + }) + } +} diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go new file mode 100644 index 00000000000..a3117d0c0c5 --- /dev/null +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go @@ -0,0 +1,32 @@ +package v1 + +import ( + yurtClient "github.com/openyurtio/openyurt/cmd/yurt-manager/app/client" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" + v1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +const ( + WebhookName = "endpoints" +) + +// EndpointsHandler implements a defaulting webhook for Endpoints. +type EndpointsHandler struct { + Client client.Client +} + +// SetupWebhookWithManager sets up Endpoints webhooks. +func (webhook *EndpointsHandler) SetupWebhookWithManager(mgr ctrl.Manager) (string, string, error) { + // init + webhook.Client = yurtClient.GetClientByControllerNameOrDie(mgr, names.NodeLifeCycleController) + + return util.RegisterWebhook(mgr, &v1.Endpoints{}, webhook) +} + +// +kubebuilder:webhook:path=/mutate-core-openyurt-io-v1-endpoints,mutating=true,failurePolicy=ignore,sideEffects=None,admissionReviewVersions=v1,groups="",resources=endpoints,verbs=update,versions=v1,name=mutate.core.v1.endpoints.openyurt.io + +var _ webhook.CustomDefaulter = &EndpointsHandler{} diff --git a/pkg/yurtmanager/webhook/server.go b/pkg/yurtmanager/webhook/server.go index 006b24ddeb6..fe19979c326 100644 --- a/pkg/yurtmanager/webhook/server.go +++ b/pkg/yurtmanager/webhook/server.go @@ -31,6 +31,7 @@ import ( "github.com/openyurtio/openyurt/cmd/yurt-manager/names" controller "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/base" v1alpha1deploymentrender "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/deploymentrender/v1alpha1" + v1endpoints "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/endpoints/v1" v1beta1gateway "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/gateway/v1beta1" v1node "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/node/v1" v1beta1nodepool "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/nodepool/v1beta1" @@ -82,6 +83,8 @@ func init() { independentWebhooks[v1node.WebhookName] = &v1node.NodeHandler{} independentWebhooks[v1alpha1pod.WebhookName] = &v1alpha1pod.PodHandler{} + independentWebhooks[v1endpoints.WebhookName] = &v1endpoints.EndpointsHandler{} + } // Note !!! @kadisi From dd09fc99057ab25b3a1dbccd419945a265b3b85a Mon Sep 17 00:00:00 2001 From: Simon Tien Date: Mon, 2 Dec 2024 10:04:32 +1100 Subject: [PATCH 2/4] fix: add license header --- .../webhook/endpoints/v1/endpoints_default.go | 16 ++++++++++++++++ .../endpoints/v1/endpoints_default_test.go | 16 ++++++++++++++++ .../webhook/endpoints/v1/endpoints_handler.go | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go index b2c7e16fe89..917de6a4fe3 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go @@ -1,3 +1,19 @@ +/* +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 v1 import ( diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go index 8340db1b02e..32eb1744856 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go @@ -1,3 +1,19 @@ +/* +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 v1_test import ( diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go index a3117d0c0c5..ece3249ee3a 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go @@ -1,3 +1,19 @@ +/* +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 v1 import ( From 28d30125fed263b58fb291e0a7abe125910bb600 Mon Sep 17 00:00:00 2001 From: Simon Tien Date: Mon, 2 Dec 2024 10:38:03 +1100 Subject: [PATCH 3/4] lint: fix gci lint error --- .../webhook/endpoints/v1/endpoints_default.go | 5 +++-- .../webhook/endpoints/v1/endpoints_default_test.go | 9 +++++---- .../webhook/endpoints/v1/endpoints_handler.go | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go index 917de6a4fe3..cf7160d168b 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go @@ -20,12 +20,13 @@ import ( "context" "fmt" - nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" - podutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/pod" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + + nodeutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" + podutil "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/pod" ) // Default satisfies the defaulting webhook interface. diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go index 32eb1744856..926575af795 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default_test.go @@ -20,10 +20,6 @@ import ( "context" "testing" - "github.com/openyurtio/openyurt/pkg/apis" - "github.com/openyurtio/openyurt/pkg/projectinfo" - nodeutils "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" - v1 "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/endpoints/v1" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -31,6 +27,11 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/openyurtio/openyurt/pkg/apis" + "github.com/openyurtio/openyurt/pkg/projectinfo" + nodeutils "github.com/openyurtio/openyurt/pkg/yurtmanager/controller/util/node" + v1 "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/endpoints/v1" ) func TestDefault_AutonomyAnnotations(t *testing.T) { diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go index ece3249ee3a..1b315681dc6 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_handler.go @@ -17,13 +17,14 @@ limitations under the License. package v1 import ( - yurtClient "github.com/openyurtio/openyurt/cmd/yurt-manager/app/client" - "github.com/openyurtio/openyurt/cmd/yurt-manager/names" - "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" v1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/webhook" + + yurtClient "github.com/openyurtio/openyurt/cmd/yurt-manager/app/client" + "github.com/openyurtio/openyurt/cmd/yurt-manager/names" + "github.com/openyurtio/openyurt/pkg/yurtmanager/webhook/util" ) const ( From 0f3847473518ef3c546756d3324208c33aa7fc52 Mon Sep 17 00:00:00 2001 From: Simon Tien Date: Tue, 3 Dec 2024 07:50:10 +1100 Subject: [PATCH 4/4] fix: pr feedback --- pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go index cf7160d168b..fe25c2e505b 100644 --- a/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go +++ b/pkg/yurtmanager/webhook/endpoints/v1/endpoints_default.go @@ -33,7 +33,7 @@ import ( func (webhook *EndpointsHandler) Default(ctx context.Context, obj runtime.Object) error { endpoints, ok := obj.(*corev1.Endpoints) if !ok { - return fmt.Errorf("expected an Endpoints object but got %T", obj) + apierrors.NewBadRequest(fmt.Sprintf("expected an Endpoints object but got %T", obj)) } return remapAutonomyEndpoints(ctx, webhook.Client, endpoints)