From d3efd5891a01be58865d62d2e54153bae3e8252f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Thu, 12 Jun 2025 17:38:00 +0800 Subject: [PATCH 01/11] feat: implement ApisixRoute controller --- api/v2/apisixglobalrule_types.go | 3 + api/v2/apisixroute_types.go | 19 +- api/v2/shared_types.go | 76 ++++ api/v2/zz_generated.deepcopy.go | 29 +- internal/controller/apisixroute_controller.go | 391 ++++++++++++++++-- internal/controller/httproutepolicy.go | 2 +- internal/controller/indexer/indexer.go | 48 +++ internal/controller/utils.go | 43 ++ internal/provider/adc/adc.go | 4 + .../provider/adc/translator/apisixroute.go | 131 ++++++ pkg/utils/datastructure.go | 43 ++ 11 files changed, 730 insertions(+), 59 deletions(-) create mode 100644 api/v2/shared_types.go create mode 100644 internal/provider/adc/translator/apisixroute.go create mode 100644 pkg/utils/datastructure.go diff --git a/api/v2/apisixglobalrule_types.go b/api/v2/apisixglobalrule_types.go index 85e1402ac..99f5474fe 100644 --- a/api/v2/apisixglobalrule_types.go +++ b/api/v2/apisixglobalrule_types.go @@ -51,5 +51,8 @@ type ApisixGlobalRuleList struct { } func init() { + var a ApisixGlobalRule + a.GetResourceVersion() + a.GetResourceVersion() SchemeBuilder.Register(&ApisixGlobalRule{}, &ApisixGlobalRuleList{}) } diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index cea41e4eb..4f9b0228c 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -13,7 +13,8 @@ package v2 import ( - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "encoding/json" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -218,7 +219,21 @@ type ApisixRouteHTTPMatchExpr struct { // ApisixRoutePluginConfig is the configuration for // any plugins. -type ApisixRoutePluginConfig map[string]apiextensionsv1.JSON +type ApisixRoutePluginConfig map[string]any + +func (p ApisixRoutePluginConfig) DeepCopyInto(out *ApisixRoutePluginConfig) { + b, _ := json.Marshal(&p) + _ = json.Unmarshal(b, out) +} + +func (p *ApisixRoutePluginConfig) DeepCopy() *ApisixRoutePluginConfig { + if p == nil { + return nil + } + out := new(ApisixRoutePluginConfig) + p.DeepCopyInto(out) + return out +} // ApisixRouteAuthenticationKeyAuth is the keyAuth-related // configuration in ApisixRouteAuthentication. diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go new file mode 100644 index 000000000..687ed69f0 --- /dev/null +++ b/api/v2/shared_types.go @@ -0,0 +1,76 @@ +package v2 + +import ( + "time" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// 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. + +type ( + // ApisixRouteConditionType is a type of condition for a route. + ApisixRouteConditionType = gatewayv1.RouteConditionType + // ApisixRouteConditionReason is a reason for a route condition. + ApisixRouteConditionReason = gatewayv1.RouteConditionReason +) + +const ( + ApisixRouteConditionTypeAccepted ApisixRouteConditionType = gatewayv1.RouteConditionAccepted + ApisixRouteConditionReasonAccepted ApisixRouteConditionReason = gatewayv1.RouteReasonAccepted + ApisixRouteConditionReasonInvalidHTTP ApisixRouteConditionReason = "InvalidHTTP" +) + +const ( + // DefaultUpstreamTimeout represents the default connect, + // read and send timeout (in seconds) with upstreams. + DefaultUpstreamTimeout = 60 * time.Second +) + +const ( + // OpEqual means the equal ("==") operator in nginxVars. + OpEqual = "Equal" + // OpNotEqual means the not equal ("~=") operator in nginxVars. + OpNotEqual = "NotEqual" + // OpGreaterThan means the greater than (">") operator in nginxVars. + OpGreaterThan = "GreaterThan" + // OpGreaterThanEqual means the greater than (">=") operator in nginxVars. + OpGreaterThanEqual = "GreaterThanEqual" + // OpLessThan means the less than ("<") operator in nginxVars. + OpLessThan = "LessThan" + // OpLessThanEqual means the less than equal ("<=") operator in nginxVars. + OpLessThanEqual = "LessThanEqual" + // OpRegexMatch means the regex match ("~~") operator in nginxVars. + OpRegexMatch = "RegexMatch" + // OpRegexNotMatch means the regex not match ("!~~") operator in nginxVars. + OpRegexNotMatch = "RegexNotMatch" + // OpRegexMatchCaseInsensitive means the regex match "~*" (case insensitive mode) operator in nginxVars. + OpRegexMatchCaseInsensitive = "RegexMatchCaseInsensitive" + // OpRegexNotMatchCaseInsensitive means the regex not match "!~*" (case insensitive mode) operator in nginxVars. + OpRegexNotMatchCaseInsensitive = "RegexNotMatchCaseInsensitive" + // OpIn means the in operator ("in") in nginxVars. + OpIn = "In" + // OpNotIn means the not in operator ("not_in") in nginxVars. + OpNotIn = "NotIn" + + // ScopeQuery means the route match expression subject is in the querystring. + ScopeQuery = "Query" + // ScopeHeader means the route match expression subject is in request headers. + ScopeHeader = "Header" + // ScopePath means the route match expression subject is the uri path. + ScopePath = "Path" + // ScopeCookie means the route match expression subject is in cookie. + ScopeCookie = "Cookie" + // ScopeVariable means the route match expression subject is in variable. + ScopeVariable = "Variable" +) diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index bb39970f1..ff06644c9 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -937,13 +937,7 @@ func (in *ApisixRouteList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRoutePlugin) DeepCopyInto(out *ApisixRoutePlugin) { *out = *in - if in.Config != nil { - in, out := &in.Config, &out.Config - *out = make(ApisixRoutePluginConfig, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } + in.Config.DeepCopyInto(&out.Config) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePlugin. @@ -956,27 +950,6 @@ func (in *ApisixRoutePlugin) DeepCopy() *ApisixRoutePlugin { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in ApisixRoutePluginConfig) DeepCopyInto(out *ApisixRoutePluginConfig) { - { - in := &in - *out = make(ApisixRoutePluginConfig, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePluginConfig. -func (in ApisixRoutePluginConfig) DeepCopy() ApisixRoutePluginConfig { - if in == nil { - return nil - } - out := new(ApisixRoutePluginConfig) - in.DeepCopyInto(out) - return *out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRouteSpec) DeepCopyInto(out *ApisixRouteSpec) { *out = *in diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 6febd1249..a53e2ec94 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -13,61 +13,396 @@ package controller import ( + "cmp" "context" + "fmt" + "slices" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/config" + "github.com/apache/apisix-ingress-controller/internal/controller/indexer" + "github.com/apache/apisix-ingress-controller/internal/controller/status" + "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" ) // ApisixRouteReconciler reconciles a ApisixRoute object type ApisixRouteReconciler struct { client.Client - Scheme *runtime.Scheme - Log logr.Logger + Scheme *runtime.Scheme + Log logr.Logger + Provider provider.Provider + Updater status.Updater } -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apisix.apache.org.github.com,resources=apisixroutes/finalizers,verbs=update +// SetupWithManager sets up the controller with the Manager. +func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv2.ApisixRoute{}). + WithEventFilter( + predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, ok := obj.(*corev1.Secret) + return ok + }), + ), + ). + Watches(&networkingv1.Ingress{}, + handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApiRouteForIngressClass)), + builder.WithPredicates( + predicate.NewPredicateFuncs(r.matchesIngressController), + ), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRouteForGatewayProxy)), + ). + Watches(&discoveryv1.EndpointSlice{}, + handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRoutesForService)), + ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRoutesForSecret)), + ). + Named("apisixroute"). + Complete(r) +} func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.Info("reconcile", "request", req.NamespacedName) + var ar apiv2.ApisixRoute + if err := r.Get(ctx, req.NamespacedName, &ar); err != nil { + if client.IgnoreNotFound(err) == nil { + ar.Namespace = req.Namespace + ar.Name = req.Name + ar.TypeMeta = metav1.TypeMeta{ + Kind: KindApisixRoute, + APIVersion: apiv2.GroupVersion.String(), + } - var obj apiv2.ApisixRoute - if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { - r.Log.Error(err, "failed to get ApisixConsumer", "request", req.NamespacedName) + if err := r.Provider.Delete(ctx, &ar); err != nil { + r.Log.Error(err, "failed to delete apisixroute", "apisixroute", ar) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } return ctrl.Result{}, err } - // FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) - obj.Status.Conditions = []metav1.Condition{ - { - Type: string(gatewayv1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: obj.GetGeneration(), - LastTransitionTime: metav1.Now(), - Reason: string(gatewayv1.RouteReasonAccepted), - }, - } + tctx := provider.NewDefaultTranslateContext(ctx) - if err := r.Status().Update(ctx, &obj); err != nil { - r.Log.Error(err, "failed to update status", "request", req.NamespacedName) + ic, err := r.getIngressClass(&ar) + if err == nil { + err = r.processIngressClassParameters(ctx, tctx, &ar, ic) + if err == nil { + err = r.processApisixRoute(ctx, tctx, &ar) + if err == nil { + err = r.Provider.Update(ctx, tctx, &ar) + } + } + } + if err != nil { + r.Log.Error(err, "failed to process", "apisixroute", ar) return ctrl.Result{}, err } + SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) + r.Updater.Update(status.Update{ + NamespacedName: req.NamespacedName, + Resource: ar.DeepCopy(), + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + ar_, ok := obj.(*apiv2.ApisixRoute) + if !ok { + err := fmt.Errorf("unsupported object type %T", obj) + panic(err) + } + ar_.Status = ar.Status + return ar_ + }), + }) + UpdateStatus(r.Updater, r.Log, tctx) + return ctrl.Result{}, nil } -// SetupWithManager sets up the controller with the Manager. -func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&apiv2.ApisixRoute{}). - Named("apisixroute"). - Complete(r) +func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *provider.TranslateContext, in *apiv2.ApisixRoute) error { + var ( + rules = make(map[string]struct{}) + ) + + for httpIndex, http := range in.Spec.HTTP { + // check rule names + if _, ok := rules[http.Name]; ok { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: "duplicate route rule name", + } + } + rules[http.Name] = struct{}{} + + // check secret + for _, plugin := range http.Plugins { + if !plugin.Enable { + continue + } + if plugin.Config == nil { + continue + } + if plugin.SecretRef == "" { + continue + } + + var ( + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: plugin.SecretRef, + Namespace: in.Namespace, + }, + } + secretNN = NamespacedName(&secret) + ) + if err := r.Get(ctx, secretNN, &secret); err != nil { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("failed to get Secret: %s", secretNN), + } + } + + tc.Secrets[NamespacedName(&secret)] = &secret + } + + // check vars + // todo: cache the result + if _, err := translator.TranslateApisixRouteVars(http.Match.NginxVars); err != nil { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf(".spec.http[%d].match.exprs: %s", httpIndex, err.Error()), + } + } + + // validate remote address + if err := translator.ValidateRemoteAddrs(http.Match.RemoteAddrs); err != nil { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf(".spec.http[%d].match.remoteAddrs: %s", httpIndex, err.Error()), + } + } + + // process backend + var backends = make(map[types.NamespacedName]struct{}) + for _, backend := range http.Backends { + var ( + service = corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: backend.ServiceName, + Namespace: in.Namespace, + }, + } + serviceNN = NamespacedName(&service) + ) + if _, ok := backends[serviceNN]; ok { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("duplicate backend service: %s", serviceNN), + } + } + backends[serviceNN] = struct{}{} + + if err := r.Get(ctx, serviceNN, &service); err != nil { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("failed to get Service: %s", serviceNN), + } + } + if service.Spec.Type == corev1.ServiceTypeExternalName { + tc.Services[serviceNN] = &service + continue + } + + if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool { + return port.Port == int32(backend.ServicePort.IntValue()) + }) { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("port %s not found in service %s", backend.ServicePort.String(), serviceNN), + } + } + 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 ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("failed to list endpoint slices: %v", err), + } + } + tc.EndpointSlices[serviceNN] = endpoints.Items + } + } + + return nil +} + +func (r *ApisixRouteReconciler) listApisixRoutesForService(ctx context.Context, obj client.Object) []reconcile.Request { + endpointSlice, ok := obj.(*discoveryv1.EndpointSlice) + if !ok { + return nil + } + + var ( + namespace = endpointSlice.GetNamespace() + serviceName = endpointSlice.Labels[discoveryv1.LabelServiceName] + 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: NamespacedName(&ar)}) + } + return requests +} + +func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + return nil + } + + var ( + arList apiv2.ApisixRouteList + ) + if err := r.List(ctx, &arList, client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }); err != nil { + r.Log.Error(err, "failed to list apisixroutes by secret", "secret", secret.Name) + return nil + } + requests := make([]reconcile.Request, 0, len(arList.Items)) + for _, ar := range arList.Items { + requests = append(requests, reconcile.Request{NamespacedName: NamespacedName(&ar)}) + } + return requests +} + +func (r *ApisixRouteReconciler) listApiRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { + ic, ok := object.(*networkingv1.IngressClass) + if !ok { + return nil + } + + isDefaultIngressClass := IsDefaultIngressClass(ic) + var arList apiv2.ApisixRouteList + if err := r.List(ctx, &arList); err != nil { + return nil + } + for _, ar := range arList.Items { + if ar.Spec.IngressClassName == ic.Name || (isDefaultIngressClass && ar.Spec.IngressClassName == "") { + requests = append(requests, reconcile.Request{NamespacedName: NamespacedName(&ar)}) + } + } + return requests +} + +func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, object client.Object) (requests []reconcile.Request) { + gp, ok := object.(*v1alpha1.GatewayProxy) + if !ok { + return nil + } + + var icList networkingv1.IngressClassList + if err := r.List(ctx, &icList, client.MatchingFields{ + indexer.IngressClassParametersRef: indexer.GenIndexKey(gp.GetNamespace(), gp.GetName()), + }); err != nil { + r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gp.GetName()) + return nil + } + + for _, ic := range icList.Items { + requests = append(requests, r.listApiRouteForIngressClass(ctx, &ic)...) + } + + return requests +} + +func (r *ApisixRouteReconciler) matchesIngressController(obj client.Object) bool { + ingressClass, ok := obj.(*networkingv1.IngressClass) + if !ok { + return false + } + return matchesController(ingressClass.Spec.Controller) +} + +func (r *ApisixRouteReconciler) getIngressClass(ar *apiv2.ApisixRoute) (*networkingv1.IngressClass, error) { + if ar.Spec.IngressClassName == "" { + return r.getDefaultIngressClass() + } + + var ic networkingv1.IngressClass + if err := r.Get(context.Background(), client.ObjectKey{Name: ar.Spec.IngressClassName}, &ic); err != nil { + return nil, err + } + return &ic, nil +} + +func (r *ApisixRouteReconciler) getDefaultIngressClass() (*networkingv1.IngressClass, error) { + var icList networkingv1.IngressClassList + if err := r.List(context.Background(), &icList, client.MatchingFields{ + indexer.IngressClass: config.GetControllerName(), + }); err != nil { + r.Log.Error(err, "failed to list ingress classes") + return nil, err + } + for _, ic := range icList.Items { + if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { + return &ic, nil + } + } + return nil, &errors.StatusError{ErrStatus: metav1.Status{ + Reason: metav1.StatusReasonNotFound, + Message: "default ingress class not found or dose not match the controller", + }} +} + +func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Context, _ *provider.TranslateContext, ar *apiv2.ApisixRoute, ic *networkingv1.IngressClass) error { + if ic == nil || ic.Spec.Parameters == nil || ic.Spec.Parameters.APIGroup == nil { + return nil + } + if *ic.Spec.Parameters.APIGroup != apiv2.GroupVersion.Group || ic.Spec.Parameters.Kind != KindGatewayProxy { + return nil + } + + parameters := ic.Spec.Parameters + ns := *cmp.Or(parameters.Namespace, &ar.Namespace) + + var gp v1alpha1.GatewayProxy + if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: parameters.Name}, &gp); err != nil { + r.Log.Error(err, "failed to get gateway proxy", "gatewayproxy", parameters.Name) + return err + } + + return nil } diff --git a/internal/controller/httproutepolicy.go b/internal/controller/httproutepolicy.go index 15fba525d..932367528 100644 --- a/internal/controller/httproutepolicy.go +++ b/internal/controller/httproutepolicy.go @@ -33,7 +33,7 @@ import ( ) func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { - // list HTTPRoutePolices which sectionName is not specified + // list HTTPRoutePolicies, which sectionName is not specified var ( list v1alpha1.HTTPRoutePolicyList key = indexer.GenIndexKeyWithGK(gatewayv1.GroupName, "HTTPRoute", httpRoute.GetNamespace(), httpRoute.GetName()) diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index d53f36150..50921e3d7 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -23,6 +23,7 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" ) const ( @@ -50,6 +51,7 @@ func SetupIndexer(mgr ctrl.Manager) error { setupGatewayProxyIndexer, setupGatewaySecretIndex, setupGatewayClassIndexer, + setupApisixRouteIndexer, } { if err := setup(mgr); err != nil { return err @@ -90,6 +92,20 @@ func setupConsumerIndexer(mgr ctrl.Manager) error { return nil } +func setupApisixRouteIndexer(mgr ctrl.Manager) error { + var indexers = map[string]func(client.Object) []string{ + ServiceIndexRef: ApisixRouteServiceIndexFunc, + SecretIndexRef: ApisixRouteRouteSecretIndexFunc, + } + for key, f := range indexers { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &apiv2.ApisixRoute{}, key, f); err != nil { + return err + } + } + + return nil +} + func ConsumerSecretIndexFunc(rawObj client.Object) []string { consumer := rawObj.(*v1alpha1.Consumer) secretKeys := make([]string, 0) @@ -412,6 +428,38 @@ func HTTPRouteServiceIndexFunc(rawObj client.Object) []string { return keys } +func ApisixRouteServiceIndexFunc(obj client.Object) (keys []string) { + ar := obj.(*apiv2.ApisixRoute) + for _, http := range ar.Spec.HTTP { + for _, backend := range http.Backends { + keys = append(keys, GenIndexKey(ar.GetNamespace(), backend.ServiceName)) + } + } + for _, stream := range ar.Spec.Stream { + keys = append(keys, stream.Backend.ServiceName) + } + return +} + +func ApisixRouteRouteSecretIndexFunc(obj client.Object) (keys []string) { + ar := obj.(*apiv2.ApisixRoute) + for _, http := range ar.Spec.HTTP { + for _, plugin := range http.Plugins { + if plugin.Enable && plugin.SecretRef != "" { + keys = append(keys, GenIndexKey(ar.GetNamespace(), plugin.SecretRef)) + } + } + } + for _, stream := range ar.Spec.Stream { + for _, plugin := range stream.Plugins { + if plugin.Enable && plugin.SecretRef != "" { + keys = append(keys, GenIndexKey(ar.GetNamespace(), plugin.SecretRef)) + } + } + } + return +} + func HTTPRouteExtensionIndexFunc(rawObj client.Object) []string { hr := rawObj.(*gatewayv1.HTTPRoute) keys := make([]string, 0, len(hr.Spec.Rules)) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 7faacb917..3fb8cf933 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -33,12 +33,14 @@ import ( "k8s.io/apimachinery/pkg/types" "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" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/provider" ) @@ -52,6 +54,7 @@ const ( KindGatewayProxy = "GatewayProxy" KindSecret = "Secret" KindService = "Service" + KindApisixRoute = "ApisixRoute" ) const defaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class" @@ -405,6 +408,28 @@ func ParseRouteParentRefs( return gateways, nil } +func SetApisixRouteConditionAccepted(status *apiv2.ApisixStatus, generation int64, err error) { + var condition = metav1.Condition{ + Type: string(apiv2.ApisixRouteConditionTypeAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: generation, + LastTransitionTime: metav1.Now(), + Reason: string(apiv2.ApisixRouteConditionReasonAccepted), + } + if err != nil { + condition.Status = metav1.ConditionFalse + condition.Reason = string(apiv2.ApisixRouteConditionReasonInvalidHTTP) + condition.Message = err.Error() + + var re ReasonError + if errors.As(err, &re) { + condition.Reason = re.Reason + } + } + + status.Conditions = []metav1.Condition{condition} +} + func checkRouteAcceptedByListener( ctx context.Context, mgrc client.Client, @@ -1154,3 +1179,21 @@ func NamespacedName(obj client.Object) types.NamespacedName { Name: obj.GetName(), } } + +func WrapMapFuncDedup(mapFunc handler.MapFunc) handler.MapFunc { + return func(ctx context.Context, object client.Object) (result []reconcile.Request) { + return DedupComparable(mapFunc(ctx, object)) + } +} + +func DedupComparable[T comparable](s []T) []T { + var keys = make(map[T]struct{}) + var results []T + for _, item := range s { + if _, ok := keys[item]; !ok { + keys[item] = struct{}{} + results = append(results, item) + } + } + return results +} diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 181d36a70..5c4619ddb 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -30,6 +30,7 @@ import ( adctypes "github.com/apache/apisix-ingress-controller/api/adc" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/label" "github.com/apache/apisix-ingress-controller/internal/provider" @@ -106,6 +107,9 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *networkingv1.IngressClass: result, err = d.translator.TranslateIngressClass(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "global_rule", "plugin_metadata") + case *apiv2.ApisixRoute: + result, err = d.translator.TranslateApisixRoute(tctx, t.DeepCopy()) + resourceTypes = append(resourceTypes, "service") } if err != nil { return err diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go new file mode 100644 index 000000000..6500e322d --- /dev/null +++ b/internal/provider/adc/translator/apisixroute.go @@ -0,0 +1,131 @@ +// 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 translator + +import ( + "net" + "slices" + "strings" + + "github.com/pkg/errors" + + "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/provider" +) + +func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, obj *apiv2.ApisixRoute) (*TranslateResult, error) { + + return nil, errors.New("not implemented yet") +} + +func TranslateApisixRouteVars(exprs []apiv2.ApisixRouteHTTPMatchExpr) (result adc.StringOrSlice, err error) { + for _, expr := range exprs { + if expr.Subject.Name == "" && expr.Subject.Scope != apiv2.ScopePath { + return result, errors.New("empty subject.name") + } + + // process key + var ( + subj string + this adc.StringOrSlice + ) + switch expr.Subject.Scope { + case apiv2.ScopeQuery: + subj = "arg_" + expr.Subject.Name + case apiv2.ScopeHeader: + subj = "http_" + strings.ReplaceAll(strings.ToLower(expr.Subject.Name), "-", "_") + case apiv2.ScopeCookie: + subj = "cookie_" + expr.Subject.Name + case apiv2.ScopePath: + subj = "uri" + case apiv2.ScopeVariable: + subj = expr.Subject.Name + default: + return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable]") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj}) + + // process operator + var ( + op string + ) + switch expr.Op { + case apiv2.OpEqual: + op = "==" + case apiv2.OpGreaterThan: + op = ">" + case apiv2.OpGreaterThanEqual: + op = ">=" + case apiv2.OpIn: + op = "in" + case apiv2.OpLessThan: + op = "<" + case apiv2.OpLessThanEqual: + op = "<=" + case apiv2.OpNotEqual: + op = "~=" + case apiv2.OpNotIn: + op = "in" + case apiv2.OpRegexMatch: + op = "~~" + case apiv2.OpRegexMatchCaseInsensitive: + op = "~*" + case apiv2.OpRegexNotMatch: + op = "~~" + case apiv2.OpRegexNotMatchCaseInsensitive: + op = "~*" + default: + return result, errors.New("unknown operator") + } + if invert := slices.Contains([]string{apiv2.OpNotIn, apiv2.OpRegexNotMatch, apiv2.OpRegexNotMatchCaseInsensitive}, op); invert { + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"}) + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op}) + + // process value + switch expr.Op { + case apiv2.OpIn, apiv2.OpNotIn: + if expr.Set == nil { + return result, errors.New("empty set value") + } + var value adc.StringOrSlice + for _, item := range expr.Set { + value.SliceVal = append(value.SliceVal, adc.StringOrSlice{StrVal: item}) + } + this.SliceVal = append(this.SliceVal, value) + default: + if expr.Value == nil { + return result, errors.New("empty value") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: *expr.Value}) + } + + // append to result + result.SliceVal = append(result.SliceVal, this) + } + + return result, nil +} + +func ValidateRemoteAddrs(remoteAddrs []string) error { + for _, addr := range remoteAddrs { + if ip := net.ParseIP(addr); ip == nil { + // addr is not an IP address, try to parse it as a CIDR. + if _, _, err := net.ParseCIDR(addr); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/utils/datastructure.go b/pkg/utils/datastructure.go new file mode 100644 index 000000000..65c719b5b --- /dev/null +++ b/pkg/utils/datastructure.go @@ -0,0 +1,43 @@ +// 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 utils + +import ( + "strings" +) + +// InsertKeyInMap takes a dot separated string and recursively goes inside the destination +// to fill the value +func InsertKeyInMap(key string, value any, dest map[string]any) { + if key == "" { + return + } + keys := strings.SplitN(key, ".", 2) + // base condition. the length of keys will be atleast 1 + if len(keys) < 2 { + dest[keys[0]] = value + return + } + + ikey := keys[0] + restKey := keys[1] + if dest[ikey] == nil { + dest[ikey] = make(map[string]any) + } + newDest, ok := dest[ikey].(map[string]any) + if !ok { + newDest = make(map[string]any) + dest[ikey] = newDest + } + InsertKeyInMap(restKey, value, newDest) +} From 71f16d8999d4bde6b0aabbf6324013358b360cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 07:28:34 +0800 Subject: [PATCH 02/11] refactor ApisixRoute reconciliation logic and enhance status updates --- internal/controller/apisixroute_controller.go | 122 +++++++++++++----- 1 file changed, 87 insertions(+), 35 deletions(-) diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index a53e2ec94..66f3086bb 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -40,6 +40,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" + "github.com/apache/apisix-ingress-controller/internal/utils" ) // ApisixRouteReconciler reconciles a ApisixRoute object @@ -103,39 +104,33 @@ func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } - tctx := provider.NewDefaultTranslateContext(ctx) + var ( + tctx = provider.NewDefaultTranslateContext(ctx) + ic *networkingv1.IngressClass + err error + ) + defer func() { + r.updateStatus(&ar, err) + }() - ic, err := r.getIngressClass(&ar) - if err == nil { - err = r.processIngressClassParameters(ctx, tctx, &ar, ic) - if err == nil { - err = r.processApisixRoute(ctx, tctx, &ar) - if err == nil { - err = r.Provider.Update(ctx, tctx, &ar) - } - } + if ic, err = r.getIngressClass(&ar); err != nil { + return ctrl.Result{}, err } - if err != nil { + if err = r.processIngressClassParameters(ctx, tctx, &ar, ic); err != nil { + return ctrl.Result{}, err + } + if err = r.processApisixRoute(ctx, tctx, &ar); err != nil { + return ctrl.Result{}, err + } + if err = r.Provider.Update(ctx, tctx, &ar); err != nil { + err = ReasonError{ + Reason: "SyncFailed", + Message: err.Error(), + } r.Log.Error(err, "failed to process", "apisixroute", ar) return ctrl.Result{}, err } - SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) - r.Updater.Update(status.Update{ - NamespacedName: req.NamespacedName, - Resource: ar.DeepCopy(), - Mutator: status.MutatorFunc(func(obj client.Object) client.Object { - ar_, ok := obj.(*apiv2.ApisixRoute) - if !ok { - err := fmt.Errorf("unsupported object type %T", obj) - panic(err) - } - ar_.Status = ar.Status - return ar_ - }), - }) - UpdateStatus(r.Updater, r.Log, tctx) - return ctrl.Result{}, nil } @@ -387,22 +382,79 @@ func (r *ApisixRouteReconciler) getDefaultIngressClass() (*networkingv1.IngressC }} } -func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Context, _ *provider.TranslateContext, ar *apiv2.ApisixRoute, ic *networkingv1.IngressClass) error { - if ic == nil || ic.Spec.Parameters == nil || ic.Spec.Parameters.APIGroup == nil { +// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy +func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Context, tc *provider.TranslateContext, ar *apiv2.ApisixRoute, ingressClass *networkingv1.IngressClass) error { + if ingressClass == nil || ingressClass.Spec.Parameters == nil { return nil } - if *ic.Spec.Parameters.APIGroup != apiv2.GroupVersion.Group || ic.Spec.Parameters.Kind != KindGatewayProxy { + + var ( + ingressClassKind = utils.NamespacedNameKind(ingressClass) + globalRuleKind = utils.NamespacedNameKind(ar) + parameters = ingressClass.Spec.Parameters + ) + if parameters.APIGroup == nil || *parameters.APIGroup != v1alpha1.GroupVersion.Group || parameters.Kind != KindGatewayProxy { return nil } - parameters := ic.Spec.Parameters - ns := *cmp.Or(parameters.Namespace, &ar.Namespace) + // check if the parameters reference GatewayProxy + var ( + gatewayProxy v1alpha1.GatewayProxy + ns = *cmp.Or(parameters.Namespace, &ar.Namespace) + ) - var gp v1alpha1.GatewayProxy - if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: parameters.Name}, &gp); err != nil { - r.Log.Error(err, "failed to get gateway proxy", "gatewayproxy", parameters.Name) + if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: parameters.Name}, &gatewayProxy); err != nil { + r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) return err } + tc.GatewayProxies[ingressClassKind] = gatewayProxy + tc.ResourceParentRefs[globalRuleKind] = append(tc.ResourceParentRefs[globalRuleKind], ingressClassKind) + + // check if the provider field references a secret + if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { + if gatewayProxy.Spec.Provider.ControlPlane != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { + + secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef + secret := &corev1.Secret{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: ns, + Name: secretRef.Name, + }, secret); err != nil { + r.Log.Error(err, "failed to get secret for GatewayProxy provider", + "namespace", ns, + "name", secretRef.Name) + return err + } + + r.Log.Info("found secret for GatewayProxy provider", + "ingressClass", ingressClass.Name, + "gatewayproxy", gatewayProxy.Name, + "secret", secretRef.Name) + + tc.Secrets[types.NamespacedName{ + Namespace: ns, + Name: secretRef.Name, + }] = secret + } + } + return nil } + +func (r *ApisixRouteReconciler) updateStatus(ar *apiv2.ApisixRoute, err error) { + SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) + r.Updater.Update(status.Update{ + NamespacedName: NamespacedName(ar), + Resource: ar.DeepCopy(), + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + cp := obj.(*apiv2.ApisixRoute).DeepCopy() + cp.Status = ar.Status + return cp + }), + }) +} From bb679d018a4cc0b91df4fe62c8532ea36262edab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 10:53:06 +0800 Subject: [PATCH 03/11] implement translator --- api/v2/apisixroute_types.go | 112 +++++++-- api/v2/shared_types.go | 18 +- api/v2/zz_generated.deepcopy.go | 52 ++++- docs/crd/api.md | 26 ++- .../controller/apisixglobalrule_controller.go | 2 +- internal/controller/apisixroute_controller.go | 43 ++-- internal/controller/consumer_controller.go | 2 +- internal/controller/gateway_controller.go | 2 +- .../controller/gatewayclass_congroller.go | 3 +- internal/controller/httproute_controller.go | 2 +- internal/controller/httproutepolicy.go | 7 +- internal/controller/ingress_controller.go | 2 +- internal/controller/policies.go | 3 +- internal/controller/utils.go | 26 --- internal/provider/adc/adc.go | 7 +- .../provider/adc/translator/apisixroute.go | 212 ++++++++++-------- internal/utils/k8s.go | 14 ++ pkg/utils/datastructure.go | 12 + 18 files changed, 372 insertions(+), 173 deletions(-) diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index 1f19411b2..05f2f0898 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -13,10 +13,15 @@ package v2 import ( - "encoding/json" + "slices" + "strings" + "github.com/pkg/errors" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/apache/apisix-ingress-controller/api/adc" ) // ApisixRouteSpec is the spec definition for ApisixRouteSpec. @@ -119,7 +124,7 @@ type ApisixRouteHTTPMatch struct { // value: // - "127.0.0.1" // - "10.0.5.11" - NginxVars []ApisixRouteHTTPMatchExpr `json:"exprs,omitempty" yaml:"exprs,omitempty"` + NginxVars ApisixRouteHTTPMatchExprs `json:"exprs,omitempty" yaml:"exprs,omitempty"` // Matches based on a user-defined filtering function. // These functions can accept an input parameter `vars` // which can be used to access the Nginx variables. @@ -220,24 +225,101 @@ type ApisixRouteHTTPMatchExpr struct { Value *string `json:"value" yaml:"value"` } -// ApisixRoutePluginConfig is the configuration for -// any plugins. -type ApisixRoutePluginConfig map[string]any +type ApisixRouteHTTPMatchExprs []ApisixRouteHTTPMatchExpr -func (p ApisixRoutePluginConfig) DeepCopyInto(out *ApisixRoutePluginConfig) { - b, _ := json.Marshal(&p) - _ = json.Unmarshal(b, out) -} +func (exprs ApisixRouteHTTPMatchExprs) ToVars() (result adc.Vars, err error) { + for _, expr := range exprs { + if expr.Subject.Name == "" && expr.Subject.Scope != ScopePath { + return result, errors.New("empty subject.name") + } + + // process key + var ( + subj string + this adc.StringOrSlice + ) + switch expr.Subject.Scope { + case ScopeQuery: + subj = "arg_" + expr.Subject.Name + case ScopeHeader: + subj = "http_" + strings.ReplaceAll(strings.ToLower(expr.Subject.Name), "-", "_") + case ScopeCookie: + subj = "cookie_" + expr.Subject.Name + case ScopePath: + subj = "uri" + case ScopeVariable: + subj = expr.Subject.Name + default: + return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable]") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj}) + + // process operator + var ( + op string + ) + switch expr.Op { + case OpEqual: + op = "==" + case OpGreaterThan: + op = ">" + case OpGreaterThanEqual: + op = ">=" + case OpIn: + op = "in" + case OpLessThan: + op = "<" + case OpLessThanEqual: + op = "<=" + case OpNotEqual: + op = "~=" + case OpNotIn: + op = "in" + case OpRegexMatch: + op = "~~" + case OpRegexMatchCaseInsensitive: + op = "~*" + case OpRegexNotMatch: + op = "~~" + case OpRegexNotMatchCaseInsensitive: + op = "~*" + default: + return result, errors.New("unknown operator") + } + if invert := slices.Contains([]string{OpNotIn, OpRegexNotMatch, OpRegexNotMatchCaseInsensitive}, op); invert { + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"}) + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op}) -func (p *ApisixRoutePluginConfig) DeepCopy() *ApisixRoutePluginConfig { - if p == nil { - return nil + // process value + switch expr.Op { + case OpIn, OpNotIn: + if expr.Set == nil { + return result, errors.New("empty set value") + } + var value adc.StringOrSlice + for _, item := range expr.Set { + value.SliceVal = append(value.SliceVal, adc.StringOrSlice{StrVal: item}) + } + this.SliceVal = append(this.SliceVal, value) + default: + if expr.Value == nil { + return result, errors.New("empty value") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: *expr.Value}) + } + + // append to result + result = append(result, this.SliceVal) } - out := new(ApisixRoutePluginConfig) - p.DeepCopyInto(out) - return out + + return result, nil } +// ApisixRoutePluginConfig is the configuration for +// any plugins. +type ApisixRoutePluginConfig map[string]apiextensionsv1.JSON + // ApisixRouteAuthenticationKeyAuth is the keyAuth-related // configuration in ApisixRouteAuthentication. type ApisixRouteAuthenticationKeyAuth struct { diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go index 687ed69f0..d9d08a9a9 100644 --- a/api/v2/shared_types.go +++ b/api/v2/shared_types.go @@ -1,11 +1,3 @@ -package v2 - -import ( - "time" - - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" -) - // 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 @@ -18,6 +10,14 @@ import ( // See the License for the specific language governing permissions and // limitations under the License. +package v2 + +import ( + "time" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + type ( // ApisixRouteConditionType is a type of condition for a route. ApisixRouteConditionType = gatewayv1.RouteConditionType @@ -35,6 +35,8 @@ const ( // DefaultUpstreamTimeout represents the default connect, // read and send timeout (in seconds) with upstreams. DefaultUpstreamTimeout = 60 * time.Second + + DefaultWeight = 100 ) const ( diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index ff06644c9..79838c237 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -844,7 +844,7 @@ func (in *ApisixRouteHTTPMatch) DeepCopyInto(out *ApisixRouteHTTPMatch) { } if in.NginxVars != nil { in, out := &in.NginxVars, &out.NginxVars - *out = make([]ApisixRouteHTTPMatchExpr, len(*in)) + *out = make(ApisixRouteHTTPMatchExprs, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -902,6 +902,27 @@ func (in *ApisixRouteHTTPMatchExprSubject) DeepCopy() *ApisixRouteHTTPMatchExprS return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ApisixRouteHTTPMatchExprs) DeepCopyInto(out *ApisixRouteHTTPMatchExprs) { + { + in := &in + *out = make(ApisixRouteHTTPMatchExprs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPMatchExprs. +func (in ApisixRouteHTTPMatchExprs) DeepCopy() ApisixRouteHTTPMatchExprs { + if in == nil { + return nil + } + out := new(ApisixRouteHTTPMatchExprs) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRouteList) DeepCopyInto(out *ApisixRouteList) { *out = *in @@ -937,7 +958,13 @@ func (in *ApisixRouteList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRoutePlugin) DeepCopyInto(out *ApisixRoutePlugin) { *out = *in - in.Config.DeepCopyInto(&out.Config) + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(ApisixRoutePluginConfig, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePlugin. @@ -950,6 +977,27 @@ func (in *ApisixRoutePlugin) DeepCopy() *ApisixRoutePlugin { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ApisixRoutePluginConfig) DeepCopyInto(out *ApisixRoutePluginConfig) { + { + in := &in + *out = make(ApisixRoutePluginConfig, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRoutePluginConfig. +func (in ApisixRoutePluginConfig) DeepCopy() ApisixRoutePluginConfig { + if in == nil { + return nil + } + out := new(ApisixRoutePluginConfig) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRouteSpec) DeepCopyInto(out *ApisixRouteSpec) { *out = *in diff --git a/docs/crd/api.md b/docs/crd/api.md index 4b137c93f..c07e6f257 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -1091,7 +1091,7 @@ ApisixRouteHTTPMatch represents the match condition for hitting this route. | `methods` _string array_ | HTTP request method predicates. | | `hosts` _string array_ | HTTP Host predicates, host can be a wildcard domain or an exact domain. For wildcard domain, only one generic level is allowed, for instance, "*.foo.com" is valid but "*.*.foo.com" is not. | | `remoteAddrs` _string array_ | Remote address predicates, items can be valid IPv4 address or IPv6 address or CIDR. | -| `exprs` _[ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr) array_ | NginxVars represents generic match predicates, it uses Nginx variable systems, so any predicate like headers, querystring and etc can be leveraged here to match the route. For instance, it can be: nginxVars: - subject: "$remote_addr" op: in value: - "127.0.0.1" - "10.0.5.11" | +| `exprs` _[ApisixRouteHTTPMatchExprs](#apisixroutehttpmatchexprs)_ | NginxVars represents generic match predicates, it uses Nginx variable systems, so any predicate like headers, querystring and etc can be leveraged here to match the route. For instance, it can be: nginxVars: - subject: "$remote_addr" op: in value: - "127.0.0.1" - "10.0.5.11" | | `filter_func` _string_ | Matches based on a user-defined filtering function. These functions can accept an input parameter `vars` which can be used to access the Nginx variables. | @@ -1114,7 +1114,7 @@ ApisixRouteHTTPMatchExpr represents a binary route match expression . _Appears in:_ -- [ApisixRouteHTTPMatch](#apisixroutehttpmatch) +- [ApisixRouteHTTPMatchExprs](#apisixroutehttpmatchexprs) #### ApisixRouteHTTPMatchExprSubject @@ -1132,6 +1132,24 @@ ApisixRouteHTTPMatchExprSubject describes the route match expression subject. _Appears in:_ - [ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr) +#### ApisixRouteHTTPMatchExprs +_Base type:_ `[ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr)` + + + + + +| Field | Description | +| --- | --- | +| `subject` _[ApisixRouteHTTPMatchExprSubject](#apisixroutehttpmatchexprsubject)_ | Subject is the expression subject, it can be any string composed by literals and nginx vars. | +| `op` _string_ | Op is the operator. | +| `set` _string array_ | Set is an array type object of the expression. It should be used when the Op is "in" or "not_in"; | +| `value` _string_ | Value is the normal type object for the expression, it should be used when the Op is not "in" and "not_in". Set and Value are exclusive so only of them can be set in the same time. | + + +_Appears in:_ +- [ApisixRouteHTTPMatch](#apisixroutehttpmatch) + #### ApisixRoutePlugin @@ -1634,6 +1652,10 @@ _Appears in:_ + + + + #### UpstreamTimeout diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index f529dbe2e..bfbf0386a 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -367,7 +367,7 @@ func (r *ApisixGlobalRuleReconciler) processIngressClassParameters(ctx context.C // updateStatus updates the ApisixGlobalRule status with the given condition func (r *ApisixGlobalRuleReconciler) updateStatus(globalRule *apiv2.ApisixGlobalRule, condition metav1.Condition) { r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(globalRule), + NamespacedName: utils.NamespacedName(globalRule), Resource: &apiv2.ApisixGlobalRule{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { gr, ok := obj.(*apiv2.ApisixGlobalRule) diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 66f3086bb..ef3dd22e1 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -39,8 +39,8 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" - "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // ApisixRouteReconciler reconciles a ApisixRoute object @@ -66,19 +66,19 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { ), ). Watches(&networkingv1.Ingress{}, - handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApiRouteForIngressClass)), + handler.EnqueueRequestsFromMapFunc(r.listApiRouteForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(r.matchesIngressController), ), ). Watches(&v1alpha1.GatewayProxy{}, - handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRouteForGatewayProxy)), + handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForGatewayProxy), ). Watches(&discoveryv1.EndpointSlice{}, - handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRoutesForService)), + handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForService), ). Watches(&corev1.Secret{}, - handler.EnqueueRequestsFromMapFunc(WrapMapFuncDedup(r.listApisixRoutesForSecret)), + handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForSecret), ). Named("apisixroute"). Complete(r) @@ -168,7 +168,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov Namespace: in.Namespace, }, } - secretNN = NamespacedName(&secret) + secretNN = utils.NamespacedName(&secret) ) if err := r.Get(ctx, secretNN, &secret); err != nil { return ReasonError{ @@ -177,12 +177,12 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov } } - tc.Secrets[NamespacedName(&secret)] = &secret + tc.Secrets[utils.NamespacedName(&secret)] = &secret } // check vars // todo: cache the result - if _, err := translator.TranslateApisixRouteVars(http.Match.NginxVars); err != nil { + if _, err := http.Match.NginxVars.ToVars(); err != nil { return ReasonError{ Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), Message: fmt.Sprintf(".spec.http[%d].match.exprs: %s", httpIndex, err.Error()), @@ -190,7 +190,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov } // validate remote address - if err := translator.ValidateRemoteAddrs(http.Match.RemoteAddrs); err != nil { + if err := utils.ValidateRemoteAddrs(http.Match.RemoteAddrs); err != nil { return ReasonError{ Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), Message: fmt.Sprintf(".spec.http[%d].match.remoteAddrs: %s", httpIndex, err.Error()), @@ -207,7 +207,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov Namespace: in.Namespace, }, } - serviceNN = NamespacedName(&service) + serviceNN = utils.NamespacedName(&service) ) if _, ok := backends[serviceNN]; ok { return ReasonError{ @@ -228,6 +228,13 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov continue } + if backend.ResolveGranularity == "service" && service.Spec.ClusterIP == "" { + return ReasonError{ + Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Message: fmt.Sprintf("service %s has no cluster IP", serviceNN), + } + } + if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool { return port.Port == int32(backend.ServicePort.IntValue()) }) { @@ -276,9 +283,9 @@ func (r *ApisixRouteReconciler) listApisixRoutesForService(ctx context.Context, } requests := make([]reconcile.Request, 0, len(arList.Items)) for _, ar := range arList.Items { - requests = append(requests, reconcile.Request{NamespacedName: NamespacedName(&ar)}) + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) } - return requests + return pkgutils.DedupComparable(requests) } func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, obj client.Object) []reconcile.Request { @@ -298,9 +305,9 @@ func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, o } requests := make([]reconcile.Request, 0, len(arList.Items)) for _, ar := range arList.Items { - requests = append(requests, reconcile.Request{NamespacedName: NamespacedName(&ar)}) + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) } - return requests + return pkgutils.DedupComparable(requests) } func (r *ApisixRouteReconciler) listApiRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { @@ -316,10 +323,10 @@ func (r *ApisixRouteReconciler) listApiRouteForIngressClass(ctx context.Context, } for _, ar := range arList.Items { if ar.Spec.IngressClassName == ic.Name || (isDefaultIngressClass && ar.Spec.IngressClassName == "") { - requests = append(requests, reconcile.Request{NamespacedName: NamespacedName(&ar)}) + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) } } - return requests + return pkgutils.DedupComparable(requests) } func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, object client.Object) (requests []reconcile.Request) { @@ -340,7 +347,7 @@ func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Conte requests = append(requests, r.listApiRouteForIngressClass(ctx, &ic)...) } - return requests + return pkgutils.DedupComparable(requests) } func (r *ApisixRouteReconciler) matchesIngressController(obj client.Object) bool { @@ -449,7 +456,7 @@ func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Contex func (r *ApisixRouteReconciler) updateStatus(ar *apiv2.ApisixRoute, err error) { SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(ar), + NamespacedName: utils.NamespacedName(ar), Resource: ar.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*apiv2.ApisixRoute).DeepCopy() diff --git a/internal/controller/consumer_controller.go b/internal/controller/consumer_controller.go index c92d04f84..d82c8fe91 100644 --- a/internal/controller/consumer_controller.go +++ b/internal/controller/consumer_controller.go @@ -279,7 +279,7 @@ func (r *ConsumerReconciler) updateStatus(consumer *v1alpha1.Consumer, err error meta.SetStatusCondition(&consumer.Status.Conditions, condition) r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(consumer), + NamespacedName: utils.NamespacedName(consumer), Resource: consumer.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.Consumer) diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index f9555186a..f4a1bc544 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -190,7 +190,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(gateway), + NamespacedName: utils.NamespacedName(gateway), Resource: &gatewayv1.Gateway{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*gatewayv1.Gateway) diff --git a/internal/controller/gatewayclass_congroller.go b/internal/controller/gatewayclass_congroller.go index 9e8094181..946afe23c 100644 --- a/internal/controller/gatewayclass_congroller.go +++ b/internal/controller/gatewayclass_congroller.go @@ -31,6 +31,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" + "github.com/apache/apisix-ingress-controller/internal/utils" ) const ( @@ -115,7 +116,7 @@ func (r *GatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request r.Log.Info("gatewayclass has been accepted", "gatewayclass", gc.Name) setGatewayClassCondition(gc, condition) r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(gc), + NamespacedName: utils.NamespacedName(gc), Resource: gc.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*gatewayv1.GatewayClass) diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index 30849c4f4..f5a698b56 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -223,7 +223,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(hr), + NamespacedName: utils.NamespacedName(hr), Resource: &gatewayv1.HTTPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { h, ok := obj.(*gatewayv1.HTTPRoute) diff --git a/internal/controller/httproutepolicy.go b/internal/controller/httproutepolicy.go index 932367528..863555f60 100644 --- a/internal/controller/httproutepolicy.go +++ b/internal/controller/httproutepolicy.go @@ -30,6 +30,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" ) func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { @@ -83,7 +84,7 @@ func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateC if updated := setAncestorsForHTTPRoutePolicyStatus(parentRefs, &policy, condition); updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) @@ -161,7 +162,7 @@ func (r *IngressReconciler) processHTTPRoutePolicies(tctx *provider.TranslateCon policy := list.Items[i] if updated := setAncestorsForHTTPRoutePolicyStatus(tctx.RouteParentRefs, &policy, condition); updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) @@ -273,7 +274,7 @@ func updateDeleteAncestors(updater status.Updater, policy v1alpha1.HTTPRoutePoli }) if length != len(policy.Status.Ancestors) { updater.Update(status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index 70cdd2b2e..74403089a 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -662,7 +662,7 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra if len(loadBalancerStatus.Ingress) > 0 && !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) { ingress.Status.LoadBalancer = loadBalancerStatus r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(ingress), + NamespacedName: utils.NamespacedName(ingress), Resource: ingress.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*networkingv1.Ingress) diff --git a/internal/controller/policies.go b/internal/controller/policies.go index ca3bd54b7..fbb874400 100644 --- a/internal/controller/policies.go +++ b/internal/controller/policies.go @@ -32,6 +32,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" ) type PolicyTargetKey struct { @@ -149,7 +150,7 @@ func ProcessBackendTrafficPolicy( } if updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(policy), + NamespacedName: utils.NamespacedName(policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.BackendTrafficPolicy) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 3426d00a0..f265e12c3 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -33,7 +33,6 @@ import ( k8stypes "k8s.io/apimachinery/pkg/types" "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" @@ -1171,28 +1170,3 @@ func checkReferenceGrant(ctx context.Context, cli client.Client, obj v1beta1.Ref } return false } - -func NamespacedName(obj client.Object) k8stypes.NamespacedName { - return k8stypes.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - } -} - -func WrapMapFuncDedup(mapFunc handler.MapFunc) handler.MapFunc { - return func(ctx context.Context, object client.Object) (result []reconcile.Request) { - return DedupComparable(mapFunc(ctx, object)) - } -} - -func DedupComparable[T comparable](s []T) []T { - var keys = make(map[T]struct{}) - var results []T - for _, item := range s { - if _, ok := keys[item]; !ok { - keys[item] = struct{}{} - results = append(results, item) - } - } - return results -} diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 9362f8d6b..98b465733 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -32,7 +32,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/label" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" - types "github.com/apache/apisix-ingress-controller/internal/types" + "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" ) @@ -223,7 +223,10 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { resourceTypes = append(resourceTypes, "consumer") labels = label.GenLabel(obj) case *networkingv1.IngressClass: - // delete all resources + // delete all resources + case *apiv2.ApisixRoute: + resourceTypes = append(resourceTypes, "service") + labels = label.GenLabel(obj) case *apiv2.ApisixGlobalRule: resourceTypes = append(resourceTypes, "global_rule") labels = label.GenLabel(obj) diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go index 6500e322d..603d0fc04 100644 --- a/internal/provider/adc/translator/apisixroute.go +++ b/internal/provider/adc/translator/apisixroute.go @@ -13,119 +13,151 @@ package translator import ( - "net" - "slices" - "strings" + "cmp" + "encoding/json" + "fmt" - "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/apache/apisix-ingress-controller/api/adc" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/label" "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/pkg/id" + "github.com/apache/apisix-ingress-controller/pkg/utils" ) -func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, obj *apiv2.ApisixRoute) (*TranslateResult, error) { +func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *apiv2.ApisixRoute) (result *TranslateResult, err error) { + result = &TranslateResult{} + for ruleIndex, http := range ar.Spec.HTTP { + var timeout *adc.Timeout + if http.Timeout != nil { + defaultTimeout := metav1.Duration{Duration: apiv2.DefaultUpstreamTimeout} + timeout = &adc.Timeout{ + Connect: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + Read: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + Send: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + } + } - return nil, errors.New("not implemented yet") -} + var plugins = make(adc.Plugins) + // todo: need unit test or e2e test + for _, plugin := range http.Plugins { + if !plugin.Enable { + continue + } -func TranslateApisixRouteVars(exprs []apiv2.ApisixRouteHTTPMatchExpr) (result adc.StringOrSlice, err error) { - for _, expr := range exprs { - if expr.Subject.Name == "" && expr.Subject.Scope != apiv2.ScopePath { - return result, errors.New("empty subject.name") + config := make(map[string]any) + if plugin.Config != nil { + for key, value := range plugin.Config { + config[key] = json.RawMessage(value.Raw) + } + } + if plugin.SecretRef != "" { + if secret, ok := tctx.Secrets[types.NamespacedName{Namespace: ar.Namespace, Name: plugin.SecretRef}]; ok { + for key, value := range secret.Data { + utils.InsertKeyInMap(key, string(value), config) + } + } + } + plugins[plugin.Name] = config } - // process key - var ( - subj string - this adc.StringOrSlice - ) - switch expr.Subject.Scope { - case apiv2.ScopeQuery: - subj = "arg_" + expr.Subject.Name - case apiv2.ScopeHeader: - subj = "http_" + strings.ReplaceAll(strings.ToLower(expr.Subject.Name), "-", "_") - case apiv2.ScopeCookie: - subj = "cookie_" + expr.Subject.Name - case apiv2.ScopePath: - subj = "uri" - case apiv2.ScopeVariable: - subj = expr.Subject.Name - default: - return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable]") + // add Authentication plugins + if http.Authentication.Enable { + switch http.Authentication.Type { + case "keyAuth": + plugins["key-auth"] = http.Authentication.KeyAuth + case "basicAuth": + plugins["basic-auth"] = make(map[string]any) + case "wolfRBAC": + plugins["wolf-rbac"] = make(map[string]any) + case "jwtAuth": + plugins["jwt-auth"] = http.Authentication.JwtAuth + case "hmacAuth": + plugins["hmac-auth"] = make(map[string]any) + case "ldapAuth": + plugins["ldap-auth"] = http.Authentication.LDAPAuth + default: + plugins["basic-auth"] = make(map[string]any) + } + } + + vars, err := http.Match.NginxVars.ToVars() + if err != nil { + return nil, err } - this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj}) - // process operator var ( - op string + route = adc.NewDefaultRoute() + upstream = adc.NewDefaultUpstream() + service = adc.NewDefaultService() + labels = label.GenLabel(ar) ) - switch expr.Op { - case apiv2.OpEqual: - op = "==" - case apiv2.OpGreaterThan: - op = ">" - case apiv2.OpGreaterThanEqual: - op = ">=" - case apiv2.OpIn: - op = "in" - case apiv2.OpLessThan: - op = "<" - case apiv2.OpLessThanEqual: - op = "<=" - case apiv2.OpNotEqual: - op = "~=" - case apiv2.OpNotIn: - op = "in" - case apiv2.OpRegexMatch: - op = "~~" - case apiv2.OpRegexMatchCaseInsensitive: - op = "~*" - case apiv2.OpRegexNotMatch: - op = "~~" - case apiv2.OpRegexNotMatchCaseInsensitive: - op = "~*" - default: - return result, errors.New("unknown operator") + // translate to adc.Route + route.Name = adc.ComposeRouteName(ar.Namespace, ar.Name, http.Name) + route.ID = id.GenID(route.Name) + route.Desc = "Created by apisix-ingress-controller, DO NOT modify it manually" + route.Labels = labels + route.EnableWebsocket = ptr.To(true) + route.FilterFunc = http.Match.FilterFunc + route.Hosts = http.Match.Hosts + route.Methods = http.Match.Methods + route.Plugins = plugins + route.Priority = ptr.To(int64(http.Priority)) + route.RemoteAddrs = http.Match.RemoteAddrs + route.Timeout = timeout + route.Uris = http.Match.Paths + route.Vars = vars + + // translate to adc.Upstream + var backendErr error + for _, backend := range http.Backends { + weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight))) + backendRef := gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(backend.ServiceName), + Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal), + }, + Weight: &weight, + } + upNodes, err := t.translateBackendRef(tctx, backendRef) + if err != nil { + backendErr = err + continue + } + t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream) + upstream.Nodes = append(upstream.Nodes, upNodes...) } - if invert := slices.Contains([]string{apiv2.OpNotIn, apiv2.OpRegexNotMatch, apiv2.OpRegexNotMatchCaseInsensitive}, op); invert { - this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"}) + //nolint:staticcheck + if len(http.Backends) == 0 && len(http.Upstreams) > 0 { + // FIXME: when the API ApisixUpstream is supported } - this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op}) - // process value - switch expr.Op { - case apiv2.OpIn, apiv2.OpNotIn: - if expr.Set == nil { - return result, errors.New("empty set value") - } - var value adc.StringOrSlice - for _, item := range expr.Set { - value.SliceVal = append(value.SliceVal, adc.StringOrSlice{StrVal: item}) + // translate to adc.Service + service.Labels = ar.Labels + service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, fmt.Sprintf("%d", ruleIndex)) + service.Hosts = http.Match.Hosts + + if backendErr != nil && len(upstream.Nodes) == 0 { + if service.Plugins == nil { + service.Plugins = make(map[string]any) } - this.SliceVal = append(this.SliceVal, value) - default: - if expr.Value == nil { - return result, errors.New("empty value") + service.Plugins["fault-injection"] = map[string]any{ + "abort": map[string]any{ + "http_status": 500, + "body": "No existing backendRef provided", + }, } - this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: *expr.Value}) } - // append to result - result.SliceVal = append(result.SliceVal, this) + service.Upstream = upstream + service.Routes = []*adc.Route{route} + result.Services = append(result.Services, service) } return result, nil } - -func ValidateRemoteAddrs(remoteAddrs []string) error { - for _, addr := range remoteAddrs { - if ip := net.ParseIP(addr); ip == nil { - // addr is not an IP address, try to parse it as a CIDR. - if _, _, err := net.ParseCIDR(addr); err != nil { - return err - } - } - } - return nil -} diff --git a/internal/utils/k8s.go b/internal/utils/k8s.go index 2f32c30a3..7010a4655 100644 --- a/internal/utils/k8s.go +++ b/internal/utils/k8s.go @@ -13,6 +13,8 @@ package utils import ( + "net" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,3 +35,15 @@ func NamespacedNameKind(obj client.Object) types.NamespacedNameKind { Kind: obj.GetObjectKind().GroupVersionKind().Kind, } } + +func ValidateRemoteAddrs(remoteAddrs []string) error { + for _, addr := range remoteAddrs { + if ip := net.ParseIP(addr); ip == nil { + // addr is not an IP address, try to parse it as a CIDR. + if _, _, err := net.ParseCIDR(addr); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/utils/datastructure.go b/pkg/utils/datastructure.go index 65c719b5b..bf9171836 100644 --- a/pkg/utils/datastructure.go +++ b/pkg/utils/datastructure.go @@ -41,3 +41,15 @@ func InsertKeyInMap(key string, value any, dest map[string]any) { } InsertKeyInMap(restKey, value, newDest) } + +func DedupComparable[T comparable](s []T) []T { + var keys = make(map[T]struct{}) + var results []T + for _, item := range s { + if _, ok := keys[item]; !ok { + keys[item] = struct{}{} + results = append(results, item) + } + } + return results +} From b1ca12178ccbe40c595f405dd59ec703b057fb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 15:01:36 +0800 Subject: [PATCH 04/11] add e2e case for ApisixRoute --- api/v2/apisixpluginconfig_types.go | 2 +- api/v2/apisixroute_types.go | 2 + api/v2/groupversion_info.go | 10 +++ api/v2/reason.go | 19 ----- api/v2/shared_types.go | 7 +- api/v2/status.go | 20 +++++ api/v2/zz_generated.deepcopy.go | 22 ----- ...apisix.apache.org_apisixpluginconfigs.yaml | 2 +- .../bases/apisix.apache.org_apisixroutes.yaml | 2 - .../controller/apisixglobalrule_controller.go | 4 +- internal/controller/apisixroute_controller.go | 18 ++-- internal/controller/utils.go | 6 +- internal/manager/controllers.go | 7 ++ internal/provider/adc/adc.go | 34 +++----- .../provider/adc/translator/apisixroute.go | 14 ++-- test/e2e/apisix/globalrule.go | 14 ++-- test/e2e/apisix/route.go | 82 +++++++++++++++++++ test/e2e/apiv2/apisixconsumer.go | 13 --- test/e2e/apiv2/apisixglobalrule.go | 13 --- test/e2e/apiv2/apisixpluginconfig.go | 13 --- test/e2e/apiv2/apisixroute.go | 13 --- test/e2e/apiv2/apisixtls.go | 13 --- test/e2e/apiv2/apisixupstream.go | 13 --- test/e2e/e2e_test.go | 1 - test/e2e/framework/assertion.go | 36 ++++---- 25 files changed, 188 insertions(+), 192 deletions(-) delete mode 100644 api/v2/reason.go create mode 100644 test/e2e/apisix/route.go delete mode 100644 test/e2e/apiv2/apisixconsumer.go delete mode 100644 test/e2e/apiv2/apisixglobalrule.go delete mode 100644 test/e2e/apiv2/apisixpluginconfig.go delete mode 100644 test/e2e/apiv2/apisixroute.go delete mode 100644 test/e2e/apiv2/apisixtls.go delete mode 100644 test/e2e/apiv2/apisixupstream.go diff --git a/api/v2/apisixpluginconfig_types.go b/api/v2/apisixpluginconfig_types.go index 4b9cd9a6b..8abbe6c75 100644 --- a/api/v2/apisixpluginconfig_types.go +++ b/api/v2/apisixpluginconfig_types.go @@ -28,7 +28,7 @@ type ApisixPluginConfigSpec struct { } // ApisixPluginConfigStatus defines the observed state of ApisixPluginConfig. -type ApisixPluginConfigStatus ApisixStatus +type ApisixPluginConfigStatus = ApisixStatus // +kubebuilder:object:root=true // +kubebuilder:subresource:status diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index 05f2f0898..35b8de0df 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -72,6 +72,7 @@ type ApisixRouteHTTP struct { // Upstreams refer to ApisixUpstream CRD Upstreams []ApisixRouteUpstreamReference `json:"upstreams,omitempty" yaml:"upstreams,omitempty"` + // +kubebuilder:validation:Optional Websocket bool `json:"websocket" yaml:"websocket"` PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"` // By default, PluginConfigNamespace will be the same as the namespace of ApisixRoute @@ -159,6 +160,7 @@ type ApisixRouteHTTPBackend struct { // default is endpoints. ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"` // Weight of this backend. + // +kubebuilder:validation:Optional Weight *int `json:"weight" yaml:"weight"` // Subset specifies a subset for the target Service. The subset should be pre-defined // in ApisixUpstream about this service. diff --git a/api/v2/groupversion_info.go b/api/v2/groupversion_info.go index 5d5d96511..84ff95241 100644 --- a/api/v2/groupversion_info.go +++ b/api/v2/groupversion_info.go @@ -17,6 +17,7 @@ package v2 import ( "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -30,3 +31,12 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +func Is(obj client.Object) bool { + switch obj.(type) { + case *ApisixConsumer, *ApisixGlobalRule, *ApisixPluginConfig, *ApisixRoute, *ApisixTls, *ApisixUpstream: + return obj.GetObjectKind().GroupVersionKind().GroupVersion() == GroupVersion + default: + return false + } +} diff --git a/api/v2/reason.go b/api/v2/reason.go deleted file mode 100644 index 723388f7a..000000000 --- a/api/v2/reason.go +++ /dev/null @@ -1,19 +0,0 @@ -// 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 v2 - -type Reason string - -const ( - ReasonSyncFailed Reason = "SyncFailed" -) diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go index d9d08a9a9..4738c5d83 100644 --- a/api/v2/shared_types.go +++ b/api/v2/shared_types.go @@ -26,9 +26,10 @@ type ( ) const ( - ApisixRouteConditionTypeAccepted ApisixRouteConditionType = gatewayv1.RouteConditionAccepted - ApisixRouteConditionReasonAccepted ApisixRouteConditionReason = gatewayv1.RouteReasonAccepted - ApisixRouteConditionReasonInvalidHTTP ApisixRouteConditionReason = "InvalidHTTP" + ConditionTypeAccepted ApisixRouteConditionType = gatewayv1.RouteConditionAccepted + ConditionReasonAccepted ApisixRouteConditionReason = gatewayv1.RouteReasonAccepted + ConditionReasonInvalidSpec ApisixRouteConditionReason = "InvalidSpec" + ConditionReasonSyncFailed ApisixRouteConditionReason = "SyncFailed" ) const ( diff --git a/api/v2/status.go b/api/v2/status.go index 45782a21a..bc1709639 100644 --- a/api/v2/status.go +++ b/api/v2/status.go @@ -14,9 +14,29 @@ package v2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) // ApisixStatus is the status report for Apisix ingress Resources type ApisixStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"` } + +func GetStatus(object client.Object) ApisixStatus { + switch t := object.(type) { + case *ApisixConsumer: + return t.Status + case *ApisixGlobalRule: + return t.Status + case *ApisixPluginConfig: + return t.Status + case *ApisixRoute: + return t.Status + case *ApisixTls: + return t.Status + case *ApisixUpstream: + return t.Status + default: + return ApisixStatus{} + } +} diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 79838c237..917d1885e 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -643,28 +643,6 @@ func (in *ApisixPluginConfigSpec) DeepCopy() *ApisixPluginConfigSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ApisixPluginConfigStatus) DeepCopyInto(out *ApisixPluginConfigStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixPluginConfigStatus. -func (in *ApisixPluginConfigStatus) DeepCopy() *ApisixPluginConfigStatus { - if in == nil { - return nil - } - out := new(ApisixPluginConfigStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) { *out = *in diff --git a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml index 083e76871..b967d2395 100644 --- a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml +++ b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml @@ -74,7 +74,7 @@ spec: - plugins type: object status: - description: ApisixPluginConfigStatus defines the observed state of ApisixPluginConfig. + description: ApisixStatus is the status report for Apisix ingress Resources properties: conditions: items: diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml b/config/crd/bases/apisix.apache.org_apisixroutes.yaml index d62986b43..454da67e2 100644 --- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml +++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml @@ -130,7 +130,6 @@ spec: required: - serviceName - servicePort - - weight type: object type: array match: @@ -303,7 +302,6 @@ spec: type: boolean required: - name - - websocket type: object type: array ingressClassName: diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index bfbf0386a..e327ca468 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -96,11 +96,11 @@ func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Req log.Error(err, "failed to sync global rule to provider") // Update status with failure condition r.updateStatus(&globalRule, metav1.Condition{ - Type: string(gatewayv1.RouteConditionAccepted), + Type: string(apiv2.ConditionTypeAccepted), Status: metav1.ConditionFalse, ObservedGeneration: globalRule.Generation, LastTransitionTime: metav1.Now(), - Reason: string(apiv2.ReasonSyncFailed), + Reason: string(apiv2.ConditionReasonSyncFailed), Message: err.Error(), }) return ctrl.Result{}, err diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index ef3dd22e1..9d8bb4638 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -143,7 +143,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov // check rule names if _, ok := rules[http.Name]; ok { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: "duplicate route rule name", } } @@ -172,7 +172,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov ) if err := r.Get(ctx, secretNN, &secret); err != nil { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("failed to get Secret: %s", secretNN), } } @@ -184,7 +184,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov // todo: cache the result if _, err := http.Match.NginxVars.ToVars(); err != nil { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf(".spec.http[%d].match.exprs: %s", httpIndex, err.Error()), } } @@ -192,7 +192,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov // validate remote address if err := utils.ValidateRemoteAddrs(http.Match.RemoteAddrs); err != nil { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf(".spec.http[%d].match.remoteAddrs: %s", httpIndex, err.Error()), } } @@ -211,7 +211,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov ) if _, ok := backends[serviceNN]; ok { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("duplicate backend service: %s", serviceNN), } } @@ -219,7 +219,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov if err := r.Get(ctx, serviceNN, &service); err != nil { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("failed to get Service: %s", serviceNN), } } @@ -230,7 +230,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov if backend.ResolveGranularity == "service" && service.Spec.ClusterIP == "" { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("service %s has no cluster IP", serviceNN), } } @@ -239,7 +239,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov return port.Port == int32(backend.ServicePort.IntValue()) }) { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("port %s not found in service %s", backend.ServicePort.String(), serviceNN), } } @@ -253,7 +253,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov }, ); err != nil { return ReasonError{ - Reason: string(apiv2.ApisixRouteConditionReasonInvalidHTTP), + Reason: string(apiv2.ConditionReasonInvalidSpec), Message: fmt.Sprintf("failed to list endpoint slices: %v", err), } } diff --git a/internal/controller/utils.go b/internal/controller/utils.go index f265e12c3..b461209ac 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -412,15 +412,15 @@ func ParseRouteParentRefs( func SetApisixRouteConditionAccepted(status *apiv2.ApisixStatus, generation int64, err error) { var condition = metav1.Condition{ - Type: string(apiv2.ApisixRouteConditionTypeAccepted), + Type: string(apiv2.ConditionTypeAccepted), Status: metav1.ConditionTrue, ObservedGeneration: generation, LastTransitionTime: metav1.Now(), - Reason: string(apiv2.ApisixRouteConditionReasonAccepted), + Reason: string(apiv2.ConditionReasonAccepted), } if err != nil { condition.Status = metav1.ConditionFalse - condition.Reason = string(apiv2.ApisixRouteConditionReasonInvalidHTTP) + condition.Reason = string(apiv2.ConditionReasonInvalidSpec) condition.Message = err.Error() var re ReasonError diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index f68ef7335..6551e6923 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -127,5 +127,12 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Provider: pro, Updater: updater, }, + &controller.ApisixRouteReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"), + Provider: pro, + Updater: updater, + }, }, nil } diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 98b465733..64ec21e47 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -179,30 +179,20 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, } } - switch d.BackendMode { - case BackendModeAPISIXStandalone: - // This mode is full synchronization, - // which only needs to be saved in cache - // and triggered by a timer for synchronization + // This mode is full synchronization, + // which only needs to be saved in cache + // and triggered by a timer for synchronization + if d.BackendMode == BackendModeAPISIXStandalone || apiv2.Is(obj) { return nil - case BackendModeAPI7EE: - // if api version is v2, then skip sync - if obj.GetObjectKind().GroupVersionKind().GroupVersion() == apiv2.GroupVersion { - log.Debugw("api version is v2, skip sync", zap.Any("obj", obj)) - return nil - } - - return d.sync(ctx, Task{ - Name: obj.GetName(), - Labels: label.GenLabel(obj), - Resources: resources, - ResourceTypes: resourceTypes, - configs: configs, - }) - default: - log.Errorw("unknown backend mode", zap.String("mode", d.BackendMode)) - return errors.New("unknown backend mode: " + d.BackendMode) } + + return d.sync(ctx, Task{ + Name: obj.GetName(), + Labels: label.GenLabel(obj), + Resources: resources, + ResourceTypes: resourceTypes, + configs: configs, + }) } func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go index 603d0fc04..ec9eada3e 100644 --- a/internal/provider/adc/translator/apisixroute.go +++ b/internal/provider/adc/translator/apisixroute.go @@ -119,8 +119,11 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight))) backendRef := gatewayv1.BackendRef{ BackendObjectReference: gatewayv1.BackendObjectReference{ - Name: gatewayv1.ObjectName(backend.ServiceName), - Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal), + Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group), + Kind: (*gatewayv1.Kind)(ptr.To("Service")), + Name: gatewayv1.ObjectName(backend.ServiceName), + Namespace: (*gatewayv1.Namespace)(&ar.Namespace), + Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal), }, Weight: &weight, } @@ -138,9 +141,12 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a } // translate to adc.Service - service.Labels = ar.Labels service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, fmt.Sprintf("%d", ruleIndex)) + service.ID = id.GenID(service.Name) + service.Labels = ar.Labels service.Hosts = http.Match.Hosts + service.Upstream = upstream + service.Routes = []*adc.Route{route} if backendErr != nil && len(upstream.Nodes) == 0 { if service.Plugins == nil { @@ -154,8 +160,6 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a } } - service.Upstream = upstream - service.Routes = []*adc.Route{route} result.Services = append(result.Services, service) } diff --git a/test/e2e/apisix/globalrule.go b/test/e2e/apisix/globalrule.go index 9702d6422..ceac7038f 100644 --- a/test/e2e/apisix/globalrule.go +++ b/test/e2e/apisix/globalrule.go @@ -23,12 +23,7 @@ import ( "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) -var _ = Describe("Test GlobalRule", func() { - s := scaffold.NewScaffold(&scaffold.Options{ - ControllerName: "apisix.apache.org/apisix-ingress-controller", - }) - - var gatewayProxyYaml = ` +const gatewayProxyYaml = ` apiVersion: apisix.apache.org/v1alpha1 kind: GatewayProxy metadata: @@ -46,7 +41,7 @@ spec: value: "%s" ` - var ingressClassYaml = ` +const ingressClassYaml = ` apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: @@ -61,6 +56,11 @@ spec: scope: "Namespace" ` +var _ = Describe("Test GlobalRule", func() { + s := scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + var ingressYaml = ` apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go new file mode 100644 index 000000000..c3a5e48aa --- /dev/null +++ b/test/e2e/apisix/route.go @@ -0,0 +1,82 @@ +// 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 apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixRoute", func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + Context("Test ApisixRoute", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: route0 + match: + hosts: + - httpbin + paths: + - /get + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("Basic tests", func() { + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + }) + }) +}) diff --git a/test/e2e/apiv2/apisixconsumer.go b/test/e2e/apiv2/apisixconsumer.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixconsumer.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixglobalrule.go b/test/e2e/apiv2/apisixglobalrule.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixglobalrule.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixpluginconfig.go b/test/e2e/apiv2/apisixpluginconfig.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixpluginconfig.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixroute.go b/test/e2e/apiv2/apisixroute.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixroute.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixtls.go b/test/e2e/apiv2/apisixtls.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixtls.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixupstream.go b/test/e2e/apiv2/apisixupstream.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixupstream.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 267e6559a..ae1503e4b 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -19,7 +19,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - _ "github.com/apache/apisix-ingress-controller/test/e2e/apiv2" _ "github.com/apache/apisix-ingress-controller/test/e2e/crds" "github.com/apache/apisix-ingress-controller/test/e2e/framework" _ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi" diff --git a/test/e2e/framework/assertion.go b/test/e2e/framework/assertion.go index 3efc479b3..b1bcfd665 100644 --- a/test/e2e/framework/assertion.go +++ b/test/e2e/framework/assertion.go @@ -31,7 +31,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "github.com/apache/apisix-ingress-controller/api/v1alpha1" - v2 "github.com/apache/apisix-ingress-controller/api/v2" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" ) func HTTPRouteMustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, refNN, hrNN types.NamespacedName, condition metav1.Condition) { @@ -96,25 +96,28 @@ func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration return genericPollResource(new(v1alpha1.HTTPRoutePolicy), cli, timeout, hrpNN, f) } -func ApisixConsumerMustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, condition metav1.Condition) { - err := PollUntilApisixConsumerMustHaveStatus(cli, timeout, nn, func(consumer *v2.ApisixConsumer) bool { - if err := kubernetes.ConditionsHaveLatestObservedGeneration(consumer, consumer.Status.Conditions); err != nil { +func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) { + err := PollUntilAPIv2MustHaveStatus(cli, timeout, nn, obj, func(object client.Object) bool { + conditions := apiv2.GetStatus(object).Conditions + if err := kubernetes.ConditionsHaveLatestObservedGeneration(object, conditions); err != nil { return false } - if findConditionInList(consumer.Status.Conditions, condition) { - return true - } - return false + return findConditionInList(conditions, cond) }) - require.NoError(t, err, "error waiting for ApisixConsumer %s status to have a Condition matching %+v", nn, condition) + require.NoError(t, err, "error waiting status to have a Condition matching %+v", nn, cond) } -func PollUntilApisixConsumerMustHaveStatus(cli client.Client, timeout time.Duration, nn types.NamespacedName, f func(consumer *v2.ApisixConsumer) bool) error { - if err := v2.AddToScheme(cli.Scheme()); err != nil { +func PollUntilAPIv2MustHaveStatus(cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, f func(client.Object) bool) error { + if err := apiv2.AddToScheme(cli.Scheme()); err != nil { return err } - return genericPollResource(new(v2.ApisixConsumer), cli, timeout, nn, f) + return wait.PollUntilContextTimeout(context.Background(), time.Second, timeout, true, func(ctx context.Context) (done bool, err error) { + if err := cli.Get(ctx, nn, obj); err != nil { + return false, errors.Wrapf(err, "error fetching Object %s", nn) + } + return f(obj), nil + }) } func parentRefToString(p gatewayv1.ParentReference) string { @@ -153,7 +156,7 @@ func NewApplier(t testing.TestingT, cli client.Client, apply func(string) error) } type Applier interface { - MustApplyApisixConsumer(nn types.NamespacedName, spec string) + MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) } type applier struct { @@ -162,11 +165,10 @@ type applier struct { apply func(string) error } -func (a *applier) MustApplyApisixConsumer(nn types.NamespacedName, spec string) { - err := a.apply(spec) - require.NoError(a.t, err, "creating ApisixConsumer", "request", nn, "spec", spec) +func (a *applier) MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) { + require.NoError(a.t, a.apply(spec), "creating %s", nn) - ApisixConsumerMustHaveCondition(a.t, a.cli, 8*time.Second, nn, metav1.Condition{ + APIv2MustHaveCondition(a.t, a.cli, 8*time.Second, nn, obj, metav1.Condition{ Type: string(gatewayv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: string(gatewayv1.GatewayReasonAccepted), From 650f3620e5faddf039280e681e7dd36eb5399cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 15:56:46 +0800 Subject: [PATCH 05/11] more e2e test cases --- api/v2/apisixroute_types.go | 2 + .../bases/apisix.apache.org_apisixroutes.yaml | 2 - test/e2e/apisix/route.go | 146 +++++++++++++++++- 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index 35b8de0df..6c64661f0 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -219,11 +219,13 @@ type ApisixRouteHTTPMatchExpr struct { Op string `json:"op" yaml:"op"` // Set is an array type object of the expression. // It should be used when the Op is "in" or "not_in"; + // +kubebuilder:validation:Optional Set []string `json:"set" yaml:"set"` // Value is the normal type object for the expression, // it should be used when the Op is not "in" and "not_in". // Set and Value are exclusive so only of them can be set // in the same time. + // +kubebuilder:validation:Optional Value *string `json:"value" yaml:"value"` } diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml b/config/crd/bases/apisix.apache.org_apisixroutes.yaml index 454da67e2..be66dd25f 100644 --- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml +++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml @@ -192,9 +192,7 @@ spec: type: string required: - op - - set - subject - - value type: object type: array filter_func: diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go index c3a5e48aa..502e4b05d 100644 --- a/test/e2e/apisix/route.go +++ b/test/e2e/apisix/route.go @@ -43,7 +43,7 @@ metadata: spec: ingressClassName: apisix http: - - name: route0 + - name: rule0 match: hosts: - httpbin @@ -78,5 +78,149 @@ spec: } Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) }) + + It("Test plugins in ApisixRoute", func() { + const apisixRouteSpecPart0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + const apisixRouteSpecPart1 = ` + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Rule: "test-response-rewrite" + X-Global-Test: "enabled" +` + By("apply ApisixRoute without plugins") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("apply ApisixRoute with plugins") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0+apisixRouteSpecPart1) + time.Sleep(5 * time.Second) + + By("verify plugin works") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEqual("test-response-rewrite") + resp.Header("X-Global-Test").IsEqual("enabled") + + By("remove plugin") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + time.Sleep(5 * time.Second) + + By("verify no plugin works") + resp = s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEmpty() + resp.Header("X-Global-Test").IsEmpty() + }) + + It("Test ApisixRoute match by vars", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithHeader("X-Foo", "bar"). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + It("Test ApisixRoute filterFunc", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + filter_func: "function(vars)\n local core = require ('apisix.core')\n local body, err = core.request.get_body()\n if not body then\n return false\n end\n\n local data, err = core.json.decode(body)\n if not data then\n return false\n end\n\n if data['foo'] == 'bar' then\n return true\n end\n\n return false\nend" + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithJSON(map[string]string{"foo": "bar"}). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + PIt("Test ApisixRoute resolveGranularity", func() { + // The `.Spec.HTTP[0].Backends[0].ResolveGranularity` can be "endpoints" or "service", + // when set to "endpoints", the pod ips will be used; or the service ClusterIP or ExternalIP will be used when it set to "service", + + // In the current implementation, pod ips are always used. + // So the case is pending for now. + }) + + PIt("Test ApisixRoute subset", func() { + // route.Spec.HTTP[].Backends[].Subset depends on ApisixUpstream. + // ApisixUpstream is not implemented yet. + // So the case is pending for now + }) + + PIt("Test ApisixRoute reference ApisixUpstream", func() { + // This case depends on ApisixUpstream. + // ApisixUpstream is not implemented yet. + // So the case is pending for now. + }) }) }) From 81d998f00fae43a64165eecc44588335e7ab9c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 16:07:26 +0800 Subject: [PATCH 06/11] typo --- api/v2/apisixglobalrule_types.go | 3 --- internal/controller/apisixroute_controller.go | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/api/v2/apisixglobalrule_types.go b/api/v2/apisixglobalrule_types.go index 99f5474fe..85e1402ac 100644 --- a/api/v2/apisixglobalrule_types.go +++ b/api/v2/apisixglobalrule_types.go @@ -51,8 +51,5 @@ type ApisixGlobalRuleList struct { } func init() { - var a ApisixGlobalRule - a.GetResourceVersion() - a.GetResourceVersion() SchemeBuilder.Register(&ApisixGlobalRule{}, &ApisixGlobalRuleList{}) } diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index 9d8bb4638..e8ec5bd17 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -65,7 +65,7 @@ func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { }), ), ). - Watches(&networkingv1.Ingress{}, + Watches(&networkingv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listApiRouteForIngressClass), builder.WithPredicates( predicate.NewPredicateFuncs(r.matchesIngressController), @@ -138,7 +138,6 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov var ( rules = make(map[string]struct{}) ) - for httpIndex, http := range in.Spec.HTTP { // check rule names if _, ok := rules[http.Name]; ok { @@ -181,7 +180,7 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov } // check vars - // todo: cache the result + // todo: cache the result to tctx if _, err := http.Match.NginxVars.ToVars(); err != nil { return ReasonError{ Reason: string(apiv2.ConditionReasonInvalidSpec), From c66c358caf76bda70e6d45805d67452f8d3484a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Fri, 13 Jun 2025 16:15:47 +0800 Subject: [PATCH 07/11] accept suggestions --- api/v2/apisixroute_types.go | 3 +- docs/crd/api.md | 80 -------------------------- internal/controller/indexer/indexer.go | 2 +- 3 files changed, 2 insertions(+), 83 deletions(-) diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index 6c64661f0..989667425 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -13,7 +13,6 @@ package v2 import ( - "slices" "strings" "github.com/pkg/errors" @@ -290,7 +289,7 @@ func (exprs ApisixRouteHTTPMatchExprs) ToVars() (result adc.Vars, err error) { default: return result, errors.New("unknown operator") } - if invert := slices.Contains([]string{OpNotIn, OpRegexNotMatch, OpRegexNotMatchCaseInsensitive}, op); invert { + if expr.Op == OpNotIn || expr.Op == OpRegexNotMatch || expr.Op == OpRegexNotMatchCaseInsensitive { this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"}) } this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op}) diff --git a/docs/crd/api.md b/docs/crd/api.md index c07e6f257..a337c2db5 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -959,8 +959,6 @@ ApisixPluginConfigSpec defines the desired state of ApisixPluginConfigSpec. _Appears in:_ - [ApisixPluginConfig](#apisixpluginconfig) - - #### ApisixRouteAuthentication @@ -1290,96 +1288,20 @@ _Appears in:_ - [ApisixUpstreamSpec](#apisixupstreamspec) - [PortLevelSettings](#portlevelsettings) -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - -ApisixStatus is the status report for Apisix ingress Resources -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) -#### ApisixStatus -ApisixStatus is the status report for Apisix ingress Resources -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - #### ApisixTlsSpec @@ -1654,8 +1576,6 @@ _Appears in:_ - - #### UpstreamTimeout diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index 50921e3d7..fcc1d068a 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -436,7 +436,7 @@ func ApisixRouteServiceIndexFunc(obj client.Object) (keys []string) { } } for _, stream := range ar.Spec.Stream { - keys = append(keys, stream.Backend.ServiceName) + keys = append(keys, GenIndexKey(ar.GetNamespace(), stream.Backend.ServiceName)) } return } From 91d18b6b2f91026411813ba4242690491e6566f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Sun, 15 Jun 2025 18:50:52 +0800 Subject: [PATCH 08/11] more tests --- internal/controller/apisixroute_controller.go | 9 ++-- internal/provider/adc/adc.go | 7 +-- .../provider/adc/translator/apisixroute.go | 3 +- test/e2e/apisix/route.go | 52 +++++++++++-------- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index e8ec5bd17..ffde4f11a 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -22,7 +22,6 @@ import ( corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -124,7 +123,7 @@ func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) } if err = r.Provider.Update(ctx, tctx, &ar); err != nil { err = ReasonError{ - Reason: "SyncFailed", + Reason: string(apiv2.ConditionReasonSyncFailed), Message: err.Error(), } r.Log.Error(err, "failed to process", "apisixroute", ar) @@ -382,10 +381,10 @@ func (r *ApisixRouteReconciler) getDefaultIngressClass() (*networkingv1.IngressC return &ic, nil } } - return nil, &errors.StatusError{ErrStatus: metav1.Status{ - Reason: metav1.StatusReasonNotFound, + return nil, ReasonError{ + Reason: string(metav1.StatusReasonNotFound), Message: "default ingress class not found or dose not match the controller", - }} + } } // processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 64ec21e47..ca80f06e8 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -201,7 +201,7 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { var resourceTypes []string var labels map[string]string switch obj.(type) { - case *gatewayv1.HTTPRoute: + case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute: resourceTypes = append(resourceTypes, "service") labels = label.GenLabel(obj) case *gatewayv1.Gateway: @@ -213,10 +213,7 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { resourceTypes = append(resourceTypes, "consumer") labels = label.GenLabel(obj) case *networkingv1.IngressClass: - // delete all resources - case *apiv2.ApisixRoute: - resourceTypes = append(resourceTypes, "service") - labels = label.GenLabel(obj) + // delete all resources case *apiv2.ApisixGlobalRule: resourceTypes = append(resourceTypes, "global_rule") labels = label.GenLabel(obj) diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go index ec9eada3e..1ec74622f 100644 --- a/internal/provider/adc/translator/apisixroute.go +++ b/internal/provider/adc/translator/apisixroute.go @@ -44,7 +44,6 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a } var plugins = make(adc.Plugins) - // todo: need unit test or e2e test for _, plugin := range http.Plugins { if !plugin.Enable { continue @@ -143,7 +142,7 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a // translate to adc.Service service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, fmt.Sprintf("%d", ruleIndex)) service.ID = id.GenID(service.Name) - service.Labels = ar.Labels + service.Labels = label.GenLabel(ar) service.Hosts = http.Match.Hosts service.Upstream = upstream service.Routes = []*adc.Route{route} diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go index 502e4b05d..1d09048a8 100644 --- a/test/e2e/apisix/route.go +++ b/test/e2e/apisix/route.go @@ -35,7 +35,21 @@ var _ = Describe("Test ApisixRoute", func() { ) Context("Test ApisixRoute", func() { - const apisixRouteSpec = ` + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("Basic tests", func() { + const apisixRouteSpec = ` apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: @@ -48,35 +62,31 @@ spec: hosts: - httpbin paths: - - /get + - %s backends: - serviceName: httpbin-service-e2e-test servicePort: 80 ` + request := func(path string) int { + return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode + } - BeforeEach(func() { - By("create GatewayProxy") - gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) - err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") - Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") - time.Sleep(5 * time.Second) - - By("create IngressClass") - err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") - Expect(err).NotTo(HaveOccurred(), "creating IngressClass") - time.Sleep(5 * time.Second) - }) - - It("Basic tests", func() { By("apply ApisixRoute") var apisixRoute apiv2.ApisixRoute - applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get")) By("verify ApisixRoute works") - request := func() int { - return s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw().StatusCode - } - Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + 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) + + By("delete ApisixRoute") + err := s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) }) It("Test plugins in ApisixRoute", func() { From f5b474a13dc67a6e912cca02d572eaf23343460c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Mon, 16 Jun 2025 10:40:26 +0800 Subject: [PATCH 09/11] resolve comments --- .../provider/adc/translator/apisixroute.go | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go index 1ec74622f..ca1d2a4fa 100644 --- a/internal/provider/adc/translator/apisixroute.go +++ b/internal/provider/adc/translator/apisixroute.go @@ -32,19 +32,19 @@ import ( func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *apiv2.ApisixRoute) (result *TranslateResult, err error) { result = &TranslateResult{} - for ruleIndex, http := range ar.Spec.HTTP { + for ruleIndex, rule := range ar.Spec.HTTP { var timeout *adc.Timeout - if http.Timeout != nil { + if rule.Timeout != nil { defaultTimeout := metav1.Duration{Duration: apiv2.DefaultUpstreamTimeout} timeout = &adc.Timeout{ - Connect: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), - Read: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), - Send: cmp.Or(int(http.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + Connect: cmp.Or(int(rule.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + Read: cmp.Or(int(rule.Timeout.Read.Seconds()), int(defaultTimeout.Seconds())), + Send: cmp.Or(int(rule.Timeout.Send.Seconds()), int(defaultTimeout.Seconds())), } } var plugins = make(adc.Plugins) - for _, plugin := range http.Plugins { + for _, plugin := range rule.Plugins { if !plugin.Enable { continue } @@ -66,26 +66,26 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a } // add Authentication plugins - if http.Authentication.Enable { - switch http.Authentication.Type { + if rule.Authentication.Enable { + switch rule.Authentication.Type { case "keyAuth": - plugins["key-auth"] = http.Authentication.KeyAuth + plugins["key-auth"] = rule.Authentication.KeyAuth case "basicAuth": plugins["basic-auth"] = make(map[string]any) case "wolfRBAC": plugins["wolf-rbac"] = make(map[string]any) case "jwtAuth": - plugins["jwt-auth"] = http.Authentication.JwtAuth + plugins["jwt-auth"] = rule.Authentication.JwtAuth case "hmacAuth": plugins["hmac-auth"] = make(map[string]any) case "ldapAuth": - plugins["ldap-auth"] = http.Authentication.LDAPAuth + plugins["ldap-auth"] = rule.Authentication.LDAPAuth default: plugins["basic-auth"] = make(map[string]any) } } - vars, err := http.Match.NginxVars.ToVars() + vars, err := rule.Match.NginxVars.ToVars() if err != nil { return nil, err } @@ -97,24 +97,24 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a labels = label.GenLabel(ar) ) // translate to adc.Route - route.Name = adc.ComposeRouteName(ar.Namespace, ar.Name, http.Name) + route.Name = adc.ComposeRouteName(ar.Namespace, ar.Name, rule.Name) route.ID = id.GenID(route.Name) route.Desc = "Created by apisix-ingress-controller, DO NOT modify it manually" route.Labels = labels route.EnableWebsocket = ptr.To(true) - route.FilterFunc = http.Match.FilterFunc - route.Hosts = http.Match.Hosts - route.Methods = http.Match.Methods + route.FilterFunc = rule.Match.FilterFunc + route.Hosts = rule.Match.Hosts + route.Methods = rule.Match.Methods route.Plugins = plugins - route.Priority = ptr.To(int64(http.Priority)) - route.RemoteAddrs = http.Match.RemoteAddrs + route.Priority = ptr.To(int64(rule.Priority)) + route.RemoteAddrs = rule.Match.RemoteAddrs route.Timeout = timeout - route.Uris = http.Match.Paths + route.Uris = rule.Match.Paths route.Vars = vars // translate to adc.Upstream var backendErr error - for _, backend := range http.Backends { + for _, backend := range rule.Backends { weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight))) backendRef := gatewayv1.BackendRef{ BackendObjectReference: gatewayv1.BackendObjectReference{ @@ -135,7 +135,7 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a upstream.Nodes = append(upstream.Nodes, upNodes...) } //nolint:staticcheck - if len(http.Backends) == 0 && len(http.Upstreams) > 0 { + if len(rule.Backends) == 0 && len(rule.Upstreams) > 0 { // FIXME: when the API ApisixUpstream is supported } @@ -143,7 +143,7 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, fmt.Sprintf("%d", ruleIndex)) service.ID = id.GenID(service.Name) service.Labels = label.GenLabel(ar) - service.Hosts = http.Match.Hosts + service.Hosts = rule.Match.Hosts service.Upstream = upstream service.Routes = []*adc.Route{route} From bd0cdc2d98c5d589aa278ce0110685e9b797a10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Mon, 16 Jun 2025 11:17:43 +0800 Subject: [PATCH 10/11] resolve comments --- api/v2/status.go | 20 ------------------- internal/controller/apisixroute_controller.go | 11 ++-------- test/e2e/framework/assertion.go | 19 +++++++++++++----- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/api/v2/status.go b/api/v2/status.go index bc1709639..45782a21a 100644 --- a/api/v2/status.go +++ b/api/v2/status.go @@ -14,29 +14,9 @@ package v2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) // ApisixStatus is the status report for Apisix ingress Resources type ApisixStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty" yaml:"conditions,omitempty"` } - -func GetStatus(object client.Object) ApisixStatus { - switch t := object.(type) { - case *ApisixConsumer: - return t.Status - case *ApisixGlobalRule: - return t.Status - case *ApisixPluginConfig: - return t.Status - case *ApisixRoute: - return t.Status - case *ApisixTls: - return t.Status - case *ApisixUpstream: - return t.Status - default: - return ApisixStatus{} - } -} diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index ffde4f11a..261d7ee04 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -149,16 +149,9 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov // check secret for _, plugin := range http.Plugins { - if !plugin.Enable { + if !plugin.Enable || plugin.Config == nil || plugin.SecretRef == "" { continue } - if plugin.Config == nil { - continue - } - if plugin.SecretRef == "" { - continue - } - var ( secret = corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -455,7 +448,7 @@ func (r *ApisixRouteReconciler) updateStatus(ar *apiv2.ApisixRoute, err error) { SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) r.Updater.Update(status.Update{ NamespacedName: utils.NamespacedName(ar), - Resource: ar.DeepCopy(), + Resource: &apiv2.ApisixRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { cp := obj.(*apiv2.ApisixRoute).DeepCopy() cp.Status = ar.Status diff --git a/test/e2e/framework/assertion.go b/test/e2e/framework/assertion.go index b1bcfd665..286dcb99d 100644 --- a/test/e2e/framework/assertion.go +++ b/test/e2e/framework/assertion.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "log" + "reflect" "slices" "strings" "time" @@ -97,13 +98,21 @@ func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration } func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) { - err := PollUntilAPIv2MustHaveStatus(cli, timeout, nn, obj, func(object client.Object) bool { - conditions := apiv2.GetStatus(object).Conditions - if err := kubernetes.ConditionsHaveLatestObservedGeneration(object, conditions); err != nil { + f := func(object client.Object) bool { + if !apiv2.Is(object) { return false } - return findConditionInList(conditions, cond) - }) + value := reflect.Indirect(reflect.ValueOf(object)) + status, ok := value.FieldByName("Status").Interface().(apiv2.ApisixStatus) + if !ok { + return false + } + if err := kubernetes.ConditionsHaveLatestObservedGeneration(object, status.Conditions); err != nil { + return false + } + return findConditionInList(status.Conditions, cond) + } + err := PollUntilAPIv2MustHaveStatus(cli, timeout, nn, obj, f) require.NoError(t, err, "error waiting status to have a Condition matching %+v", nn, cond) } From b1bf04dbeb9e9e3626e247b9f46ecce145c7cf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=82=9F=E7=A9=BA?= Date: Mon, 16 Jun 2025 15:23:05 +0800 Subject: [PATCH 11/11] more timeout --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 74b77c438..3349e2107 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ GATEAY_API_VERSION ?= v1.2.0 DASHBOARD_VERSION ?= dev ADC_VERSION ?= 0.19.0 -TEST_TIMEOUT ?= 60m +TEST_TIMEOUT ?= 80m TEST_DIR ?= ./test/e2e/ # CRD Reference Documentation