diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 388c7cd2d..afd8c1f26 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -7,13 +7,7 @@ rules: - apiGroups: - "" resources: - - events - verbs: - - create - - patch -- apiGroups: - - "" - resources: + - endpoints - namespaces - pods - secrets @@ -22,6 +16,13 @@ rules: - get - list - watch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch - apiGroups: - apisix.apache.org resources: diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index c4dde0d5a..9c5685932 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -59,19 +59,29 @@ type ApisixRouteReconciler struct { Provider provider.Provider Updater status.Updater Readier readiness.ReadinessManager + + // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API + supportsEndpointSlice bool } // SetupWithManager sets up the controller with the Manager. func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). + // Check and store EndpointSlice API support + r.supportsEndpointSlice = pkgutils.HasAPIResource(mgr, &discoveryv1.EndpointSlice{}) + + eventFilters := []predicate.Predicate{ + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), + } + + if !r.supportsEndpointSlice { + eventFilters = append(eventFilters, predicate.NewPredicateFuncs(TypePredicate[*corev1.Endpoints]())) + } + + bdr := ctrl.NewControllerManagedBy(mgr). For(&apiv2.ApisixRoute{}). - WithEventFilter( - predicate.Or( - predicate.GenerationChangedPredicate{}, - predicate.AnnotationChangedPredicate{}, - predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), - ), - ). + WithEventFilter(predicate.Or(eventFilters...)). Watches( &networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForIngressClass), @@ -81,10 +91,15 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { ). Watches(&v1alpha1.GatewayProxy{}, handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForGatewayProxy), - ). - Watches(&discoveryv1.EndpointSlice{}, - handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForService), - ). + ) + + // Conditionally watch EndpointSlice or Endpoints based on cluster API support + bdr = watchEndpointSliceOrEndpoints(bdr, r.supportsEndpointSlice, + r.listApisixRoutesForService, + r.listApisixRoutesForEndpoints, + r.Log) + + return bdr. Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForSecret), ). @@ -341,23 +356,46 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid } tc.Services[serviceNN] = &service - var endpoints discoveryv1.EndpointSliceList - if err := r.List(ctx, &endpoints, - client.InNamespace(service.Namespace), - client.MatchingLabels{ - discoveryv1.LabelServiceName: service.Name, - }, - ); err != nil { - return types.ReasonError{ - Reason: string(apiv2.ConditionReasonInvalidSpec), - Message: fmt.Sprintf("failed to list endpoint slices: %v", err), + // Conditionally collect EndpointSlice or Endpoints based on cluster API support + if r.supportsEndpointSlice { + var endpoints discoveryv1.EndpointSliceList + if err := r.List(ctx, &endpoints, + client.InNamespace(service.Namespace), + client.MatchingLabels{ + discoveryv1.LabelServiceName: service.Name, + }, + ); err != nil { + return types.ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to list endpoint slices: %v", err), + } } - } - // backend.subset specifies a subset of upstream nodes. - // It specifies that the target pod's label should be a superset of the subset labels of the ApisixUpstream of the serviceName - subsetLabels := r.getSubsetLabels(tc, serviceNN, backend) - tc.EndpointSlices[serviceNN] = r.filterEndpointSlicesBySubsetLabels(ctx, endpoints.Items, subsetLabels) + // backend.subset specifies a subset of upstream nodes. + // It specifies that the target pod's label should be a superset of the subset labels of the ApisixUpstream of the serviceName + subsetLabels := r.getSubsetLabels(tc, serviceNN, backend) + tc.EndpointSlices[serviceNN] = r.filterEndpointSlicesBySubsetLabels(ctx, endpoints.Items, subsetLabels) + } else { + // Fallback to Endpoints API for Kubernetes 1.18 compatibility + var ep corev1.Endpoints + if err := r.Get(ctx, serviceNN, &ep); err != nil { + if client.IgnoreNotFound(err) != nil { + return types.ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to get endpoints: %v", err), + } + } + // If endpoints not found, create empty EndpointSlice list + tc.EndpointSlices[serviceNN] = []discoveryv1.EndpointSlice{} + } else { + // Convert Endpoints to EndpointSlice format for internal consistency + convertedEndpointSlices := pkgutils.ConvertEndpointsToEndpointSlice(&ep) + + // Apply subset filtering to converted EndpointSlices + subsetLabels := r.getSubsetLabels(tc, serviceNN, backend) + tc.EndpointSlices[serviceNN] = r.filterEndpointSlicesBySubsetLabels(ctx, convertedEndpointSlices, subsetLabels) + } + } } return nil @@ -443,6 +481,32 @@ func (r *ApisixRouteReconciler) listApisixRoutesForService(ctx context.Context, return pkgutils.DedupComparable(requests) } +// listApisixRoutesForEndpoints handles Endpoints objects and converts them to ApisixRoute reconcile requests. +// This function provides backward compatibility for Kubernetes 1.18 clusters that don't support EndpointSlice. +func (r *ApisixRouteReconciler) listApisixRoutesForEndpoints(ctx context.Context, obj client.Object) []reconcile.Request { + endpoint, ok := obj.(*corev1.Endpoints) + if !ok { + return nil + } + + var ( + namespace = endpoint.GetNamespace() + serviceName = endpoint.GetName() // For Endpoints, the name is the service name + arList apiv2.ApisixRouteList + ) + if err := r.List(ctx, &arList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, serviceName), + }); err != nil { + r.Log.Error(err, "failed to list apisixroutes by service", "service", serviceName) + return nil + } + requests := make([]reconcile.Request, 0, len(arList.Items)) + for _, ar := range arList.Items { + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) + } + return pkgutils.DedupComparable(requests) +} + func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, obj client.Object) []reconcile.Request { secret, ok := obj.(*corev1.Secret) if !ok { diff --git a/internal/controller/gatewayproxy_controller.go b/internal/controller/gatewayproxy_controller.go index 43ba32f74..daee41b07 100644 --- a/internal/controller/gatewayproxy_controller.go +++ b/internal/controller/gatewayproxy_controller.go @@ -38,6 +38,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // GatewayProxyController reconciles a GatewayProxy object. @@ -47,23 +48,38 @@ type GatewayProxyController struct { Scheme *runtime.Scheme Log logr.Logger Provider provider.Provider + + // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API + supportsEndpointSlice bool } func (r *GatewayProxyController) SetupWithManager(mrg ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mrg). + // Check and store EndpointSlice API support + r.supportsEndpointSlice = pkgutils.HasAPIResource(mrg, &discoveryv1.EndpointSlice{}) + + eventFilters := []predicate.Predicate{ + predicate.GenerationChangedPredicate{}, + predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), + } + + if !r.supportsEndpointSlice { + eventFilters = append(eventFilters, predicate.NewPredicateFuncs(TypePredicate[*corev1.Endpoints]())) + } + + bdr := ctrl.NewControllerManagedBy(mrg). For(&v1alpha1.GatewayProxy{}). - WithEventFilter( - predicate.Or( - predicate.GenerationChangedPredicate{}, - predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), - ), - ). + WithEventFilter(predicate.Or(eventFilters...)). Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForProviderService), - ). - Watches(&discoveryv1.EndpointSlice{}, - handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForProviderEndpointSlice), - ). + ) + + // Conditionally watch EndpointSlice or Endpoints based on cluster API support + bdr = watchEndpointSliceOrEndpoints(bdr, r.supportsEndpointSlice, + r.listGatewayProxiesForProviderEndpointSlice, + r.listGatewayProxiesForProviderEndpoints, + r.Log) + + return bdr. Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.listGatewayProxiesForSecret), ). @@ -93,10 +109,10 @@ func (r *GatewayProxyController) Reconcile(ctx context.Context, req ctrl.Request if providerService == nil { tctx.EndpointSlices[req.NamespacedName] = nil } else { - if err := addProviderEndpointsToTranslateContext(tctx, r.Client, types.NamespacedName{ + if err := addProviderEndpointsToTranslateContextWithEndpointSliceSupport(tctx, r.Client, types.NamespacedName{ Namespace: gp.Namespace, Name: providerService.Name, - }); err != nil { + }, r.supportsEndpointSlice); err != nil { return reconcile.Result{}, err } } @@ -174,6 +190,20 @@ func (r *GatewayProxyController) listGatewayProxiesForProviderEndpointSlice(ctx }) } +// listGatewayProxiesForProviderEndpoints handles Endpoints objects and converts them to GatewayProxy reconcile requests. +// This function provides backward compatibility for Kubernetes 1.18 clusters that don't support EndpointSlice. +func (r *GatewayProxyController) listGatewayProxiesForProviderEndpoints(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + endpoint, ok := obj.(*corev1.Endpoints) + if !ok { + r.Log.Error(errors.New("unexpected object type"), "failed to convert object to Endpoints") + return nil + } + + return ListRequests(ctx, r.Client, r.Log, &v1alpha1.GatewayProxyList{}, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(endpoint.GetNamespace(), endpoint.GetName()), + }) +} + func (r *GatewayProxyController) listGatewayProxiesForSecret(ctx context.Context, object client.Object) []reconcile.Request { secret, ok := object.(*corev1.Secret) if !ok { diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index b6f392878..f924f2880 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -52,6 +52,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // HTTPRouteReconciler reconciles a GatewayClass object. @@ -67,18 +68,37 @@ type HTTPRouteReconciler struct { //nolint:revive Updater status.Updater Readier readiness.ReadinessManager + + // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API + supportsEndpointSlice bool } // SetupWithManager sets up the controller with the Manager. func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { r.genericEvent = make(chan event.GenericEvent, 100) + // Check and store EndpointSlice API support + r.supportsEndpointSlice = pkgutils.HasAPIResource(mgr, &discoveryv1.EndpointSlice{}) + + eventFilters := []predicate.Predicate{ + predicate.GenerationChangedPredicate{}, + } + + if !r.supportsEndpointSlice { + eventFilters = append(eventFilters, predicate.NewPredicateFuncs(TypePredicate[*corev1.Endpoints]())) + } + bdr := ctrl.NewControllerManagedBy(mgr). For(&gatewayv1.HTTPRoute{}). - WithEventFilter(predicate.GenerationChangedPredicate{}). - Watches(&discoveryv1.EndpointSlice{}, - handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByServiceBef), - ). + WithEventFilter(predicate.Or(eventFilters...)) + + // Conditionally watch EndpointSlice or Endpoints based on cluster API support + bdr = watchEndpointSliceOrEndpoints(bdr, r.supportsEndpointSlice, + r.listHTTPRoutesByServiceBef, + r.listHTTPRoutesByServiceForEndpoints, + r.Log) + + bdr = bdr. Watches(&v1alpha1.PluginConfig{}, handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByExtensionRef), ). @@ -288,6 +308,36 @@ func (r *HTTPRouteReconciler) listHTTPRoutesByServiceBef(ctx context.Context, ob return requests } +// listHTTPRoutesByServiceForEndpoints handles Endpoints objects and converts them to HTTPRoute reconcile requests. +// This function provides backward compatibility for Kubernetes 1.18 clusters that don't support EndpointSlice. +func (r *HTTPRouteReconciler) listHTTPRoutesByServiceForEndpoints(ctx context.Context, obj client.Object) []reconcile.Request { + endpoint, ok := obj.(*corev1.Endpoints) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to Endpoints") + return nil + } + namespace := endpoint.GetNamespace() + serviceName := endpoint.GetName() // For Endpoints, the name is the service name + + hrList := &gatewayv1.HTTPRouteList{} + if err := r.List(ctx, hrList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, serviceName), + }); err != nil { + r.Log.Error(err, "failed to list httproutes by service", "service", serviceName) + return nil + } + requests := make([]reconcile.Request, 0, len(hrList.Items)) + for _, hr := range hrList.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: hr.Namespace, + Name: hr.Name, + }, + }) + } + return requests +} + func (r *HTTPRouteReconciler) listHTTPRoutesByExtensionRef(ctx context.Context, obj client.Object) []reconcile.Request { pluginconfig, ok := obj.(*v1alpha1.PluginConfig) if !ok { @@ -520,19 +570,37 @@ func (r *HTTPRouteReconciler) processHTTPRouteBackendRefs(tctx *provider.Transla } tctx.Services[targetNN] = &service - endpointSliceList := new(discoveryv1.EndpointSliceList) - if err := r.List(tctx, endpointSliceList, - client.InNamespace(targetNN.Namespace), - client.MatchingLabels{ - discoveryv1.LabelServiceName: targetNN.Name, - }, - ); err != nil { - r.Log.Error(err, "failed to list endpoint slices", "Service", targetNN) - terr = err - continue + // Conditionally collect EndpointSlice or Endpoints based on cluster API support + if r.supportsEndpointSlice { + endpointSliceList := new(discoveryv1.EndpointSliceList) + if err := r.List(tctx, endpointSliceList, + client.InNamespace(targetNN.Namespace), + client.MatchingLabels{ + discoveryv1.LabelServiceName: targetNN.Name, + }, + ); err != nil { + r.Log.Error(err, "failed to list endpoint slices", "Service", targetNN) + terr = err + continue + } + tctx.EndpointSlices[targetNN] = endpointSliceList.Items + } else { + // Fallback to Endpoints API for Kubernetes 1.18 compatibility + var endpoints corev1.Endpoints + if err := r.Get(tctx, targetNN, &endpoints); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "failed to get endpoints", "Service", targetNN) + terr = err + continue + } + // If endpoints not found, create empty EndpointSlice list + tctx.EndpointSlices[targetNN] = []discoveryv1.EndpointSlice{} + } else { + // Convert Endpoints to EndpointSlice format for internal consistency + convertedEndpointSlices := pkgutils.ConvertEndpointsToEndpointSlice(&endpoints) + tctx.EndpointSlices[targetNN] = convertedEndpointSlices + } } - - tctx.EndpointSlices[targetNN] = endpointSliceList.Items } return terr } diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index 25b52990c..84ffbfc7d 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -48,6 +48,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/manager/readiness" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // IngressReconciler reconciles a Ingress object. @@ -61,36 +62,50 @@ type IngressReconciler struct { //nolint:revive Updater status.Updater Readier readiness.ReadinessManager + + // supportsEndpointSlice indicates whether the cluster supports EndpointSlice API + supportsEndpointSlice bool } // SetupWithManager sets up the controller with the Manager. func (r *IngressReconciler) SetupWithManager(mgr ctrl.Manager) error { r.genericEvent = make(chan event.GenericEvent, 100) - return ctrl.NewControllerManagedBy(mgr). + // Check and store EndpointSlice API support + r.supportsEndpointSlice = pkgutils.HasAPIResource(mgr, &discoveryv1.EndpointSlice{}) + + eventFilters := []predicate.Predicate{ + predicate.GenerationChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), + } + + if !r.supportsEndpointSlice { + eventFilters = append(eventFilters, predicate.NewPredicateFuncs(TypePredicate[*corev1.Endpoints]())) + } + + bdr := ctrl.NewControllerManagedBy(mgr). For(&networkingv1.Ingress{}, builder.WithPredicates( predicate.NewPredicateFuncs(r.checkIngressClass), ), ). - WithEventFilter( - predicate.Or( - predicate.GenerationChangedPredicate{}, - predicate.AnnotationChangedPredicate{}, - predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()), - ), - ). + WithEventFilter(predicate.Or(eventFilters...)). Watches( &networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listIngressForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(r.matchesIngressController), ), - ). - Watches( - &discoveryv1.EndpointSlice{}, - handler.EnqueueRequestsFromMapFunc(r.listIngressesByService), - ). + ) + + // Conditionally watch EndpointSlice or Endpoints based on cluster API support + bdr = watchEndpointSliceOrEndpoints(bdr, r.supportsEndpointSlice, + r.listIngressesByService, + r.listIngressesByEndpoints, + r.Log) + + return bdr. Watches( &corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(r.listIngressesBySecret), @@ -319,6 +334,40 @@ func (r *IngressReconciler) listIngressesByService(ctx context.Context, obj clie return requests } +// listIngressesByEndpoints handles Endpoints objects and converts them to Ingress reconcile requests. +// This function provides backward compatibility for Kubernetes 1.18 clusters that don't support EndpointSlice. +func (r *IngressReconciler) listIngressesByEndpoints(ctx context.Context, obj client.Object) []reconcile.Request { + endpoint, ok := obj.(*corev1.Endpoints) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to Endpoints") + return nil + } + + namespace := endpoint.GetNamespace() + serviceName := endpoint.GetName() // For Endpoints, the name is the service name + + ingressList := &networkingv1.IngressList{} + if err := r.List(ctx, ingressList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, serviceName), + }); err != nil { + r.Log.Error(err, "failed to list ingresses by service", "service", serviceName) + return nil + } + + requests := make([]reconcile.Request, 0, len(ingressList.Items)) + for _, ingress := range ingressList.Items { + if r.checkIngressClass(&ingress) { + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: ingress.Namespace, + Name: ingress.Name, + }, + }) + } + } + return requests +} + // listIngressesBySecret list all ingresses that use a specific secret func (r *IngressReconciler) listIngressesBySecret(ctx context.Context, obj client.Object) []reconcile.Request { secret, ok := obj.(*corev1.Secret) @@ -557,20 +606,38 @@ func (r *IngressReconciler) processBackendService(tctx *provider.TranslateContex return err } - // get the endpoint slices - endpointSliceList := &discoveryv1.EndpointSliceList{} - if err := r.List(tctx, endpointSliceList, - client.InNamespace(namespace), - client.MatchingLabels{ - discoveryv1.LabelServiceName: backendService.Name, - }, - ); err != nil { - r.Log.Error(err, "failed to list endpoint slices", "namespace", namespace, "name", backendService.Name) - return err - } + // Conditionally get EndpointSlice or Endpoints based on cluster API support + if r.supportsEndpointSlice { + // get the endpoint slices + endpointSliceList := &discoveryv1.EndpointSliceList{} + if err := r.List(tctx, endpointSliceList, + client.InNamespace(namespace), + client.MatchingLabels{ + discoveryv1.LabelServiceName: backendService.Name, + }, + ); err != nil { + r.Log.Error(err, "failed to list endpoint slices", "namespace", namespace, "name", backendService.Name) + return err + } - // save the endpoint slices to the translate context - tctx.EndpointSlices[serviceNS] = endpointSliceList.Items + // save the endpoint slices to the translate context + tctx.EndpointSlices[serviceNS] = endpointSliceList.Items + } else { + // Fallback to Endpoints API for Kubernetes 1.18 compatibility + var endpoints corev1.Endpoints + if err := r.Get(tctx, serviceNS, &endpoints); err != nil { + if client.IgnoreNotFound(err) != nil { + r.Log.Error(err, "failed to get endpoints", "namespace", namespace, "name", backendService.Name) + return err + } + // If endpoints not found, create empty EndpointSlice list + tctx.EndpointSlices[serviceNS] = []discoveryv1.EndpointSlice{} + } else { + // Convert Endpoints to EndpointSlice format for internal consistency + convertedEndpointSlices := pkgutils.ConvertEndpointsToEndpointSlice(&endpoints) + tctx.EndpointSlices[serviceNS] = convertedEndpointSlices + } + } tctx.Services[serviceNS] = &service return nil } diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 0917cf815..d776a8a34 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -40,8 +40,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -54,6 +56,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) const ( @@ -1416,6 +1419,10 @@ func distinctRequests(requests []reconcile.Request) []reconcile.Request { } func addProviderEndpointsToTranslateContext(tctx *provider.TranslateContext, c client.Client, serviceNN k8stypes.NamespacedName) error { + return addProviderEndpointsToTranslateContextWithEndpointSliceSupport(tctx, c, serviceNN, true) +} + +func addProviderEndpointsToTranslateContextWithEndpointSliceSupport(tctx *provider.TranslateContext, c client.Client, serviceNN k8stypes.NamespacedName, supportsEndpointSlice bool) error { log.Debugw("to process provider endpoints by provider.service", zap.Any("service", serviceNN)) var ( service corev1.Service @@ -1426,19 +1433,37 @@ func addProviderEndpointsToTranslateContext(tctx *provider.TranslateContext, c c } tctx.Services[serviceNN] = &service - // get es - var ( - esList discoveryv1.EndpointSliceList - ) - if err := c.List(tctx, &esList, - client.InNamespace(serviceNN.Namespace), - client.MatchingLabels{ - discoveryv1.LabelServiceName: serviceNN.Name, - }); err != nil { - log.Errorw("failed to get endpoints for GatewayProxy provider", zap.Error(err), zap.Any("endpoints", serviceNN)) - return err + // Conditionally get EndpointSlice or Endpoints based on cluster API support + if supportsEndpointSlice { + // get es + var ( + esList discoveryv1.EndpointSliceList + ) + if err := c.List(tctx, &esList, + client.InNamespace(serviceNN.Namespace), + client.MatchingLabels{ + discoveryv1.LabelServiceName: serviceNN.Name, + }); err != nil { + log.Errorw("failed to get endpoints for GatewayProxy provider", zap.Error(err), zap.Any("endpoints", serviceNN)) + return err + } + tctx.EndpointSlices[serviceNN] = esList.Items + } else { + // Fallback to Endpoints API for Kubernetes 1.18 compatibility + var endpoints corev1.Endpoints + if err := c.Get(tctx, serviceNN, &endpoints); err != nil { + if client.IgnoreNotFound(err) != nil { + log.Errorw("failed to get endpoints for GatewayProxy provider", zap.Error(err), zap.Any("endpoints", serviceNN)) + return err + } + // If endpoints not found, create empty EndpointSlice list + tctx.EndpointSlices[serviceNN] = []discoveryv1.EndpointSlice{} + } else { + // Convert Endpoints to EndpointSlice format for internal consistency + convertedEndpointSlices := pkgutils.ConvertEndpointsToEndpointSlice(&endpoints) + tctx.EndpointSlices[serviceNN] = convertedEndpointSlices + } } - tctx.EndpointSlices[serviceNN] = esList.Items return nil } @@ -1483,3 +1508,13 @@ func MatchConsumerGatewayRef(ctx context.Context, c client.Client, log logr.Logg } return matchesController(string(gatewayClass.Spec.ControllerName)) } + +// watchEndpointSliceOrEndpoints adds watcher for EndpointSlice or Endpoints based on cluster API support +func watchEndpointSliceOrEndpoints(bdr *ctrl.Builder, supportsEndpointSlice bool, endpointSliceMapFunc, endpointsMapFunc handler.MapFunc, log logr.Logger) *ctrl.Builder { + if supportsEndpointSlice { + return bdr.Watches(&discoveryv1.EndpointSlice{}, handler.EnqueueRequestsFromMapFunc(endpointSliceMapFunc)) + } else { + log.Info("EndpointSlice API not available, falling back to Endpoints API for service discovery") + return bdr.Watches(&corev1.Endpoints{}, handler.EnqueueRequestsFromMapFunc(endpointsMapFunc)) + } +} diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index e1f19df8f..f872c16e7 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -48,6 +48,7 @@ import ( // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch // CustomResourceDefinition v2 // +kubebuilder:rbac:groups=apisix.apache.org,resources=apisixconsumers,verbs=get;list;watch diff --git a/pkg/utils/cluster.go b/pkg/utils/cluster.go index 77d7ec48c..7385373e0 100644 --- a/pkg/utils/cluster.go +++ b/pkg/utils/cluster.go @@ -22,6 +22,7 @@ import ( "k8s.io/client-go/discovery" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "github.com/apache/apisix-ingress-controller/internal/types" ) @@ -36,7 +37,12 @@ func HasAPIResource(mgr ctrl.Manager, obj client.Object) bool { // HasAPIResourceWithLogger is the same as HasAPIResource but accepts a custom logger // for more detailed debugging information. func HasAPIResourceWithLogger(mgr ctrl.Manager, obj client.Object, logger logr.Logger) bool { - gvk := types.GvkOf(obj) + gvk, err := apiutil.GVKForObject(obj, mgr.GetScheme()) + if err != nil { + logger.Info("cannot derive GVK from scheme", "error", err) + return false + } + groupVersion := gvk.GroupVersion().String() logger = logger.WithValues( diff --git a/pkg/utils/endpoints.go b/pkg/utils/endpoints.go new file mode 100644 index 000000000..e8c0954e7 --- /dev/null +++ b/pkg/utils/endpoints.go @@ -0,0 +1,150 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 utils + +import ( + "fmt" + "net" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +// ConvertEndpointsToEndpointSlice converts a Kubernetes Endpoints object to one +// or more EndpointSlice objects, supporting IPv4/IPv6 dual stack. +// This function is used to provide backward compatibility for Kubernetes 1.18 clusters that don't +// have EndpointSlice support but still use the older Endpoints API. +// +// The conversion follows these rules: +// - Each Endpoints subset is split into separate IPv4 and IPv6 EndpointSlices +// - Uses net.ParseIP for reliable address family detection instead of string matching +// - IPv4 and IPv6 endpoints from the same subset are separated into different slices +// - Naming convention: --v4 / -v6 +// - Port information is preserved +// - Ready state is mapped from Endpoints addresses vs notReadyAddresses +// +// Note: Some EndpointSlice features like topology and conditions may not be fully represented +// since they don't exist in the Endpoints API. +func ConvertEndpointsToEndpointSlice(ep *corev1.Endpoints) []discoveryv1.EndpointSlice { + if ep == nil { + return nil + } + + var endpointSlices []discoveryv1.EndpointSlice + + // If there are no subsets, create an empty EndpointSlice + if len(ep.Subsets) == 0 { + endpointSlices = append(endpointSlices, discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: ep.Name + "-v4", // Default to v4 + Namespace: ep.Namespace, + Labels: map[string]string{discoveryv1.LabelServiceName: ep.Name}, + OwnerReferences: ep.OwnerReferences, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Ports: []discoveryv1.EndpointPort{}, + Endpoints: []discoveryv1.Endpoint{}, + }) + return endpointSlices + } + + for i, subset := range ep.Subsets { + // Create ports array + ports := make([]discoveryv1.EndpointPort, 0, len(subset.Ports)) + for _, p := range subset.Ports { + epPort := discoveryv1.EndpointPort{ + Port: &p.Port, + Protocol: &p.Protocol, + } + if p.Name != "" { + epPort.Name = &p.Name + } + ports = append(ports, epPort) + } + + // Separate IPv4 and IPv6 addresses + var ( + ipv4Endpoints []discoveryv1.Endpoint + ipv6Endpoints []discoveryv1.Endpoint + ) + buildEndpoint := func(addr corev1.EndpointAddress, ready bool) discoveryv1.Endpoint { + e := discoveryv1.Endpoint{ + Addresses: []string{addr.IP}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(ready), + }, + } + if addr.TargetRef != nil { + e.TargetRef = addr.TargetRef + } + if addr.Hostname != "" { + e.Hostname = &addr.Hostname + } + return e + } + + // Process ready addresses + for _, a := range subset.Addresses { + if isIPv6(a.IP) { + ipv6Endpoints = append(ipv6Endpoints, buildEndpoint(a, true)) + } else { + ipv4Endpoints = append(ipv4Endpoints, buildEndpoint(a, true)) + } + } + // Process not ready addresses + for _, a := range subset.NotReadyAddresses { + if isIPv6(a.IP) { + ipv6Endpoints = append(ipv6Endpoints, buildEndpoint(a, false)) + } else { + ipv4Endpoints = append(ipv4Endpoints, buildEndpoint(a, false)) + } + } + + // Create EndpointSlices for each address type + makeSlice := func(suffix string, addrType discoveryv1.AddressType, eps []discoveryv1.Endpoint) discoveryv1.EndpointSlice { + return discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%d-%s", ep.Name, i, suffix), + Namespace: ep.Namespace, + Labels: map[string]string{discoveryv1.LabelServiceName: ep.Name}, + OwnerReferences: ep.OwnerReferences, + }, + AddressType: addrType, + Ports: ports, + Endpoints: eps, + } + } + + if len(ipv4Endpoints) > 0 { + endpointSlices = append(endpointSlices, makeSlice("v4", discoveryv1.AddressTypeIPv4, ipv4Endpoints)) + } + if len(ipv6Endpoints) > 0 { + endpointSlices = append(endpointSlices, makeSlice("v6", discoveryv1.AddressTypeIPv6, ipv6Endpoints)) + } + } + + return endpointSlices +} + +// isIPv6 uses net.ParseIP to determine if an IP address is IPv6 +func isIPv6(ip string) bool { + parsed := net.ParseIP(ip) + return parsed != nil && parsed.To4() == nil +} diff --git a/pkg/utils/endpoints_test.go b/pkg/utils/endpoints_test.go new file mode 100644 index 000000000..a032f08dd --- /dev/null +++ b/pkg/utils/endpoints_test.go @@ -0,0 +1,503 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 utils + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func TestIsIPv6(t *testing.T) { + tests := []struct { + name string + ip string + want bool + }{ + { + name: "IPv4 address", + ip: "192.168.1.1", + want: false, + }, + { + name: "IPv6 address", + ip: "2001:db8::1", + want: true, + }, + { + name: "IPv6 loopback", + ip: "::1", + want: true, + }, + { + name: "IPv4 loopback", + ip: "127.0.0.1", + want: false, + }, + { + name: "invalid IP", + ip: "invalid", + want: false, + }, + { + name: "IPv4-mapped IPv6", + ip: "::ffff:192.168.1.1", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isIPv6(tt.ip); got != tt.want { + t.Errorf("isIPv6() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestConvertEndpointsToEndpointSlice(t *testing.T) { + tests := []struct { + name string + endpoints *corev1.Endpoints + want []discoveryv1.EndpointSlice + }{ + { + name: "nil endpoints", + endpoints: nil, + want: nil, + }, + { + name: "empty subsets", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{}, + }, + want: []discoveryv1.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service-v4", + Namespace: "default", + Labels: map[string]string{ + discoveryv1.LabelServiceName: "test-service", + }, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Ports: []discoveryv1.EndpointPort{}, + Endpoints: []discoveryv1.Endpoint{}, + }, + }, + }, + { + name: "IPv4 only", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + {IP: "192.168.1.1"}, + {IP: "192.168.1.2"}, + }, + Ports: []corev1.EndpointPort{ + {Port: 80, Protocol: corev1.ProtocolTCP, Name: "http"}, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v4", discoveryv1.AddressTypeIPv4, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{ + createReadyEndpoint("192.168.1.1"), + createReadyEndpoint("192.168.1.2"), + }), + }, + }, + { + name: "IPv6 only", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + {IP: "2001:db8::1"}, + {IP: "2001:db8::2"}, + }, + Ports: []corev1.EndpointPort{ + {Port: 80, Protocol: corev1.ProtocolTCP, Name: "http"}, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v6", discoveryv1.AddressTypeIPv6, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{ + createReadyEndpoint("2001:db8::1"), + createReadyEndpoint("2001:db8::2"), + }), + }, + }, + { + name: "dual stack (IPv4 and IPv6)", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + {IP: "192.168.1.1"}, + {IP: "2001:db8::1"}, + }, + Ports: []corev1.EndpointPort{ + {Port: 80, Protocol: corev1.ProtocolTCP, Name: "http"}, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v4", discoveryv1.AddressTypeIPv4, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{createReadyEndpoint("192.168.1.1")}), + createTestEndpointSlice("test-service-0-v6", discoveryv1.AddressTypeIPv6, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{createReadyEndpoint("2001:db8::1")}), + }, + }, + { + name: "ready and not ready addresses", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "192.168.1.1", + }, + }, + NotReadyAddresses: []corev1.EndpointAddress{ + { + IP: "192.168.1.2", + }, + }, + Ports: []corev1.EndpointPort{ + { + Port: 80, + Protocol: corev1.ProtocolTCP, + Name: "http", + }, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v4", discoveryv1.AddressTypeIPv4, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{ + createReadyEndpoint("192.168.1.1"), + createNotReadyEndpoint("192.168.1.2"), + }), + }, + }, + { + name: "multiple subsets", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "192.168.1.1", + }, + }, + Ports: []corev1.EndpointPort{ + { + Port: 80, + Protocol: corev1.ProtocolTCP, + Name: "http", + }, + }, + }, + { + Addresses: []corev1.EndpointAddress{ + { + IP: "2001:db8::1", + }, + }, + Ports: []corev1.EndpointPort{ + { + Port: 443, + Protocol: corev1.ProtocolTCP, + Name: "https", + }, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v4", discoveryv1.AddressTypeIPv4, + []discoveryv1.EndpointPort{createHTTPPort()}, + []discoveryv1.Endpoint{createReadyEndpoint("192.168.1.1")}), + createTestEndpointSlice("test-service-1-v6", discoveryv1.AddressTypeIPv6, + []discoveryv1.EndpointPort{createHTTPSPort(443)}, + []discoveryv1.Endpoint{createReadyEndpoint("2001:db8::1")}), + }, + }, + { + name: "with target ref and hostname", + endpoints: &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "192.168.1.1", + Hostname: "pod-1", + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: "pod-1", + Namespace: "default", + }, + }, + }, + Ports: []corev1.EndpointPort{ + { + Port: 80, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + }, + }, + want: []discoveryv1.EndpointSlice{ + createTestEndpointSlice("test-service-0-v4", discoveryv1.AddressTypeIPv4, + []discoveryv1.EndpointPort{createPlainPort(80)}, + []discoveryv1.Endpoint{createEndpointWithHostname("192.168.1.1", "pod-1")}), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertEndpointsToEndpointSlice(tt.endpoints) + if len(got) != len(tt.want) { + t.Errorf("ConvertEndpointsToEndpointSlice() returned %d slices, want %d", len(got), len(tt.want)) + return + } + + for i, slice := range got { + if i >= len(tt.want) { + t.Errorf("ConvertEndpointsToEndpointSlice() returned more slices than expected") + return + } + assertEndpointSliceEqual(t, slice, tt.want[i], i) + } + }) + } +} + +func assertEndpointSliceEqual(t *testing.T, got, want discoveryv1.EndpointSlice, index int) { + t.Helper() + + if got.Name != want.Name { + t.Errorf("EndpointSlice[%d].Name = %v, want %v", index, got.Name, want.Name) + } + if got.Namespace != want.Namespace { + t.Errorf("EndpointSlice[%d].Namespace = %v, want %v", index, got.Namespace, want.Namespace) + } + if len(got.Labels) != len(want.Labels) { + t.Errorf("EndpointSlice[%d].Labels length = %v, want %v", index, len(got.Labels), len(want.Labels)) + } + for k, v := range want.Labels { + if got.Labels[k] != v { + t.Errorf("EndpointSlice[%d].Labels[%s] = %v, want %v", index, k, got.Labels[k], v) + } + } + + if got.AddressType != want.AddressType { + t.Errorf("EndpointSlice[%d].AddressType = %v, want %v", index, got.AddressType, want.AddressType) + } + + assertEndpointPortsEqual(t, got.Ports, want.Ports, index) + assertEndpointsEqual(t, got.Endpoints, want.Endpoints, index) +} + +func assertEndpointPortsEqual(t *testing.T, got, want []discoveryv1.EndpointPort, sliceIndex int) { + t.Helper() + + if len(got) != len(want) { + t.Errorf("EndpointSlice[%d].Ports length = %v, want %v", sliceIndex, len(got), len(want)) + return + } + + for j, port := range got { + if j >= len(want) { + continue + } + wantPort := want[j] + if (port.Name == nil) != (wantPort.Name == nil) || (port.Name != nil && *port.Name != *wantPort.Name) { + t.Errorf("EndpointSlice[%d].Ports[%d].Name = %v, want %v", sliceIndex, j, port.Name, wantPort.Name) + } + if *port.Port != *wantPort.Port { + t.Errorf("EndpointSlice[%d].Ports[%d].Port = %v, want %v", sliceIndex, j, *port.Port, *wantPort.Port) + } + if *port.Protocol != *wantPort.Protocol { + t.Errorf("EndpointSlice[%d].Ports[%d].Protocol = %v, want %v", sliceIndex, j, *port.Protocol, *wantPort.Protocol) + } + } +} + +func assertEndpointsEqual(t *testing.T, got, want []discoveryv1.Endpoint, sliceIndex int) { + t.Helper() + + if len(got) != len(want) { + t.Errorf("EndpointSlice[%d].Endpoints length = %v, want %v", sliceIndex, len(got), len(want)) + return + } + + for j, endpoint := range got { + if j >= len(want) { + continue + } + wantEndpoint := want[j] + + if len(endpoint.Addresses) != len(wantEndpoint.Addresses) { + t.Errorf("EndpointSlice[%d].Endpoints[%d].Addresses length = %v, want %v", sliceIndex, j, len(endpoint.Addresses), len(wantEndpoint.Addresses)) + } + for k, addr := range endpoint.Addresses { + if k >= len(wantEndpoint.Addresses) { + continue + } + if addr != wantEndpoint.Addresses[k] { + t.Errorf("EndpointSlice[%d].Endpoints[%d].Addresses[%d] = %v, want %v", sliceIndex, j, k, addr, wantEndpoint.Addresses[k]) + } + } + + if *endpoint.Conditions.Ready != *wantEndpoint.Conditions.Ready { + t.Errorf("EndpointSlice[%d].Endpoints[%d].Conditions.Ready = %v, want %v", sliceIndex, j, *endpoint.Conditions.Ready, *wantEndpoint.Conditions.Ready) + } + + if (endpoint.Hostname == nil) != (wantEndpoint.Hostname == nil) || (endpoint.Hostname != nil && *endpoint.Hostname != *wantEndpoint.Hostname) { + t.Errorf("EndpointSlice[%d].Endpoints[%d].Hostname = %v, want %v", sliceIndex, j, endpoint.Hostname, wantEndpoint.Hostname) + } + + if (endpoint.TargetRef == nil) != (wantEndpoint.TargetRef == nil) { + t.Errorf("EndpointSlice[%d].Endpoints[%d].TargetRef presence mismatch", sliceIndex, j) + } else if endpoint.TargetRef != nil && wantEndpoint.TargetRef != nil { + if endpoint.TargetRef.Kind != wantEndpoint.TargetRef.Kind || + endpoint.TargetRef.Name != wantEndpoint.TargetRef.Name || + endpoint.TargetRef.Namespace != wantEndpoint.TargetRef.Namespace { + t.Errorf("EndpointSlice[%d].Endpoints[%d].TargetRef = %v, want %v", sliceIndex, j, endpoint.TargetRef, wantEndpoint.TargetRef) + } + } + } +} + +func createTestEndpointSlice(name string, addressType discoveryv1.AddressType, + ports []discoveryv1.EndpointPort, endpoints []discoveryv1.Endpoint) discoveryv1.EndpointSlice { + return discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + Labels: map[string]string{ + discoveryv1.LabelServiceName: "test-service", + }, + }, + AddressType: addressType, + Ports: ports, + Endpoints: endpoints, + } +} + +func createHTTPPort() discoveryv1.EndpointPort { + return discoveryv1.EndpointPort{ + Name: ptr.To("http"), + Port: ptr.To[int32](80), + Protocol: ptr.To(corev1.ProtocolTCP), + } +} + +func createHTTPSPort(port int32) discoveryv1.EndpointPort { + return discoveryv1.EndpointPort{ + Name: ptr.To("https"), + Port: ptr.To(port), + Protocol: ptr.To(corev1.ProtocolTCP), + } +} + +func createPlainPort(port int32) discoveryv1.EndpointPort { + return discoveryv1.EndpointPort{ + Port: ptr.To(port), + Protocol: ptr.To(corev1.ProtocolTCP), + } +} + +func createReadyEndpoint(address string) discoveryv1.Endpoint { + return discoveryv1.Endpoint{ + Addresses: []string{address}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(true), + }, + } +} + +func createNotReadyEndpoint(address string) discoveryv1.Endpoint { + return discoveryv1.Endpoint{ + Addresses: []string{address}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(false), + }, + } +} + +func createEndpointWithHostname(address, hostname string) discoveryv1.Endpoint { + return discoveryv1.Endpoint{ + Addresses: []string{address}, + Conditions: discoveryv1.EndpointConditions{ + Ready: ptr.To(true), + }, + Hostname: ptr.To(hostname), + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: hostname, + Namespace: "default", + }, + } +} diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index a35f25627..279bd4028 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -92,7 +92,12 @@ spec: By("update ApisixRoute") applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/headers")) Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) - s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK) + s.RequestAssert(&scaffold.RequestAssert{ + Method: "GET", + Path: "/headers", + Host: "httpbin", + Check: scaffold.WithExpectedStatus(http.StatusOK), + }) By("delete ApisixRoute") err := s.DeleteResource("ApisixRoute", "default") diff --git a/test/e2e/framework/manifests/ingress.yaml b/test/e2e/framework/manifests/ingress.yaml index 7cb652fe7..fcb3bce1e 100644 --- a/test/e2e/framework/manifests/ingress.yaml +++ b/test/e2e/framework/manifests/ingress.yaml @@ -85,6 +85,14 @@ rules: - get - list - watch +- apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - list + - watch - apiGroups: - "" resources: