Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AV-219152 Moving HTTPRoute Validation to Graph layer #1547

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ako-gateway-api/k8s/ako_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
4 changes: 2 additions & 2 deletions ako-gateway-api/k8s/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
335 changes: 84 additions & 251 deletions ako-gateway-api/k8s/validator.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions ako-gateway-api/nodes/dequeue_ingestion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkoshtavmware: Should we include this as part of HTTPRouteChanges function? How transition from valid httproute to invalid HTTP route will be handled with return into picture? Are we deleting previously created objects with invalid transition?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be taken care as part of partial listener PR

return
}
}
}

gatewayNsNameList, found := schema.GetGateways(namespace, name, key)
if !found {
Expand Down
2 changes: 1 addition & 1 deletion ako-gateway-api/nodes/gateway_model_rel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
271 changes: 271 additions & 0 deletions ako-gateway-api/nodes/route_validator.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 10 additions & 10 deletions ako-gateway-api/status/gateway_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package status
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"

Expand Down Expand Up @@ -159,13 +160,12 @@ func (o *gateway) Update(key string, option status.StatusOptions) {
conditionStatus := metav1.ConditionTrue

if option.Options.Message != "" {
conditionType = string(gatewayv1.GatewayConditionAccepted)
conditionType = string(gatewayv1.GatewayConditionProgrammed)
conditionStatus = metav1.ConditionFalse
reason = string(gatewayv1.GatewayReasonInvalid)
message = option.Options.Message
} else {
conditionType = string(gatewayv1.GatewayConditionProgrammed)
conditionStatus = metav1.ConditionTrue
reason = string(gatewayv1.GatewayReasonProgrammed)
message = "Virtual service configured/updated"
}
Expand All @@ -180,9 +180,9 @@ func (o *gateway) Update(key string, option status.StatusOptions) {
for i := range gatewaystatus.Listeners {
listenerCondition := NewCondition()
listenerCondition.
Type(conditionType).
Type(string(gatewayv1.ListenerConditionProgrammed)).
Status(conditionStatus).
Reason(reason).
Reason(string(gatewayv1.ListenerReasonProgrammed)).
ObservedGeneration(gw.ObjectMeta.Generation).
Message(message).
SetIn(&gatewaystatus.Listeners[i].Conditions)
Expand Down Expand Up @@ -219,19 +219,19 @@ 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
return errors.New("Patch retried 5 times, aborting")
}
}

gw := obj.(*gatewayv1.Gateway)
if o.isStatusEqual(&gw.Status, status.GatewayStatus) {
return
return nil
}

patchPayload, _ := json.Marshal(map[string]interface{}{
Expand All @@ -243,13 +243,13 @@ func (o *gateway) Patch(key string, obj runtime.Object, status *status.Status, r
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
return err
}
o.Patch(key, updatedGW, status, retry+1)
return
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 nil
}

func (o *gateway) isStatusEqual(old, new *gatewayv1.GatewayStatus) bool {
Expand Down
Loading
Loading