diff --git a/ako-gateway-api/k8s/ako_init.go b/ako-gateway-api/k8s/ako_init.go index 571e6b86c..ad330dd12 100644 --- a/ako-gateway-api/k8s/ako_init.go +++ b/ako-gateway-api/k8s/ako_init.go @@ -304,7 +304,7 @@ func (c *GatewayController) FullSyncK8s(sync bool) error { resVer := meta.GetResourceVersion() objects.SharedResourceVerInstanceLister().Save(key, resVer) } - if IsHTTPRouteValid(key, httpRouteObj) { + if IsHTTPRouteConfigValid(key, httpRouteObj) { filteredHTTPRoutes = append(filteredHTTPRoutes, httpRouteObj) } } diff --git a/ako-gateway-api/k8s/gateway_controller.go b/ako-gateway-api/k8s/gateway_controller.go index b9367aaa0..f6beca038 100644 --- a/ako-gateway-api/k8s/gateway_controller.go +++ b/ako-gateway-api/k8s/gateway_controller.go @@ -635,7 +635,7 @@ func (c *GatewayController) SetupGatewayApiEventHandlers(numWorkers uint32) { utils.AviLog.Debugf("key: %s, msg: same resource version returning", key) return } - if !IsHTTPRouteValid(key, httpRoute) { + if !IsHTTPRouteConfigValid(key, httpRoute) { return } namespace, _, _ := cache.SplitMetaNamespaceKey(utils.ObjKey(httpRoute)) @@ -676,7 +676,7 @@ func (c *GatewayController) SetupGatewayApiEventHandlers(numWorkers uint32) { newHTTPRoute := obj.(*gatewayv1.HTTPRoute) if IsHTTPRouteUpdated(oldHTTPRoute, newHTTPRoute) { key := lib.HTTPRoute + "/" + utils.ObjKey(newHTTPRoute) - if !IsHTTPRouteValid(key, newHTTPRoute) { + if !IsHTTPRouteConfigValid(key, newHTTPRoute) { return } namespace, _, _ := cache.SplitMetaNamespaceKey(utils.ObjKey(newHTTPRoute)) diff --git a/ako-gateway-api/k8s/validator.go b/ako-gateway-api/k8s/validator.go index 274e80ac1..24c9a80d4 100644 --- a/ako-gateway-api/k8s/validator.go +++ b/ako-gateway-api/k8s/validator.go @@ -17,35 +17,18 @@ package k8s import ( "context" "fmt" - "regexp" - "strings" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "k8s.io/apimachinery/pkg/labels" akogatewayapilib "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/lib" - akogatewayapiobjects "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/objects" akogatewayapistatus "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/status" - "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/internal/lib" "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/internal/status" "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/pkg/utils" ) -func isRegexMatch(stringWithWildCard string, stringToBeMatched string, key string) bool { - // replace the wildcard character with a regex - replacedHostname := strings.Replace(stringWithWildCard, utils.WILDCARD, utils.FQDN_LABEL_REGEX, 1) - // create the expression for pattern matching - pattern := fmt.Sprintf("^%s$", replacedHostname) - expr, err := regexp.Compile(pattern) - if err != nil { - utils.AviLog.Warnf("key: %s, msg: unable to compile wildcard string to regex object. Err: %s", key, err) - } - return expr.MatchString(stringToBeMatched) -} - func IsGatewayClassValid(key string, gatewayClass *gatewayv1.GatewayClass) bool { controllerName := string(gatewayClass.Spec.ControllerName) @@ -92,10 +75,7 @@ func IsValidGateway(key string, gateway *gatewayv1.Gateway) bool { SetIn(&gatewayStatus.Conditions) programmedCondition. SetIn(&gatewayStatus.Conditions) - _, err := akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) - if err != nil { - utils.AviLog.Errorf("key: %s, msg: Gateway status patch was not successful :%s ", key, err.Error()) - } + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) return false } @@ -108,10 +88,7 @@ func IsValidGateway(key string, gateway *gatewayv1.Gateway) bool { programmedCondition. Reason(string(gatewayv1.GatewayReasonAddressNotUsable)). SetIn(&gatewayStatus.Conditions) - _, err := akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) - if err != nil { - utils.AviLog.Errorf("key: %s, msg: Gateway status patch was not successful :%s ", key, err.Error()) - } + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) return false } @@ -124,10 +101,7 @@ func IsValidGateway(key string, gateway *gatewayv1.Gateway) bool { programmedCondition. Reason(string(gatewayv1.GatewayReasonAddressNotUsable)). SetIn(&gatewayStatus.Conditions) - _, err := akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) - if err != nil { - utils.AviLog.Errorf("key: %s, msg: Gateway status patch was not successful :%s ", key, err.Error()) - } + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) return false } @@ -150,21 +124,15 @@ func IsValidGateway(key string, gateway *gatewayv1.Gateway) bool { programmedCondition. SetIn(&gatewayStatus.Conditions) akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) - gateway.Status = *gatewayStatus.DeepCopy() return false } else if validListenerCount < len(spec.Listeners) { defaultCondition. Reason(string(gatewayv1.GatewayReasonListenersNotValid)). Message("Gateway contains atleast one valid listener"). SetIn(&gatewayStatus.Conditions) - _, err := akogatewayapistatus.Record(key, gateway, &akogatewayapistatus.Status{GatewayStatus: gatewayStatus}) + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) utils.AviLog.Infof("key: %s, msg: Gateway %s contains atleast one valid listener", key, gateway.Name) - if err == nil { - return false - } else { - utils.AviLog.Errorf("key: %s, msg: Gateway status patch was not successful :%s ", key, err.Error()) - return false - } + return false } defaultCondition. @@ -172,14 +140,9 @@ func IsValidGateway(key string, gateway *gatewayv1.Gateway) bool { Status(metav1.ConditionTrue). Message("Gateway configuration is valid"). SetIn(&gatewayStatus.Conditions) - _, err := akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) utils.AviLog.Infof("key: %s, msg: Gateway %s is valid", key, gateway.Name) - if err == nil { - return true - } else { - utils.AviLog.Errorf("key: %s, msg: Gateway status patch was not successful :%s ", key, err.Error()) - return false - } + return true } func isValidListener(key string, gateway *gatewayv1.Gateway, gatewayStatus *gatewayv1.GatewayStatus, index int) bool { @@ -339,234 +302,12 @@ func isValidListener(key string, gateway *gatewayv1.Gateway, gatewayStatus *gate return true } -func IsHTTPRouteValid(key string, obj *gatewayv1.HTTPRoute) bool { +func IsHTTPRouteConfigValid(key string, obj *gatewayv1.HTTPRoute) bool { httpRoute := obj.DeepCopy() if len(httpRoute.Spec.ParentRefs) == 0 { utils.AviLog.Errorf("key: %s, msg: Parent Reference is empty for the HTTPRoute %s", key, httpRoute.Name) return false } - - httpRouteStatus := obj.Status.DeepCopy() - httpRouteStatus.Parents = make([]gatewayv1.RouteParentStatus, 0, len(httpRoute.Spec.ParentRefs)) - var invalidParentRefCount int - parentRefIndexInHttpRouteStatus := 0 - for parentRefIndexFromSpec := range httpRoute.Spec.ParentRefs { - err := validateParentReference(key, httpRoute, httpRouteStatus, parentRefIndexFromSpec, &parentRefIndexInHttpRouteStatus) - if err != nil { - invalidParentRefCount++ - parentRefName := httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Name - utils.AviLog.Warnf("key: %s, msg: Parent Reference %s of HTTPRoute object %s is not valid, err: %v", key, parentRefName, httpRoute.Name, err) - } - } - akogatewayapistatus.Record(key, httpRoute, &status.Status{HTTPRouteStatus: httpRouteStatus}) - - // No valid attachment, we can't proceed with this HTTPRoute object. - if invalidParentRefCount == len(httpRoute.Spec.ParentRefs) { - utils.AviLog.Errorf("key: %s, msg: HTTPRoute object %s is not valid", key, httpRoute.Name) - akogatewayapilib.AKOControlConfig().EventRecorder().Eventf(httpRoute, corev1.EventTypeWarning, - lib.Detached, "HTTPRoute object %s is not valid", httpRoute.Name) - return false - } - utils.AviLog.Infof("key: %s, msg: HTTPRoute object %s is valid", key, httpRoute.Name) return true } - -func validateParentReference(key string, httpRoute *gatewayv1.HTTPRoute, httpRouteStatus *gatewayv1.HTTPRouteStatus, parentRefIndexFromSpec int, parentRefIndexInHttpRouteStatus *int) error { - - name := string(httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Name) - namespace := httpRoute.Namespace - if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Namespace != nil { - namespace = string(*httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Namespace) - } - gwNsName := namespace + "/" + name - obj, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().GatewayInformer.Lister().Gateways(namespace).Get(name) - if err != nil { - utils.AviLog.Errorf("key: %s, msg: unable to get the gateway object. err: %s", key, err) - return err - } - gateway := obj.DeepCopy() - - gwClass := string(gateway.Spec.GatewayClassName) - _, isAKOCtrl := akogatewayapiobjects.GatewayApiLister().IsGatewayClassControllerAKO(gwClass) - if !isAKOCtrl { - utils.AviLog.Warnf("key: %s, msg: controller for the parent reference %s of HTTPRoute object %s is not ako", key, name, httpRoute.Name) - return fmt.Errorf("controller for the parent reference %s of HTTPRoute object %s is not ako", name, httpRoute.Name) - } - // creates the Parent status only when the AKO is the gateway controller - httpRouteStatus.Parents = append(httpRouteStatus.Parents, gatewayv1.RouteParentStatus{}) - httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ControllerName = akogatewayapilib.GatewayController - httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.Name = gatewayv1.ObjectName(name) - httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.Namespace = (*gatewayv1.Namespace)(&namespace) - if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName != nil { - httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.SectionName = httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName - } - - defaultCondition := akogatewayapistatus.NewCondition(). - Type(string(gatewayv1.RouteConditionAccepted)). - Status(metav1.ConditionFalse). - ObservedGeneration(httpRoute.ObjectMeta.Generation) - - gwStatus := akogatewayapiobjects.GatewayApiLister().GetGatewayToGatewayStatusMapping(gwNsName) - if len(gwStatus.Conditions) == 0 { - // Gateway processing by AKO has not started. - utils.AviLog.Errorf("key: %s, msg: AKO is yet to process Gateway %s for parent reference %s.", key, gateway.Name, name) - err := fmt.Errorf("AKO is yet to process Gateway %s for parent reference %s", gateway.Name, name) - defaultCondition. - Reason(string(gatewayv1.RouteReasonPending)). - Message(err.Error()). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - return err - } - - // Attach only when gateway configuration is valid - currentGatewayStatusCondition := gwStatus.Conditions[0] - if currentGatewayStatusCondition.Status != metav1.ConditionTrue { - // Gateway is not in an expected state. - utils.AviLog.Errorf("key: %s, msg: Gateway %s for parent reference %s is in Invalid State", key, gateway.Name, name) - err := fmt.Errorf("Gateway %s is in Invalid State", gateway.Name) - defaultCondition. - Reason(string(gatewayv1.RouteReasonPending)). - Message(err.Error()). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - return err - } - - //section name is optional - var listenersForRoute []gatewayv1.Listener - if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName != nil { - listenerName := *httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName - i := akogatewayapilib.FindListenerByName(string(listenerName), gateway.Spec.Listeners) - if i == -1 { - // listener is not present in gateway - utils.AviLog.Errorf("key: %s, msg: unable to find the listener from the Section Name %s in Parent Reference %s", key, name, listenerName) - err := fmt.Errorf("Invalid listener name provided") - defaultCondition. - Reason(string(gatewayv1.RouteReasonNoMatchingParent)). - Message(err.Error()). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - return err - } - listenersForRoute = append(listenersForRoute, gateway.Spec.Listeners[i]) - } else { - listenersForRoute = append(listenersForRoute, gateway.Spec.Listeners...) - } - - // TODO: Validation for hostname (those being fqdns) need to validate as per the K8 gateway req. - var listenersMatchedToRoute []gatewayv1.Listener - for _, listenerObj := range listenersForRoute { - // check from store - hostInListener := listenerObj.Hostname - isListenerFqdnWildcard := false - matched := false - // TODO: - // Use case to handle for validations of hostname: - // USe case 1: Shouldn't contain mor than 1 * - // USe case 2: * should be at the beginning only - if hostInListener == nil || *hostInListener == "" || *hostInListener == utils.WILDCARD { - matched = true - } else { - // mark listener fqdn if it has * - if strings.HasPrefix(string(*hostInListener), utils.WILDCARD) { - isListenerFqdnWildcard = true - } - for _, host := range httpRoute.Spec.Hostnames { - // casese to consider: - // Case 1: hostname of gateway is wildcard(empty) and hostname from httproute is not wild card - // Case 2: hostname of gateway is not wild card and hostname from httproute is wildcard - // case 3: hostname of gateway is wildcard(empty) and hostname from httproute is wildcard - // case 4: hostname of gateway is not wildcard and hostname from httproute is not wildcard - isHttpRouteHostFqdnWildcard := false - if strings.HasPrefix(string(host), utils.WILDCARD) { - isHttpRouteHostFqdnWildcard = true - } - if isHttpRouteHostFqdnWildcard && isListenerFqdnWildcard { - // both are true. Match nonwildcard part - // Use case: 1. GW: *.avi.internal HttpRoute: *.bar.avi.internal - // USe case: 2. GW: *.bar.avi.internal HttpRoute: *.avi.internal - if utils.CheckSubdomainOverlapping(string(host), string(*hostInListener)) { - matched = true - break - } - - } else if !isHttpRouteHostFqdnWildcard && !isListenerFqdnWildcard { - // both are complete fqdn - if string(host) == string(*hostInListener) { - matched = true - break - } - } else { - if isHttpRouteHostFqdnWildcard { - // httpRoute hostFqdn is wildcard - matched = matched || isRegexMatch(string(host), string(*hostInListener), key) - } else if isListenerFqdnWildcard { - // listener fqdn is wildcard - matched = matched || isRegexMatch(string(*hostInListener), string(host), key) - } - - } - - } - // if there are no hostnames specified, all parent listneres should be matched. - if len(httpRoute.Spec.Hostnames) == 0 { - matched = true - } - } - if !matched { - utils.AviLog.Warnf("key: %s, msg: Gateway object %s don't have any listeners that matches the hostnames in HTTPRoute %s", key, gateway.Name, httpRoute.Name) - continue - } - listenersMatchedToRoute = append(listenersMatchedToRoute, listenerObj) - } - if len(listenersMatchedToRoute) == 0 { - err := fmt.Errorf("Hostname in Gateway Listener doesn't match with any of the hostnames in HTTPRoute") - defaultCondition. - Reason(string(gatewayv1.RouteReasonNoMatchingListenerHostname)). - Message(err.Error()). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - gwRouteNsName := fmt.Sprintf("%s/%s/%s/%s", gwNsName, lib.HTTPRoute, httpRoute.Namespace, httpRoute.Name) - found, hosts := akogatewayapiobjects.GatewayApiLister().GetGatewayRouteToHostname(gwRouteNsName) - if found { - utils.AviLog.Warnf("key: %s, msg: Hostname in Gateway Listener doesn't match with any of the hostnames in HTTPRoute", key) - utils.AviLog.Debugf("key: %s, msg: %d hosts mapped to the route %s/%s/%s", key, len(hosts), "HTTPRoute", httpRoute.Namespace, httpRoute.Name) - return nil - } - return err - } - - //TODO: Add a condition to check whether this route is allowed by the parent gateways allowedroute field and set gatewayv1.RouteReasonNotAllowedByListeners reason while implemenating gateway->listener->allowedRoutes->Selector - - gatewayStatus := gwStatus.DeepCopy() - for _, listenerObj := range listenersMatchedToRoute { - listenerName := listenerObj.Name - // Increment the attached routes of the listener in the Gateway object - - i := akogatewayapilib.FindListenerStatusByName(string(listenerName), gatewayStatus.Listeners) - if i == -1 { - utils.AviLog.Errorf("key: %s, msg: Gateway status is missing for the listener with name %s", key, listenerName) - err := fmt.Errorf("Couldn't find the listener %s in the Gateway status", listenerName) - defaultCondition. - Reason(string(gatewayv1.RouteReasonNoMatchingParent)). - Message(err.Error()). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - return err - } - - gatewayStatus.Listeners[i].AttachedRoutes += 1 - } - akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) - - defaultCondition. - Reason(string(gatewayv1.RouteReasonAccepted)). - Status(metav1.ConditionTrue). - Message("Parent reference is valid"). - SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) - utils.AviLog.Infof("key: %s, msg: Parent Reference %s of HTTPRoute object %s is valid", key, name, httpRoute.Name) - *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 - return nil -} diff --git a/ako-gateway-api/nodes/dequeue_ingestion.go b/ako-gateway-api/nodes/dequeue_ingestion.go index 90f36a1f1..9283b8273 100644 --- a/ako-gateway-api/nodes/dequeue_ingestion.go +++ b/ako-gateway-api/nodes/dequeue_ingestion.go @@ -36,6 +36,15 @@ func DequeueIngestion(key string, fullsync bool) { if !valid { return } + if objType == lib.HTTPRoute { + httpRoute, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().HTTPRouteInformer.Lister().HTTPRoutes(namespace).Get(name) + if err == nil { + utils.AviLog.Debugf("key: %s, msg: Successfully retrieved the HTTPRoute object %s", key, name) + if !IsHTTPRouteValid(key, httpRoute) { + return + } + } + } gatewayNsNameList, found := schema.GetGateways(namespace, name, key) if !found { diff --git a/ako-gateway-api/nodes/gateway_model_rel.go b/ako-gateway-api/nodes/gateway_model_rel.go index bd5856337..35c100761 100644 --- a/ako-gateway-api/nodes/gateway_model_rel.go +++ b/ako-gateway-api/nodes/gateway_model_rel.go @@ -343,7 +343,7 @@ func HTTPRouteChanges(namespace, name, key string) ([]string, bool) { hrObj, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().HTTPRouteInformer.Lister().HTTPRoutes(namespace).Get(name) if err != nil { if !errors.IsNotFound(err) { - utils.AviLog.Errorf("key: %s, msg: got error while getting gateway: %v", key, err) + utils.AviLog.Errorf("key: %s, msg: got error while getting httproute: %v", key, err) return []string{}, false } // httproute must be deleted so remove mappings diff --git a/ako-gateway-api/nodes/route_validator.go b/ako-gateway-api/nodes/route_validator.go new file mode 100644 index 000000000..767dded9a --- /dev/null +++ b/ako-gateway-api/nodes/route_validator.go @@ -0,0 +1,271 @@ +/* + * Copyright 2023-2024 VMware, Inc. + * All Rights Reserved. +* 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 nodes + +import ( + "fmt" + "regexp" + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + akogatewayapilib "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/lib" + akogatewayapiobjects "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/objects" + akogatewayapistatus "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/ako-gateway-api/status" + "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/internal/lib" + "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/internal/status" + "github.com/vmware/load-balancer-and-ingress-services-for-kubernetes/pkg/utils" +) + +func isRegexMatch(stringWithWildCard string, stringToBeMatched string, key string) bool { + // replace the wildcard character with a regex + replacedHostname := strings.Replace(stringWithWildCard, utils.WILDCARD, utils.FQDN_LABEL_REGEX, 1) + // create the expression for pattern matching + pattern := fmt.Sprintf("^%s$", replacedHostname) + expr, err := regexp.Compile(pattern) + if err != nil { + utils.AviLog.Warnf("key: %s, msg: unable to compile wildcard string to regex object. Err: %s", key, err) + } + return expr.MatchString(stringToBeMatched) +} + +func IsHTTPRouteValid(key string, obj *gatewayv1.HTTPRoute) bool { + + httpRoute := obj.DeepCopy() + httpRouteStatus := obj.Status.DeepCopy() + httpRouteStatus.Parents = make([]gatewayv1.RouteParentStatus, 0, len(httpRoute.Spec.ParentRefs)) + var invalidParentRefCount int + parentRefIndexInHttpRouteStatus := 0 + for parentRefIndexFromSpec := range httpRoute.Spec.ParentRefs { + err := validateParentReference(key, httpRoute, httpRouteStatus, parentRefIndexFromSpec, &parentRefIndexInHttpRouteStatus) + if err != nil { + invalidParentRefCount++ + parentRefName := httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Name + utils.AviLog.Warnf("key: %s, msg: Parent Reference %s of HTTPRoute object %s is not valid, err: %v", key, parentRefName, httpRoute.Name, err) + } + } + akogatewayapistatus.Record(key, httpRoute, &status.Status{HTTPRouteStatus: httpRouteStatus}) + + // No valid attachment, we can't proceed with this HTTPRoute object. + if invalidParentRefCount == len(httpRoute.Spec.ParentRefs) { + utils.AviLog.Errorf("key: %s, msg: HTTPRoute object %s is not valid", key, httpRoute.Name) + akogatewayapilib.AKOControlConfig().EventRecorder().Eventf(httpRoute, corev1.EventTypeWarning, + lib.Detached, "HTTPRoute object %s is not valid", httpRoute.Name) + return false + } + utils.AviLog.Infof("key: %s, msg: HTTPRoute object %s is valid", key, httpRoute.Name) + return true +} + +func validateParentReference(key string, httpRoute *gatewayv1.HTTPRoute, httpRouteStatus *gatewayv1.HTTPRouteStatus, parentRefIndexFromSpec int, parentRefIndexInHttpRouteStatus *int) error { + + name := string(httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Name) + namespace := httpRoute.Namespace + if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Namespace != nil { + namespace = string(*httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].Namespace) + } + gwNsName := namespace + "/" + name + obj, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().GatewayInformer.Lister().Gateways(namespace).Get(name) + if err != nil { + utils.AviLog.Errorf("key: %s, msg: unable to get the gateway object. err: %s", key, err) + return err + } + gateway := obj.DeepCopy() + + gwClass := string(gateway.Spec.GatewayClassName) + _, isAKOCtrl := akogatewayapiobjects.GatewayApiLister().IsGatewayClassControllerAKO(gwClass) + if !isAKOCtrl { + utils.AviLog.Warnf("key: %s, msg: controller for the parent reference %s of HTTPRoute object %s is not ako", key, name, httpRoute.Name) + return fmt.Errorf("controller for the parent reference %s of HTTPRoute object %s is not ako", name, httpRoute.Name) + } + // creates the Parent status only when the AKO is the gateway controller + httpRouteStatus.Parents = append(httpRouteStatus.Parents, gatewayv1.RouteParentStatus{}) + httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ControllerName = akogatewayapilib.GatewayController + httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.Name = gatewayv1.ObjectName(name) + httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.Namespace = (*gatewayv1.Namespace)(&namespace) + if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName != nil { + httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].ParentRef.SectionName = httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName + } + + defaultCondition := akogatewayapistatus.NewCondition(). + Type(string(gatewayv1.RouteConditionAccepted)). + Status(metav1.ConditionFalse). + ObservedGeneration(httpRoute.ObjectMeta.Generation) + + gwStatus := akogatewayapiobjects.GatewayApiLister().GetGatewayToGatewayStatusMapping(gwNsName) + if len(gwStatus.Conditions) == 0 { + // Gateway processing by AKO has not started. + utils.AviLog.Errorf("key: %s, msg: AKO is yet to process Gateway %s for parent reference %s.", key, gateway.Name, name) + err := fmt.Errorf("AKO is yet to process Gateway %s for parent reference %s", gateway.Name, name) + defaultCondition. + Reason(string(gatewayv1.RouteReasonPending)). + Message(err.Error()). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + return err + } + + // Attach only when gateway configuration is valid + currentGatewayStatusCondition := gwStatus.Conditions[0] + if currentGatewayStatusCondition.Status != metav1.ConditionTrue { + // Gateway is not in an expected state. + utils.AviLog.Errorf("key: %s, msg: Gateway %s for parent reference %s is in Invalid State", key, gateway.Name, name) + err := fmt.Errorf("Gateway %s is in Invalid State", gateway.Name) + defaultCondition. + Reason(string(gatewayv1.RouteReasonPending)). + Message(err.Error()). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + return err + } + + //section name is optional + var listenersForRoute []gatewayv1.Listener + if httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName != nil { + listenerName := *httpRoute.Spec.ParentRefs[parentRefIndexFromSpec].SectionName + i := akogatewayapilib.FindListenerByName(string(listenerName), gateway.Spec.Listeners) + if i == -1 { + // listener is not present in gateway + utils.AviLog.Errorf("key: %s, msg: unable to find the listener from the Section Name %s in Parent Reference %s", key, name, listenerName) + err := fmt.Errorf("Invalid listener name provided") + defaultCondition. + Reason(string(gatewayv1.RouteReasonNoMatchingParent)). + Message(err.Error()). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + return err + } + listenersForRoute = append(listenersForRoute, gateway.Spec.Listeners[i]) + } else { + listenersForRoute = append(listenersForRoute, gateway.Spec.Listeners...) + } + + // TODO: Validation for hostname (those being fqdns) need to validate as per the K8 gateway req. + var listenersMatchedToRoute []gatewayv1.Listener + for _, listenerObj := range listenersForRoute { + // check from store + hostInListener := listenerObj.Hostname + isListenerFqdnWildcard := false + matched := false + // TODO: + // Use case to handle for validations of hostname: + // USe case 1: Shouldn't contain mor than 1 * + // USe case 2: * should be at the beginning only + if hostInListener == nil || *hostInListener == "" || *hostInListener == utils.WILDCARD { + matched = true + } else { + // mark listener fqdn if it has * + if strings.HasPrefix(string(*hostInListener), utils.WILDCARD) { + isListenerFqdnWildcard = true + } + for _, host := range httpRoute.Spec.Hostnames { + // casese to consider: + // Case 1: hostname of gateway is wildcard(empty) and hostname from httproute is not wild card + // Case 2: hostname of gateway is not wild card and hostname from httproute is wildcard + // case 3: hostname of gateway is wildcard(empty) and hostname from httproute is wildcard + // case 4: hostname of gateway is not wildcard and hostname from httproute is not wildcard + isHttpRouteHostFqdnWildcard := false + if strings.HasPrefix(string(host), utils.WILDCARD) { + isHttpRouteHostFqdnWildcard = true + } + if isHttpRouteHostFqdnWildcard && isListenerFqdnWildcard { + // both are true. Match nonwildcard part + // Use case: 1. GW: *.avi.internal HttpRoute: *.bar.avi.internal + // USe case: 2. GW: *.bar.avi.internal HttpRoute: *.avi.internal + if utils.CheckSubdomainOverlapping(string(host), string(*hostInListener)) { + matched = true + break + } + + } else if !isHttpRouteHostFqdnWildcard && !isListenerFqdnWildcard { + // both are complete fqdn + if string(host) == string(*hostInListener) { + matched = true + break + } + } else { + if isHttpRouteHostFqdnWildcard { + // httpRoute hostFqdn is wildcard + matched = matched || isRegexMatch(string(host), string(*hostInListener), key) + } else if isListenerFqdnWildcard { + // listener fqdn is wildcard + matched = matched || isRegexMatch(string(*hostInListener), string(host), key) + } + + } + + } + // if there are no hostnames specified, all parent listneres should be matched. + if len(httpRoute.Spec.Hostnames) == 0 { + matched = true + } + } + if !matched { + utils.AviLog.Warnf("key: %s, msg: Gateway object %s don't have any listeners that matches the hostnames in HTTPRoute %s", key, gateway.Name, httpRoute.Name) + continue + } + listenersMatchedToRoute = append(listenersMatchedToRoute, listenerObj) + } + if len(listenersMatchedToRoute) == 0 { + err := fmt.Errorf("Hostname in Gateway Listener doesn't match with any of the hostnames in HTTPRoute") + defaultCondition. + Reason(string(gatewayv1.RouteReasonNoMatchingListenerHostname)). + Message(err.Error()). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + gwRouteNsName := fmt.Sprintf("%s/%s/%s/%s", gwNsName, lib.HTTPRoute, httpRoute.Namespace, httpRoute.Name) + found, hosts := akogatewayapiobjects.GatewayApiLister().GetGatewayRouteToHostname(gwRouteNsName) + if found { + utils.AviLog.Warnf("key: %s, msg: Hostname in Gateway Listener doesn't match with any of the hostnames in HTTPRoute", key) + utils.AviLog.Debugf("key: %s, msg: %d hosts mapped to the route %s/%s/%s", key, len(hosts), "HTTPRoute", httpRoute.Namespace, httpRoute.Name) + return nil + } + return err + } + + //TODO: Add a condition to check whether this route is allowed by the parent gateways allowedroute field and set gatewayv1.RouteReasonNotAllowedByListeners reason while implemenating gateway->listener->allowedRoutes->Selector + + gatewayStatus := gwStatus.DeepCopy() + for _, listenerObj := range listenersMatchedToRoute { + listenerName := listenerObj.Name + // Increment the attached routes of the listener in the Gateway object + + i := akogatewayapilib.FindListenerStatusByName(string(listenerName), gatewayStatus.Listeners) + if i == -1 { + utils.AviLog.Errorf("key: %s, msg: Gateway status is missing for the listener with name %s", key, listenerName) + err := fmt.Errorf("Couldn't find the listener %s in the Gateway status", listenerName) + defaultCondition. + Reason(string(gatewayv1.RouteReasonNoMatchingParent)). + Message(err.Error()). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + return err + } + + gatewayStatus.Listeners[i].AttachedRoutes += 1 + } + akogatewayapistatus.Record(key, gateway, &status.Status{GatewayStatus: gatewayStatus}) + + defaultCondition. + Reason(string(gatewayv1.RouteReasonAccepted)). + Status(metav1.ConditionTrue). + Message("Parent reference is valid"). + SetIn(&httpRouteStatus.Parents[*parentRefIndexInHttpRouteStatus].Conditions) + utils.AviLog.Infof("key: %s, msg: Parent Reference %s of HTTPRoute object %s is valid", key, name, httpRoute.Name) + *parentRefIndexInHttpRouteStatus = *parentRefIndexInHttpRouteStatus + 1 + return nil +} diff --git a/ako-gateway-api/status/gateway_status.go b/ako-gateway-api/status/gateway_status.go index 684550286..383a9e2b8 100644 --- a/ako-gateway-api/status/gateway_status.go +++ b/ako-gateway-api/status/gateway_status.go @@ -219,37 +219,37 @@ func (o *gateway) BulkUpdate(key string, options []status.StatusOptions) { } } -func (o *gateway) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) { +func (o *gateway) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) error { retry := 0 if len(retryNum) > 0 { retry = retryNum[0] if retry >= 5 { utils.AviLog.Errorf("key: %s, msg: Patch retried 5 times, aborting", key) - return obj, errors.New("Patch retried 5 times, aborting") + return errors.New("Patch retried 5 times, aborting") } } gw := obj.(*gatewayv1.Gateway) if o.isStatusEqual(&gw.Status, status.GatewayStatus) { - return obj, nil + return nil } patchPayload, _ := json.Marshal(map[string]interface{}{ "status": status.GatewayStatus, }) - updatedGateway, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().Gateways(gw.Namespace).Patch(context.TODO(), gw.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") + _, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().Gateways(gw.Namespace).Patch(context.TODO(), gw.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") if err != nil { utils.AviLog.Warnf("key: %s, msg: there was an error in updating the gateway status. err: %+v, retry: %d", key, err, retry) updatedGW, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().GatewayInformer.Lister().Gateways(gw.Namespace).Get(gw.Name) if err != nil { utils.AviLog.Warnf("gateway not found %v", err) - return updatedGW, err + return err } return o.Patch(key, updatedGW, status, retry+1) } utils.AviLog.Infof("key: %s, msg: Successfully updated the gateway %s/%s status %+v", key, gw.Namespace, gw.Name, utils.Stringify(status)) - return updatedGateway, nil + return nil } func (o *gateway) isStatusEqual(old, new *gatewayv1.GatewayStatus) bool { diff --git a/ako-gateway-api/status/gatewayclass_status.go b/ako-gateway-api/status/gatewayclass_status.go index 390f50c2f..e312130d0 100644 --- a/ako-gateway-api/status/gatewayclass_status.go +++ b/ako-gateway-api/status/gatewayclass_status.go @@ -73,36 +73,36 @@ func (o *gatewayClass) BulkUpdate(key string, options []status.StatusOptions) { // TODO: Add this code when we publish the status from the rest layer } -func (o *gatewayClass) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) { +func (o *gatewayClass) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) error { retry := 0 if len(retryNum) > 0 { retry = retryNum[0] if retry >= 5 { utils.AviLog.Errorf("key: %s, msg: Patch retried 5 times, aborting", key) - return obj, errors.New("Patch retried 5 times, aborting") + return errors.New("Patch retried 5 times, aborting") } } gatewayClass := obj.(*gatewayv1.GatewayClass) if o.isStatusEqual(&gatewayClass.Status, status.GatewayClassStatus) { - return obj, nil + return nil } patchPayload, _ := json.Marshal(map[string]interface{}{ "status": status.GatewayClassStatus, }) - updatedObject, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().GatewayClasses().Patch(context.TODO(), gatewayClass.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") + _, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().GatewayClasses().Patch(context.TODO(), gatewayClass.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") if err != nil { utils.AviLog.Warnf("key: %s, msg: there was an error in updating the GatewayClass status. err: %+v, retry: %d", key, err, retry) updatedObj, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().GatewayClassInformer.Lister().Get(gatewayClass.Name) if err != nil { utils.AviLog.Warnf("GatewayClass not found %v", err) - return updatedObj, err + return err } return o.Patch(key, updatedObj, status, retry+1) } utils.AviLog.Infof("key: %s, msg: Successfully updated the GatewayClass %s status %+v %v", key, gatewayClass.Name, utils.Stringify(status), err) - return updatedObject, nil + return nil } func (o *gatewayClass) isStatusEqual(old, new *gatewayv1.GatewayClassStatus) bool { diff --git a/ako-gateway-api/status/httproute_status.go b/ako-gateway-api/status/httproute_status.go index 45e08b913..99a528ed1 100644 --- a/ako-gateway-api/status/httproute_status.go +++ b/ako-gateway-api/status/httproute_status.go @@ -84,37 +84,37 @@ func (o *httproute) BulkUpdate(key string, options []status.StatusOptions) { // TODO: Add this code when we publish the status from the rest layer } -func (o *httproute) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) { +func (o *httproute) Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) error { retry := 0 if len(retryNum) > 0 { retry = retryNum[0] if retry >= 5 { utils.AviLog.Errorf("key: %s, msg: Patch retried 5 times, aborting", key) - return obj, errors.New("Patch retried 5 times, aborting") + return errors.New("Patch retried 5 times, aborting") } } httpRoute := obj.(*gatewayv1.HTTPRoute) if o.isStatusEqual(&httpRoute.Status, status.HTTPRouteStatus) { - return obj, nil + return nil } patchPayload, _ := json.Marshal(map[string]interface{}{ "status": status.HTTPRouteStatus, }) - updatedObject, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().HTTPRoutes(httpRoute.Namespace).Patch(context.TODO(), httpRoute.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") + _, err := akogatewayapilib.AKOControlConfig().GatewayAPIClientset().GatewayV1().HTTPRoutes(httpRoute.Namespace).Patch(context.TODO(), httpRoute.Name, types.MergePatchType, patchPayload, metav1.PatchOptions{}, "status") if err != nil { utils.AviLog.Warnf("key: %s, msg: there was an error in updating the HTTPRoute status. err: %+v, retry: %d", key, err, retry) updatedObj, err := akogatewayapilib.AKOControlConfig().GatewayApiInformers().HTTPRouteInformer.Lister().HTTPRoutes(httpRoute.Namespace).Get(httpRoute.Name) if err != nil { utils.AviLog.Warnf("HTTPRoute not found %v", err) - return updatedObj, err + return err } return o.Patch(key, updatedObj, status, retry+1) } utils.AviLog.Infof("key: %s, msg: Successfully updated the HTTPRoute %s/%s status %+v", key, httpRoute.Namespace, httpRoute.Name, utils.Stringify(status)) - return updatedObject, nil + return nil } func (o *httproute) isStatusEqual(old, new *gatewayv1.HTTPRouteStatus) bool { diff --git a/ako-gateway-api/status/status.go b/ako-gateway-api/status/status.go index 00ca1b129..7f7d2a58c 100644 --- a/ako-gateway-api/status/status.go +++ b/ako-gateway-api/status/status.go @@ -15,9 +15,6 @@ package status import ( - "errors" - - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -30,7 +27,7 @@ import ( type StatusUpdater interface { Update(key string, option status.StatusOptions) BulkUpdate(key string, options []status.StatusOptions) - Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) + Patch(key string, obj runtime.Object, status *status.Status, retryNum ...int) error Delete(key string, option status.StatusOptions) } @@ -77,7 +74,6 @@ func BulkUpdate(key string, objectType string, options []status.StatusOptions) e utils.AviLog.Debugf("key: %s, msg: Bulk update successful for object %s", key, objectType) return nil } - func Record(key string, obj runtime.Object, objStatus *status.Status) { var objectType string var statusOption status.StatusOptions @@ -101,7 +97,7 @@ func Record(key string, obj runtime.Object, objStatus *status.Status) { key = serviceMetadata.HTTPRoute default: utils.AviLog.Warnf("key %s, msg: Unsupported object received at the status layer, %T", key, obj) - return obj, errors.New("Unsupported object received at the status layer") + return } updateOption.Status = objStatus updateOption.ServiceMetadata = serviceMetadata diff --git a/tests/gatewayapitests/ingestion/httproute_test.go b/tests/gatewayapitests/ingestion/httproute_test.go index 863267c57..98b0fa93a 100644 --- a/tests/gatewayapitests/ingestion/httproute_test.go +++ b/tests/gatewayapitests/ingestion/httproute_test.go @@ -83,7 +83,7 @@ func TestHTTPRouteHostnameInvalid(t *testing.T) { parentRefs := akogatewayapitests.GetParentReferencesV1([]string{gatewayName}, namespace, ports) hostnames := []gatewayv1.Hostname{"*.example.com"} akogatewayapitests.SetupHTTPRoute(t, httpRouteName, namespace, parentRefs, hostnames, nil) - waitAndverify(t, "") + waitAndverify(t, key) // update hostnames = []gatewayv1.Hostname{"foo-8080.com"} @@ -117,7 +117,7 @@ func TestHTTPRouteGatewayNotPresent(t *testing.T) { parentRefs := akogatewayapitests.GetParentReferencesV1([]string{gatewayName}, namespace, ports) hostnames := []gatewayv1.Hostname{"foo-8080.com", "foo-8081.com"} akogatewayapitests.SetupHTTPRoute(t, httpRouteName, namespace, parentRefs, hostnames, nil) - waitAndverify(t, "") + waitAndverify(t, key) // update listeners := akogatewayapitests.GetListenersV1(ports, false, false)