diff --git a/Makefile b/Makefile index 3dada9b39..da8a844b3 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ KIND_NAME ?= api7-ingress-cluster GATEAY_API_VERSION ?= v1.2.0 DASHBOARD_VERSION ?= dev -TEST_TIMEOUT ?= 30m +TEST_TIMEOUT ?= 45m export KUBECONFIG = /tmp/$(KIND_NAME).kubeconfig diff --git a/api/v1alpha1/httproutepolicy_types.go b/api/v1alpha1/httproutepolicy_types.go index d2fd74a3d..e307b4f31 100644 --- a/api/v1alpha1/httproutepolicy_types.go +++ b/api/v1alpha1/httproutepolicy_types.go @@ -27,10 +27,6 @@ type HTTPRoutePolicySpec struct { // TargetRef identifies an API object (enum: HTTPRoute, Ingress) to apply HTTPRoutePolicy to. // // target references. - // +listType=map - // +listMapKey=group - // +listMapKey=kind - // +listMapKey=name // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=16 TargetRefs []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRefs"` @@ -47,8 +43,8 @@ type HTTPRoutePolicy struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec HTTPRoutePolicySpec `json:"spec,omitempty"` - Status gatewayv1alpha2.PolicyStatus `json:"status,omitempty"` + Spec HTTPRoutePolicySpec `json:"spec,omitempty"` + Status PolicyStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/charts/values.yaml b/charts/values.yaml index 317896489..6b7122770 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -2,7 +2,7 @@ nameOverride: "" labelsOverride: {} annotations: {} podAnnotations: {} -controllerName: gateway.api7.io/api7-ingress-controller +controllerName: gateway.apisix.io/api7-ingress-controller replicas: 1 admin: key: '' # Pass the admin key generated for the ingress gateway group diff --git a/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml b/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml index f9afb85c8..41fbba562 100644 --- a/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml +++ b/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml @@ -104,11 +104,6 @@ spec: maxItems: 16 minItems: 1 type: array - x-kubernetes-list-map-keys: - - group - - kind - - name - x-kubernetes-list-type: map vars: items: x-kubernetes-preserve-unknown-fields: true diff --git a/config/samples/config.yaml b/config/samples/config.yaml index 1858b3660..8485cd3c8 100644 --- a/config/samples/config.yaml +++ b/config/samples/config.yaml @@ -1,7 +1,7 @@ log_level: "info" # The log level of the API7 Ingress Controller. # the default value is "info". -controller_name: gateway.api7.io/api7-ingress-controller # The controller name of the API7 Ingress Controller, +controller_name: gateway.apisix.io/api7-ingress-controller # The controller name of the API7 Ingress Controller, # which is used to identify the controller in the GatewayClass. # The default value is "gateway.api7.io/api7-ingress-controller". leader_election_id: "api7-ingress-controller-leader" # The leader election ID for the API7 Ingress Controller. diff --git a/docs/configure.md b/docs/configure.md index 80862e669..ed9fda2c5 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -8,7 +8,7 @@ The API7 Ingress Controller is a Kubernetes Ingress Controller that implements t log_level: "info" # The log level of the API7 Ingress Controller. # the default value is "info". -controller_name: gateway.api7.io/api7-ingress-controller # The controller name of the API7 Ingress Controller, +controller_name: gateway.apisix.io/api7-ingress-controller # The controller name of the API7 Ingress Controller, # which is used to identify the controller in the GatewayClass. # The default value is "gateway.api7.io/api7-ingress-controller". diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index 361735c45..bf390f0b0 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/go-logr/logr" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/api7/api7-ingress-controller/api/v1alpha1" "github.com/api7/api7-ingress-controller/internal/controller/indexer" @@ -118,6 +120,23 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { }, ), ). + Watches(&v1alpha1.HTTPRoutePolicy{}, + handler.EnqueueRequestsFromMapFunc(r.listHTTPRouteByHTTPRoutePolicy), + builder.WithPredicates( + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + UpdateFunc: r.httpRoutePolicyPredicateOnUpdate, + GenericFunc: func(e event.GenericEvent) bool { + return false + }, + }, + ), + ). WatchesRawSource( source.Channel( r.genericEvent, @@ -127,43 +146,6 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *HTTPRouteReconciler) listHTTPRouteForGenericEvent(ctx context.Context, obj client.Object) []reconcile.Request { - var namespacedNameMap = make(map[types.NamespacedName]struct{}) - requests := []reconcile.Request{} - switch v := obj.(type) { - case *v1alpha1.BackendTrafficPolicy: - httprouteAll := []gatewayv1.HTTPRoute{} - for _, ref := range v.Spec.TargetRefs { - httprouteList := &gatewayv1.HTTPRouteList{} - if err := r.List(ctx, httprouteList, client.MatchingFields{ - indexer.ServiceIndexRef: indexer.GenIndexKey(v.GetNamespace(), string(ref.Name)), - }); err != nil { - r.Log.Error(err, "failed to list HTTPRoutes for BackendTrafficPolicy", "namespace", v.GetNamespace(), "ref", ref.Name) - return nil - } - httprouteAll = append(httprouteAll, httprouteList.Items...) - } - for _, hr := range httprouteAll { - key := types.NamespacedName{ - Namespace: hr.Namespace, - Name: hr.Name, - } - if _, ok := namespacedNameMap[key]; !ok { - namespacedNameMap[key] = struct{}{} - requests = append(requests, reconcile.Request{ - NamespacedName: client.ObjectKey{ - Namespace: hr.Namespace, - Name: hr.Name, - }, - }) - } - } - default: - r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to BackendTrafficPolicy") - } - return requests -} - func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { hr := new(gatewayv1.HTTPRoute) if err := r.Get(ctx, req.NamespacedName, hr); err != nil { @@ -228,6 +210,11 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( acceptStatus.msg = err.Error() } + if err := r.processHTTPRoutePolicies(tctx, hr); err != nil { + acceptStatus.status = false + acceptStatus.msg = err.Error() + } + if err := r.processHTTPRouteBackendRefs(tctx); err != nil { resolveRefStatus = status{ status: false, @@ -392,6 +379,105 @@ func (r *HTTPRouteReconciler) listHTTPRoutesForGateway(ctx context.Context, obj return requests } +func (r *HTTPRouteReconciler) listHTTPRouteByHTTPRoutePolicy(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + httpRoutePolicy, ok := obj.(*v1alpha1.HTTPRoutePolicy) + if !ok { + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to HTTPRoutePolicy") + return nil + } + + var keys = make(map[types.NamespacedName]struct{}) + for _, ref := range httpRoutePolicy.Spec.TargetRefs { + if ref.Kind != "HTTPRoute" { + continue + } + key := types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: string(ref.Name), + } + if _, ok := keys[key]; ok { + continue + } + + var httpRoute gatewayv1.HTTPRoute + if err := r.Get(ctx, client.ObjectKey{Namespace: key.Namespace, Name: key.Name}, &httpRoute); err != nil { + r.Log.Error(err, "failed to get HTTPRoute by HTTPRoutePolicy targetRef", "namespace", key.Namespace, "name", key.Name) + continue + } + if ref.SectionName != nil { + var matchRuleName bool + for _, rule := range httpRoute.Spec.Rules { + if rule.Name != nil && *rule.Name == *ref.SectionName { + matchRuleName = true + break + } + } + if !matchRuleName { + r.Log.Error(errors.Errorf("failed to get HTTPRoute rule by HTTPRoutePolicy targetRef"), "namespace", key.Namespace, "name", key.Name, "sectionName", *ref.SectionName) + continue + } + } + keys[key] = struct{}{} + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: key.Namespace, + Name: key.Name, + }, + }) + } + + return requests +} + +func (r *HTTPRouteReconciler) listHTTPRouteForGenericEvent(ctx context.Context, obj client.Object) (requests []reconcile.Request) { + var namespacedNameMap = make(map[types.NamespacedName]struct{}) + + switch v := obj.(type) { + case *v1alpha1.BackendTrafficPolicy: + httprouteAll := []gatewayv1.HTTPRoute{} + for _, ref := range v.Spec.TargetRefs { + httprouteList := &gatewayv1.HTTPRouteList{} + if err := r.List(ctx, httprouteList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(v.GetNamespace(), string(ref.Name)), + }); err != nil { + r.Log.Error(err, "failed to list HTTPRoutes for BackendTrafficPolicy", "namespace", v.GetNamespace(), "ref", ref.Name) + return nil + } + httprouteAll = append(httprouteAll, httprouteList.Items...) + } + for _, hr := range httprouteAll { + key := types.NamespacedName{ + Namespace: hr.Namespace, + Name: hr.Name, + } + if _, ok := namespacedNameMap[key]; !ok { + namespacedNameMap[key] = struct{}{} + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Namespace: hr.Namespace, + Name: hr.Name, + }, + }) + } + } + case *v1alpha1.HTTPRoutePolicy: + for _, ref := range v.Spec.TargetRefs { + namespacedName := types.NamespacedName{Namespace: v.GetNamespace(), Name: string(ref.Name)} + if _, ok := namespacedNameMap[namespacedName]; !ok { + namespacedNameMap[namespacedName] = struct{}{} + if err := r.Get(ctx, namespacedName, new(gatewayv1.HTTPRoute)); err != nil { + r.Log.Info("failed to Get HTTPRoute", "namespace", namespacedName.Namespace, "name", namespacedName.Name) + continue + } + requests = append(requests, reconcile.Request{NamespacedName: namespacedName}) + } + } + default: + r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to BackendTrafficPolicy") + } + return requests +} + func (r *HTTPRouteReconciler) processHTTPRouteBackendRefs(tctx *provider.TranslateContext) error { var terr error for _, backend := range tctx.BackendRefs { @@ -454,7 +540,7 @@ func (r *HTTPRouteReconciler) processHTTPRouteBackendRefs(tctx *provider.Transla return terr } -func (t *HTTPRouteReconciler) processHTTPRoute(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { +func (r *HTTPRouteReconciler) processHTTPRoute(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { var terror error for _, rule := range httpRoute.Spec.Rules { for _, filter := range rule.Filters { @@ -463,7 +549,7 @@ func (t *HTTPRouteReconciler) processHTTPRoute(tctx *provider.TranslateContext, } if filter.ExtensionRef.Kind == "PluginConfig" { pluginconfig := new(v1alpha1.PluginConfig) - if err := t.Get(context.Background(), client.ObjectKey{ + if err := r.Get(context.Background(), client.ObjectKey{ Namespace: httpRoute.GetNamespace(), Name: string(filter.ExtensionRef.Name), }, pluginconfig); err != nil { @@ -508,3 +594,29 @@ func (t *HTTPRouteReconciler) processHTTPRoute(tctx *provider.TranslateContext, return terror } + +func (r *HTTPRouteReconciler) httpRoutePolicyPredicateOnUpdate(e event.UpdateEvent) bool { + oldPolicy, ok0 := e.ObjectOld.(*v1alpha1.HTTPRoutePolicy) + newPolicy, ok1 := e.ObjectNew.(*v1alpha1.HTTPRoutePolicy) + if !ok0 || !ok1 { + return false + } + var discardsRefs = make(map[string]v1alpha2.LocalPolicyTargetReferenceWithSectionName) + for _, ref := range oldPolicy.Spec.TargetRefs { + key := indexer.GenHTTPRoutePolicyIndexKey(string(ref.Group), string(ref.Kind), e.ObjectOld.GetNamespace(), string(ref.Name), "") + discardsRefs[key] = ref + } + for _, ref := range newPolicy.Spec.TargetRefs { + key := indexer.GenHTTPRoutePolicyIndexKey(string(ref.Group), string(ref.Kind), e.ObjectOld.GetNamespace(), string(ref.Name), "") + delete(discardsRefs, key) + } + if len(discardsRefs) > 0 { + dump := oldPolicy.DeepCopy() + dump.Spec.TargetRefs = make([]v1alpha2.LocalPolicyTargetReferenceWithSectionName, 0, len(discardsRefs)) + for _, ref := range discardsRefs { + dump.Spec.TargetRefs = append(dump.Spec.TargetRefs, ref) + } + r.genericEvent <- event.GenericEvent{Object: dump} + } + return true +} diff --git a/internal/controller/httproutepolicy.go b/internal/controller/httproutepolicy.go new file mode 100644 index 000000000..9ca9bac14 --- /dev/null +++ b/internal/controller/httproutepolicy.go @@ -0,0 +1,147 @@ +package controller + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/api7/api7-ingress-controller/api/v1alpha1" + "github.com/api7/api7-ingress-controller/internal/controller/indexer" + "github.com/api7/api7-ingress-controller/internal/provider" +) + +func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { + // list HTTPRoutePolices which sectionName is not specified + var ( + checker = conflictChecker{ + httpRoute: httpRoute, + policies: make(map[targetRefKey][]v1alpha1.HTTPRoutePolicy), + } + listForAllRules v1alpha1.HTTPRoutePolicyList + key = indexer.GenHTTPRoutePolicyIndexKey(gatewayv1.GroupName, "HTTPRoute", httpRoute.GetNamespace(), httpRoute.GetName(), "") + ) + if err := r.List(context.Background(), &listForAllRules, client.MatchingFields{indexer.PolicyTargetRefs: key}); err != nil { + return err + } + + for _, item := range listForAllRules.Items { + checker.append("", item) + tctx.HTTPRoutePolicies["*"] = append(tctx.HTTPRoutePolicies["*"], item) + } + + for _, rule := range httpRoute.Spec.Rules { + if rule.Name == nil { + continue + } + + var ( + ruleName = string(*rule.Name) + listForSectionRules v1alpha1.HTTPRoutePolicyList + key = indexer.GenHTTPRoutePolicyIndexKey(gatewayv1.GroupName, "HTTPRoute", httpRoute.GetNamespace(), httpRoute.GetName(), ruleName) + ) + if err := r.List(context.Background(), &listForSectionRules, client.MatchingFields{indexer.PolicyTargetRefs: key}); err != nil { + continue + } + for _, item := range listForSectionRules.Items { + checker.append(ruleName, item) + tctx.HTTPRoutePolicies[ruleName] = append(tctx.HTTPRoutePolicies[ruleName], item) + } + } + + // todo: unreachable + // if the HTTPRoute is deleted, clear tctx.HTTPRoutePolicies and delete Ancestors from HTTPRoutePolicies status + // if !httpRoute.GetDeletionTimestamp().IsZero() { + // for _, policies := range checker.policies { + // for i := range policies { + // policy := policies[i] + // _ = DeleteAncestors(&policy.Status, httpRoute.Spec.ParentRefs) + // data, _ := json.Marshal(policy.Status) + // r.Log.Info("policy status after delete ancestor", "data", string(data)) + // if err := r.Status().Update(context.Background(), &policy); err != nil { + // r.Log.Error(err, "failed to Update policy status") + // } + // // tctx.StatusUpdaters = append(tctx.StatusUpdaters, &policy) + // } + // } + // return nil + // } + + var ( + status = true + reason = string(v1alpha2.PolicyReasonAccepted) + message string + ) + if checker.conflict { + status = false + reason = string(v1alpha2.PolicyReasonConflicted) + message = "HTTPRoutePolicy conflict with others target to the HTTPRoute" + + // clear HTTPRoutePolices from TranslateContext + tctx.HTTPRoutePolicies = make(map[string][]v1alpha1.HTTPRoutePolicy) + } + + for _, policies := range checker.policies { + for i := range policies { + policy := policies[i] + r.modifyHTTPRoutePolicyStatus(httpRoute, &policy, status, reason, message) + tctx.StatusUpdaters = append(tctx.StatusUpdaters, &policy) + } + } + + return nil +} + +func (r *HTTPRouteReconciler) modifyHTTPRoutePolicyStatus(httpRoute *gatewayv1.HTTPRoute, policy *v1alpha1.HTTPRoutePolicy, status bool, reason, message string) { + condition := metav1.Condition{ + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: policy.GetGeneration(), + LastTransitionTime: metav1.Time{Time: time.Now()}, + Reason: reason, + Message: message, + } + if !status { + condition.Status = metav1.ConditionFalse + } + _ = SetAncestors(&policy.Status, httpRoute.Spec.ParentRefs, condition) +} + +type conflictChecker struct { + httpRoute *gatewayv1.HTTPRoute + policies map[targetRefKey][]v1alpha1.HTTPRoutePolicy + conflict bool +} + +type targetRefKey struct { + Group gatewayv1.Group + Namespace gatewayv1.Namespace + Name gatewayv1.ObjectName + SectionName gatewayv1.SectionName +} + +func (c *conflictChecker) append(sectionName string, policy v1alpha1.HTTPRoutePolicy) { + key := targetRefKey{ + Group: gatewayv1.GroupName, + Namespace: gatewayv1.Namespace(c.httpRoute.GetNamespace()), + Name: gatewayv1.ObjectName(c.httpRoute.GetName()), + SectionName: gatewayv1.SectionName(sectionName), + } + c.policies[key] = append(c.policies[key], policy) + + if !c.conflict { + Loop: + for _, items := range c.policies { + for _, item := range items { + if !ptr.Equal(item.Spec.Priority, policy.Spec.Priority) { + c.conflict = true + break Loop + } + } + } + } +} diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index f245e2374..7a606b274 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -3,13 +3,14 @@ package indexer import ( "context" - "github.com/api7/api7-ingress-controller/api/v1alpha1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/api7/api7-ingress-controller/api/v1alpha1" ) const ( @@ -134,6 +135,16 @@ func setupHTTPRouteIndexer(mgr ctrl.Manager) error { ); err != nil { return err } + + if err := mgr.GetFieldIndexer().IndexField( + context.Background(), + &v1alpha1.HTTPRoutePolicy{}, + PolicyTargetRefs, + HTTPRoutePolicyIndexFunc, + ); err != nil { + return err + } + return nil } @@ -311,6 +322,23 @@ func HTTPRouteExtensionIndexFunc(rawObj client.Object) []string { return keys } +func GenHTTPRoutePolicyIndexKey(group, kind, namespace, name, sectionName string) string { + return schema.GroupKind{Group: group, Kind: kind}.String() + "/" + client.ObjectKey{Namespace: namespace, Name: name}.String() + "/" + sectionName +} + +func HTTPRoutePolicyIndexFunc(rawObj client.Object) []string { + hrp := rawObj.(*v1alpha1.HTTPRoutePolicy) + var keys = make([]string, 0, len(hrp.Spec.TargetRefs)) + for _, ref := range hrp.Spec.TargetRefs { + var sectionName string + if ref.SectionName != nil { + sectionName = string(*ref.SectionName) + } + keys = append(keys, GenHTTPRoutePolicyIndexKey(string(ref.Group), string(ref.Kind), hrp.GetNamespace(), string(ref.Name), sectionName)) + } + return keys +} + func GatewayParametersRefIndexFunc(rawObj client.Object) []string { gw := rawObj.(*gatewayv1.Gateway) if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.ParametersRef != nil { diff --git a/internal/controller/policies.go b/internal/controller/policies.go index 0ac10bfc9..ac6de8317 100644 --- a/internal/controller/policies.go +++ b/internal/controller/policies.go @@ -2,21 +2,23 @@ package controller import ( "fmt" + "slices" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "github.com/api7/api7-ingress-controller/api/v1alpha1" - "github.com/api7/api7-ingress-controller/internal/controller/config" - "github.com/api7/api7-ingress-controller/internal/controller/indexer" - "github.com/api7/api7-ingress-controller/internal/provider" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/api7/api7-ingress-controller/api/v1alpha1" + "github.com/api7/api7-ingress-controller/internal/controller/config" + "github.com/api7/api7-ingress-controller/internal/controller/indexer" + "github.com/api7/api7-ingress-controller/internal/provider" ) type PolicyTargetKey struct { @@ -144,3 +146,20 @@ func SetAncestorStatus(status *v1alpha1.PolicyStatus, ancestorStatus gatewayv1al status.Ancestors = append(status.Ancestors, ancestorStatus) return true } + +func DeleteAncestors(status *v1alpha1.PolicyStatus, parentRefs []gatewayv1.ParentReference) bool { + var length = len(status.Ancestors) + for _, parentRef := range parentRefs { + status.Ancestors = slices.DeleteFunc(status.Ancestors, func(status gatewayv1alpha2.PolicyAncestorStatus) bool { + return parentRefValueEqual(parentRef, status.AncestorRef) + }) + } + return length != len(status.Ancestors) +} + +func parentRefValueEqual(a, b gatewayv1.ParentReference) bool { + return ptr.Equal(a.Group, b.Group) && + ptr.Equal(a.Kind, b.Kind) && + ptr.Equal(a.Namespace, b.Namespace) && + a.Name == b.Name +} diff --git a/internal/provider/adc/translator/httproute.go b/internal/provider/adc/translator/httproute.go index 812f908ef..688f52e18 100644 --- a/internal/provider/adc/translator/httproute.go +++ b/internal/provider/adc/translator/httproute.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap" discoveryv1 "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/api7/gopkg/pkg/log" @@ -230,6 +231,27 @@ func (t *Translator) fillPluginFromHTTPRequestRedirectFilter(plugins adctypes.Pl plugin.URI = uri } +func (t *Translator) fillHTTPRoutePolicies(tctx *provider.TranslateContext, rule gatewayv1.HTTPRouteRule, routes []*adctypes.Route) { + policies := tctx.HTTPRoutePolicies["*"] // policies which not specify a sectionName + if rule.Name != nil { + policies = append(policies, tctx.HTTPRoutePolicies[string(*rule.Name)]...) // append policies which specify the sectionName as the same as rule.name + } + + for _, policy := range policies { + for _, route := range routes { + route.Priority = policy.Spec.Priority + for _, data := range policy.Spec.Vars { + var v []adctypes.StringOrSlice + if err := json.Unmarshal(data.Raw, &v); err != nil { + log.Errorf("failed to unmarshal spec.Vars item to []StringOrSlice, data: %s", string(data.Raw)) // todo: update status + continue + } + route.Vars = append(route.Vars, v) + } + } + } +} + func (t *Translator) translateEndpointSlice(weight int, endpointSlices []discoveryv1.EndpointSlice) adctypes.UpstreamNodes { var nodes adctypes.UpstreamNodes if len(endpointSlices) == 0 { @@ -327,8 +349,10 @@ func (t *Translator) TranslateHTTPRoute(tctx *provider.TranslateContext, httpRou route.Name = name route.ID = id.GenID(name) route.Labels = labels + route.EnableWebsocket = ptr.To(true) routes = append(routes, route) } + t.fillHTTPRoutePolicies(tctx, rule, routes) service.Routes = routes result.Services = append(result.Services, service) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4c1a54f9a..cc0a362ff 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,9 +7,10 @@ import ( discoveryv1 "k8s.io/api/discovery/v1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - "github.com/api7/api7-ingress-controller/api/v1alpha1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/api7/api7-ingress-controller/api/v1alpha1" ) type Provider interface { @@ -37,6 +38,7 @@ type TranslateContext struct { BackendTrafficPolicies map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy GatewayProxies map[ResourceKind]v1alpha1.GatewayProxy ResourceParentRefs map[ResourceKind][]ResourceKind + HTTPRoutePolicies map[string][]v1alpha1.HTTPRoutePolicy StatusUpdaters []client.Object } @@ -51,5 +53,6 @@ func NewDefaultTranslateContext(ctx context.Context) *TranslateContext { BackendTrafficPolicies: make(map[types.NamespacedName]*v1alpha1.BackendTrafficPolicy), GatewayProxies: make(map[ResourceKind]v1alpha1.GatewayProxy), ResourceParentRefs: make(map[ResourceKind][]ResourceKind), + HTTPRoutePolicies: make(map[string][]v1alpha1.HTTPRoutePolicy), } } diff --git a/test/e2e/framework/manifests/ingress.yaml b/test/e2e/framework/manifests/ingress.yaml index 0e8f4d7e3..61699ae4b 100644 --- a/test/e2e/framework/manifests/ingress.yaml +++ b/test/e2e/framework/manifests/ingress.yaml @@ -144,6 +144,21 @@ rules: - get - list - watch +- apiGroups: + - gateway.apisix.io + resources: + - httproutepolicies + verbs: + - get + - list + - watch +- apiGroups: + - gateway.apisix.io + resources: + - httproutepolicies/status + verbs: + - get + - update - apiGroups: - gateway.apisix.io resources: diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go index 2adb0d465..4b6fd0b17 100644 --- a/test/e2e/gatewayapi/httproute.go +++ b/test/e2e/gatewayapi/httproute.go @@ -6,8 +6,10 @@ import ( "strings" "time" + "github.com/gruntwork-io/terratest/modules/retry" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/pkg/errors" "github.com/api7/api7-ingress-controller/test/e2e/framework" "github.com/api7/api7-ingress-controller/test/e2e/scaffold" @@ -475,10 +477,35 @@ spec: - type: Exact name: X-Route-Name value: httpbin + # name: get backendRefs: - name: httpbin-service-e2e-test port: 80 ` + const httpRoutePolicy = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: HTTPRoutePolicy +metadata: + name: http-route-policy-0 +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin + # sectionName: get + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin-1 + sectionName: get + priority: 10 + vars: + - - http_x_hrp_name + - == + - http-route-policy-0 + - - arg_hrp_name + - == + - http-route-policy-0 +` var prefixRouteByStatus = ` apiVersion: gateway.networking.k8s.io/v1 @@ -604,6 +631,226 @@ spec: Expect(). Status(http.StatusOK) }) + + It("HTTPRoutePolicy in effect", func() { + By("create HTTPRoute") + ResourceApplied("HTTPRoute", "httpbin", varsRoute, 1) + + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + Expect(). + Status(http.StatusOK) + + By("create HTTPRoutePolicy") + ResourceApplied("HTTPRoutePolicy", "http-route-policy-0", httpRoutePolicy, 1) + + By("access dataplane to check the HTTPRoutePolicy") + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + Expect(). + Status(http.StatusNotFound) + + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + WithHeader("X-HRP-Name", "http-route-policy-0"). + WithQuery("hrp_name", "http-route-policy-0"). + Expect(). + Status(http.StatusOK) + + By("update HTTPRoutePolicy") + const changedHTTPRoutePolicy = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: HTTPRoutePolicy +metadata: + name: http-route-policy-0 +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin + # sectionName: get + priority: 10 + vars: + - - http_x_hrp_name + - == + - new-hrp-name +` + ResourceApplied("HTTPRoutePolicy", "http-route-policy-0", changedHTTPRoutePolicy, 1) + // use the old vars cannot match any route + Eventually(func() int { + return s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + WithHeader("X-HRP-Name", "http-route-policy-0"). + WithQuery("hrp_name", "http-route-policy-0"). + Expect().Raw().StatusCode + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + + // use the new vars can match the route + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + WithHeader("X-HRP-Name", "new-hrp-name"). + Expect(). + Status(http.StatusOK) + + By("delete the HTTPRoutePolicy") + err := s.DeleteResource("HTTPRoutePolicy", "http-route-policy-0") + Expect(err).NotTo(HaveOccurred(), "deleting HTTPRoutePolicy") + Eventually(func() string { + _, err := s.GetResourceYaml("HTTPRoutePolicy", "http-route-policy-0") + return err.Error() + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(ContainSubstring(`httproutepolicies.gateway.apisix.io "http-route-policy-0" not found`)) + // access the route without additional vars should be OK + message := retry.DoWithRetry(s.GinkgoT, "", 10, time.Second, func() (string, error) { + statusCode := s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + Expect().Raw().StatusCode + if statusCode != http.StatusOK { + return "", errors.Errorf("unexpected status code: %v", statusCode) + } + return "request OK", nil + }) + s.Logf(message) + }) + + It("HTTPRoutePolicy conflicts", func() { + const httpRoutePolicy0 = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: HTTPRoutePolicy +metadata: + name: http-route-policy-0 +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin + priority: 10 + vars: + - - http_x_hrp_name + - == + - http-route-policy-0 +` + const httpRoutePolicy1 = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: HTTPRoutePolicy +metadata: + name: http-route-policy-1 +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin + priority: 20 + vars: + - - http_x_hrp_name + - == + - http-route-policy-0 +` + const httpRoutePolicy2 = ` +apiVersion: gateway.apisix.io/v1alpha1 +kind: HTTPRoutePolicy +metadata: + name: http-route-policy-2 +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: httpbin-1 + priority: 30 + vars: + - - http_x_hrp_name + - == + - http-route-policy-0 +` + By("create HTTPRoute") + ResourceApplied("HTTPRoute", "httpbin", varsRoute, 1) + + By("create HTTPRoutePolices") + for _, spec := range []string{httpRoutePolicy0, httpRoutePolicy1, httpRoutePolicy2} { + err := s.CreateResourceFromString(spec) + Expect(err).NotTo(HaveOccurred(), "creating HTTPRoutePolicy") + } + for _, name := range []string{"http-route-policy-0", "http-route-policy-1", "http-route-policy-2"} { + Eventually(func() string { + spec, err := s.GetResourceYaml("HTTPRoutePolicy", name) + Expect(err).NotTo(HaveOccurred(), "getting HTTPRoutePolicy yaml") + return spec + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second). + Should(ContainSubstring("reason: Conflicted")) + } + + // assert that conflict policies are not in effect + Eventually(func() int { + return s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + Expect().Raw().StatusCode + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + }) + + PIt("HTTPRoutePolicy status changes on HTTPRoute deleting", func() { + By("create HTTPRoute") + ResourceApplied("HTTPRoute", "httpbin", varsRoute, 1) + + By("create HTTPRoutePolicy") + ResourceApplied("HTTPRoutePolicy", "http-route-policy-0", httpRoutePolicy, 1) + + By("access dataplane to check the HTTPRoutePolicy") + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + Expect(). + Status(http.StatusNotFound) + + s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + WithHeader("X-HRP-Name", "http-route-policy-0"). + WithQuery("hrp_name", "http-route-policy-0"). + Expect(). + Status(http.StatusOK) + + By("delete the HTTPRoute, assert the HTTPRoutePolicy's status will be changed") + err := s.DeleteResource("HTTPRoute", "httpbin") + Expect(err).NotTo(HaveOccurred(), "deleting HTTPRoute") + message := retry.DoWithRetry(s.GinkgoT, "request the deleted route", 10, time.Second, func() (string, error) { + statusCode := s.NewAPISIXClient(). + GET("/get"). + WithHost("httpbin.example"). + WithHeader("X-Route-Name", "httpbin"). + WithHeader("X-HRP-Name", "http-route-policy-0"). + WithQuery("hrp_name", "http-route-policy-0"). + Expect().Raw().StatusCode + if statusCode != http.StatusNotFound { + return "", errors.Errorf("unexpected status code: %v", statusCode) + } + return "the route is deleted", nil + }) + s.Logf(message) + + Eventually(func() string { + spec, err := s.GetResourceYaml("HTTPRoutePolicy", "http-route-policy-0") + Expect(err).NotTo(HaveOccurred(), "getting HTTPRoutePolicy") + return spec + }).WithTimeout(8 * time.Second).ProbeEvery(time.Second).ShouldNot(ContainSubstring("ancestorRef:")) + }) }) Context("HTTPRoute Filters", func() {