@@ -20,30 +20,26 @@ import (
2020 "context"
2121 "fmt"
2222
23- "github.com/aws/aws-application-networking-k8s/pkg/aws/services"
24- "github.com/aws/aws-application-networking-k8s/pkg/utils"
25- "github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog"
26-
2723 "github.com/pkg/errors"
28-
2924 corev1 "k8s.io/api/core/v1"
25+ apierrors "k8s.io/apimachinery/pkg/api/errors"
26+ "k8s.io/apimachinery/pkg/api/meta"
3027 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3128 "k8s.io/apimachinery/pkg/runtime"
3229 "k8s.io/apimachinery/pkg/types"
3330 "k8s.io/client-go/tools/record"
3431 ctrl "sigs.k8s.io/controller-runtime"
35- "sigs.k8s.io/controller-runtime/pkg/client"
36- gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
37- gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
38-
39- "sigs.k8s.io/external-dns/endpoint"
40-
4132 "sigs.k8s.io/controller-runtime/pkg/builder"
33+ "sigs.k8s.io/controller-runtime/pkg/client"
4234 "sigs.k8s.io/controller-runtime/pkg/predicate"
35+ "sigs.k8s.io/external-dns/endpoint"
4336 gwv1 "sigs.k8s.io/gateway-api/apis/v1"
37+ gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
38+ gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
4439
4540 anv1alpha1 "github.com/aws/aws-application-networking-k8s/pkg/apis/applicationnetworking/v1alpha1"
4641 "github.com/aws/aws-application-networking-k8s/pkg/aws"
42+ "github.com/aws/aws-application-networking-k8s/pkg/aws/services"
4743 "github.com/aws/aws-application-networking-k8s/pkg/config"
4844 "github.com/aws/aws-application-networking-k8s/pkg/controllers/eventhandlers"
4945 "github.com/aws/aws-application-networking-k8s/pkg/deploy"
@@ -52,6 +48,9 @@ import (
5248 "github.com/aws/aws-application-networking-k8s/pkg/k8s"
5349 "github.com/aws/aws-application-networking-k8s/pkg/model/core"
5450 lattice_runtime "github.com/aws/aws-application-networking-k8s/pkg/runtime"
51+ "github.com/aws/aws-application-networking-k8s/pkg/utils"
52+ k8sutils "github.com/aws/aws-application-networking-k8s/pkg/utils"
53+ "github.com/aws/aws-application-networking-k8s/pkg/utils/gwlog"
5554)
5655
5756var routeTypeToFinalizer = map [core.RouteType ]string {
@@ -317,6 +316,14 @@ func (r *routeReconciler) reconcileUpsert(ctx context.Context, req ctrl.Request,
317316 r .eventRecorder .Event (route .K8sObject (), corev1 .EventTypeWarning , k8s .RouteEventReasonFailedAddFinalizer , fmt .Sprintf ("Failed add finalizer due to %s" , err ))
318317 }
319318
319+ if err := r .validateRoute (ctx , route ); err != nil {
320+ // TODO: we suppose to stop reconciliation here, but that will create problem when
321+ // we delete Service and we suppose to delete TargetGroup, this validation will
322+ // throw error if Service is not found. For now just update route status and log
323+ // error.
324+ r .log .Infof ("route: %s: %s" , route .Name (), err )
325+ }
326+
320327 backendRefIPFamiliesErr := r .validateBackendRefsIpFamilies (ctx , route )
321328
322329 if backendRefIPFamiliesErr != nil {
@@ -361,7 +368,7 @@ func (r *routeReconciler) reconcileUpsert(ctx context.Context, req ctrl.Request,
361368 r .eventRecorder .Event (route .K8sObject (), corev1 .EventTypeNormal ,
362369 k8s .RouteEventReasonDeploySucceed , "Adding/Updating reconcile Done!" )
363370
364- svcName := utils .LatticeServiceName (route .Name (), route .Namespace ())
371+ svcName := k8sutils .LatticeServiceName (route .Name (), route .Namespace ())
365372 svc , err := r .cloud .Lattice ().FindService (ctx , svcName )
366373 if err != nil && ! services .IsNotFoundError (err ) {
367374 return err
@@ -372,15 +379,15 @@ func (r *routeReconciler) reconcileUpsert(ctx context.Context, req ctrl.Request,
372379 return errors .New (lattice .LATTICE_RETRY )
373380 }
374381
375- if err := r .updateRouteStatus (ctx , * svc .DnsEntry .DomainName , route ); err != nil {
382+ if err := r .updateRouteAnnotation (ctx , * svc .DnsEntry .DomainName , route ); err != nil {
376383 return err
377384 }
378385
379386 r .log .Infow ("reconciled" , "name" , req .Name )
380387 return nil
381388}
382389
383- func (r * routeReconciler ) updateRouteStatus (ctx context.Context , dns string , route core.Route ) error {
390+ func (r * routeReconciler ) updateRouteAnnotation (ctx context.Context , dns string , route core.Route ) error {
384391 r .log .Debugf ("Updating route %s-%s with DNS %s" , route .Name (), route .Namespace (), dns )
385392 routeOld := route .DeepCopy ()
386393
@@ -392,39 +399,6 @@ func (r *routeReconciler) updateRouteStatus(ctx context.Context, dns string, rou
392399 if err := r .client .Patch (ctx , route .K8sObject (), client .MergeFrom (routeOld .K8sObject ())); err != nil {
393400 return fmt .Errorf ("failed to update route status due to err %w" , err )
394401 }
395- routeOld = route .DeepCopy ()
396-
397- route .Status ().UpdateParentRefs (route .Spec ().ParentRefs ()[0 ], config .LatticeGatewayControllerName )
398-
399- // Update listener Status
400- if err := updateRouteListenerStatus (ctx , r .client , route ); err != nil {
401- route .Status ().UpdateRouteCondition (metav1.Condition {
402- Type : string (gwv1beta1 .RouteConditionAccepted ),
403- Status : metav1 .ConditionFalse ,
404- ObservedGeneration : route .K8sObject ().GetGeneration (),
405- Reason : string (gwv1 .RouteReasonNoMatchingParent ),
406- Message : fmt .Sprintf ("Could not match gateway %s: %s" , route .Spec ().ParentRefs ()[0 ].Name , err ),
407- })
408- } else {
409- route .Status ().UpdateRouteCondition (metav1.Condition {
410- Type : string (gwv1beta1 .RouteConditionAccepted ),
411- Status : metav1 .ConditionTrue ,
412- ObservedGeneration : route .K8sObject ().GetGeneration (),
413- Reason : string (gwv1beta1 .RouteReasonAccepted ),
414- Message : fmt .Sprintf ("DNS Name: %s" , dns ),
415- })
416- route .Status ().UpdateRouteCondition (metav1.Condition {
417- Type : string (gwv1beta1 .RouteConditionResolvedRefs ),
418- Status : metav1 .ConditionTrue ,
419- ObservedGeneration : route .K8sObject ().GetGeneration (),
420- Reason : string (gwv1beta1 .RouteReasonResolvedRefs ),
421- Message : fmt .Sprintf ("DNS Name: %s" , dns ),
422- })
423- }
424-
425- if err := r .client .Status ().Patch (ctx , route .K8sObject (), client .MergeFrom (routeOld .K8sObject ())); err != nil {
426- return fmt .Errorf ("failed to update route status due to err %w" , err )
427- }
428402
429403 r .log .Debugf ("Successfully updated route %s-%s with DNS %s" , route .Name (), route .Namespace (), dns )
430404 return nil
@@ -456,3 +430,196 @@ func (r *routeReconciler) validateBackendRefsIpFamilies(ctx context.Context, rou
456430
457431 return nil
458432}
433+
434+ var (
435+ ErrValidation = errors .New ("validation" )
436+ ErrParentRefsNotFound = errors .New ("parentRefs are not found" )
437+ ErrRouteGKNotSupported = errors .New ("route GroupKind is not supported" )
438+ )
439+
440+ // Validation for route spec. Will validate and update route status. Returns error if not valid.
441+ // Validation rules are suppose to be compliant to Gateway API Spec.
442+ //
443+ // https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.RouteConditionType
444+ //
445+ // There are 3 condition types: Accepted, PartiallyInvalid, ResolvedRefs.
446+ // We dont support PartiallyInvalid for now and reject entire route if there is at least one invalid field.
447+ // Accepted type is related to parentRefs, and ResolvedRefs to backendRefs. These 2 are validated independently.
448+ func (r * routeReconciler ) validateRoute (ctx context.Context , route core.Route ) error {
449+ parentRefsAccepted , err := r .validateRouteParentRefs (ctx , route )
450+ if err != nil {
451+ return err
452+ }
453+
454+ resolvedRefsCnd , err := r .validateBackedRefs (ctx , route )
455+ if err != nil {
456+ return err
457+ }
458+
459+ // we need to update each parentRef with backendRef status
460+ parentRefsAcceptedResolvedRefs := make ([]gwv1.RouteParentStatus , len (parentRefsAccepted ))
461+ for i , rps := range parentRefsAccepted {
462+ meta .SetStatusCondition (& rps .Conditions , resolvedRefsCnd )
463+ parentRefsAcceptedResolvedRefs [i ] = rps
464+ }
465+
466+ route .Status ().SetParents (parentRefsAcceptedResolvedRefs )
467+
468+ err = r .client .Status ().Update (ctx , route .K8sObject ())
469+ if err != nil {
470+ return fmt .Errorf ("validate route: %w" , err )
471+ }
472+
473+ if r .hasNotAcceptedCondition (route ) {
474+ return fmt .Errorf ("%w: route has validation errors, see status" , ErrValidation )
475+ }
476+
477+ return nil
478+ }
479+
480+ // checks if route has at least single condition with status = false
481+ func (r * routeReconciler ) hasNotAcceptedCondition (route core.Route ) bool {
482+ rps := route .Status ().Parents ()
483+ for _ , ps := range rps {
484+ for _ , cnd := range ps .Conditions {
485+ if cnd .Status != metav1 .ConditionTrue {
486+ return true
487+ }
488+ }
489+ }
490+ return false
491+ }
492+
493+ // find Gateway by Route and parentRef, returns nil if not found
494+ func (r * routeReconciler ) findRouteParentGw (ctx context.Context , route core.Route , parentRef gwv1beta1.ParentReference ) (* gwv1beta1.Gateway , error ) {
495+ ns := route .Namespace ()
496+ if parentRef .Namespace != nil && * parentRef .Namespace != "" {
497+ ns = string (* parentRef .Namespace )
498+ }
499+ gwName := types.NamespacedName {
500+ Namespace : ns ,
501+ Name : string (parentRef .Name ),
502+ }
503+ gw := & gwv1beta1.Gateway {}
504+ err := r .client .Get (ctx , gwName , gw )
505+ if err != nil {
506+ return nil , client .IgnoreNotFound (err )
507+ }
508+ return gw , nil
509+ }
510+
511+ // Validation rules for route parentRefs
512+ //
513+ // Will ignore status update when:
514+ // - parentRef does not exists, includes when parentRef Kind is not Gateway
515+ //
516+ // If parent GW exists will check:
517+ // - NoMatchingParent: parentRef sectionName and port matches Listener name and port
518+ // - TODO: NoMatchingListenerHostname: listener hostname matches one of route hostnames
519+ // - TODO: NotAllowedByListeners: listener allowedRoutes contains route GroupKind
520+ func (r * routeReconciler ) validateRouteParentRefs (ctx context.Context , route core.Route ) ([]gwv1beta1.RouteParentStatus , error ) {
521+ if len (route .Spec ().ParentRefs ()) == 0 {
522+ return nil , ErrParentRefsNotFound
523+ }
524+
525+ parentStatuses := []gwv1beta1.RouteParentStatus {}
526+ for _ , parentRef := range route .Spec ().ParentRefs () {
527+ gw , err := r .findRouteParentGw (ctx , route , parentRef )
528+ if err != nil {
529+ return nil , err
530+ }
531+ if gw == nil {
532+ continue // ignore status update if gw not found
533+ }
534+
535+ noMatchingParent := true
536+ for _ , listener := range gw .Spec .Listeners {
537+ if parentRef .Port != nil && * parentRef .Port != listener .Port {
538+ continue
539+ }
540+ if parentRef .SectionName != nil && * parentRef .SectionName != listener .Name {
541+ continue
542+ }
543+ noMatchingParent = false
544+ }
545+
546+ parentStatus := gwv1beta1.RouteParentStatus {
547+ ParentRef : parentRef ,
548+ ControllerName : "application-networking.k8s.aws/gateway-api-controller" ,
549+ Conditions : []metav1.Condition {},
550+ }
551+
552+ var cnd metav1.Condition
553+ switch {
554+ case noMatchingParent :
555+ cnd = r .newCondition (route , gwv1beta1 .RouteConditionAccepted , gwv1 .RouteReasonNoMatchingParent , "" )
556+ default :
557+ cnd = r .newCondition (route , gwv1beta1 .RouteConditionAccepted , gwv1beta1 .RouteReasonAccepted , "" )
558+ }
559+ meta .SetStatusCondition (& parentStatus .Conditions , cnd )
560+ parentStatuses = append (parentStatuses , parentStatus )
561+ }
562+
563+ return parentStatuses , nil
564+ }
565+
566+ // set of valid Kinds for Route Backend References
567+ var validBackendKinds = utils .NewSet ("Service" , "ServiceImport" )
568+
569+ // validate route's backed references, will return non-accepted
570+ // condition if at least one backendRef not in a valid state
571+ func (r * routeReconciler ) validateBackedRefs (ctx context.Context , route core.Route ) (metav1.Condition , error ) {
572+ var empty metav1.Condition
573+ for _ , rule := range route .Spec ().Rules () {
574+ for _ , ref := range rule .BackendRefs () {
575+ kind := "Service"
576+ if ref .Kind () != nil {
577+ kind = string (* ref .Kind ())
578+ }
579+ if ! validBackendKinds .Contains (kind ) {
580+ return r .newCondition (route , gwv1beta1 .RouteConditionResolvedRefs , gwv1beta1 .RouteReasonInvalidKind , kind ), nil
581+ }
582+
583+ namespace := route .Namespace ()
584+ if ref .Namespace () != nil {
585+ namespace = string (* ref .Namespace ())
586+ }
587+ objKey := types.NamespacedName {
588+ Namespace : namespace ,
589+ Name : string (ref .Name ()),
590+ }
591+ var obj client.Object
592+
593+ switch kind {
594+ case "Service" :
595+ obj = & corev1.Service {}
596+ case "ServiceImport" :
597+ obj = & anv1alpha1.ServiceImport {}
598+ default :
599+ return empty , fmt .Errorf ("invalid backed end ref kind, must be validated before, kind=%s" , kind )
600+ }
601+ err := r .client .Get (ctx , objKey , obj )
602+ if err != nil {
603+ if apierrors .IsNotFound (err ) {
604+ msg := fmt .Sprintf ("backendRef name: %s" , ref .Name ())
605+ return r .newCondition (route , gwv1beta1 .RouteConditionResolvedRefs , gwv1beta1 .RouteReasonBackendNotFound , msg ), nil
606+ }
607+ }
608+ }
609+ }
610+ return r .newCondition (route , gwv1beta1 .RouteConditionResolvedRefs , gwv1beta1 .RouteReasonResolvedRefs , "" ), nil
611+ }
612+
613+ func (r * routeReconciler ) newCondition (route core.Route , t gwv1beta1.RouteConditionType , reason gwv1beta1.RouteConditionReason , msg string ) metav1.Condition {
614+ status := metav1 .ConditionTrue
615+ if reason != gwv1beta1 .RouteReasonAccepted && reason != gwv1beta1 .RouteReasonResolvedRefs {
616+ status = metav1 .ConditionFalse
617+ }
618+ return metav1.Condition {
619+ Type : string (t ),
620+ Status : status ,
621+ ObservedGeneration : route .K8sObject ().GetGeneration (),
622+ Reason : string (reason ),
623+ Message : msg ,
624+ }
625+ }
0 commit comments