diff --git a/pkg/controllers/route_controller.go b/pkg/controllers/route_controller.go index 849bab93..8be0e076 100644 --- a/pkg/controllers/route_controller.go +++ b/pkg/controllers/route_controller.go @@ -482,6 +482,14 @@ func (r *routeReconciler) validateRoute(ctx context.Context, route core.Route) e return fmt.Errorf("validate route: %w", err) } + if core.HasAllParentRefsRejected(route) { + r.eventRecorder.Event(route.K8sObject(), corev1.EventTypeWarning, + k8s.RouteEventReasonFailedBuildModel, + "No VPC Lattice resources created. Route's parentRefs rejected by all Gateway listeners due to allowedRoutes policies. Check route status conditions for more detail.") + return fmt.Errorf("%w: route has validation errors, see status", ErrValidation) + } + + // Additional broader validation check for any issues if r.hasNotAcceptedCondition(route) { return fmt.Errorf("%w: route has validation errors, see status", ErrValidation) } @@ -509,8 +517,9 @@ func (r *routeReconciler) hasNotAcceptedCondition(route core.Route) bool { // // If parent GW exists will check: // - NoMatchingParent: parentRef sectionName and port matches Listener name and port +// - NotAllowedByListeners: listener allowedRoutes.namespaces allows route +// - NotAllowedByListeners: listener allowedRoutes.kinds contains route GroupKind // - TODO: NoMatchingListenerHostname: listener hostname matches one of route hostnames -// - TODO: NotAllowedByListeners: listener allowedRoutes contains route GroupKind func (r *routeReconciler) validateRouteParentRefs(ctx context.Context, route core.Route) ([]gwv1.RouteParentStatus, error) { if len(route.Spec().ParentRefs()) == 0 { return nil, ErrParentRefsNotFound @@ -525,6 +534,8 @@ func (r *routeReconciler) validateRouteParentRefs(ctx context.Context, route cor gw := gws[0] for _, parentRef := range route.Spec().ParentRefs() { noMatchingParent := true + notAllowedByAnyMatchingListener := true + for _, listener := range gw.Spec.Listeners { if parentRef.Port != nil && *parentRef.Port != listener.Port { continue @@ -533,6 +544,16 @@ func (r *routeReconciler) validateRouteParentRefs(ctx context.Context, route cor continue } noMatchingParent = false + + allowed, err := core.IsRouteAllowedByListener(ctx, r.client, route, gw, listener) + if err != nil { + return nil, err + } + + if allowed { + notAllowedByAnyMatchingListener = false + break + } } parentStatus := gwv1.RouteParentStatus{ @@ -545,6 +566,9 @@ func (r *routeReconciler) validateRouteParentRefs(ctx context.Context, route cor switch { case noMatchingParent: cnd = r.newCondition(route, gwv1.RouteConditionAccepted, gwv1.RouteReasonNoMatchingParent, "") + case notAllowedByAnyMatchingListener: + cnd = r.newCondition(route, gwv1.RouteConditionAccepted, gwv1.RouteReasonNotAllowedByListeners, + "No matching listeners allow this route. Check Gateway listener allowedRoutes policies") default: cnd = r.newCondition(route, gwv1.RouteConditionAccepted, gwv1.RouteReasonAccepted, "") } diff --git a/pkg/gateway/model_build_lattice_service.go b/pkg/gateway/model_build_lattice_service.go index e2a84733..045bd710 100644 --- a/pkg/gateway/model_build_lattice_service.go +++ b/pkg/gateway/model_build_lattice_service.go @@ -74,6 +74,11 @@ func (t *latticeServiceModelBuildTask) buildModel(ctx context.Context) error { return err } + if modelSvc == nil { + t.log.Debugf(ctx, "Service creation skipped no further processing needed") + return nil + } + err = t.buildListeners(ctx, modelSvc.ID()) if err != nil { return fmt.Errorf("failed to build listener due to %w", err) @@ -103,6 +108,11 @@ func (t *latticeServiceModelBuildTask) buildModel(ctx context.Context) error { } func (t *latticeServiceModelBuildTask) buildLatticeService(ctx context.Context) (*model.Service, error) { + if core.HasAllParentRefsRejected(t.route) { + t.log.Debugf(ctx, "Skipping VPC Lattice Service creation all parentRefs rejected") + return nil, nil + } + var routeType core.RouteType switch t.route.(type) { case *core.HTTPRoute: @@ -141,7 +151,15 @@ func (t *latticeServiceModelBuildTask) buildLatticeService(ctx context.Context) if !standalone { // Standard mode: populate ServiceNetworkNames from parent references + + // For deduping ServiceNetworkNames since 2 parentRefs can point to same ServiceNetwork(Gateway) + serviceNetworkSet := make(map[string]struct{}) for _, parentRef := range t.route.Spec().ParentRefs() { + if !core.IsParentRefAccepted(t.route, parentRef) { + t.log.Debugf(ctx, "Skipping service network association for rejected parentRef %s", parentRef.Name) + continue + } + gw := &gwv1.Gateway{} parentNamespace := t.route.Namespace() if parentRef.Namespace != nil { @@ -153,11 +171,16 @@ func (t *latticeServiceModelBuildTask) buildLatticeService(ctx context.Context) continue } if k8s.IsControlledByLatticeGatewayController(ctx, t.client, gw) { - spec.ServiceNetworkNames = append(spec.ServiceNetworkNames, string(parentRef.Name)) + serviceNetworkSet[string(parentRef.Name)] = struct{}{} } else { t.log.Infof(ctx, "Ignoring route %s because gateway %s is not managed by lattice gateway controller", t.route.Name(), gw.Name) } } + + for serviceNetwork := range serviceNetworkSet { + spec.ServiceNetworkNames = append(spec.ServiceNetworkNames, serviceNetwork) + } + if config.ServiceNetworkOverrideMode { spec.ServiceNetworkNames = []string{config.DefaultServiceNetwork} } diff --git a/pkg/gateway/model_build_lattice_service_test.go b/pkg/gateway/model_build_lattice_service_test.go index 2545802a..529e9944 100644 --- a/pkg/gateway/model_build_lattice_service_test.go +++ b/pkg/gateway/model_build_lattice_service_test.go @@ -86,26 +86,43 @@ func Test_LatticeServiceModelBuild(t *testing.T) { wantErrIsNil: true, gwClass: vpcLatticeGatewayClass, gws: []gwv1.Gateway{vpcLatticeGateway}, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "test", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "test", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, }, }, + Hostnames: []gwv1.Hostname{ + "test1.test.com", + "test2.test.com", + }, }, - Hostnames: []gwv1.Hostname{ - "test1.test.com", - "test2.test.com", + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -124,22 +141,39 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -157,22 +191,39 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewGRPCRoute(gwv1.GRPCRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "test", - }, - Spec: gwv1.GRPCRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewGRPCRoute(gwv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "test", + }, + Spec: gwv1.GRPCRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -190,37 +241,55 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service2", - Namespace: "ns1", - Finalizers: []string{"gateway.k8s.aws/resources"}, - DeletionTimestamp: &now, // <- the important bit - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), - SectionName: &httpSectionName, - }, - }, + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service2", + Namespace: "ns1", + Finalizers: []string{"gateway.k8s.aws/resources"}, + DeletionTimestamp: &now, // <- the important bit }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ { - BackendRef: backendRef1, + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSectionName, }, - { - BackendRef: backendRef2, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef1, + }, + { + BackendRef: backendRef2, + }, }, }, }, }, - }, - }), + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSectionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service2", @@ -257,23 +326,41 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &tlsSectionName, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &tlsSectionName, + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), - SectionName: &tlsSectionName, + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -290,22 +377,39 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "not-a-real-gateway", + Namespace: namespacePtr("default"), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "not-a-real-gateway", + Namespace: namespacePtr("default"), + }, + Conditions: []metav1.Condition{ { - Name: "not-a-real-gateway", - Namespace: namespacePtr("default"), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), wantErrIsNil: false, }, { @@ -332,23 +436,41 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &tlsSectionName, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &tlsSectionName, + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), - SectionName: &tlsSectionName, + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -375,26 +497,55 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + { + Name: "gateway2", + Namespace: namespacePtr("ns2"), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: "gateway2", + Namespace: namespacePtr("ns2"), + }, + Conditions: []metav1.Condition{ { - Name: "gateway2", - Namespace: namespacePtr("ns2"), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -412,22 +563,39 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewTLSRoute(gwv1alpha2.TLSRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1alpha2.TLSRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewTLSRoute(gwv1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1alpha2.TLSRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), }, { name: "Multiple service networks with one different controller", @@ -447,27 +615,44 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - // has two parent refs and one is not managed by lattice - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), - }, - { - Name: "not-lattice", - Namespace: namespacePtr("ns2"), - }, - }, + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", }, - }, - }), + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + // has two parent refs and one is not managed by lattice + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + { + Name: "not-lattice", + Namespace: namespacePtr("ns2"), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service1", @@ -486,25 +671,42 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "standalone-service", - Namespace: "default", - Annotations: map[string]string{ - k8s.StandaloneAnnotation: "true", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "standalone-service", + Namespace: "default", + Annotations: map[string]string{ + k8s.StandaloneAnnotation: "true", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "standalone-service", @@ -533,22 +735,39 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-inherits-standalone", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-inherits-standalone", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "standalone-gateway", + Namespace: namespacePtr("default"), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "standalone-gateway", + Namespace: namespacePtr("default"), + }, + Conditions: []metav1.Condition{ { - Name: "standalone-gateway", - Namespace: namespacePtr("default"), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service-inherits-standalone", @@ -577,25 +796,42 @@ func Test_LatticeServiceModelBuild(t *testing.T) { }, }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-overrides-gateway", - Namespace: "default", - Annotations: map[string]string{ - k8s.StandaloneAnnotation: "false", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-overrides-gateway", + Namespace: "default", + Annotations: map[string]string{ + k8s.StandaloneAnnotation: "false", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "standalone-gateway", + Namespace: namespacePtr("default"), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "standalone-gateway", + Namespace: namespacePtr("default"), + }, + Conditions: []metav1.Condition{ { - Name: "standalone-gateway", - Namespace: namespacePtr("default"), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "service-overrides-gateway", @@ -613,25 +849,42 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "standalone-with-override", - Namespace: "default", - Annotations: map[string]string{ - k8s.StandaloneAnnotation: "true", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "standalone-with-override", + Namespace: "default", + Annotations: map[string]string{ + k8s.StandaloneAnnotation: "true", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "standalone-with-override", @@ -649,28 +902,45 @@ func Test_LatticeServiceModelBuild(t *testing.T) { gws: []gwv1.Gateway{ vpcLatticeGateway, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "standalone-with-hostname", - Namespace: "default", - Annotations: map[string]string{ - k8s.StandaloneAnnotation: "true", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "standalone-with-hostname", + Namespace: "default", + Annotations: map[string]string{ + k8s.StandaloneAnnotation: "true", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, }, }, + Hostnames: []gwv1.Hostname{ + "standalone.example.com", + }, }, - Hostnames: []gwv1.Hostname{ - "standalone.example.com", + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, }, - }, - }), + }) + return route + }(), expected: model.ServiceSpec{ ServiceTagFields: model.ServiceTagFields{ RouteName: "standalone-with-hostname", @@ -1283,25 +1553,42 @@ func Test_LatticeServiceModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotati }{ { name: "HTTPRoute with additional tags annotation", - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service-with-tags", - Namespace: "default", - Annotations: map[string]string{ - k8s.TagsAnnotationKey: "Environment=Prod,Project=ServiceTest,Team=Platform", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-with-tags", + Namespace: "default", + Annotations: map[string]string{ + k8s.TagsAnnotationKey: "Environment=Prod,Project=ServiceTest,Team=Platform", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - Namespace: namespacePtr(vpcLatticeGateway.Namespace), + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expectedAdditionalTags: k8s.Tags{ "Environment": &[]string{"Prod"}[0], "Project": &[]string{"ServiceTest"}[0], @@ -1311,9 +1598,186 @@ func Test_LatticeServiceModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotati }, { name: "HTTPRoute without additional tags annotation", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-no-tags", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedAdditionalTags: nil, + description: "should have nil additional tags when no annotation present in service spec", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + gwv1.Install(k8sSchema) + gwv1alpha2.Install(k8sSchema) + k8sClient := testclient.NewClientBuilder().WithScheme(k8sSchema).Build() + + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGatewayClass.DeepCopy())) + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGateway.DeepCopy())) + + stack := core.NewDefaultStack(core.StackID(k8s.NamespacedName(tt.route.K8sObject()))) + + task := &latticeServiceModelBuildTask{ + log: gwlog.FallbackLogger, + route: tt.route, + stack: stack, + client: k8sClient, + } + + svc, err := task.buildLatticeService(ctx) + assert.NoError(t, err, tt.description) + + assert.Equal(t, tt.expectedAdditionalTags, svc.Spec.AdditionalTags, tt.description) + }) + } +} + +func Test_LatticeServiceModelBuild_ChecksParentRefsStatusBeforeServiceCreation(t *testing.T) { + vpcLatticeGatewayClass := gwv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gwClass", + }, + Spec: gwv1.GatewayClassSpec{ + ControllerName: config.LatticeGatewayControllerName, + }, + } + + vpcLatticeGateway := gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway1", + Namespace: "default", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + }, + } + + namespacePtr := func(ns string) *gwv1.Namespace { + p := gwv1.Namespace(ns) + return &p + } + + tests := []struct { + name string + route core.Route + expectServiceCreated bool + description string + }{ + { + name: "all parentRefs rejected skips service creation", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rejected-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }) + return route + }(), + expectServiceCreated: false, + description: "Service creation should be skipped when all parentRefs are rejected", + }, + { + name: "some parentRefs accepted should create service", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "accepted-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectServiceCreated: true, + description: "Service creation should proceed when some parentRefs are accepted", + }, + { + name: "no parentRefs in status should skip service creation", route: core.NewHTTPRoute(gwv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Name: "service-no-tags", + Name: "no-status-route", Namespace: "default", }, Spec: gwv1.HTTPRouteSpec{ @@ -1327,8 +1791,8 @@ func Test_LatticeServiceModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotati }, }, }), - expectedAdditionalTags: nil, - description: "should have nil additional tags when no annotation present in service spec", + expectServiceCreated: false, + description: "Service creation should be skipped when no parentRefs in status", }, } @@ -1339,7 +1803,6 @@ func Test_LatticeServiceModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotati k8sSchema := runtime.NewScheme() clientgoscheme.AddToScheme(k8sSchema) gwv1.Install(k8sSchema) - gwv1alpha2.Install(k8sSchema) k8sClient := testclient.NewClientBuilder().WithScheme(k8sSchema).Build() assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGatewayClass.DeepCopy())) @@ -1357,7 +1820,252 @@ func Test_LatticeServiceModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotati svc, err := task.buildLatticeService(ctx) assert.NoError(t, err, tt.description) - assert.Equal(t, tt.expectedAdditionalTags, svc.Spec.AdditionalTags, tt.description) + if tt.expectServiceCreated { + assert.NotNil(t, svc, tt.description) + } else { + assert.Nil(t, svc, tt.description) + } + }) + } +} + +func Test_LatticeServiceModelBuild_FilterRejectedParentRefsForServiceNetworkAssociation(t *testing.T) { + vpcLatticeGatewayClass := gwv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gwClass", + }, + Spec: gwv1.GatewayClassSpec{ + ControllerName: config.LatticeGatewayControllerName, + }, + } + + gateway1 := gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway1", + Namespace: "default", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + }, + } + + gateway2 := gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway2", + Namespace: "default", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + }, + } + + namespacePtr := func(ns string) *gwv1.Namespace { + p := gwv1.Namespace(ns) + return &p + } + + tests := []struct { + name string + route core.Route + expectedServiceNetworkNames []string + description string + }{ + { + name: "only accepted parentRefs included in service network association", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-acceptance-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + }, + { + Name: gwv1.ObjectName(gateway2.Name), + Namespace: namespacePtr(gateway2.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway2.Name), + Namespace: namespacePtr(gateway2.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }) + return route + }(), + expectedServiceNetworkNames: []string{"gateway1"}, + description: "Only accepted parentRefs should be included in service network names", + }, + { + name: "all accepted parentRefs included", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "all-accepted-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + }, + { + Name: gwv1.ObjectName(gateway2.Name), + Namespace: namespacePtr(gateway2.Namespace), + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway2.Name), + Namespace: namespacePtr(gateway2.Namespace), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedServiceNetworkNames: []string{"gateway1", "gateway2"}, + description: "All accepted parentRefs should be included in service network names", + }, + { + name: "two accepted parentRefs to same gateway should be deduplicated in service network names", + route: func() core.Route { + httpSection := gwv1.SectionName("http") + httpsSection := gwv1.SectionName("https") + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "duplicate-gateway-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + SectionName: &httpSection, + }, + { + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + SectionName: &httpsSection, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + SectionName: &httpSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(gateway1.Name), + Namespace: namespacePtr(gateway1.Namespace), + SectionName: &httpsSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedServiceNetworkNames: []string{"gateway1"}, + description: "Multiple parentRefs to same gateway should result in deduplicated service network names", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + gwv1.Install(k8sSchema) + k8sClient := testclient.NewClientBuilder().WithScheme(k8sSchema).Build() + + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGatewayClass.DeepCopy())) + assert.NoError(t, k8sClient.Create(ctx, gateway1.DeepCopy())) + assert.NoError(t, k8sClient.Create(ctx, gateway2.DeepCopy())) + + stack := core.NewDefaultStack(core.StackID(k8s.NamespacedName(tt.route.K8sObject()))) + + task := &latticeServiceModelBuildTask{ + log: gwlog.FallbackLogger, + route: tt.route, + stack: stack, + client: k8sClient, + } + + svc, err := task.buildLatticeService(ctx) + assert.NoError(t, err, tt.description) + assert.NotNil(t, svc, tt.description) + assert.Equal(t, tt.expectedServiceNetworkNames, svc.Spec.ServiceNetworkNames, tt.description) }) } } diff --git a/pkg/gateway/model_build_listener.go b/pkg/gateway/model_build_listener.go index 2867c827..47ca1ad1 100644 --- a/pkg/gateway/model_build_listener.go +++ b/pkg/gateway/model_build_listener.go @@ -2,7 +2,6 @@ package gateway import ( "context" - "errors" "fmt" "github.com/aws/aws-sdk-go/aws" @@ -11,6 +10,7 @@ import ( gwv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/aws/aws-application-networking-k8s/pkg/k8s" + "github.com/aws/aws-application-networking-k8s/pkg/model/core" model "github.com/aws/aws-application-networking-k8s/pkg/model/lattice" ) @@ -18,38 +18,10 @@ const ( awsCustomCertARN = "application-networking.k8s.aws/certificate-arn" ) -func (t *latticeServiceModelBuildTask) extractListenerInfo( - ctx context.Context, - parentRef gwv1.ParentReference, - gw *gwv1.Gateway, -) (int64, string, error) { - if parentRef.SectionName != nil { - t.log.Debugf(ctx, "Listener parentRef SectionName is %s", *parentRef.SectionName) - } - - t.log.Debugf(ctx, "Building Listener for Route %s-%s", t.route.Name(), t.route.Namespace()) - // If no SectionName is specified, use the first listener port - if parentRef.SectionName == nil { - if len(gw.Spec.Listeners) == 0 { - return 0, "", errors.New("error building listener, there is NO listeners on GW") - } - listenerPort := int(gw.Spec.Listeners[0].Port) - protocol := gw.Spec.Listeners[0].Protocol - return int64(listenerPort), string(protocol), nil - } - // Find the matching section name - for _, section := range gw.Spec.Listeners { - if section.Name == *parentRef.SectionName { - listenerPort := int(section.Port) - protocol := section.Protocol - if isTLSPassthroughGatewayListener(§ion) { - t.log.Debugf(ctx, "Found TLS passthrough section %v", section.TLS) - protocol = vpclattice.ListenerProtocolTlsPassthrough - } - return int64(listenerPort), string(protocol), nil - } - } - return 0, "", fmt.Errorf("error building listener, no matching sectionName in parentRef for Name %s, Section %s", parentRef.Name, *parentRef.SectionName) +type ListenerConfig struct { + Name string + Port int64 + Protocol string } func isTLSPassthroughGatewayListener(listener *gwv1.Listener) bool { @@ -98,26 +70,64 @@ func (t *latticeServiceModelBuildTask) buildListeners(ctx context.Context, stack return err } + listenersToCreate := make(map[string]ListenerConfig) + for _, parentRef := range t.route.Spec().ParentRefs() { if string(parentRef.Name) != gw.Name { continue } - port, protocol, err := t.extractListenerInfo(ctx, parentRef, gw) - if err != nil { - return err + // Check if this parentRef was accepted during validation + if !core.IsParentRefAccepted(t.route, parentRef) { + t.log.Debugf(ctx, "Skipping VPC Lattice Listener creation for rejected parentRef %s", parentRef.Name) + continue + } + + // Find all gateway listeners that match this parentRef + for _, gwListener := range gw.Spec.Listeners { + if parentRef.Port != nil && *parentRef.Port != gwListener.Port { + continue + } + if parentRef.SectionName != nil && *parentRef.SectionName != gwListener.Name { + continue + } + + allowed, err := core.IsRouteAllowedByListener(ctx, t.client, t.route, gw, gwListener) + if err != nil { + return fmt.Errorf("error checking allowedRoutes policy for listener %s: %w", gwListener.Name, err) + } + if !allowed { + t.log.Debugf(ctx, "Skipping listener %s due to allowedRoutes policy", gwListener.Name) + continue + } + + protocol := string(gwListener.Protocol) + if isTLSPassthroughGatewayListener(&gwListener) { + t.log.Debugf(ctx, "Found TLS passthrough listener %s", gwListener.Name) + protocol = vpclattice.ListenerProtocolTlsPassthrough + } + + listenerName := string(gwListener.Name) + listenersToCreate[listenerName] = ListenerConfig{ + Name: listenerName, + Port: int64(gwListener.Port), + Protocol: protocol, + } } + } - defaultAction, err := t.getListenerDefaultAction(ctx, protocol) + for _, listenerConfig := range listenersToCreate { + defaultAction, err := t.getListenerDefaultAction(ctx, listenerConfig.Protocol) if err != nil { return err } + spec := model.ListenerSpec{ StackServiceId: stackSvcId, K8SRouteName: t.route.Name(), K8SRouteNamespace: t.route.Namespace(), - Port: port, - Protocol: protocol, + Port: listenerConfig.Port, + Protocol: listenerConfig.Protocol, DefaultAction: defaultAction, } diff --git a/pkg/gateway/model_build_listener_test.go b/pkg/gateway/model_build_listener_test.go index 0736f9d1..ff35cf37 100644 --- a/pkg/gateway/model_build_listener_test.go +++ b/pkg/gateway/model_build_listener_test.go @@ -88,31 +88,48 @@ func Test_ListenerModelBuild(t *testing.T) { Protocol: "HTTP", Name: sectionName, }), - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expectedSpec: []model.ListenerSpec{ { StackServiceId: "svc-id", @@ -139,31 +156,48 @@ func Test_ListenerModelBuild(t *testing.T) { Mode: &tlsModeTerminate, }, }), - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expectedSpec: []model.ListenerSpec{ { StackServiceId: "svc-id", @@ -192,49 +226,66 @@ func Test_ListenerModelBuild(t *testing.T) { Mode: &tlsModePassthrough, }, }), - route: core.NewTLSRoute(gwv1alpha2.TLSRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1alpha2.TLSRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, - }, - }, + route: func() core.Route { + route := core.NewTLSRoute(gwv1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", }, - Rules: []gwv1alpha2.TLSRouteRule{ - { - BackendRefs: []gwv1.BackendRef{ + Spec: gwv1alpha2.TLSRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ { - BackendObjectReference: gwv1.BackendObjectReference{ - Name: "k8s-service1", - Kind: &serviceKind, - // No weight specified, default to 1 - }, + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, }, - { - BackendObjectReference: gwv1.BackendObjectReference{ - Name: "k8s-service2", - Kind: &serviceKind, + }, + }, + Rules: []gwv1alpha2.TLSRouteRule{ + { + BackendRefs: []gwv1.BackendRef{ + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "k8s-service1", + Kind: &serviceKind, + // No weight specified, default to 1 + }, }, - Weight: aws.Int32(10), - }, - { - BackendObjectReference: gwv1.BackendObjectReference{ - Name: serviceImportName, - Kind: &serviceImportKind, + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "k8s-service2", + Kind: &serviceKind, + }, + Weight: aws.Int32(10), + }, + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: serviceImportName, + Kind: &serviceImportKind, + }, + Weight: aws.Int32(90), }, - Weight: aws.Int32(90), }, }, }, }, - }, - }), + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), expectedSpec: []model.ListenerSpec{ { StackServiceId: "svc-id", @@ -282,44 +333,61 @@ func Test_ListenerModelBuild(t *testing.T) { Mode: &tlsModePassthrough, }, }), - route: core.NewTLSRoute(gwv1alpha2.TLSRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1alpha2.TLSRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ - { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, - }, - }, + route: func() core.Route { + route := core.NewTLSRoute(gwv1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", }, - Rules: []gwv1alpha2.TLSRouteRule{ - { - BackendRefs: []gwv1.BackendRef{ + Spec: gwv1alpha2.TLSRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ { - BackendObjectReference: gwv1.BackendObjectReference{ - Name: "k8s-service1", - Kind: &serviceKind, - }, + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, }, }, }, - { - BackendRefs: []gwv1.BackendRef{ - { - BackendObjectReference: gwv1.BackendObjectReference{ - Name: "k8s-service2", - Kind: &serviceKind, + Rules: []gwv1alpha2.TLSRouteRule{ + { + BackendRefs: []gwv1.BackendRef{ + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "k8s-service1", + Kind: &serviceKind, + }, + }, + }, + }, + { + BackendRefs: []gwv1.BackendRef{ + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "k8s-service2", + Kind: &serviceKind, + }, }, }, }, }, }, - }, - }), + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), expectedSpec: []model.ListenerSpec{ { StackServiceId: "svc-id", @@ -406,7 +474,7 @@ func Test_ListenerModelBuild(t *testing.T) { }), }, { - name: "No gateway managed by vpc lattice", + name: "No gateway managed by vpc lattice results in no listeners", wantErrIsNil: false, k8sGetGatewayCall: true, gw: gwv1.Gateway{ @@ -415,38 +483,56 @@ func Test_ListenerModelBuild(t *testing.T) { Namespace: "default", }, Spec: gwv1.GatewaySpec{ - GatewayClassName: gwv1.ObjectName("gwClass"), + GatewayClassName: gwv1.ObjectName("non-lattice-controller"), }, }, - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "non-lattice", + SectionName: §ionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: "non-lattice", - SectionName: §ionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "non-lattice", + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), + expectedSpec: []model.ListenerSpec{}, }, { - name: "no section name", - wantErrIsNil: false, + name: "no section name match results in no listeners created", + wantErrIsNil: true, k8sGetGatewayCall: true, gw: vpcLatticeGatewayWithListeners( gwv1.Listener{ @@ -454,31 +540,49 @@ func Test_ListenerModelBuild(t *testing.T) { Protocol: "HTTP", Name: sectionName, }), - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "service1", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: &missingSectionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: &missingSectionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: &missingSectionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), + expectedSpec: []model.ListenerSpec{}, }, } @@ -607,34 +711,51 @@ func Test_ListenerModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotation(t * }{ { name: "HTTPRoute with additional tags annotation", - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "route-with-tags", - Namespace: "default", - Annotations: map[string]string{ - k8s.TagsAnnotationKey: "Environment=Prod,Project=ListenerTest,Team=Platform", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-with-tags", + Namespace: "default", + Annotations: map[string]string{ + k8s.TagsAnnotationKey: "Environment=Prod,Project=ListenerTest,Team=Platform", + }, }, - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expectedAdditionalTags: k8s.Tags{ "Environment": &[]string{"Prod"}[0], "Project": &[]string{"ListenerTest"}[0], @@ -644,31 +765,48 @@ func Test_ListenerModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotation(t * }, { name: "HTTPRoute without additional tags annotation", - route: core.NewHTTPRoute(gwv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "route-no-tags", - Namespace: "default", - }, - Spec: gwv1.HTTPRouteSpec{ - CommonRouteSpec: gwv1.CommonRouteSpec{ - ParentRefs: []gwv1.ParentReference{ + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route-no-tags", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ { - Name: gwv1.ObjectName(vpcLatticeGateway.Name), - SectionName: §ionName, + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: backendRef, + }, + }, }, }, }, - Rules: []gwv1.HTTPRouteRule{ - { - BackendRefs: []gwv1.HTTPBackendRef{ - { - BackendRef: backendRef, - }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + SectionName: §ionName, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, }, }, }, - }, - }), + }) + return route + }(), expectedAdditionalTags: nil, description: "should have nil additional tags when no annotation present in listener spec", }, @@ -712,3 +850,541 @@ func Test_ListenerModelBuild_HTTPRouteWithAndWithoutAdditionalTagsAnnotation(t * }) } } + +func Test_BuildListeners_SkipRejectedParentRefs(t *testing.T) { + var httpSection gwv1.SectionName = "http" + var httpsSection gwv1.SectionName = "https" + var serviceKind gwv1.Kind = "Service" + var backendRef = gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "targetgroup1", + Kind: &serviceKind, + }, + } + + vpcLatticeGatewayClass := gwv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gwClass", + }, + Spec: gwv1.GatewayClassSpec{ + ControllerName: config.LatticeGatewayControllerName, + }, + } + + vpcLatticeGateway := gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway1", + Namespace: "default", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + Listeners: []gwv1.Listener{ + { + Port: 80, + Protocol: "HTTP", + Name: httpSection, + }, + { + Port: 443, + Protocol: "HTTPS", + Name: httpsSection, + }, + }, + }, + } + + namespacePtr := func(ns string) *gwv1.Namespace { + p := gwv1.Namespace(ns) + return &p + } + + tests := []struct { + name string + route core.Route + expectedListenerCount int + expectedListenerPorts []int64 + description string + }{ + { + name: "all parentRefs rejected should skip all listener creation", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "all-rejected-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + {BackendRef: backendRef}, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }) + return route + }(), + expectedListenerCount: 0, + expectedListenerPorts: []int64{}, + description: "All rejected parentRefs should not create any listeners", + }, + { + name: "mixed parentRef acceptance should create listeners only for accepted ones", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-acceptance-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + {BackendRef: backendRef}, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }) + return route + }(), + expectedListenerCount: 1, + expectedListenerPorts: []int64{80}, + description: "Mixed acceptance should create listeners only for accepted parentRefs", + }, + { + name: "all accepted parentRefs should create all listeners", + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "all-accepted-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + { + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + {BackendRef: backendRef}, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gwv1.ParentReference{ + Name: gwv1.ObjectName(vpcLatticeGateway.Name), + Namespace: namespacePtr(vpcLatticeGateway.Namespace), + SectionName: &httpsSection, + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedListenerCount: 2, + expectedListenerPorts: []int64{80, 443}, + description: "All accepted parentRefs should create all corresponding listeners", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := gomock.NewController(t) + defer c.Finish() + ctx := context.TODO() + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + gwv1.Install(k8sSchema) + k8sClient := testclient.NewClientBuilder().WithScheme(k8sSchema).Build() + + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGatewayClass.DeepCopy())) + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGateway.DeepCopy())) + + mockBrTgBuilder := NewMockBackendRefTargetGroupModelBuilder(c) + stack := core.NewDefaultStack(core.StackID(k8s.NamespacedName(tt.route.K8sObject()))) + + task := &latticeServiceModelBuildTask{ + log: gwlog.FallbackLogger, + route: tt.route, + client: k8sClient, + stack: stack, + brTgBuilder: mockBrTgBuilder, + } + + err := task.buildListeners(ctx, "svc-id") + assert.NoError(t, err, tt.description) + + var resListener []*model.Listener + stack.ListResources(&resListener) + assert.Equal(t, tt.expectedListenerCount, len(resListener), tt.description) + + if len(tt.expectedListenerPorts) > 0 { + actualPorts := make([]int64, len(resListener)) + for i, listener := range resListener { + actualPorts[i] = listener.Spec.Port + } + assert.ElementsMatch(t, tt.expectedListenerPorts, actualPorts, tt.description) + } + }) + } +} + +// ParentRef will have accepted status if atleast one listener on gateway allows that route +// We check which listener allows the Route +func Test_BuildListeners_AllowedRoutesFilterIndividualListeners(t *testing.T) { + var httpSection gwv1.SectionName = "http" + var httpsSection gwv1.SectionName = "https" + var tlsSection gwv1.SectionName = "tls" + var serviceKind gwv1.Kind = "Service" + var backendRef = gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: "targetgroup1", + Kind: &serviceKind, + }, + } + + vpcLatticeGatewayClass := gwv1.GatewayClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gwClass", + }, + Spec: gwv1.GatewayClassSpec{ + ControllerName: config.LatticeGatewayControllerName, + }, + } + + namespacePtr := func(ns string) *gwv1.Namespace { + p := gwv1.Namespace(ns) + return &p + } + + tests := []struct { + name string + gateway gwv1.Gateway + route core.Route + expectedListenerCount int + expectedListenerPorts []int64 + description string + }{ + { + name: "HTTPRoute accepted by parentRef but filtered to compatible listeners only", + gateway: gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway1", + Namespace: "default", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + Listeners: []gwv1.Listener{ + { + Port: 80, + Protocol: "HTTP", + Name: httpSection, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + { + Port: 443, + Protocol: "HTTPS", + Name: httpsSection, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + { + Port: 444, + Protocol: "TLS", + Name: tlsSection, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + }, + }, + }, + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "http-route", + Namespace: "default", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "gateway1", + Namespace: namespacePtr("default"), + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + {BackendRef: backendRef}, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "gateway1", + Namespace: namespacePtr("default"), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedListenerCount: 2, + expectedListenerPorts: []int64{80, 443}, + description: "HTTPRoute should create listeners only for compatible protocols (HTTP/HTTPS, not TLS)", + }, + { + name: "HTTPRoute from different namespace with mixed listener policies", + gateway: gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway1", + Namespace: "gw-namespace", + }, + Spec: gwv1.GatewaySpec{ + GatewayClassName: gwv1.ObjectName(vpcLatticeGatewayClass.Name), + Listeners: []gwv1.Listener{ + { + Port: 80, + Protocol: "HTTP", + Name: httpSection, + }, + { + Port: 443, + Protocol: "HTTPS", + Name: httpsSection, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + }, + }, + }, + route: func() core.Route { + route := core.NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mixed-policy-route", + Namespace: "route-namespace", + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: "gateway1", + Namespace: namespacePtr("gw-namespace"), + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + {BackendRef: backendRef}, + }, + }, + }, + }, + }) + route.Status().SetParents([]gwv1.RouteParentStatus{ + { + ParentRef: gwv1.ParentReference{ + Name: "gateway1", + Namespace: namespacePtr("gw-namespace"), + }, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }) + return route + }(), + expectedListenerCount: 1, + expectedListenerPorts: []int64{443}, + description: "Mixed namespace policies should create listeners only for permissive ones", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := gomock.NewController(t) + defer c.Finish() + ctx := context.TODO() + + k8sSchema := runtime.NewScheme() + clientgoscheme.AddToScheme(k8sSchema) + gwv1.Install(k8sSchema) + gwv1alpha2.Install(k8sSchema) + k8sClient := testclient.NewClientBuilder().WithScheme(k8sSchema).Build() + + assert.NoError(t, k8sClient.Create(ctx, vpcLatticeGatewayClass.DeepCopy())) + assert.NoError(t, k8sClient.Create(ctx, tt.gateway.DeepCopy())) + + mockBrTgBuilder := NewMockBackendRefTargetGroupModelBuilder(c) + stack := core.NewDefaultStack(core.StackID(k8s.NamespacedName(tt.route.K8sObject()))) + + task := &latticeServiceModelBuildTask{ + log: gwlog.FallbackLogger, + route: tt.route, + client: k8sClient, + stack: stack, + brTgBuilder: mockBrTgBuilder, + } + + err := task.buildListeners(ctx, "svc-id") + assert.NoError(t, err, tt.description) + + var resListener []*model.Listener + stack.ListResources(&resListener) + assert.Equal(t, tt.expectedListenerCount, len(resListener), tt.description) + + if len(tt.expectedListenerPorts) > 0 { + actualPorts := make([]int64, len(resListener)) + for i, listener := range resListener { + actualPorts[i] = listener.Spec.Port + } + assert.ElementsMatch(t, tt.expectedListenerPorts, actualPorts, tt.description) + } + }) + } +} diff --git a/pkg/model/core/route.go b/pkg/model/core/route.go index 12c3ff46..ba3227d7 100644 --- a/pkg/model/core/route.go +++ b/pkg/model/core/route.go @@ -3,8 +3,11 @@ package core import ( "context" "fmt" + "reflect" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/controller-runtime/pkg/client" gwv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -99,3 +102,91 @@ type HeaderMatch interface { Value() string Equals(headerMatch HeaderMatch) bool } + +// HasAllParentRefsRejected checks if all parentRefs are rejected +func HasAllParentRefsRejected(route Route) bool { + rps := route.Status().Parents() + if len(rps) == 0 { + return true + } + + for _, ps := range rps { + for _, cnd := range ps.Conditions { + if cnd.Type == string(gwv1.RouteConditionAccepted) && cnd.Status == metav1.ConditionTrue { + return false + } + } + } + return true +} + +// IsRouteAllowedByListener checks if route is allowed by listener's namespace and kind policies +// checks allowedRoutes.namespaces (Same, All, Selector) and allowedRoutes.kinds +func IsRouteAllowedByListener(ctx context.Context, k8sClient client.Client, route Route, gw *gwv1.Gateway, listener gwv1.Listener) (bool, error) { + if !isRouteKindAllowedByListener(route, listener) { + return false, nil + } + + if listener.AllowedRoutes != nil && listener.AllowedRoutes.Namespaces != nil && listener.AllowedRoutes.Namespaces.From != nil { + switch *listener.AllowedRoutes.Namespaces.From { + case gwv1.NamespacesFromSame: + return route.Namespace() == gw.Namespace, nil + case gwv1.NamespacesFromAll: + return true, nil + case gwv1.NamespacesFromSelector: + selector, err := metav1.LabelSelectorAsSelector(listener.AllowedRoutes.Namespaces.Selector) + if err != nil { + return false, fmt.Errorf("invalid label selector for listener %s: %w", listener.Name, err) + } + + routeNs := &corev1.Namespace{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: route.Namespace()}, routeNs); err != nil { + return false, fmt.Errorf("failed to get namespace %s for route %s/%s: %w", + route.Namespace(), route.Namespace(), route.Name(), err) + } + return selector.Matches(labels.Set(routeNs.Labels)), nil + default: + // Unknown policy, default to same namespace + return route.Namespace() == gw.Namespace, nil + } + } + return route.Namespace() == gw.Namespace, nil +} + +func isRouteKindAllowedByListener(route Route, listener gwv1.Listener) bool { + routeKind := route.GroupKind().Kind + + if listener.AllowedRoutes != nil && len(listener.AllowedRoutes.Kinds) > 0 { + for _, allowedKind := range listener.AllowedRoutes.Kinds { + if string(allowedKind.Kind) == routeKind { + return true + } + } + return false + } + + // No explicit kinds, use protocol-based defaults + switch listener.Protocol { + case gwv1.HTTPProtocolType: + return routeKind == "HTTPRoute" + case gwv1.HTTPSProtocolType: + return routeKind == "HTTPRoute" || routeKind == "GRPCRoute" + case gwv1.TLSProtocolType: + return routeKind == "TLSRoute" + default: + return false + } +} + +func IsParentRefAccepted(route Route, parentRef gwv1.ParentReference) bool { + for _, parent := range route.Status().Parents() { + if reflect.DeepEqual(parent.ParentRef, parentRef) { + for _, condition := range parent.Conditions { + if condition.Type == string(gwv1.RouteConditionAccepted) { + return condition.Status == metav1.ConditionTrue + } + } + } + } + return false +} diff --git a/pkg/model/core/route_test.go b/pkg/model/core/route_test.go new file mode 100644 index 00000000..e392f27f --- /dev/null +++ b/pkg/model/core/route_test.go @@ -0,0 +1,689 @@ +package core + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestHasAllParentRefsRejected(t *testing.T) { + tests := []struct { + name string + routeStatusParents []gwv1.RouteParentStatus + expected bool + description string + }{ + { + name: "empty_parents", + routeStatusParents: []gwv1.RouteParentStatus{}, + expected: true, + description: "No parents should be considered fully rejected", + }, + { + name: "all_rejected", + routeStatusParents: []gwv1.RouteParentStatus{ + { + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + { + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }, + expected: true, + description: "All rejected parentRefs should return true", + }, + { + name: "some_accepted", + routeStatusParents: []gwv1.RouteParentStatus{ + { + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + { + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }, + expected: false, + description: "Some accepted parentRefs should return false", + }, + { + name: "all_accepted", + routeStatusParents: []gwv1.RouteParentStatus{ + { + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + expected: false, + description: "All accepted parentRefs should return false", + }, + { + name: "no_accepted_condition", + routeStatusParents: []gwv1.RouteParentStatus{ + { + Conditions: []metav1.Condition{ + { + Type: "SomeOtherCondition", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + expected: true, + description: "ParentRefs without Accepted condition should be considered rejected", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + route := &HTTPRoute{ + r: gwv1.HTTPRoute{ + Status: gwv1.HTTPRouteStatus{ + RouteStatus: gwv1.RouteStatus{ + Parents: test.routeStatusParents, + }, + }, + }, + } + + result := HasAllParentRefsRejected(route) + assert.Equal(t, test.expected, result, test.description) + }) + } +} + +func TestIsRouteKindAllowedByListener(t *testing.T) { + tests := []struct { + name string + routeKind string + protocol gwv1.ProtocolType + kinds []gwv1.RouteGroupKind + expected bool + description string + }{ + { + name: "http_route_http_listener_default", + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + kinds: nil, + expected: true, + description: "HTTPRoute should be allowed by HTTP listener with default kinds", + }, + { + name: "http_route_https_listener_default", + routeKind: "HTTPRoute", + protocol: gwv1.HTTPSProtocolType, + kinds: nil, + expected: true, + description: "HTTPRoute should be allowed by HTTPS listener with default kinds", + }, + { + name: "http_route_tls_listener_default", + routeKind: "HTTPRoute", + protocol: gwv1.TLSProtocolType, + kinds: nil, + expected: false, + description: "HTTPRoute should not be allowed by TLS listener with default kinds", + }, + { + name: "grpc_route_http_listener_default", + routeKind: "GRPCRoute", + protocol: gwv1.HTTPProtocolType, + kinds: nil, + expected: false, + description: "GRPCRoute should not be allowed by HTTP listener with default kinds", + }, + { + name: "grpc_route_https_listener_default", + routeKind: "GRPCRoute", + protocol: gwv1.HTTPSProtocolType, + kinds: nil, + expected: true, + description: "GRPCRoute should be allowed by HTTPS listener with default kinds", + }, + { + name: "tls_route_tls_listener_default", + routeKind: "TLSRoute", + protocol: gwv1.TLSProtocolType, + kinds: nil, + expected: true, + description: "TLSRoute should be allowed by TLS listener with default kinds", + }, + { + name: "tls_route_http_listener_default", + routeKind: "TLSRoute", + protocol: gwv1.HTTPProtocolType, + kinds: nil, + expected: false, + description: "TLSRoute should not be allowed by HTTP listener with default kinds", + }, + { + name: "http_route_https_listener_explicit_grpc_only", + routeKind: "HTTPRoute", + protocol: gwv1.HTTPSProtocolType, + kinds: []gwv1.RouteGroupKind{ + {Kind: "GRPCRoute"}, + }, + expected: false, + description: "HTTPRoute should not be allowed by HTTPS listener configured for GRPCRoute only", + }, + { + name: "grpc_route_https_listener_explicit_grpc_only", + routeKind: "GRPCRoute", + protocol: gwv1.HTTPSProtocolType, + kinds: []gwv1.RouteGroupKind{ + {Kind: "GRPCRoute"}, + }, + expected: true, + description: "GRPCRoute should be allowed by HTTPS listener configured for GRPCRoute only", + }, + { + name: "http_route_tls_listener_explicit_http_allowed", + routeKind: "HTTPRoute", + protocol: gwv1.TLSProtocolType, + kinds: []gwv1.RouteGroupKind{ + {Kind: "HTTPRoute"}, + }, + expected: true, + description: "HTTPRoute should be allowed by TLS listener with explicit HTTPRoute kinds", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var route Route + switch test.routeKind { + case "HTTPRoute": + route = NewHTTPRoute(gwv1.HTTPRoute{}) + case "GRPCRoute": + route = NewGRPCRoute(gwv1.GRPCRoute{}) + case "TLSRoute": + route = NewTLSRoute(gwv1alpha2.TLSRoute{}) + } + + listener := gwv1.Listener{ + Protocol: test.protocol, + } + if test.kinds != nil { + listener.AllowedRoutes = &gwv1.AllowedRoutes{ + Kinds: test.kinds, + } + } + + result := isRouteKindAllowedByListener(route, listener) + assert.Equal(t, test.expected, result, test.description) + }) + } +} + +func TestIsRouteAllowedByListener(t *testing.T) { + tests := []struct { + name string + routeNamespace string + gwNamespace string + fromPolicy *gwv1.FromNamespaces + selector *metav1.LabelSelector + nsLabels map[string]string + routeKind string + protocol gwv1.ProtocolType + expected bool + expectError bool + description string + }{ + { + name: "same_namespace_policy_same_ns", + routeNamespace: "test-ns", + gwNamespace: "test-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSame}[0], + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: true, + expectError: false, + description: "Route from same namespace should be allowed with Same policy", + }, + { + name: "same_namespace_policy_diff_ns", + routeNamespace: "route-ns", + gwNamespace: "gw-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSame}[0], + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: false, + expectError: false, + description: "Route from different namespace should not be allowed with Same policy", + }, + { + name: "all_namespace_policy_same_ns", + routeNamespace: "test-ns", + gwNamespace: "test-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: true, + expectError: false, + description: "Route from same namespace should be allowed with All policy", + }, + { + name: "all_namespace_policy_diff_ns", + routeNamespace: "route-ns", + gwNamespace: "gw-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: true, + expectError: false, + description: "Route from different namespace should be allowed with All policy", + }, + { + name: "selector_policy_matching_label", + routeNamespace: "route-ns", + gwNamespace: "gw-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + nsLabels: map[string]string{"env": "prod"}, + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: true, + expectError: false, + description: "Route from namespace with matching label should be allowed", + }, + { + name: "selector_policy_non_matching_label", + routeNamespace: "route-ns", + gwNamespace: "gw-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + nsLabels: map[string]string{"env": "dev"}, + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: false, + expectError: false, + description: "Route from namespace with non-matching label should not be allowed", + }, + { + name: "no_allowed_routes_defaults_to_same", + routeNamespace: "route-ns", + gwNamespace: "gw-ns", + fromPolicy: nil, + routeKind: "HTTPRoute", + protocol: gwv1.HTTPProtocolType, + expected: false, + expectError: false, + description: "No allowedRoutes should default to Same namespace behavior", + }, + { + name: "incompatible_route_kind", + routeNamespace: "test-ns", + gwNamespace: "test-ns", + fromPolicy: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + routeKind: "TLSRoute", + protocol: gwv1.HTTPProtocolType, + expected: false, + expectError: false, + description: "TLSRoute should not be allowed by HTTP listener even with All namespace policy", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + + objs := []client.Object{} + if test.nsLabels != nil { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: test.routeNamespace, + Labels: test.nsLabels, + }, + } + objs = append(objs, ns) + } + + k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build() + + var route Route + switch test.routeKind { + case "HTTPRoute": + route = NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: test.routeNamespace, + }, + }) + case "GRPCRoute": + route = NewGRPCRoute(gwv1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: test.routeNamespace, + }, + }) + case "TLSRoute": + route = NewTLSRoute(gwv1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: test.routeNamespace, + }, + }) + } + + gw := &gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gateway", + Namespace: test.gwNamespace, + }, + } + + listener := gwv1.Listener{ + Name: "test-listener", + Protocol: test.protocol, + } + + if test.fromPolicy != nil || test.selector != nil { + listener.AllowedRoutes = &gwv1.AllowedRoutes{} + if test.fromPolicy != nil || test.selector != nil { + listener.AllowedRoutes.Namespaces = &gwv1.RouteNamespaces{} + if test.fromPolicy != nil { + listener.AllowedRoutes.Namespaces.From = test.fromPolicy + } + if test.selector != nil { + listener.AllowedRoutes.Namespaces.Selector = test.selector + } + } + } + result, err := IsRouteAllowedByListener(context.Background(), k8sClient, route, gw, listener) + + if test.expectError { + assert.Error(t, err, test.description) + } else { + assert.NoError(t, err, test.description) + assert.Equal(t, test.expected, result, test.description) + } + }) + } +} + +func TestIsParentRefAccepted(t *testing.T) { + parentRef1 := gwv1.ParentReference{ + Name: "gateway1", + Namespace: &[]gwv1.Namespace{"default"}[0], + } + parentRef2 := gwv1.ParentReference{ + Name: "gateway2", + Namespace: &[]gwv1.Namespace{"default"}[0], + } + sectionName := gwv1.SectionName("http") + parentRefWithSection := gwv1.ParentReference{ + Name: "gateway1", + Namespace: &[]gwv1.Namespace{"default"}[0], + SectionName: §ionName, + } + + tests := []struct { + name string + routeStatusParents []gwv1.RouteParentStatus + checkParentRef gwv1.ParentReference + expected bool + description string + }{ + { + name: "no_parents_in_status", + routeStatusParents: []gwv1.RouteParentStatus{}, + checkParentRef: parentRef1, + expected: false, + description: "No parents in status should return false", + }, + { + name: "matching_parentref_accepted", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRef1, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + checkParentRef: parentRef1, + expected: true, + description: "Matching parentRef with accepted condition should return true", + }, + { + name: "matching_parentref_rejected", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRef1, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + }, + checkParentRef: parentRef1, + expected: false, + description: "Matching parentRef with rejected condition should return false", + }, + { + name: "non_matching_parentref", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRef1, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + checkParentRef: parentRef2, + expected: false, + description: "Non-matching parentRef should return false", + }, + { + name: "no_accepted_condition", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRef1, + Conditions: []metav1.Condition{ + { + Type: "SomeOtherCondition", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + checkParentRef: parentRef1, + expected: false, + description: "Matching parentRef without accepted condition should return false", + }, + { + name: "parentref_with_section_name", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRefWithSection, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + checkParentRef: parentRefWithSection, + expected: true, + description: "Matching parentRef with sectionName should work correctly", + }, + { + name: "multiple_parents_check_specific", + routeStatusParents: []gwv1.RouteParentStatus{ + { + ParentRef: parentRef1, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionFalse, + }, + }, + }, + { + ParentRef: parentRef2, + Conditions: []metav1.Condition{ + { + Type: string(gwv1.RouteConditionAccepted), + Status: metav1.ConditionTrue, + }, + }, + }, + }, + checkParentRef: parentRef2, + expected: true, + description: "Should find correct parentRef among multiple parents", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + route := &HTTPRoute{ + r: gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: "test-namespace", + }, + Status: gwv1.HTTPRouteStatus{ + RouteStatus: gwv1.RouteStatus{ + Parents: test.routeStatusParents, + }, + }, + }, + } + + result := IsParentRefAccepted(route, test.checkParentRef) + assert.Equal(t, test.expected, result, test.description) + }) + } +} + +func TestIsRouteAllowedByListener_ErrorCases(t *testing.T) { + tests := []struct { + name string + setupClient func() client.Client + description string + }{ + { + name: "invalid_label_selector", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + return fake.NewClientBuilder().WithScheme(scheme).Build() + }, + description: "Invalid label selector should return error", + }, + { + name: "namespace_not_found", + setupClient: func() client.Client { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + return fake.NewClientBuilder().WithScheme(scheme).Build() + }, + description: "Missing namespace should return error", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + k8sClient := test.setupClient() + + route := NewHTTPRoute(gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: "missing-ns", + }, + }) + + gw := &gwv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-gateway", + Namespace: "gw-ns", + }, + } + + var listener gwv1.Listener + if test.name == "invalid_label_selector" { + listener = gwv1.Listener{ + Name: "test-listener", + Protocol: gwv1.HTTPProtocolType, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "", // Invalid empty key + Operator: metav1.LabelSelectorOpIn, + Values: []string{}, + }, + }, + }, + }, + }, + } + } else { + listener = gwv1.Listener{ + Name: "test-listener", + Protocol: gwv1.HTTPProtocolType, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + }, + }, + } + } + + _, err := IsRouteAllowedByListener(context.Background(), k8sClient, route, gw, listener) + assert.Error(t, err, test.description) + }) + } +} diff --git a/test/suites/integration/allowed_routes_test.go b/test/suites/integration/allowed_routes_test.go new file mode 100644 index 00000000..71610ad0 --- /dev/null +++ b/test/suites/integration/allowed_routes_test.go @@ -0,0 +1,739 @@ +package integration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/aws/aws-application-networking-k8s/pkg/model/core" + "github.com/aws/aws-application-networking-k8s/pkg/utils" + "github.com/aws/aws-application-networking-k8s/test/pkg/test" + "github.com/aws/aws-sdk-go/service/vpclattice" +) + +var _ = Describe("AllowedRoutes Test", Ordered, func() { + var ( + diffNS = "diff-namespace" + + deployment *appsv1.Deployment + service *corev1.Service + diffNamespace *corev1.Namespace + httpRoute *gwv1.HTTPRoute + tlsRoute *gwv1alpha2.TLSRoute + + originalGatewaySpec gwv1.GatewaySpec + ) + + BeforeAll(func() { + // Backup common testGateway spec + originalGatewaySpec = *testGateway.Spec.DeepCopy() + + diffNamespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: diffNS}, + } + testFramework.ExpectCreated(ctx, diffNamespace) + + deployment, service = testFramework.NewNginxApp(test.ElasticSearchOptions{ + Name: "inventory-ver1", + Namespace: diffNS, + }) + testFramework.ExpectCreated(ctx, deployment, service) + }) + + Context("Listeners with default policy to allow routes from same namespace", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + }, + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute from different namespace should be rejected", func() { + httpRoute = testFramework.NewHttpRoute(testGateway, service, "Service") + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) + + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(acceptedCondition.Reason).To(Equal("NotAllowedByListeners")) + g.Expect(acceptedCondition.Message).To(ContainSubstring("No matching listeners allow this route")) + }).Should(Succeed()) + + Consistently(func(g Gomega) { + route := core.NewHTTPRoute(*httpRoute) + _, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).To(HaveOccurred()) + }, "30s", "5s").Should(Succeed()) + }) + }) + + Context("Listeners with one allowing routes from default same and other from all namespaces", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + }, + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute from different namespace should be accepted by HTTPS listener allowing routes from all namespaces", func() { + httpRoute = testFramework.NewHttpRoute(testGateway, service, "Service") + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) + + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(acceptedCondition.Reason).To(Equal("Accepted")) + + route := core.NewHTTPRoute(*updatedRoute) + vpcLatticeService, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(vpcLatticeService).ToNot(BeNil()) + + listListenersResp, err := testFramework.LatticeClient.ListListenersWithContext(ctx, &vpclattice.ListListenersInput{ + ServiceIdentifier: vpcLatticeService.Id, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(listListenersResp.Items).To(HaveLen(1)) + + listener := listListenersResp.Items[0] + g.Expect(*listener.Port).To(Equal(int64(443))) + g.Expect(*listener.Protocol).To(Equal("HTTPS")) + }).Should(Succeed()) + }) + }) + + Context("Listeners with namespace selector allowing routes from specific labeled namespaces", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + ns := &corev1.Namespace{} + g.Expect(testFramework.Get(ctx, client.ObjectKey{Name: diffNS}, ns)).To(Succeed()) + if ns.Labels == nil { + ns.Labels = make(map[string]string) + } + ns.Labels["env"] = "prod" + g.Expect(testFramework.Update(ctx, ns)).To(Succeed()) + }).Should(Succeed()) + + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "prod"}, + }, + }, + }, + }, + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSelector}[0], + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"env": "dev"}, + }, + }, + }, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute from prod labeled namespace should be accepted by HTTP listener with matching selector", func() { + httpRoute = testFramework.NewHttpRoute(testGateway, service, "Service") + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) + + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(acceptedCondition.Reason).To(Equal("Accepted")) + + route := core.NewHTTPRoute(*updatedRoute) + vpcLatticeService, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).ToNot(HaveOccurred()) + + listListenersResp, err := testFramework.LatticeClient.ListListenersWithContext(ctx, &vpclattice.ListListenersInput{ + ServiceIdentifier: vpcLatticeService.Id, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(listListenersResp.Items).To(HaveLen(1)) + + listener := listListenersResp.Items[0] + g.Expect(*listener.Port).To(Equal(int64(80))) + g.Expect(*listener.Protocol).To(Equal("HTTP")) + }).Should(Succeed()) + }) + }) + + Context("Both listeners allowing routes from all namespaces", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + { + Name: "http1", + Protocol: gwv1.HTTPProtocolType, + Port: 90, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute with multiple parentRefs should be accepted by both listeners", func() { + httpRoute = &gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "t5-httproute", + Namespace: diffNS, + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + SectionName: &[]gwv1.SectionName{"http"}[0], + }, + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + SectionName: &[]gwv1.SectionName{"http1"}[0], + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(service.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(service.Namespace)}[0], + Kind: &[]gwv1.Kind{"Service"}[0], + Port: &[]gwv1.PortNumber{gwv1.PortNumber(service.Spec.Ports[0].Port)}[0], + }, + }, + }, + }, + }, + }, + }, + } + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(2)) + + for _, parent := range updatedRoute.Status.Parents { + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(acceptedCondition.Reason).To(Equal("Accepted")) + } + + route := core.NewHTTPRoute(*updatedRoute) + vpcLatticeService, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).ToNot(HaveOccurred()) + + listListenersResp, err := testFramework.LatticeClient.ListListenersWithContext(ctx, &vpclattice.ListListenersInput{ + ServiceIdentifier: vpcLatticeService.Id, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(listListenersResp.Items).To(HaveLen(2)) + ports := []int64{} + for _, listener := range listListenersResp.Items { + ports = append(ports, *listener.Port) + } + g.Expect(ports).To(ContainElement(int64(80))) + g.Expect(ports).To(ContainElement(int64(90))) + }).Should(Succeed()) + }) + }) + + Context("Listeners with mixed namespace policies allowing routes from all and same namespace respectively", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromAll}[0], + }, + }, + }, + { + Name: "http1", + Protocol: gwv1.HTTPProtocolType, + Port: 90, + AllowedRoutes: &gwv1.AllowedRoutes{ + Namespaces: &gwv1.RouteNamespaces{ + From: &[]gwv1.FromNamespaces{gwv1.NamespacesFromSame}[0], + }, + }, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute with multiple parentRefs should have one accepted parentRef by all namespace listener and one rejected by same namespace listener", func() { + httpRoute = &gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "t6-httproute", + Namespace: diffNS, + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + SectionName: &[]gwv1.SectionName{"http"}[0], + Port: &[]gwv1.PortNumber{80}[0], + }, + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + SectionName: &[]gwv1.SectionName{"http1"}[0], + Port: &[]gwv1.PortNumber{90}[0], + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(service.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(service.Namespace)}[0], + Kind: &[]gwv1.Kind{"Service"}[0], + Port: &[]gwv1.PortNumber{gwv1.PortNumber(service.Spec.Ports[0].Port)}[0], + }, + }, + }, + }, + }, + }, + }, + } + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(2)) + + var httpParent, http1Parent *gwv1.RouteParentStatus + for i, parent := range updatedRoute.Status.Parents { + if parent.ParentRef.SectionName != nil && *parent.ParentRef.SectionName == "http" { + httpParent = &updatedRoute.Status.Parents[i] + } else if parent.ParentRef.SectionName != nil && *parent.ParentRef.SectionName == "http1" { + http1Parent = &updatedRoute.Status.Parents[i] + } + } + g.Expect(httpParent).ToNot(BeNil()) + httpAcceptedCondition := findCondition(httpParent.Conditions, "Accepted") + g.Expect(httpAcceptedCondition).ToNot(BeNil()) + g.Expect(httpAcceptedCondition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(httpAcceptedCondition.Reason).To(Equal("Accepted")) + + g.Expect(http1Parent).ToNot(BeNil()) + http1AcceptedCondition := findCondition(http1Parent.Conditions, "Accepted") + g.Expect(http1AcceptedCondition).ToNot(BeNil()) + g.Expect(http1AcceptedCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(http1AcceptedCondition.Reason).To(Equal("NotAllowedByListeners")) + + route := core.NewHTTPRoute(*updatedRoute) + vpcLatticeService, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).ToNot(HaveOccurred()) + + listListenersResp, err := testFramework.LatticeClient.ListListenersWithContext(ctx, &vpclattice.ListListenersInput{ + ServiceIdentifier: vpcLatticeService.Id, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(listListenersResp.Items).To(HaveLen(1)) + + listener := listListenersResp.Items[0] + g.Expect(*listener.Port).To(Equal(int64(80))) + g.Expect(*listener.Protocol).To(Equal("HTTP")) + }).Should(Succeed()) + }) + }) + + Context("Listeners with default protocol based kind policy", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80}, + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + }, + { + Name: "tls", + Protocol: gwv1.TLSProtocolType, + Port: 444, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute should be accepted by compatible HTTP and HTTPS listeners but filtered out from TLS listener", func() { + httpRoute = &gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "k1-httproute", + Namespace: testGateway.Namespace, + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(service.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(service.Namespace)}[0], + Kind: &[]gwv1.Kind{"Service"}[0], + Port: &[]gwv1.PortNumber{gwv1.PortNumber(service.Spec.Ports[0].Port)}[0], + }, + }, + }, + }, + }, + }, + }, + } + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(acceptedCondition.Reason).To(Equal("Accepted")) + + route := core.NewHTTPRoute(*updatedRoute) + vpcLatticeService, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).ToNot(HaveOccurred()) + + listListenersResp, err := testFramework.LatticeClient.ListListenersWithContext(ctx, &vpclattice.ListListenersInput{ + ServiceIdentifier: vpcLatticeService.Id, + }) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(listListenersResp.Items).To(HaveLen(2)) + + ports := []int64{} + protocols := []string{} + for _, listener := range listListenersResp.Items { + ports = append(ports, *listener.Port) + protocols = append(protocols, *listener.Protocol) + } + g.Expect(ports).To(ContainElement(int64(80))) + g.Expect(ports).To(ContainElement(int64(443))) + g.Expect(ports).ToNot(ContainElement(int64(444))) + g.Expect(protocols).To(ContainElement("HTTP")) + g.Expect(protocols).To(ContainElement("HTTPS")) + }).Should(Succeed()) + }) + }) + + Context("HTTPS listener configured to allow only GRPCRoute", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + AllowedRoutes: &gwv1.AllowedRoutes{ + Kinds: []gwv1.RouteGroupKind{ + { + Kind: "GRPCRoute", + }, + }, + }, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("HTTPRoute should be rejected by HTTPS listener configured to allow only GRPCRoute", func() { + httpRoute = &gwv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "k2-httproute", + Namespace: testGateway.Namespace, + }, + Spec: gwv1.HTTPRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + }, + }, + }, + Rules: []gwv1.HTTPRouteRule{ + { + BackendRefs: []gwv1.HTTPBackendRef{ + { + BackendRef: gwv1.BackendRef{ + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(service.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(service.Namespace)}[0], + Kind: &[]gwv1.Kind{"Service"}[0], + Port: &[]gwv1.PortNumber{gwv1.PortNumber(service.Spec.Ports[0].Port)}[0], + }, + }, + }, + }, + }, + }, + }, + } + testFramework.ExpectCreated(ctx, httpRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1.HTTPRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(httpRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) + + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(acceptedCondition.Reason).To(Equal("NotAllowedByListeners")) + g.Expect(acceptedCondition.Message).To(ContainSubstring("No matching listeners allow this route")) + }).Should(Succeed()) + + Consistently(func(g Gomega) { + route := core.NewHTTPRoute(*httpRoute) + _, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).To(HaveOccurred()) + }, "30s", "5s").Should(Succeed()) + }) + }) + + Context("HTTP and HTTPS listeners with default kind policies incompatible with TLSRoute", func() { + BeforeEach(func() { + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + + currentGateway.Spec.Listeners = []gwv1.Listener{ + { + Name: "http", + Protocol: gwv1.HTTPProtocolType, + Port: 80, + }, + { + Name: "https", + Protocol: gwv1.HTTPSProtocolType, + Port: 443, + }, + } + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + It("TLSRoute should be rejected by HTTP and HTTPS listeners due to protocol incompatibility", func() { + tlsRoute = &gwv1alpha2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "k4-tlsroute", + Namespace: testGateway.Namespace, + }, + Spec: gwv1alpha2.TLSRouteSpec{ + CommonRouteSpec: gwv1.CommonRouteSpec{ + ParentRefs: []gwv1.ParentReference{ + { + Name: gwv1.ObjectName(testGateway.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(testGateway.Namespace)}[0], + }, + }, + }, + Hostnames: []gwv1alpha2.Hostname{"test.example.com"}, + Rules: []gwv1alpha2.TLSRouteRule{ + { + BackendRefs: []gwv1alpha2.BackendRef{ + { + BackendObjectReference: gwv1.BackendObjectReference{ + Name: gwv1.ObjectName(service.Name), + Namespace: &[]gwv1.Namespace{gwv1.Namespace(service.Namespace)}[0], + Kind: &[]gwv1.Kind{"Service"}[0], + Port: &[]gwv1.PortNumber{gwv1.PortNumber(service.Spec.Ports[0].Port)}[0], + }, + }, + }, + }, + }, + }, + } + testFramework.ExpectCreated(ctx, tlsRoute) + + Eventually(func(g Gomega) { + updatedRoute := &gwv1alpha2.TLSRoute{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(tlsRoute), updatedRoute)).To(Succeed()) + g.Expect(updatedRoute.Status.Parents).To(HaveLen(1)) // Single parentRef + + parent := updatedRoute.Status.Parents[0] + acceptedCondition := findCondition(parent.Conditions, "Accepted") + g.Expect(acceptedCondition).ToNot(BeNil()) + g.Expect(acceptedCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(acceptedCondition.Reason).To(Equal("NotAllowedByListeners")) + g.Expect(acceptedCondition.Message).To(ContainSubstring("No matching listeners allow this route")) + }).Should(Succeed()) + + Consistently(func(g Gomega) { + route := core.NewTLSRoute(*tlsRoute) + _, err := testFramework.LatticeClient.FindService(ctx, utils.LatticeServiceName(route.Name(), route.Namespace())) + g.Expect(err).To(HaveOccurred()) + }, "30s", "5s").Should(Succeed()) + }) + }) + + AfterEach(func() { + if httpRoute != nil { + testFramework.ExpectDeletedThenNotFound(ctx, httpRoute) + httpRoute = nil + } + if tlsRoute != nil { + testFramework.ExpectDeletedThenNotFound(ctx, tlsRoute) + tlsRoute = nil + } + + Eventually(func(g Gomega) { + currentGateway := &gwv1.Gateway{} + g.Expect(testFramework.Get(ctx, client.ObjectKeyFromObject(testGateway), currentGateway)).To(Succeed()) + currentGateway.Spec = *originalGatewaySpec.DeepCopy() + g.Expect(testFramework.Update(ctx, currentGateway)).To(Succeed()) + }).Should(Succeed()) + }) + + AfterAll(func() { + testFramework.ExpectDeletedThenNotFound(ctx, deployment, service, diffNamespace) + }) +}) + +func findCondition(conditions []metav1.Condition, conditionType string) *metav1.Condition { + for _, condition := range conditions { + if condition.Type == conditionType { + return &condition + } + } + return nil +} diff --git a/test/suites/integration/httproute_rule_priority_test.go b/test/suites/integration/httproute_rule_priority_test.go index 3d1de602..1a7dc843 100644 --- a/test/suites/integration/httproute_rule_priority_test.go +++ b/test/suites/integration/httproute_rule_priority_test.go @@ -1,6 +1,9 @@ package integration import ( + "log" + "os" + "github.com/aws/aws-application-networking-k8s/pkg/model/core" "github.com/aws/aws-application-networking-k8s/test/pkg/test" "github.com/aws/aws-sdk-go/service/vpclattice" @@ -10,8 +13,6 @@ import ( appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "log" - "os" gwv1 "sigs.k8s.io/gateway-api/apis/v1" ) @@ -42,7 +43,8 @@ var _ = Describe("HTTPRoute rule priorities", func() { CommonRouteSpec: gwv1.CommonRouteSpec{ ParentRefs: []gwv1.ParentReference{ { - Name: gwv1.ObjectName(testGateway.Name), + Name: gwv1.ObjectName(testGateway.Name), + SectionName: lo.ToPtr(gwv1.SectionName("http")), }, }, },