Skip to content

Commit 771efb5

Browse files
authored
update validations and status on route reconciler (#563)
* update validations and status on route reconciler
1 parent 2fe2506 commit 771efb5

File tree

7 files changed

+237
-50
lines changed

7 files changed

+237
-50
lines changed

pkg/controllers/gateway_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func (r *gatewayReconciler) reconcileDelete(ctx context.Context, gw *gwv1beta1.G
191191
}
192192

193193
if httpGw.Name == gw.Name && httpGw.Namespace == gw.Namespace {
194-
return fmt.Errorf("Cannot delete gateway %s/%s - found referencing route %s/%s",
194+
return fmt.Errorf("cannot delete gateway %s/%s - found referencing route %s/%s",
195195
gw.Namespace, gw.Name, route.Namespace(), route.Name())
196196
}
197197
}

pkg/controllers/route_controller.go

Lines changed: 214 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5756
var 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+
}

pkg/controllers/route_controller_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ func TestRouteReconciler_ReconcileCreates(t *testing.T) {
4242
gwv1beta1.AddToScheme(k8sScheme)
4343
addOptionalCRDs(k8sScheme)
4444

45-
k8sClient := testclient.NewClientBuilder().WithScheme(k8sScheme).Build()
45+
k8sClient := testclient.
46+
NewClientBuilder().
47+
WithScheme(k8sScheme).
48+
WithStatusSubresource(&gwv1beta1.HTTPRoute{}).
49+
Build()
4650

4751
gwClass := &gwv1beta1.GatewayClass{
4852
ObjectMeta: metav1.ObjectMeta{

pkg/deploy/lattice/service_network_manager.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ func (m *defaultServiceNetworkManager) CreateOrUpdate(ctx context.Context, servi
193193
return model.ServiceNetworkStatus{}, err
194194
}
195195
if snva != nil {
196-
m.log.Debugf("ServiceNetwork %s already has VPC association %s", serviceNetwork.Spec.Name, snva.Arn)
196+
m.log.Debugf("ServiceNetwork %s already has VPC association %s",
197+
serviceNetwork.Spec.Name, aws.StringValue(snva.Arn))
197198
return model.ServiceNetworkStatus{ServiceNetworkARN: serviceNetworkArn, ServiceNetworkID: serviceNetworkId}, nil
198199
}
199200
}

pkg/model/core/grpcroute.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ func (r *GRPCRoute) Inner() *gwv1alpha2.GRPCRoute {
7979
return &r.r
8080
}
8181

82+
func (r *GRPCRoute) GroupKind() metav1.GroupKind {
83+
return metav1.GroupKind{
84+
Group: gwv1beta1.GroupName,
85+
Kind: "GRPCRoute",
86+
}
87+
}
88+
8289
type GRPCRouteSpec struct {
8390
s gwv1alpha2.GRPCRouteSpec
8491
}

pkg/model/core/httproute.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ func (r *HTTPRoute) Inner() *gwv1beta1.HTTPRoute {
7878
return &r.r
7979
}
8080

81+
func (r *HTTPRoute) GroupKind() metav1.GroupKind {
82+
return metav1.GroupKind{
83+
Group: gwv1beta1.GroupName,
84+
Kind: "HTTPRoute",
85+
}
86+
}
87+
8188
type HTTPRouteSpec struct {
8289
s gwv1beta1.HTTPRouteSpec
8390
}

pkg/model/core/route.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Route interface {
2222
DeletionTimestamp() *metav1.Time
2323
DeepCopy() Route
2424
K8sObject() client.Object
25+
GroupKind() metav1.GroupKind
2526
}
2627

2728
func NewRoute(object client.Object) (Route, error) {

0 commit comments

Comments
 (0)