From c03f3c59800a59db17afabebf52d1d8203ede76a Mon Sep 17 00:00:00 2001 From: Omar Hammami <58956785+puertomontt@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:24:00 -0600 Subject: [PATCH] initial TLSRoute support (#10601) Co-authored-by: Nathan Fudenberg Co-authored-by: changelog-bot --- .github/workflows/pr-kubernetes-tests.yaml | 2 +- Makefile | 2 +- changelog/v1.19.0-beta11/tls-route.yaml | 6 + install/helm/gloo/templates/44-rbac.yaml | 2 + projects/gateway2/controller/controller.go | 31 ++ projects/gateway2/controller/start.go | 9 + .../gateway2/proxy_syncer/proxy_syncer.go | 35 ++ projects/gateway2/query/httproute.go | 46 ++- projects/gateway2/query/indexers.go | 21 ++ projects/gateway2/query/query.go | 41 ++- projects/gateway2/query/query_test.go | 304 +++++++++++++++++ projects/gateway2/reports/reporter.go | 19 +- projects/gateway2/reports/reporter_test.go | 34 ++ projects/gateway2/reports/status.go | 6 + .../listener/gateway_listener_translator.go | 244 ++++++++++---- .../translator/listener/validation.go | 5 + projects/gateway2/wellknown/gwapi.go | 6 +- projects/gloo/pkg/plugins/tcp/plugin.go | 11 + .../gloo/pkg/plugins/tls_inspector/plugin.go | 9 + .../pkg/plugins/tls_inspector/plugin_test.go | 49 ++- .../e2e/features/services/tlsroute/suite.go | 305 ++++++++++++++++++ .../services/tlsroute/testdata/ca-cert.crt | 37 +++ .../testdata/cross-ns-backend-ns.yaml | 4 + .../testdata/cross-ns-backend-service.yaml | 59 ++++ .../tlsroute/testdata/cross-ns-client-ns.yaml | 4 + .../testdata/cross-ns-gateway-and-client.yaml | 41 +++ .../cross-ns-no-refgrant-backend-ns.yaml | 4 + .../cross-ns-no-refgrant-backend-service.yaml | 59 ++++ .../cross-ns-no-refgrant-client-ns.yaml | 4 + ...oss-ns-no-refgrant-gateway-and-client.yaml | 42 +++ .../cross-ns-no-refgrant-tlsroute.yaml | 14 + .../testdata/cross-ns-referencegrant.yaml | 12 + .../tlsroute/testdata/cross-ns-tlsroute.yaml | 14 + .../testdata/multi-backend-service.yaml | 125 +++++++ .../multi-listener-gateway-and-client.yaml | 44 +++ .../services/tlsroute/testdata/multi-ns.yaml | 4 + .../tlsroute/testdata/multi-tlsroute.yaml | 31 ++ .../testdata/single-backend-service.yaml | 61 ++++ .../single-listener-gateway-and-client.yaml | 46 +++ .../services/tlsroute/testdata/single-ns.yaml | 4 + .../tlsroute/testdata/single-tlsroute.yaml | 14 + .../tlsroute/testdata/tls-secret.yaml | 20 ++ .../e2e/features/services/tlsroute/types.go | 163 ++++++++++ test/kubernetes/e2e/tests/k8s_gw_tests.go | 2 + .../kubernetes/testutils/assertions/status.go | 29 ++ 45 files changed, 1939 insertions(+), 85 deletions(-) create mode 100644 changelog/v1.19.0-beta11/tls-route.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/suite.go create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/ca-cert.crt create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-service.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-client-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-gateway-and-client.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-service.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-client-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-gateway-and-client.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-tlsroute.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-referencegrant.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-tlsroute.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/multi-backend-service.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/multi-listener-gateway-and-client.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/multi-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/multi-tlsroute.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/single-backend-service.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/single-listener-gateway-and-client.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/single-ns.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/single-tlsroute.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/testdata/tls-secret.yaml create mode 100644 test/kubernetes/e2e/features/services/tlsroute/types.go diff --git a/.github/workflows/pr-kubernetes-tests.yaml b/.github/workflows/pr-kubernetes-tests.yaml index 2fd0a359803..0a84ae6eda6 100644 --- a/.github/workflows/pr-kubernetes-tests.yaml +++ b/.github/workflows/pr-kubernetes-tests.yaml @@ -95,7 +95,7 @@ jobs: # 2025-02-13: 26m29s - cluster-name: 'cluster-seven' go-test-args: '-v -timeout=25m' - go-test-run-regex: '^TestK8sGateway$$/^CRDCategories$$|^TestK8sGateway$$/^Metrics$$|^TestGloomtlsGatewayEdgeGateway$$|^TestGloomtlsGatewayK8sGateway$$|^TestGlooGatewayEdgeGatewayClearMetrics$$|^TestWatchNamespaceSelector$$' + go-test-run-regex: '^TestK8sGateway$$/^CRDCategories$$|^TestK8sGateway$$/^Metrics$$|^TestGloomtlsGatewayEdgeGateway$$|^TestGloomtlsGatewayK8sGateway$$|^TestGlooGatewayEdgeGatewayClearMetrics$$|^TestWatchNamespaceSelector$$|^TestK8sGateway$$/^TLSRouteServices$$' # In our PR tests, we run the suite of tests using the upper ends of versions that we claim to support # The versions should mirror: https://docs.solo.io/gloo-edge/latest/reference/support/ diff --git a/Makefile b/Makefile index 6828193e0c7..4e752c0c15b 100644 --- a/Makefile +++ b/Makefile @@ -1243,7 +1243,7 @@ $(TEST_ASSET_DIR)/conformance/conformance_test.go: cat $(shell go list -json -m sigs.k8s.io/gateway-api | jq -r '.Dir')/conformance/conformance_test.go >> $@ go fmt $@ -CONFORMANCE_SUPPORTED_FEATURES ?= -supported-features=Gateway,ReferenceGrant,HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRouteResponseHeaderModification,HTTPRoutePortRedirect,HTTPRouteHostRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteRequestMirror +CONFORMANCE_SUPPORTED_FEATURES ?= -supported-features=Gateway,ReferenceGrant,HTTPRoute,HTTPRouteQueryParamMatching,HTTPRouteMethodMatching,HTTPRouteResponseHeaderModification,HTTPRoutePortRedirect,HTTPRouteHostRewrite,HTTPRouteSchemeRedirect,HTTPRoutePathRedirect,HTTPRouteHostRewrite,HTTPRoutePathRewrite,HTTPRouteRequestMirror,TLSRoute CONFORMANCE_SUPPORTED_PROFILES ?= -conformance-profiles=GATEWAY-HTTP CONFORMANCE_REPORT_ARGS ?= -report-output=$(TEST_ASSET_DIR)/conformance/$(VERSION)-report.yaml -organization=solo.io -project=gloo-gateway -version=$(VERSION) -url=github.com/solo-io/gloo -contact=github.com/solo-io/gloo/issues/new/choose CONFORMANCE_ARGS := -gateway-class=gloo-gateway $(CONFORMANCE_SUPPORTED_FEATURES) $(CONFORMANCE_SUPPORTED_PROFILES) $(CONFORMANCE_REPORT_ARGS) diff --git a/changelog/v1.19.0-beta11/tls-route.yaml b/changelog/v1.19.0-beta11/tls-route.yaml new file mode 100644 index 00000000000..e35502ee47c --- /dev/null +++ b/changelog/v1.19.0-beta11/tls-route.yaml @@ -0,0 +1,6 @@ +changelog: + - type: NEW_FEATURE + issueLink: https://github.com/kgateway-dev/kgateway/issues/10074 + resolvesIssue: false + description: >- + "Add support for sig gateway's TLS Routes." diff --git a/install/helm/gloo/templates/44-rbac.yaml b/install/helm/gloo/templates/44-rbac.yaml index 68a4530a9fb..4d193b96131 100644 --- a/install/helm/gloo/templates/44-rbac.yaml +++ b/install/helm/gloo/templates/44-rbac.yaml @@ -13,6 +13,7 @@ rules: - gatewayclasses - gateways - tcproutes + - tlsroutes - httproutes - referencegrants verbs: ["get", "list", "watch"] @@ -50,6 +51,7 @@ rules: - gateways/status - httproutes/status - tcproutes/status + - tlsroutes/status verbs: ["update", "patch"] - apiGroups: - apiextensions.k8s.io diff --git a/projects/gateway2/controller/controller.go b/projects/gateway2/controller/controller.go index 941a5f6610f..92410b15a25 100644 --- a/projects/gateway2/controller/controller.go +++ b/projects/gateway2/controller/controller.go @@ -80,6 +80,7 @@ func NewBaseGatewayController(ctx context.Context, cfg GatewayConfig) error { controllerBuilder.watchGw, controllerBuilder.watchHttpRoute, controllerBuilder.watchTcpRoute, + controllerBuilder.watchTlsRoute, controllerBuilder.watchReferenceGrant, controllerBuilder.watchNamespaces, controllerBuilder.watchHttpListenerOptions, @@ -140,6 +141,12 @@ func (c *controllerBuilder) addIndexes(ctx context.Context) error { } } + if c.cfg.CRDs.Has(wellknown.TLSRouteCRDName) { + if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1a2.TLSRoute{}, query.TlsRouteTargetField, query.IndexerByObjType); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) } @@ -361,6 +368,19 @@ func (c *controllerBuilder) watchTcpRoute(ctx context.Context) error { Complete(reconcile.Func(c.reconciler.ReconcileTcpRoutes)) } +func (c *controllerBuilder) watchTlsRoute(ctx context.Context) error { + if !c.cfg.CRDs.Has(wellknown.TLSRouteCRDName) { + log.FromContext(ctx).Info("TLSRoute type not registered in scheme; skipping TLSRoute controller setup") + return nil + } + + // Proceed to set up the controller for TLSRoute + return ctrl.NewControllerManagedBy(c.cfg.Mgr). + WithEventFilter(predicate.GenerationChangedPredicate{}). + For(&apiv1a2.TLSRoute{}). + Complete(reconcile.Func(c.reconciler.ReconcileTlsRoutes)) +} + func (c *controllerBuilder) watchReferenceGrant(_ context.Context) error { return ctrl.NewControllerManagedBy(c.cfg.Mgr). WithEventFilter(predicate.GenerationChangedPredicate{}). @@ -551,6 +571,17 @@ func (r *controllerReconciler) ReconcileTcpRoutes(ctx context.Context, req ctrl. return ctrl.Result{}, nil } +func (r *controllerReconciler) ReconcileTlsRoutes(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // TODO: consider finding impacted gateways and queue them + // TODO: consider enabling this + // // reconcile this specific route: + // queries := query.NewData(r.cli, r.scheme) + // httproute.TranslateGatewayHTTPRouteRules(queries, hr, nil) + + r.kick(ctx) + return ctrl.Result{}, nil +} + func (r *controllerReconciler) ReconcileReferenceGrants(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // reconcile all things?! https://github.com/solo-io/gloo/issues/9997 r.kick(ctx) diff --git a/projects/gateway2/controller/start.go b/projects/gateway2/controller/start.go index 613b634613e..1246f07ea97 100644 --- a/projects/gateway2/controller/start.go +++ b/projects/gateway2/controller/start.go @@ -290,5 +290,14 @@ func getGatewayCRDs(restConfig *rest.Config) (sets.Set[string], error) { crds.Insert(wellknown.TCPRouteCRDName) } + tlsRouteExists, err := glooschemes.CRDExists(restConfig, gwv1a2.GroupVersion.Group, gwv1a2.GroupVersion.Version, wellknown.TLSRouteKind) + if err != nil { + return nil, err + } + + if tlsRouteExists { + crds.Insert(wellknown.TLSRouteCRDName) + } + return crds, nil } diff --git a/projects/gateway2/proxy_syncer/proxy_syncer.go b/projects/gateway2/proxy_syncer/proxy_syncer.go index f1c38e8bd97..d8c0862e989 100644 --- a/projects/gateway2/proxy_syncer/proxy_syncer.go +++ b/projects/gateway2/proxy_syncer/proxy_syncer.go @@ -247,6 +247,9 @@ func (p glooProxy) Equals(in glooProxy) bool { if !maps.Equal(p.reportMap.TCPRoutes, in.reportMap.TCPRoutes) { return false } + if !maps.Equal(p.reportMap.TLSRoutes, in.reportMap.TLSRoutes) { + return false + } return true } @@ -273,6 +276,9 @@ func (r report) Equals(in report) bool { if !maps.Equal(r.ReportMap.TCPRoutes, in.ReportMap.TCPRoutes) { return false } + if !maps.Equal(r.ReportMap.TLSRoutes, in.ReportMap.TLSRoutes) { + return false + } return true } @@ -491,6 +497,19 @@ func (s *ProxySyncer) Init(ctx context.Context, dbg *krt.DebugHandler) error { // obsGen will stay as-is... maps.Copy(p.reportMap.TCPRoutes[rnn].Parents, rr.Parents) } + + // 4. merge tlsroute parentRefs into RouteReports + for rnn, rr := range p.reportMap.TLSRoutes { + // if we haven't encountered this route, just copy it over completely + old := merged.TLSRoutes[rnn] + if old == nil { + merged.TLSRoutes[rnn] = rr + continue + } + // else, let's merge our parentRefs into the existing map + // obsGen will stay as-is... + maps.Copy(p.reportMap.TLSRoutes[rnn].Parents, rr.Parents) + } } return &report{merged} }) @@ -902,6 +921,12 @@ func (s *ProxySyncer) syncRouteStatus(ctx context.Context, rm reports.ReportMap) return nil } r.Status.RouteStatus = *status + case *gwv1a2.TLSRoute: + status = rm.BuildRouteStatus(ctx, r, s.controllerName) + if status == nil || isRouteStatusEqual(&r.Status.RouteStatus, status) { + return nil + } + r.Status.RouteStatus = *status default: logger.Warnw(fmt.Sprintf("unsupported route type for %s", routeType), "route", route) return nil @@ -930,6 +955,16 @@ func (s *ProxySyncer) syncRouteStatus(ctx context.Context, rm reports.ReportMap) logger.Errorw("all attempts failed at updating TCPRoute status", "error", err, "route", rnn) } } + + // Sync TLSRoute statuses + for rnn := range rm.TLSRoutes { + err := syncStatusWithRetry(wellknown.TLSRouteKind, rnn, func() client.Object { return new(gwv1a2.TLSRoute) }, func(route client.Object) error { + return buildAndUpdateStatus(route, wellknown.TLSRouteKind) + }) + if err != nil { + logger.Errorw("all attempts failed at updating TLSRoute status", "error", err, "route", rnn) + } + } } // syncGatewayStatus will build and update status for all Gateways in a reportMap diff --git a/projects/gateway2/query/httproute.go b/projects/gateway2/query/httproute.go index 90959d8201b..f2064386c66 100644 --- a/projects/gateway2/query/httproute.go +++ b/projects/gateway2/query/httproute.go @@ -128,6 +128,8 @@ func (r *gatewayQueries) GetRouteChain( case *gwv1a2.TCPRoute: backends = r.resolveRouteBackends(ctx, typedRoute) // TODO (danehans): Should TCPRoute delegation support be added in the future? + case *gwv1a2.TLSRoute: + backends = r.resolveRouteBackends(ctx, typedRoute) default: return nil } @@ -151,7 +153,7 @@ func (r *gatewayQueries) allowedRoutes(gw *gwv1.Gateway, l *gwv1.Listener) (func case gwv1.HTTPProtocolType: allowedKinds = []metav1.GroupKind{{Kind: wellknown.HTTPRouteKind, Group: gwv1.GroupName}} case gwv1.TLSProtocolType: - fallthrough + allowedKinds = []metav1.GroupKind{{Kind: wellknown.TLSRouteKind, Group: gwv1a2.GroupName}} case gwv1.TCPProtocolType: allowedKinds = []metav1.GroupKind{{Kind: wellknown.TCPRouteKind, Group: gwv1a2.GroupName}} case gwv1.UDPProtocolType: @@ -228,6 +230,14 @@ func (r *gatewayQueries) resolveRouteBackends(ctx context.Context, obj client.Ob } processBackendRefs(refs) } + case *gwv1a2.TLSRoute: + for _, rule := range rt.Spec.Rules { + var refs []gwv1.BackendObjectReference + for _, ref := range rule.BackendRefs { + refs = append(refs, ref.BackendObjectReference) + } + processBackendRefs(refs) + } default: return out } @@ -365,6 +375,16 @@ func (r *gatewayQueries) GetRoutesForGateway(ctx context.Context, gw *gwv1.Gatew routeListTypes = append(routeListTypes, &gwv1a2.TCPRouteList{}) } + // Conditionally include TLSRouteList + tlsRouteGVK := schema.GroupVersionKind{ + Group: gwv1a2.GroupVersion.Group, + Version: gwv1a2.GroupVersion.Version, + Kind: wellknown.TLSRouteKind, + } + if r.scheme.Recognizes(tlsRouteGVK) { + routeListTypes = append(routeListTypes, &gwv1a2.TLSRouteList{}) + } + var routes []client.Object for _, routeList := range routeListTypes { if err := fetchRoutes(ctx, r, routeList, nns, &routes); err != nil { @@ -406,6 +426,10 @@ func fetchRoutes(ctx context.Context, r *gatewayQueries, routeList client.Object if err := listAndAppendRoutes(list, TcpRouteTargetField); err != nil { return fmt.Errorf("failed to list TCPRoutes: %w", err) } + case *gwv1a2.TLSRouteList: + if err := listAndAppendRoutes(list, TlsRouteTargetField); err != nil { + return fmt.Errorf("failed to list TLSRoutes: %w", err) + } default: return fmt.Errorf("unsupported route list type: %T", list) } @@ -452,12 +476,22 @@ func (r *gatewayQueries) processRoute(ctx context.Context, gw *gwv1.Gateway, rou } anyListenerMatched = true - // If the route is an HTTPRoute, check the hostname intersection + // If the route is an HTTPRoute or TLSRoute, check the hostname intersection var hostnames []string if routeKind == wellknown.HTTPRouteKind { if hr, ok := route.(*gwv1.HTTPRoute); ok { var ok bool - ok, hostnames = hostnameIntersect(&l, hr) + ok, hostnames = hostnameIntersect(&l, hr.Spec.Hostnames) + if !ok { + continue + } + anyHostsMatch = true + } + } + if routeKind == wellknown.TLSRouteKind { + if tr, ok := route.(*gwv1a2.TLSRoute); ok { + var ok bool + ok, hostnames = hostnameIntersect(&l, tr.Spec.Hostnames) if !ok { continue } @@ -532,6 +566,12 @@ func getRouteItems(list client.ObjectList) ([]client.Object, error) { objs = append(objs, &routes.Items[i]) } return objs, nil + case *gwv1a2.TLSRouteList: + var objs []client.Object + for i := range routes.Items { + objs = append(objs, &routes.Items[i]) + } + return objs, nil default: return nil, fmt.Errorf("unsupported route type %T", list) } diff --git a/projects/gateway2/query/indexers.go b/projects/gateway2/query/indexers.go index ecf63d25c88..96b5525015f 100644 --- a/projects/gateway2/query/indexers.go +++ b/projects/gateway2/query/indexers.go @@ -16,6 +16,7 @@ const ( HttpRouteTargetField = "http-route-target" HttpRouteDelegatedLabelSelector = "http-route-delegated-label-selector" TcpRouteTargetField = "tcp-route-target" + TlsRouteTargetField = "tls-route-target" ReferenceGrantFromField = "ref-grant-from" ) @@ -25,6 +26,7 @@ func IterateIndices(f func(client.Object, string, client.IndexerFunc) error) err f(&gwv1.HTTPRoute{}, HttpRouteTargetField, IndexerByObjType), f(&gwv1.HTTPRoute{}, HttpRouteDelegatedLabelSelector, IndexByHTTPRouteDelegationLabelSelector), f(&gwv1a2.TCPRoute{}, TcpRouteTargetField, IndexerByObjType), + f(&gwv1a2.TLSRoute{}, TlsRouteTargetField, IndexerByObjType), f(&gwv1b1.ReferenceGrant{}, ReferenceGrantFromField, IndexerByObjType), ) } @@ -33,6 +35,7 @@ func IterateIndices(f func(client.Object, string, client.IndexerFunc) error) err // // - HTTPRoute // - TCPRoute +// - TLSRoute // - ReferenceGrant func IndexerByObjType(obj client.Object) []string { var results []string @@ -74,6 +77,24 @@ func IndexerByObjType(obj client.Object) []string { } results = append(results, nns.String()) } + case *gwv1a2.TLSRoute: + for _, pRef := range resource.Spec.ParentRefs { + if pRef.Group != nil && *pRef.Group != gwv1a2.GroupName { + continue + } + if pRef.Kind != nil && *pRef.Kind != wellknown.GatewayKind { + continue + } + ns := resolveNs(pRef.Namespace) + if ns == "" { + ns = resource.Namespace + } + nns := types.NamespacedName{ + Namespace: ns, + Name: string(pRef.Name), + } + results = append(results, nns.String()) + } case *gwv1b1.ReferenceGrant: for _, from := range resource.Spec.From { if from.Namespace != "" { diff --git a/projects/gateway2/query/query.go b/projects/gateway2/query/query.go index c7a6ff53351..9ffbaf8f872 100644 --- a/projects/gateway2/query/query.go +++ b/projects/gateway2/query/query.go @@ -221,6 +221,7 @@ func parentRefMatchListener(ref *apiv1.ParentReference, l *apiv1.Listener) bool // // - HTTPRoute // - TCPRoute +// - TLSRoute func getParentRefsForGw(gw *apiv1.Gateway, obj client.Object) []apiv1.ParentReference { var ret []apiv1.ParentReference @@ -237,6 +238,12 @@ func getParentRefsForGw(gw *apiv1.Gateway, obj client.Object) []apiv1.ParentRefe ret = append(ret, pRef) } } + case *apiv1alpha2.TLSRoute: + for _, pRef := range route.Spec.ParentRefs { + if isParentRefForGw(&pRef, gw, route.Namespace) { + ret = append(ret, pRef) + } + } default: // Unsupported route type // TODO (danehans): Should we should capture this as a metric? @@ -267,13 +274,13 @@ func isParentRefForGw(pRef *apiv1.ParentReference, gw *apiv1.Gateway, defaultNs return ns == gw.Namespace && string(pRef.Name) == gw.Name } -func hostnameIntersect(l *apiv1.Listener, hr *apiv1.HTTPRoute) (bool, []string) { +func hostnameIntersect(l *apiv1.Listener, routeHostnames []apiv1.Hostname) (bool, []string) { var hostnames []string - if l == nil || hr == nil { + if l == nil { return false, hostnames } if l.Hostname == nil { - for _, h := range hr.Spec.Hostnames { + for _, h := range routeHostnames { hostnames = append(hostnames, string(h)) } return true, hostnames @@ -281,34 +288,34 @@ func hostnameIntersect(l *apiv1.Listener, hr *apiv1.HTTPRoute) (bool, []string) var listenerHostname string = string(*l.Hostname) if strings.HasPrefix(listenerHostname, "*.") { - if hr.Spec.Hostnames == nil { + if len(routeHostnames) == 0 { return true, []string{listenerHostname} } - for _, hostname := range hr.Spec.Hostnames { + for _, hostname := range routeHostnames { hrHost := string(hostname) if strings.HasSuffix(hrHost, listenerHostname[1:]) { hostnames = append(hostnames, hrHost) } } return len(hostnames) > 0, hostnames - } else { - if len(hr.Spec.Hostnames) == 0 { + } + + if len(routeHostnames) == 0 { + return true, []string{listenerHostname} + } + for _, hostname := range routeHostnames { + hrHost := string(hostname) + if hrHost == listenerHostname { return true, []string{listenerHostname} } - for _, hostname := range hr.Spec.Hostnames { - hrHost := string(hostname) - if hrHost == listenerHostname { - return true, []string{listenerHostname} - } - if strings.HasPrefix(hrHost, "*.") { - if strings.HasSuffix(listenerHostname, hrHost[1:]) { - return true, []string{listenerHostname} - } + if strings.HasPrefix(hrHost, "*.") { + if strings.HasSuffix(listenerHostname, hrHost[1:]) { + return true, []string{listenerHostname} } - // also possible that listener hostname is more specific than the hr hostname } + // also possible that listener hostname is more specific than the hr hostname } return false, nil diff --git a/projects/gateway2/query/query_test.go b/projects/gateway2/query/query_test.go index aeb23382826..6474257fdb8 100644 --- a/projects/gateway2/query/query_test.go +++ b/projects/gateway2/query/query_test.go @@ -813,6 +813,273 @@ var _ = Describe("Query", func() { Expect(err).To(MatchError(query.ErrMissingReferenceGrant)) Expect(backend).To(BeNil()) }) + + It("should match TLSRoutes for Listener", func() { + gw := gw() + gw.Spec.Listeners = []apiv1.Listener{ + { + Name: "foo-tls", + Protocol: apiv1.TLSProtocolType, + }, + } + + tlsRoute := tlsRoute("test-tls-route", gw.Namespace) + tlsRoute.Spec = apiv1a2.TLSRouteSpec{ + CommonRouteSpec: apiv1.CommonRouteSpec{ + ParentRefs: []apiv1.ParentReference{ + { + Name: apiv1.ObjectName(gw.Name), + }, + }, + }, + } + + fakeClient := builder.WithObjects(tlsRoute).Build() + gq := query.NewData(fakeClient, scheme) + routes, err := gq.GetRoutesForGateway(context.Background(), gw) + + Expect(err).NotTo(HaveOccurred()) + Expect(routes.ListenerResults[string(gw.Spec.Listeners[0].Name)].Routes).To(HaveLen(1)) + Expect(routes.ListenerResults[string(gw.Spec.Listeners[0].Name)].Error).NotTo(HaveOccurred()) + }) + + It("should get TLSRoutes in other namespace for listener", func() { + gw := gw() + gw.Spec.Listeners = []apiv1.Listener{ + { + Name: "foo-tls", + Protocol: apiv1.TLSProtocolType, + AllowedRoutes: &apiv1.AllowedRoutes{ + Namespaces: &apiv1.RouteNamespaces{ + From: ptr.To(apiv1.NamespacesFromAll), + }, + }, + }, + } + + tlsRoute := tlsRoute("test-tls-route", "other-ns") + tlsRoute.Spec = apiv1a2.TLSRouteSpec{ + CommonRouteSpec: apiv1.CommonRouteSpec{ + ParentRefs: []apiv1.ParentReference{ + { + Name: apiv1.ObjectName(gw.Name), + Namespace: ptr.To(apiv1.Namespace(gw.Namespace)), + }, + }, + }, + } + + fakeClient := builder.WithObjects(tlsRoute).Build() + gq := query.NewData(fakeClient, scheme) + routes, err := gq.GetRoutesForGateway(context.Background(), gw) + + Expect(err).NotTo(HaveOccurred()) + Expect(routes.ListenerResults["foo-tls"].Error).NotTo(HaveOccurred()) + Expect(routes.ListenerResults["foo-tls"].Routes).To(HaveLen(1)) + }) + + It("should error when listeners don't match TLSRoute", func() { + gw := gw() + gw.Spec.Listeners = []apiv1.Listener{ + { + Name: "foo-tls", + Protocol: apiv1.TLSProtocolType, + Port: 8080, + }, + { + Name: "bar-tls", + Protocol: apiv1.TLSProtocolType, + Port: 8081, + }, + } + + tlsRoute := tlsRoute("test-tls-route", gw.Namespace) + var badPort apiv1.PortNumber = 9999 + tlsRoute.Spec = apiv1a2.TLSRouteSpec{ + CommonRouteSpec: apiv1.CommonRouteSpec{ + ParentRefs: []apiv1.ParentReference{ + { + Name: apiv1.ObjectName(gw.Name), + Port: &badPort, + }, + }, + }, + } + + fakeClient := builder.WithObjects(tlsRoute).Build() + gq := query.NewData(fakeClient, scheme) + routes, err := gq.GetRoutesForGateway(context.Background(), gw) + + Expect(err).NotTo(HaveOccurred()) + Expect(routes.RouteErrors).To(HaveLen(1)) + Expect(routes.RouteErrors[0].Error.E).To(MatchError(query.ErrNoMatchingParent)) + Expect(routes.RouteErrors[0].Error.Reason).To(Equal(apiv1.RouteReasonNoMatchingParent)) + Expect(routes.RouteErrors[0].ParentRef).To(Equal(tlsRoute.Spec.ParentRefs[0])) + }) + + It("should error when listener does not allow TLSRoute kind", func() { + gw := gw() + gw.Spec.Listeners = []apiv1.Listener{ + { + Name: "foo-tls", + Protocol: apiv1.TLSProtocolType, + AllowedRoutes: &apiv1.AllowedRoutes{ + Kinds: []apiv1.RouteGroupKind{{Kind: "FakeKind"}}, + }, + }, + } + + tlsRoute := tlsRoute("test-tls-route", gw.Namespace) + tlsRoute.Spec = apiv1a2.TLSRouteSpec{ + CommonRouteSpec: apiv1.CommonRouteSpec{ + ParentRefs: []apiv1.ParentReference{ + { + Name: apiv1.ObjectName(gw.Name), + }, + }, + }, + } + + fakeClient := builder.WithObjects(tlsRoute).Build() + gq := query.NewData(fakeClient, scheme) + + routes, err := gq.GetRoutesForGateway(context.Background(), gw) + Expect(err).NotTo(HaveOccurred()) + Expect(routes.RouteErrors).To(HaveLen(1)) + Expect(routes.RouteErrors[0].Error.E).To(MatchError(query.ErrNotAllowedByListeners)) + }) + + It("should allow TLSRoute for one listener", func() { + gw := gw() + gw.Spec.Listeners = []apiv1.Listener{ + { + Name: "foo-tls", + Protocol: apiv1.TLSProtocolType, + AllowedRoutes: &apiv1.AllowedRoutes{ + Kinds: []apiv1.RouteGroupKind{{Kind: wellknown.TLSRouteKind}}, + }, + }, + { + Name: "bar", + Protocol: apiv1.TLSProtocolType, + AllowedRoutes: &apiv1.AllowedRoutes{ + Kinds: []apiv1.RouteGroupKind{{Kind: "FakeKind"}}, + }, + }, + } + + tlsRoute := tlsRoute("test-tls-route", gw.Namespace) + tlsRoute.Spec = apiv1a2.TLSRouteSpec{ + CommonRouteSpec: apiv1.CommonRouteSpec{ + ParentRefs: []apiv1.ParentReference{ + { + Name: apiv1.ObjectName(gw.Name), + }, + }, + }, + } + + fakeClient := builder.WithObjects(tlsRoute).Build() + gq := query.NewData(fakeClient, scheme) + + routes, err := gq.GetRoutesForGateway(context.Background(), gw) + Expect(err).NotTo(HaveOccurred()) + Expect(routes.RouteErrors).To(BeEmpty()) + Expect(routes.ListenerResults["foo-tls"].Routes).To(HaveLen(1)) + Expect(routes.ListenerResults["bar"].Routes).To(BeEmpty()) + }) + + It("should get service from same namespace with TlSRoute", func() { + fakeClient := fake.NewFakeClient(svc("default")) + + gq := query.NewData(fakeClient, scheme) + ref := &apiv1.BackendObjectReference{ + Name: "foo", + } + + fromTLSRoute := tlsRoute("test-tls-route", "default") + + backend, err := gq.GetBackendForRef(context.Background(), tofrom(fromTLSRoute), ref) + Expect(err).NotTo(HaveOccurred()) + Expect(backend).NotTo(BeNil()) + Expect(backend.GetName()).To(Equal("foo")) + Expect(backend.GetNamespace()).To(Equal("default")) + }) + + It("should get service from different ns with TLSRoute if we have a ref grant", func() { + rg := refGrantForTLSRoute() + fakeClient := builder.WithObjects(svc("default2"), rg).Build() + gq := query.NewData(fakeClient, scheme) + ref := &apiv1.BackendObjectReference{ + Name: "foo", + Namespace: nsptr("default2"), + } + + fromTLSRoute := tlsRoute("test-tls-route", "default") + + backend, err := gq.GetBackendForRef(context.Background(), tofrom(fromTLSRoute), ref) + Expect(err).NotTo(HaveOccurred()) + Expect(backend).NotTo(BeNil()) + Expect(backend.GetName()).To(Equal("foo")) + Expect(backend.GetNamespace()).To(Equal("default2")) + }) + + It("should fail getting a service from different ns with TLSRoute when no ref grant", func() { + fakeClient := builder.WithObjects(svc("default2")).Build() + gq := query.NewData(fakeClient, scheme) + ref := &apiv1.BackendObjectReference{ + Name: "foo", + Namespace: nsptr("default2"), + } + + fromTLSRoute := tlsRoute("test-tls-route", "default") + + backend, err := gq.GetBackendForRef(context.Background(), tofrom(fromTLSRoute), ref) + Expect(err).To(MatchError(query.ErrMissingReferenceGrant)) + Expect(backend).To(BeNil()) + }) + + It("should fail getting a service with TLSRoute when ref grant has wrong from", func() { + rg := &apiv1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default2", + Name: "foo", + }, + Spec: apiv1beta1.ReferenceGrantSpec{ + From: []apiv1beta1.ReferenceGrantFrom{ + { + Group: apiv1.Group(apiv1a2.GroupName), + Kind: apiv1.Kind("NotTLSRoute"), + Namespace: apiv1.Namespace("default"), + }, + { + Group: apiv1.Group(apiv1a2.GroupName), + Kind: apiv1.Kind("TLSRoute"), + Namespace: apiv1.Namespace("default2"), + }, + }, + To: []apiv1beta1.ReferenceGrantTo{ + { + Group: apiv1.Group("core"), + Kind: apiv1.Kind("Service"), + }, + }, + }, + } + fakeClient := builder.WithObjects(rg, svc("default2")).Build() + + gq := query.NewData(fakeClient, scheme) + ref := &apiv1.BackendObjectReference{ + Name: "foo", + Namespace: nsptr("default2"), + } + + fromTLSRoute := tlsRoute("test-tls-route", "default") + + backend, err := gq.GetBackendForRef(context.Background(), tofrom(fromTLSRoute), ref) + Expect(err).To(MatchError(query.ErrMissingReferenceGrant)) + Expect(backend).To(BeNil()) + }) }) }) @@ -917,6 +1184,19 @@ func tcpRoute(name, ns string) *apiv1a2.TCPRoute { } } +func tlsRoute(name, ns string) *apiv1a2.TLSRoute { + return &apiv1a2.TLSRoute{ + TypeMeta: metav1.TypeMeta{ + Kind: wellknown.TLSRouteKind, + APIVersion: apiv1a2.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + } +} + func nsptr(s string) *apiv1.Namespace { var ns apiv1.Namespace = apiv1.Namespace(s) return &ns @@ -945,3 +1225,27 @@ func refGrantForTCPRoute() *apiv1beta1.ReferenceGrant { }, } } + +func refGrantForTLSRoute() *apiv1beta1.ReferenceGrant { + return &apiv1beta1.ReferenceGrant{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default2", + Name: "foo", + }, + Spec: apiv1beta1.ReferenceGrantSpec{ + From: []apiv1beta1.ReferenceGrantFrom{ + { + Group: apiv1.Group(apiv1a2.GroupName), + Kind: apiv1.Kind("TLSRoute"), + Namespace: apiv1.Namespace("default"), + }, + }, + To: []apiv1beta1.ReferenceGrantTo{ + { + Group: apiv1.Group("core"), + Kind: apiv1.Kind("Service"), + }, + }, + }, + } +} diff --git a/projects/gateway2/reports/reporter.go b/projects/gateway2/reports/reporter.go index 8df8d7aac08..e5710c50928 100644 --- a/projects/gateway2/reports/reporter.go +++ b/projects/gateway2/reports/reporter.go @@ -16,6 +16,7 @@ type ReportMap struct { Gateways map[types.NamespacedName]*GatewayReport HTTPRoutes map[types.NamespacedName]*RouteReport TCPRoutes map[types.NamespacedName]*RouteReport + TLSRoutes map[types.NamespacedName]*RouteReport } type GatewayReport struct { @@ -45,13 +46,15 @@ type ParentRefKey struct { } func NewReportMap() ReportMap { - gr := make(map[types.NamespacedName]*GatewayReport) - hr := make(map[types.NamespacedName]*RouteReport) - tr := make(map[types.NamespacedName]*RouteReport) + gateways := make(map[types.NamespacedName]*GatewayReport) + httpRoutes := make(map[types.NamespacedName]*RouteReport) + tcpRoutes := make(map[types.NamespacedName]*RouteReport) + tlsRoutes := make(map[types.NamespacedName]*RouteReport) return ReportMap{ - Gateways: gr, - HTTPRoutes: hr, - TCPRoutes: tr, + Gateways: gateways, + HTTPRoutes: httpRoutes, + TCPRoutes: tcpRoutes, + TLSRoutes: tlsRoutes, } } @@ -87,6 +90,8 @@ func (r *ReportMap) route(obj client.Object) *RouteReport { return r.HTTPRoutes[key] case *gwv1alpha2.TCPRoute: return r.TCPRoutes[key] + case *gwv1alpha2.TLSRoute: + return r.TLSRoutes[key] default: contextutils.LoggerFrom(context.TODO()).Warnf("Unsupported route type: %T", obj) return nil @@ -105,6 +110,8 @@ func (r *ReportMap) newRouteReport(obj client.Object) *RouteReport { r.HTTPRoutes[key] = rr case *gwv1alpha2.TCPRoute: r.TCPRoutes[key] = rr + case *gwv1alpha2.TLSRoute: + r.TLSRoutes[key] = rr default: contextutils.LoggerFrom(context.TODO()).Warnf("Unsupported route type: %T", obj) return nil diff --git a/projects/gateway2/reports/reporter_test.go b/projects/gateway2/reports/reporter_test.go index 700d61f7959..18be1cb7b8a 100644 --- a/projects/gateway2/reports/reporter_test.go +++ b/projects/gateway2/reports/reporter_test.go @@ -129,6 +129,7 @@ var _ = Describe("Reporting Infrastructure", func() { }, Entry("regular httproute", httpRoute()), Entry("regular tcproute", tcpRoute()), + Entry("regular tlsroute", tlsRoute()), Entry("delegatee route", delegateeRoute()), ) @@ -153,6 +154,7 @@ var _ = Describe("Reporting Infrastructure", func() { }, Entry("regular httproute", httpRoute(), parentRef()), Entry("regular tcproute", tcpRoute(), parentRef()), + Entry("regular tlsroute", tlsRoute(), parentRef()), Entry("delegatee route", delegateeRoute(), parentRouteRef()), ) @@ -182,6 +184,7 @@ var _ = Describe("Reporting Infrastructure", func() { }, Entry("regular httproute", httpRoute(), parentRef()), Entry("regular tcproute", tcpRoute(), parentRef()), + Entry("regular tlsroute", tlsRoute(), parentRef()), Entry("delegatee route", delegateeRoute(), parentRouteRef()), ) @@ -208,6 +211,8 @@ var _ = Describe("Reporting Infrastructure", func() { route.Status.RouteStatus = *status case *gwv1a2.TCPRoute: route.Status.RouteStatus = *status + case *gwv1a2.TLSRoute: + route.Status.RouteStatus = *status default: Fail(fmt.Sprintf("unsupported route type: %T", obj)) } @@ -225,6 +230,7 @@ var _ = Describe("Reporting Infrastructure", func() { Entry("regular httproute", httpRoute()), Entry("delegatee route", delegateeRoute()), Entry("regular tcproute", tcpRoute()), + Entry("regular tlsroute", tlsRoute()), ) DescribeTable("should correctly handle multiple ParentRefs on a route", @@ -239,6 +245,10 @@ var _ = Describe("Reporting Infrastructure", func() { route.Spec.ParentRefs = append(route.Spec.ParentRefs, gwv1.ParentReference{ Name: "additional-gateway", }) + case *gwv1a2.TLSRoute: + route.Spec.ParentRefs = append(route.Spec.ParentRefs, gwv1.ParentReference{ + Name: "additional-gateway", + }) default: Fail(fmt.Sprintf("unsupported route type: %T", obj)) } @@ -261,6 +271,7 @@ var _ = Describe("Reporting Infrastructure", func() { }, Entry("regular HTTPRoute", httpRoute()), Entry("regular TCPRoute", tcpRoute()), + Entry("regular TLSRoute", tlsRoute()), ) DescribeTable("should correctly associate multiple routes with shared and separate listeners", @@ -274,6 +285,8 @@ var _ = Describe("Reporting Infrastructure", func() { r1.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener1.Name)) case *gwv1a2.TCPRoute: r1.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener1.Name)) + case *gwv1a2.TLSRoute: + r1.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener1.Name)) } // Assign the second listener to the second route's parent ref @@ -282,6 +295,8 @@ var _ = Describe("Reporting Infrastructure", func() { r2.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener2.Name)) case *gwv1a2.TCPRoute: r2.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener2.Name)) + case *gwv1a2.TLSRoute: + r2.Spec.ParentRefs[0].SectionName = ptr.To(gwv1.SectionName(listener2.Name)) } rm := reports.NewReportMap() @@ -310,6 +325,11 @@ var _ = Describe("Reporting Infrastructure", func() { gwv1.Listener{Name: "foo-tcp", Protocol: gwv1.TCPProtocolType}, gwv1.Listener{Name: "bar-tcp", Protocol: gwv1.TCPProtocolType}, ), + Entry("TLSRoutes with shared and separate listeners", + tlsRoute(), tlsRoute(), + gwv1.Listener{Name: "foo-tls", Protocol: gwv1.TLSProtocolType}, + gwv1.Listener{Name: "bar-tls", Protocol: gwv1.TLSProtocolType}, + ), ) }) @@ -321,6 +341,8 @@ var _ = Describe("Reporting Infrastructure", func() { r.Spec.ParentRefs = nil case *gwv1a2.TCPRoute: r.Spec.ParentRefs = nil + case *gwv1a2.TLSRoute: + r.Spec.ParentRefs = nil } rm := reports.NewReportMap() @@ -335,6 +357,7 @@ var _ = Describe("Reporting Infrastructure", func() { }, Entry("HTTPRoute with missing parent reference", httpRoute()), Entry("TCPRoute with missing parent reference", tcpRoute()), + Entry("TLSRoute with missing parent reference", tlsRoute()), ) }) @@ -360,6 +383,17 @@ func tcpRoute() client.Object { return route } +func tlsRoute() client.Object { + route := &gwv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "route", + Namespace: "default", + }, + } + route.Spec.CommonRouteSpec.ParentRefs = append(route.Spec.CommonRouteSpec.ParentRefs, *parentRef()) + return route +} + func parentRef() *gwv1.ParentReference { return &gwv1.ParentReference{ Name: "parent", diff --git a/projects/gateway2/reports/status.go b/projects/gateway2/reports/status.go index b241413b69b..4f913ccf588 100644 --- a/projects/gateway2/reports/status.go +++ b/projects/gateway2/reports/status.go @@ -102,6 +102,12 @@ func (r *ReportMap) BuildRouteStatus(ctx context.Context, obj client.Object, cNa if len(parentRefs) == 0 { parentRefs = append(parentRefs, routeReport.parentRefs()...) } + case *gwv1a2.TLSRoute: + existingStatus = route.Status.RouteStatus + parentRefs = append(parentRefs, route.Spec.ParentRefs...) + if len(parentRefs) == 0 { + parentRefs = append(parentRefs, routeReport.parentRefs()...) + } default: contextutils.LoggerFrom(ctx).Error(fmt.Errorf("unsupported route type %T", obj), "failed to build route status") return nil diff --git a/projects/gateway2/translator/listener/gateway_listener_translator.go b/projects/gateway2/translator/listener/gateway_listener_translator.go index 24aae35db75..48f3aad922f 100644 --- a/projects/gateway2/translator/listener/gateway_listener_translator.go +++ b/projects/gateway2/translator/listener/gateway_listener_translator.go @@ -96,6 +96,8 @@ func (ml *MergedListeners) AppendListener( // TODO default handling case gwv1.TCPProtocolType: ml.AppendTcpListener(listener, routes, reporter) + case gwv1.TLSProtocolType: + ml.AppendTlsListener(listener, routes, reporter) default: return eris.Errorf("unsupported protocol: %v", listener.Protocol) } @@ -342,6 +344,70 @@ func getWeight(backendRef gwv1.BackendRef) *wrapperspb.UInt32Value { return &wrapperspb.UInt32Value{Value: 1} } +func (ml *MergedListeners) AppendTlsListener( + listener gwv1.Listener, + routeInfos []*query.RouteInfo, + reporter reports.ListenerReporter, +) { + var validRouteInfos []*query.RouteInfo + + for _, routeInfo := range routeInfos { + tRoute, ok := routeInfo.Object.(*gwv1a2.TLSRoute) + if !ok { + continue + } + + if len(tRoute.Spec.ParentRefs) == 0 { + contextutils.LoggerFrom(context.Background()).Warnf( + "No parent references found for TLSRoute %s", tRoute.Name, + ) + continue + } + + validRouteInfos = append(validRouteInfos, routeInfo) + } + + // If no valid routes are found, do not create a listener + if len(validRouteInfos) == 0 { + contextutils.LoggerFrom(context.Background()).Errorf( + "No valid routes found for listener %s", listener.Name, + ) + return + } + + parent := tcpFilterChainParent{ + gatewayListenerName: string(listener.Name), + routesWithHosts: validRouteInfos, + } + + fc := tcpFilterChain{ + parents: []tcpFilterChainParent{parent}, + tls: listener.TLS, + sniDomain: listener.Hostname, + } + listenerName := string(listener.Name) + finalPort := gwv1.PortNumber(ports.TranslatePort(uint16(listener.Port))) + + for _, lis := range ml.Listeners { + if lis.port == finalPort { + // concatenate the names on the parent output listener + lis.name += "~" + listenerName + lis.TcpFilterChains = append(lis.TcpFilterChains, fc) + return + } + } + + // create a new filter chain for the listener + ml.Listeners = append(ml.Listeners, &MergedListener{ + name: listenerName, + gatewayNamespace: ml.GatewayNamespace, + port: finalPort, + TcpFilterChains: []tcpFilterChain{fc}, + listenerReporter: reporter, + listener: listener, + }) +} + func (ml *MergedListeners) translateListeners( ctx context.Context, pluginRegistry registry.PluginRegistry, @@ -446,7 +512,14 @@ func (ml *MergedListener) TranslateListener( // Translate TCP listeners (if any exist) for _, tfc := range ml.TcpFilterChains { - if tcpListener := tfc.translateTcpFilterChain(ml.listener, reporter); tcpListener != nil { + if tcpListener := tfc.translateTcpFilterChain( + ctx, + ml.listener, + reporter, + ml.listenerReporter, + queries, + ml.gatewayNamespace, + ); tcpListener != nil { matchedTcpListeners = append(matchedTcpListeners, &v1.MatchedTcpListener{ TcpListener: tcpListener, }) @@ -491,7 +564,9 @@ func (ml *MergedListener) TranslateListener( // (with distinct filter chains). In the case where no Gateway listener merging takes place, every listener // will use a Gloo AggregatedListener with one TCP filter chain. type tcpFilterChain struct { - parents []tcpFilterChainParent + parents []tcpFilterChainParent + tls *gwv1.GatewayTLSConfig + sniDomain *gwv1.Hostname } type tcpFilterChainParent struct { @@ -499,33 +574,88 @@ type tcpFilterChainParent struct { routesWithHosts []*query.RouteInfo } -func (tc *tcpFilterChain) translateTcpFilterChain(listener gwv1.Listener, reporter reports.Reporter) *v1.TcpListener { +func (tc *tcpFilterChain) translateTcpFilterChain( + ctx context.Context, + listener gwv1.Listener, + reporter reports.Reporter, + listenerReporter reports.ListenerReporter, + queries query.GatewayQueries, + gatewayNamespace string, +) *v1.TcpListener { var tcpHosts []*v1.TcpHost for _, parent := range tc.parents { for _, r := range parent.routesWithHosts { - tRoute, ok := r.Object.(*gwv1a2.TCPRoute) - if !ok { - continue - } + // TODO(puertomontt): deduplicate logic for TLSRoute and TCPRoute + switch r.Object.(type) { + case *gwv1a2.TLSRoute: + tRoute := r.Object.(*gwv1a2.TLSRoute) + // Collect ParentRefReporters for the TLSRoute + parentRefReporters := make([]reports.ParentRefReporter, 0, len(tRoute.Spec.ParentRefs)) + for _, parentRef := range tRoute.Spec.ParentRefs { + parentRefReporter := reporter.Route(tRoute).ParentRef(&parentRef) + parentRefReporter.SetCondition(reports.RouteCondition{ + Type: gwv1.RouteConditionAccepted, + Status: metav1.ConditionTrue, + Reason: gwv1.RouteReasonAccepted, + }) + parentRefReporters = append(parentRefReporters, parentRefReporter) + } - // Collect ParentRefReporters for the TCPRoute - parentRefReporters := make([]reports.ParentRefReporter, 0, len(tRoute.Spec.ParentRefs)) - for _, parentRef := range tRoute.Spec.ParentRefs { - parentRefReporter := reporter.Route(tRoute).ParentRef(&parentRef) - parentRefReporter.SetCondition(reports.RouteCondition{ - Type: gwv1.RouteConditionAccepted, - Status: metav1.ConditionTrue, - Reason: gwv1.RouteReasonAccepted, - }) - parentRefReporters = append(parentRefReporters, parentRefReporter) - } + for i, rule := range tRoute.Spec.Rules { + // Ensure unique names by appending the rule index to the TLSRoute name + tcpHostName := fmt.Sprintf("%s-rule-%d", tRoute.Name, i) + tcpHost := buildTcpHost(r, parentRefReporters, tcpHostName, listener.Port, rule.BackendRefs) + if tcpHost != nil { + sslConfig, err := translateSslConfig( + ctx, + gatewayNamespace, + tc.sniDomain, + tc.tls, + queries, + ) + if err != nil { + reason := gwv1.ListenerReasonRefNotPermitted + if !errors.Is(err, query.ErrMissingReferenceGrant) { + reason = gwv1.ListenerReasonInvalidCertificateRef + } + listenerReporter.SetCondition(reports.ListenerCondition{ + Type: gwv1.ListenerConditionResolvedRefs, + Status: metav1.ConditionFalse, + Reason: reason, + }) + // listener with no ssl is invalid. We return nil so set programmed to false + listenerReporter.SetCondition(reports.ListenerCondition{ + Type: gwv1.ListenerConditionProgrammed, + Status: metav1.ConditionFalse, + Reason: gwv1.ListenerReasonInvalid, + }) + return nil + } + tcpHost.SslConfig = sslConfig + tcpHosts = append(tcpHosts, tcpHost) + } + } + case *gwv1a2.TCPRoute: + tRoute := r.Object.(*gwv1a2.TCPRoute) + // Collect ParentRefReporters for the TCPRoute + parentRefReporters := make([]reports.ParentRefReporter, 0, len(tRoute.Spec.ParentRefs)) + for _, parentRef := range tRoute.Spec.ParentRefs { + parentRefReporter := reporter.Route(tRoute).ParentRef(&parentRef) + parentRefReporter.SetCondition(reports.RouteCondition{ + Type: gwv1.RouteConditionAccepted, + Status: metav1.ConditionTrue, + Reason: gwv1.RouteReasonAccepted, + }) + parentRefReporters = append(parentRefReporters, parentRefReporter) + } - for i, rule := range tRoute.Spec.Rules { - // Ensure unique names by appending the rule index to the TCPRoute name - tcpHostName := fmt.Sprintf("%s-rule-%d", tRoute.Name, i) - tcpHost := buildTcpHost(r, parentRefReporters, tcpHostName, listener.Port, rule.BackendRefs) - if tcpHost != nil { - tcpHosts = append(tcpHosts, tcpHost) + for i, rule := range tRoute.Spec.Rules { + // Ensure unique names by appending the rule index to the TCPRoute name + tcpHostName := fmt.Sprintf("%s-rule-%d", tRoute.Name, i) + tcpHost := buildTcpHost(r, parentRefReporters, tcpHostName, listener.Port, rule.BackendRefs) + if tcpHost != nil { + tcpHosts = append(tcpHosts, tcpHost) + } } } } @@ -722,43 +852,44 @@ func translateSslConfig( } // TODO support passthrough mode - if tls.Mode == nil || - *tls.Mode != gwv1.TLSModeTerminate { + if tls.Mode == nil { return nil, nil } var secretRef *core.ResourceRef - for _, certRef := range tls.CertificateRefs { - // validate via query - secret, err := queries.GetSecretForRef(ctx, query.FromGkNs{ - Gk: metav1.GroupKind{ - Group: gwv1.GroupName, - Kind: "Gateway", - }, - Ns: parentNamespace, - }, certRef) - if err != nil { - return nil, err - } - // The resulting sslconfig will still have to go through a real translation where we run through this again. - // This means that while its nice to still fail early here we dont need to scrub the actual contents of the secret. - if _, err := sslutils.ValidateTlsSecret(secret.(*corev1.Secret)); err != nil { - return nil, err - } + if *tls.Mode == gwv1.TLSModeTerminate { + for _, certRef := range tls.CertificateRefs { + // validate via query + secret, err := queries.GetSecretForRef(ctx, query.FromGkNs{ + Gk: metav1.GroupKind{ + Group: gwv1.GroupName, + Kind: wellknown.GatewayKind, + }, + Ns: parentNamespace, + }, certRef) + if err != nil { + return nil, err + } + // The resulting sslconfig will still have to go through a real translation where we run through this again. + // This means that while its nice to still fail early here we dont need to scrub the actual contents of the secret. + if _, err := sslutils.ValidateTlsSecret(secret.(*corev1.Secret)); err != nil { + return nil, err + } - // TODO verify secret ref / grant using query - secretNamespace := parentNamespace - if certRef.Namespace != nil { - secretNamespace = string(*certRef.Namespace) + // TODO verify secret ref / grant using query + secretNamespace := parentNamespace + if certRef.Namespace != nil { + secretNamespace = string(*certRef.Namespace) + } + secretRef = &core.ResourceRef{ + Name: string(certRef.Name), + Namespace: secretNamespace, + } + break // TODO support multiple certs } - secretRef = &core.ResourceRef{ - Name: string(certRef.Name), - Namespace: secretNamespace, + if secretRef == nil { + return nil, nil } - break // TODO support multiple certs - } - if secretRef == nil { - return nil, nil } var sniDomains []string @@ -766,7 +897,7 @@ func translateSslConfig( sniDomains = []string{string(*sniDomain)} } cfg := &ssl.SslConfig{ - SslSecrets: &ssl.SslConfig_SecretRef{SecretRef: secretRef}, + SslSecrets: nil, SniDomains: sniDomains, VerifySubjectAltName: nil, Parameters: nil, @@ -776,6 +907,9 @@ func translateSslConfig( TransportSocketConnectTimeout: nil, OcspStaplePolicy: 0, } + if secretRef != nil { + cfg.SslSecrets = &ssl.SslConfig_SecretRef{SecretRef: secretRef} + } // Apply known SSL Extension options sslutils.ApplySslExtensionOptions(ctx, tls, cfg) diff --git a/projects/gateway2/translator/listener/validation.go b/projects/gateway2/translator/listener/validation.go index 5fcd174cf6d..ca0d39fd832 100644 --- a/projects/gateway2/translator/listener/validation.go +++ b/projects/gateway2/translator/listener/validation.go @@ -41,6 +41,11 @@ func getSupportedProtocolsRoutes() map[protocol]map[groupName][]routeKind { wellknown.TCPRouteKind, }, }, + string(gwv1.TLSProtocolType): { + gwv1.GroupName: []string{ + wellknown.TLSRouteKind, + }, + }, } return supportedProtocolToKinds } diff --git a/projects/gateway2/wellknown/gwapi.go b/projects/gateway2/wellknown/gwapi.go index 4dc308bb109..633165c66ea 100644 --- a/projects/gateway2/wellknown/gwapi.go +++ b/projects/gateway2/wellknown/gwapi.go @@ -23,6 +23,9 @@ const ( // Kind string for TCPRoute resource TCPRouteKind = "TCPRoute" + // Kind string for TLSRoute resource + TLSRouteKind = "TLSRoute" + // Kind string for Gateway resource GatewayKind = "Gateway" @@ -40,6 +43,7 @@ const ( // Gateway API CRD names TCPRouteCRDName = "tcproutes.gateway.networking.k8s.io" + TLSRouteCRDName = "tlsroutes.gateway.networking.k8s.io" ) var ( @@ -79,7 +83,7 @@ var ( Version: apiv1.GroupVersion.Version, Kind: HTTPRouteListKind, } - HTCPRouteListGVK = schema.GroupVersionKind{ + HTCPRouteListGVK = schema.GroupVersionKind{ // Remove? Group: GatewayGroup, Version: apiv1alpha2.GroupVersion.Version, Kind: HTTPRouteListKind, diff --git a/projects/gloo/pkg/plugins/tcp/plugin.go b/projects/gloo/pkg/plugins/tcp/plugin.go index 13d282c5f1e..928e4c8bf7b 100644 --- a/projects/gloo/pkg/plugins/tcp/plugin.go +++ b/projects/gloo/pkg/plugins/tcp/plugin.go @@ -248,6 +248,17 @@ func (p *plugin) computeTcpFilterChain( }, nil } + // needed to handle passthrough + sniDomains := sslConfig.GetSniDomains() + if sslConfig.GetSslSecrets() == nil && len(sniDomains) != 0 { + return &envoy_config_listener_v3.FilterChain{ + Filters: listenerFilters, + FilterChainMatch: &envoy_config_listener_v3.FilterChainMatch{ + ServerNames: sniDomains, + }, + }, nil + } + downstreamConfig, err := p.sslConfigTranslator.ResolveDownstreamSslConfig(snap.Secrets, sslConfig) if err != nil { return nil, InvalidSecretsError(err, host.GetName()) diff --git a/projects/gloo/pkg/plugins/tls_inspector/plugin.go b/projects/gloo/pkg/plugins/tls_inspector/plugin.go index 102efb62353..03a966ff6b0 100644 --- a/projects/gloo/pkg/plugins/tls_inspector/plugin.go +++ b/projects/gloo/pkg/plugins/tls_inspector/plugin.go @@ -66,6 +66,15 @@ func includeTlsInspectorForAggregateListener(in *v1.AggregateListener) bool { return true } } + // for the case of TLS Passthrough over TCP, which is the usecase for TLSRoutes, + // we need to add the tls inspector + for _, tcpListener := range in.GetTcpListeners() { + for _, host := range tcpListener.GetTcpListener().GetTcpHosts() { + if host.GetSslConfig() != nil { + return true + } + } + } return false } diff --git a/projects/gloo/pkg/plugins/tls_inspector/plugin_test.go b/projects/gloo/pkg/plugins/tls_inspector/plugin_test.go index d6cabbe37aa..1600de0b64b 100644 --- a/projects/gloo/pkg/plugins/tls_inspector/plugin_test.go +++ b/projects/gloo/pkg/plugins/tls_inspector/plugin_test.go @@ -460,7 +460,41 @@ var _ = Describe("Plugin", func() { params = plugins.Params{} }) - It("tls inspector is added", func() { + It("tls inspector is added for http", func() { + in := &v1.Listener{ + ListenerType: &v1.Listener_AggregateListener{ + AggregateListener: &v1.AggregateListener{ + HttpFilterChains: []*v1.AggregateListener_HttpFilterChain{ + {Matcher: &v1.Matcher{ + SslConfig: &ssl.SslConfig{}, + }}, + }, + }, + }, + } + + filters := []*envoy_config_listener_v3.Filter{{}} + + out := &envoy_config_listener_v3.Listener{ + FilterChains: []*envoy_config_listener_v3.FilterChain{{ + Filters: filters, + }}, + } + + p := NewPlugin() + err := p.ProcessListener(params, in, out) + Expect(err).NotTo(HaveOccurred()) + + configEnvoy := &envoy_tls_inspector.TlsInspector{} + config, _ := utils.MessageToAny(configEnvoy) + + Expect(out.ListenerFilters).To(HaveLen(1)) + Expect(out.ListenerFilters[0].GetName()).To(Equal(wellknown.TlsInspector)) + Expect(out.ListenerFilters[0].GetTypedConfig()).To(Equal(config)) + + }) + + It("tls inspector is added for tcp", func() { in := &v1.Listener{ ListenerType: &v1.Listener_AggregateListener{ AggregateListener: &v1.AggregateListener{ @@ -469,6 +503,19 @@ var _ = Describe("Plugin", func() { SslConfig: &ssl.SslConfig{}, }}, }, + TcpListeners: []*v1.MatchedTcpListener{ + { + TcpListener: &v1.TcpListener{ + TcpHosts: []*v1.TcpHost{ + { + SslConfig: &ssl.SslConfig{ + SniDomains: []string{"foo.com"}, + }, + }, + }, + }, + }, + }, }, }, } diff --git a/test/kubernetes/e2e/features/services/tlsroute/suite.go b/test/kubernetes/e2e/features/services/tlsroute/suite.go new file mode 100644 index 00000000000..437f4c87fad --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/suite.go @@ -0,0 +1,305 @@ +package tlsroute + +import ( + "context" + "fmt" + "os" + + "github.com/stretchr/testify/suite" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/solo-io/gloo/pkg/utils/kubeutils" + "github.com/solo-io/gloo/pkg/utils/kubeutils/kubectl" + "github.com/solo-io/gloo/pkg/utils/requestutils/curl" + "github.com/solo-io/gloo/test/gomega/matchers" + "github.com/solo-io/gloo/test/kubernetes/e2e" + "github.com/solo-io/gloo/test/kubernetes/e2e/defaults" +) + +// testingSuite is the entire suite of tests for testing K8s Service-specific features/fixes +type testingSuite struct { + suite.Suite + + ctx context.Context + + // testInstallation contains all the metadata/utilities necessary to execute a series of tests + // against an installation of Gloo Gateway + testInstallation *e2e.TestInstallation +} + +func NewTestingSuite(ctx context.Context, testInst *e2e.TestInstallation) suite.TestingSuite { + return &testingSuite{ + ctx: ctx, + testInstallation: testInst, + } +} + +func (s *testingSuite) SetupSuite() { + var cancel context.CancelFunc + s.ctx, cancel = context.WithTimeout(context.Background(), ctxTimeout) + s.T().Cleanup(cancel) + + manifests := []string{ + singleSvcNsManifest, + singleSvcGatewayAndClientManifest, + singleSvcBackendManifest, + singleSvcTLSRouteManifest, + multiSvcNsManifest, + multiSvcGatewayAndClientManifest, + multiSvcBackendManifest, + multiSvcTlsRouteManifest, + } + for _, file := range manifests { + s.Require().NoError(validateManifestFile(file), "Invalid manifest file: %s", file) + } +} + +type tlsRouteTestCase struct { + name string + nsManifest string + gtwName string + gtwNs string + gtwManifest string + svcManifest string + tlsRouteManifest string + tlsSecretManifest string + proxyService *corev1.Service + proxyDeployment *appsv1.Deployment + expectedResponses []*matchers.HttpResponse + expectedErrorCode int + ports []int + listenerNames []v1.SectionName + expectedRouteCounts []int32 + tlsRouteNames []string +} + +func (s *testingSuite) TestConfigureTLSRouteBackingDestinations() { + testCases := []tlsRouteTestCase{ + { + name: "SingleServiceTLSRoute", + nsManifest: singleSvcNsManifest, + gtwName: singleSvcGatewayName, + gtwNs: singleSvcNsName, + gtwManifest: singleSvcGatewayAndClientManifest, + svcManifest: singleSvcBackendManifest, + tlsRouteManifest: singleSvcTLSRouteManifest, + tlsSecretManifest: singleSecretManifest, + proxyService: singleSvcProxyService, + proxyDeployment: singleSvcProxyDeployment, + expectedResponses: []*matchers.HttpResponse{ + expectedSingleSvcResp, + }, + ports: []int{6443}, + listenerNames: []v1.SectionName{ + v1.SectionName(singleSvcListenerName443), + }, + expectedRouteCounts: []int32{1}, + tlsRouteNames: []string{singleSvcTLSRouteName}, + }, + { + name: "MultiServicesTLSRoute", + nsManifest: multiSvcNsManifest, + gtwName: multiSvcGatewayName, + gtwNs: multiSvcNsName, + gtwManifest: multiSvcGatewayAndClientManifest, + svcManifest: multiSvcBackendManifest, + tlsRouteManifest: multiSvcTlsRouteManifest, + tlsSecretManifest: singleSecretManifest, + proxyService: multiProxyService, + proxyDeployment: multiProxyDeployment, + expectedResponses: []*matchers.HttpResponse{ + expectedMultiSvc1Resp, + expectedMultiSvc2Resp, + }, + ports: []int{6443, 8443}, + listenerNames: []v1.SectionName{ + v1.SectionName(multiSvcListenerName6443), + v1.SectionName(multiSvcListenerName8443), + }, + expectedRouteCounts: []int32{1, 1}, + tlsRouteNames: []string{multiSvcTLSRouteName1, multiSvcTLSRouteName2}, + }, + { + name: crossNsTestName, + nsManifest: crossNsClientNsManifest, + gtwName: crossNsGatewayName, + gtwNs: crossNsClientName, + gtwManifest: crossNsGatewayManifest, + svcManifest: crossNsBackendSvcManifest, + tlsRouteManifest: crossNsTLSRouteManifest, + tlsSecretManifest: singleSecretManifest, + proxyService: crossNsProxyService, + proxyDeployment: crossNsProxyDeployment, + expectedResponses: []*matchers.HttpResponse{ + expectedCrossNsResp, + }, + ports: []int{8443}, + listenerNames: []v1.SectionName{ + v1.SectionName(crossNsListenerName), + }, + expectedRouteCounts: []int32{1}, + tlsRouteNames: []string{crossNsTLSRouteName}, + }, + { + name: crossNsNoRefGrantTestName, + nsManifest: crossNsNoRefGrantClientNsManifest, + gtwName: crossNsNoRefGrantGatewayName, + gtwNs: crossNsNoRefGrantClientNsName, + gtwManifest: crossNsNoRefGrantGatewayManifest, + svcManifest: crossNsNoRefGrantBackendSvcManifest, + tlsRouteManifest: crossNsNoRefGrantTLSRouteManifest, + tlsSecretManifest: singleSecretManifest, + proxyService: crossNsNoRefGrantProxyService, + proxyDeployment: crossNsNoRefGrantProxyDeployment, + expectedErrorCode: 7, + ports: []int{8443}, + listenerNames: []v1.SectionName{ + v1.SectionName(crossNsNoRefGrantListenerName), + }, + expectedRouteCounts: []int32{1}, + tlsRouteNames: []string{crossNsNoRefGrantTLSRouteName}, + }, + } + + for _, tc := range testCases { + tc := tc // capture range variable + s.Run(tc.name, func() { + // Cleanup function + s.T().Cleanup(func() { + s.deleteManifests(tc.nsManifest) + + // Delete additional namespaces if any + if tc.name == "CrossNamespaceTLSRouteWithReferenceGrant" { + s.deleteManifests(crossNsBackendNsManifest) + } + + if tc.name == crossNsNoRefGrantTestName { + s.deleteManifests(crossNsNoRefGrantBackendNsManifest) + } + + s.testInstallation.Assertions.EventuallyObjectsNotExist(s.ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: tc.gtwNs}}) + }) + + // Setup environment for ReferenceGrant test cases + if tc.name == crossNsTestName { + s.applyManifests(crossNsBackendNsName, crossNsBackendNsManifest) + s.applyManifests(crossNsBackendNsName, crossNsBackendSvcManifest) + s.applyManifests(crossNsBackendNsName, crossNsRefGrantManifest) + s.applyManifests(crossNsBackendNsName, singleSecretManifest) + } + + if tc.name == crossNsNoRefGrantTestName { + s.applyManifests(crossNsNoRefGrantBackendNsName, crossNsNoRefGrantBackendNsManifest) + s.applyManifests(crossNsNoRefGrantBackendNsName, crossNsNoRefGrantBackendSvcManifest) + s.applyManifests(crossNsNoRefGrantBackendNsName, singleSecretManifest) + // ReferenceGrant not applied + } + + // Setup environment + s.setupTestEnvironment( + tc.nsManifest, + tc.gtwName, + tc.gtwNs, + tc.gtwManifest, + tc.svcManifest, + tc.proxyService, + tc.proxyDeployment, + ) + + s.applyManifests(tc.gtwNs, tc.tlsSecretManifest) + + // Apply TLSRoute manifest + s.applyManifests(tc.gtwNs, tc.tlsRouteManifest) + + // Set the expected status conditions based on the test case + expected := metav1.ConditionTrue + if tc.name == crossNsNoRefGrantTestName { + expected = metav1.ConditionFalse + } + + // Assert TLSRoute conditions + for _, tcpRouteName := range tc.tlsRouteNames { + s.testInstallation.Assertions.EventuallyTLSRouteCondition(s.ctx, tcpRouteName, tc.gtwNs, v1.RouteConditionAccepted, metav1.ConditionTrue, timeout) + s.testInstallation.Assertions.EventuallyTLSRouteCondition(s.ctx, tcpRouteName, tc.gtwNs, v1.RouteConditionResolvedRefs, expected, timeout) + } + + // Assert gateway programmed condition + s.testInstallation.Assertions.EventuallyGatewayCondition(s.ctx, tc.gtwName, tc.gtwNs, v1.GatewayConditionProgrammed, metav1.ConditionTrue, timeout) + + // Assert listener attached routes + for i, listenerName := range tc.listenerNames { + expectedRouteCount := tc.expectedRouteCounts[i] + s.testInstallation.Assertions.EventuallyGatewayListenerAttachedRoutes(s.ctx, tc.gtwName, tc.gtwNs, listenerName, expectedRouteCount, timeout) + } + + // Assert expected responses + for i, port := range tc.ports { + if tc.expectedErrorCode != 0 { + s.testInstallation.Assertions.AssertEventualCurlError( + s.ctx, + s.execOpts(tc.gtwNs), + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(tc.proxyService.ObjectMeta)), + curl.WithPort(port), + curl.VerboseOutput(), + }, + tc.expectedErrorCode) + } else { + s.testInstallation.Assertions.AssertEventualCurlResponse( + s.ctx, + s.execOpts(tc.gtwNs), + []curl.Option{ + curl.WithHost(kubeutils.ServiceFQDN(tc.proxyService.ObjectMeta)), + curl.WithPort(port), + curl.WithCaFile("/etc/server-certs/tls.crt"), + curl.WithScheme("https"), + curl.WithSni("example.com"), + curl.IgnoreServerCert(), + curl.VerboseOutput(), + }, + tc.expectedResponses[i]) + } + } + }) + } +} + +func validateManifestFile(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + return fmt.Errorf("Manifest file not found: %s", path) + } + return nil +} + +func (s *testingSuite) setupTestEnvironment(nsManifest, gtwName, gtwNs, gtwManifest, svcManifest string, proxySvc *corev1.Service, proxyDeploy *appsv1.Deployment) { + s.applyManifests(gtwNs, nsManifest) + + s.applyManifests(gtwNs, gtwManifest) + s.testInstallation.Assertions.EventuallyGatewayCondition(s.ctx, gtwName, gtwNs, v1.GatewayConditionAccepted, metav1.ConditionTrue, timeout) + + s.applyManifests(gtwNs, svcManifest) + s.testInstallation.Assertions.EventuallyObjectsExist(s.ctx, proxySvc, proxyDeploy) +} + +func (s *testingSuite) applyManifests(ns string, manifests ...string) { + for _, manifest := range manifests { + err := s.testInstallation.Actions.Kubectl().ApplyFile(s.ctx, manifest, "-n", ns) + s.Require().NoError(err, fmt.Sprintf("Failed to apply manifest %s", manifest)) + } +} + +func (s *testingSuite) deleteManifests(manifests ...string) { + for _, manifest := range manifests { + err := s.testInstallation.Actions.Kubectl().DeleteFileSafe(s.ctx, manifest) + s.Require().NoError(err, fmt.Sprintf("Failed to delete manifest %s", manifest)) + } +} + +func (s *testingSuite) execOpts(ns string) kubectl.PodExecOptions { + opts := defaults.CurlPodExecOpt + opts.Namespace = ns + return opts +} diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/ca-cert.crt b/test/kubernetes/e2e/features/services/tlsroute/testdata/ca-cert.crt new file mode 100644 index 00000000000..06b798489cd --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/ca-cert.crt @@ -0,0 +1,37 @@ +Common Name: * +Subject Alternative Names: * +Organization: gateway +Organization Unit: +Locality: +State: +Country: +Valid From: November 8, 2023 +Valid To: November 5, 2033 +Issuer: *, root +Key Size: 2048 bit +Serial Number: 0 (0x0) +-----BEGIN CERTIFICATE----- +MIIEPTCCAiWgAwIBAgIBADANBgkqhkiG9w0BAQsFADAbMQowCAYDVQQDDAEqMQ0w +CwYDVQQKDARyb290MB4XDTIzMTEwODE2NDQ1N1oXDTMzMTEwNTE2NDQ1N1owHjEK +MAgGA1UEAwwBKjEQMA4GA1UECgwHZ2F0ZXdheTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAN+tWhhkt/5QPML8Pj+gRRqC5nZyLoRznb+xOk7PMZ3FwmaG +58omXOFmzfbe+EZha4RPa+PitXEn+cfC9jYXEN6tc5XLVR9J+WBEtaIJhfXvW0/n +khH41aYkcBAS2LHuSyxYgwTDLG259LUuREOuFIXmYFIheee6zWwQ1y4R95W4hTas +/IVOpbkmm+23FUCT7E7/73tDXwCWizG7Ru2gZvi/m+FQUBBfaOLlszT/Tsp50wbe +cHqcoTmbMYBiX95DPXMkggh93TvnpVoKZaVaX3NtyFDbNfq2/6ZOgZ4YMeX3oELR +bVYicMkSyYDrVmocxvA1gPAK1wcdTMNr9gcAuoECAwEAAaOBiDCBhTAJBgNVHRME +AjAAMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEw +DAYDVR0RBAUwA4IBKjAdBgNVHQ4EFgQUqin4HHW7O0GsKlh0PBvHQYK4POMwHwYD +VR0jBBgwFoAUYoywMzI7PiXkIwqLI7d709JbgVcwDQYJKoZIhvcNAQELBQADggIB +ABCzv5E+oxXOtAR/TYa6abLVoxXCOXVUyQQzYyTRhzBNcEcnMMx0Gseu4tWyIfzo +6d9/4d9gft9Q6uSKTYRHXSEHAAl2iDXgPM6hJM/6jqBQ67Q+VEkQ2VUC2H6Db1uC +6TgevOLvP5x8kKaceSga7fHvIqoy8ufnmAK9awfhlOaj0cQg1AuzimxnXsMT/vCU +oWklOe+tL068Gw/+H0RP12z7kzTbCmvnEe8nMPIskSmMe7/RvKqAna7t4qC9WrEx +ZQY+Ce9XkNr7DbZfzkNjjPUv8J3tsvw69fmGqPDYZG2mGB4sxar7/fdn+lgw05lG +AlHaijWNUFMkfqx/S6gjjD/COeZQqYm3XKctBZARR3IEDtEcDRDs6Yos8BQ0sm/D +Hg4mWYdxco7ZZgS1zeiavCpElCiq3ZpysA/KSKKNzcTHFNZqpqaaMuT/i0niB52a +fjEH53vMpaxT+R/pR1CSLDFzT2791LhJkfBVUl3BGg9V6T+znIu6I5iU5FVYmYwP +nm/Db+gpOIjQ2SL9ZmgVCov1o50SSvK7TXHUHiqheynp02GVXazEOoy80Vpm8uoI +IoKcE9/HRWNOAyRN5qkXD0gjZI9c5iKcWndCPurpEdCMeGbzk5qdT4hYHQl3dJyo +4rhfoL7NuGUTkJ5d+FtPpitHJCDTgToNKMSFq71o4h1y +-----END CERTIFICATE----- diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-ns.yaml new file mode 100644 index 00000000000..45c5d20e19c --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cross-namespace-allowed-backend-ns diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-service.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-service.yaml new file mode 100644 index 00000000000..487d4852b91 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-backend-service.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend-svc +spec: + selector: + app: backend-svc + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-svc + labels: + app: backend-svc +spec: + replicas: 1 + selector: + matchLabels: + app: backend-svc + template: + metadata: + labels: + app: backend-svc + spec: + containers: + - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + name: echo + ports: + - containerPort: 8443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: backend-svc + - name: HTTPS_PORT + value: "8443" + - name: TLS_SERVER_CERT + value: /etc/server-certs/tls.crt + - name: TLS_SERVER_PRIVKEY + value: /etc/server-certs/tls.key + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-client-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-client-ns.yaml new file mode 100644 index 00000000000..62abb0ee99d --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-client-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cross-namespace-allowed-client-ns diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-gateway-and-client.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-gateway-and-client.yaml new file mode 100644 index 00000000000..d5238d19848 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-gateway-and-client.yaml @@ -0,0 +1,41 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: gloo-gateway + listeners: + - name: listener-8443 + port: 8443 + protocol: TLS + hostname: "example.com" +--- +apiVersion: v1 +kind: Pod +metadata: + name: curl + labels: + app: curl + version: v1 +spec: + containers: + - name: curl + image: curlimages/curl:7.83.1 + imagePullPolicy: IfNotPresent + command: + - "tail" + - "-f" + - "/dev/null" + resources: + requests: + cpu: "100m" + limits: + cpu: "200m" + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret \ No newline at end of file diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-ns.yaml new file mode 100644 index 00000000000..c57c76f7908 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: backend-ns-no-refgrant diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-service.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-service.yaml new file mode 100644 index 00000000000..487d4852b91 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-backend-service.yaml @@ -0,0 +1,59 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend-svc +spec: + selector: + app: backend-svc + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-svc + labels: + app: backend-svc +spec: + replicas: 1 + selector: + matchLabels: + app: backend-svc + template: + metadata: + labels: + app: backend-svc + spec: + containers: + - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + name: echo + ports: + - containerPort: 8443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: backend-svc + - name: HTTPS_PORT + value: "8443" + - name: TLS_SERVER_CERT + value: /etc/server-certs/tls.crt + - name: TLS_SERVER_PRIVKEY + value: /etc/server-certs/tls.key + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-client-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-client-ns.yaml new file mode 100644 index 00000000000..643959f035e --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-client-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: client-ns-no-refgrant diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-gateway-and-client.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-gateway-and-client.yaml new file mode 100644 index 00000000000..0dcf339c001 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-gateway-and-client.yaml @@ -0,0 +1,42 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: gloo-gateway + listeners: + - name: listener-8443 + port: 8443 + protocol: TLS + hostname: "example.com" +--- +apiVersion: v1 +kind: Pod +metadata: + name: curl + labels: + app: curl + version: v1 +spec: + containers: + - name: curl + image: curlimages/curl:7.83.1 + imagePullPolicy: IfNotPresent + command: + - "tail" + - "-f" + - "/dev/null" + resources: + requests: + cpu: "100m" + limits: + cpu: "200m" + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret + \ No newline at end of file diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-tlsroute.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-tlsroute.yaml new file mode 100644 index 00000000000..6253a547e98 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-no-refgrant-tlsroute.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-route +spec: + parentRefs: + - name: gateway + hostnames: + - "example.com" + rules: + - backendRefs: + - name: backend-svc + namespace: backend-ns-no-refgrant + port: 443 diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-referencegrant.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-referencegrant.yaml new file mode 100644 index 00000000000..ff38ca651b8 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-referencegrant.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1beta1 +kind: ReferenceGrant +metadata: + name: reference-grant +spec: + from: + - group: gateway.networking.k8s.io + kind: TLSRoute + namespace: cross-namespace-allowed-client-ns + to: + - group: "" + kind: Service diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-tlsroute.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-tlsroute.yaml new file mode 100644 index 00000000000..2cee3e0d4c7 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/cross-ns-tlsroute.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-route +spec: + parentRefs: + - name: gateway + hostnames: + - "example.com" + rules: + - backendRefs: + - name: backend-svc + namespace: cross-namespace-allowed-backend-ns + port: 443 diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-backend-service.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-backend-service.yaml new file mode 100644 index 00000000000..73fe21dc7d1 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-backend-service.yaml @@ -0,0 +1,125 @@ +apiVersion: v1 +kind: Service +metadata: + name: multi-svc-1 + labels: + app: multi-svc +spec: + ports: + - protocol: TCP + port: 3001 + targetPort: 8443 + selector: + app: backend-1 +--- +apiVersion: v1 +kind: Service +metadata: + name: multi-svc-2 + labels: + app: multi-svc +spec: + ports: + - protocol: TCP + port: 3002 + targetPort: 8443 + selector: + app: backend-2 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-1 + namespace: multi-tls-route +spec: + replicas: 1 + selector: + matchLabels: + app: backend-1 + version: v1 + template: + metadata: + labels: + app: backend-1 + version: v1 + spec: + containers: + - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + name: backend-1 + ports: + - containerPort: 8443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: multi-svc-1 + - name: HTTPS_PORT + value: "8443" + - name: TLS_SERVER_CERT + value: /etc/server-certs/tls.crt + - name: TLS_SERVER_PRIVKEY + value: /etc/server-certs/tls.key + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-2 + namespace: multi-tls-route +spec: + replicas: 1 + selector: + matchLabels: + app: backend-2 + version: v1 + template: + metadata: + labels: + app: backend-2 + version: v1 + spec: + containers: + - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + name: backend-2 + ports: + - containerPort: 8443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: multi-svc-2 + - name: HTTPS_PORT + value: "8443" + - name: TLS_SERVER_CERT + value: /etc/server-certs/tls.crt + - name: TLS_SERVER_PRIVKEY + value: /etc/server-certs/tls.key + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-listener-gateway-and-client.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-listener-gateway-and-client.yaml new file mode 100644 index 00000000000..331d73a74a6 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-listener-gateway-and-client.yaml @@ -0,0 +1,44 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: multi-tls-gateway +spec: + gatewayClassName: gloo-gateway + listeners: + - name: listener-6443 # do one listener with multiple hostnames? + protocol: TLS + port: 6443 + hostname: "example.com" + tls: + mode: Passthrough + allowedRoutes: + kinds: + - kind: TLSRoute + - name: listener-8443 + protocol: TLS + port: 8443 + allowedRoutes: + kinds: + - kind: TLSRoute +--- +apiVersion: v1 +kind: Pod +metadata: + name: curl + labels: + app: curl + version: v1 +spec: + containers: + - name: curl + image: curlimages/curl:7.83.1 + imagePullPolicy: IfNotPresent + command: + - "tail" + - "-f" + - "/dev/null" + resources: + requests: + cpu: "100m" + limits: + cpu: "200m" diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-ns.yaml new file mode 100644 index 00000000000..d0f5dacf377 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: multi-tls-route diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-tlsroute.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-tlsroute.yaml new file mode 100644 index 00000000000..4ab52f40b69 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/multi-tlsroute.yaml @@ -0,0 +1,31 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-route-1 +spec: + parentRefs: + - name: multi-tls-gateway + sectionName: listener-6443 + hostnames: + - "example.com" + rules: + - backendRefs: + - name: multi-svc-1 + port: 3001 + weight: 60 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: tls-route-2 +spec: + parentRefs: + - name: multi-tls-gateway + sectionName: listener-8443 + hostnames: + - "example.com" + rules: + - backendRefs: + - name: multi-svc-2 + port: 3002 + weight: 40 diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/single-backend-service.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-backend-service.yaml new file mode 100644 index 00000000000..cd05c34ba54 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-backend-service.yaml @@ -0,0 +1,61 @@ +apiVersion: v1 +kind: Service +metadata: + name: single-svc + labels: + app: single-svc +spec: + ports: + - port: 443 + targetPort: 8443 + protocol: TCP + selector: + app: backend-0 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend-0 +spec: + replicas: 1 + selector: + matchLabels: + app: backend-0 + version: v1 + template: + metadata: + labels: + app: backend-0 + version: v1 + spec: + containers: + - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e + imagePullPolicy: IfNotPresent + name: backend-0 + ports: + - containerPort: 8443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: single-svc + - name: HTTPS_PORT + value: "8443" + - name: TLS_SERVER_CERT + value: /etc/server-certs/tls.crt + - name: TLS_SERVER_PRIVKEY + value: /etc/server-certs/tls.key + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/single-listener-gateway-and-client.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-listener-gateway-and-client.yaml new file mode 100644 index 00000000000..2d6d98233f5 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-listener-gateway-and-client.yaml @@ -0,0 +1,46 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: single-tls-gateway +spec: + gatewayClassName: gloo-gateway + listeners: + - name: listener-443 + protocol: TLS + port: 6443 + hostname: "example.com" + tls: + mode: Passthrough + allowedRoutes: + kinds: + - kind: TLSRoute +--- +apiVersion: v1 +kind: Pod +metadata: + name: curl + labels: + app: curl + version: v1 +spec: + containers: + - name: curl + image: curlimages/curl:7.83.1 + imagePullPolicy: IfNotPresent + command: + - "tail" + - "-f" + - "/dev/null" + resources: + requests: + cpu: "100m" + limits: + cpu: "200m" + volumeMounts: + - name: server-certs + mountPath: /etc/server-certs + readOnly: true + volumes: + - name: server-certs + secret: + secretName: tls-secret \ No newline at end of file diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/single-ns.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-ns.yaml new file mode 100644 index 00000000000..a879719550b --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-ns.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: single-tls-route diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/single-tlsroute.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-tlsroute.yaml new file mode 100644 index 00000000000..d5aeef46309 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/single-tlsroute.yaml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: TLSRoute +metadata: + name: single-tls-route +spec: + parentRefs: + - name: single-tls-gateway + sectionName: listener-443 + hostnames: + - "example.com" + rules: + - backendRefs: + - name: single-svc + port: 443 diff --git a/test/kubernetes/e2e/features/services/tlsroute/testdata/tls-secret.yaml b/test/kubernetes/e2e/features/services/tlsroute/testdata/tls-secret.yaml new file mode 100644 index 00000000000..22ca38cce96 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/testdata/tls-secret.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Secret +type: kubernetes.io/tls +metadata: + name: tls-secret +data: + # Common Name: * + # Subject Alternative Names: * + # Organization: gateway + # Organization Unit: + # Locality: + # State: + # Country: + # Valid From: November 8, 2023 + # Valid To: November 5, 2033 + # Issuer: *, root + # Key Size: 2048 bit + # Serial Number: 0 (0x0) + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVQVENDQWlXZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFiTVFvd0NBWURWUVFEREFFcU1RMHcKQ3dZRFZRUUtEQVJ5YjI5ME1CNFhEVEl6TVRFd09ERTJORFExTjFvWERUTXpNVEV3TlRFMk5EUTFOMW93SGpFSwpNQWdHQTFVRUF3d0JLakVRTUE0R0ExVUVDZ3dIWjJGMFpYZGhlVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTit0V2hoa3QvNVFQTUw4UGorZ1JScUM1blp5TG9Sem5iK3hPazdQTVozRndtYUcKNThvbVhPRm16ZmJlK0VaaGE0UlBhK1BpdFhFbitjZkM5allYRU42dGM1WExWUjlKK1dCRXRhSUpoZlh2VzAvbgpraEg0MWFZa2NCQVMyTEh1U3l4WWd3VERMRzI1OUxVdVJFT3VGSVhtWUZJaGVlZTZ6V3dRMXk0Ujk1VzRoVGFzCi9JVk9wYmttbSsyM0ZVQ1Q3RTcvNzN0RFh3Q1dpekc3UnUyZ1p2aS9tK0ZRVUJCZmFPTGxzelQvVHNwNTB3YmUKY0hxY29UbWJNWUJpWDk1RFBYTWtnZ2g5M1R2bnBWb0taYVZhWDNOdHlGRGJOZnEyLzZaT2daNFlNZVgzb0VMUgpiVllpY01rU3lZRHJWbW9jeHZBMWdQQUsxd2NkVE1OcjlnY0F1b0VDQXdFQUFhT0JpRENCaFRBSkJnTlZIUk1FCkFqQUFNQXNHQTFVZER3UUVBd0lGNERBZEJnTlZIU1VFRmpBVUJnZ3JCZ0VGQlFjREFnWUlLd1lCQlFVSEF3RXcKREFZRFZSMFJCQVV3QTRJQktqQWRCZ05WSFE0RUZnUVVxaW40SEhXN08wR3NLbGgwUEJ2SFFZSzRQT013SHdZRApWUjBqQkJnd0ZvQVVZb3l3TXpJN1BpWGtJd3FMSTdkNzA5SmJnVmN3RFFZSktvWklodmNOQVFFTEJRQURnZ0lCCkFCQ3p2NUUrb3hYT3RBUi9UWWE2YWJMVm94WENPWFZVeVFRell5VFJoekJOY0Vjbk1NeDBHc2V1NHRXeUlmem8KNmQ5LzRkOWdmdDlRNnVTS1RZUkhYU0VIQUFsMmlEWGdQTTZoSk0vNmpxQlE2N1ErVkVrUTJWVUMySDZEYjF1Qwo2VGdldk9MdlA1eDhrS2FjZVNnYTdmSHZJcW95OHVmbm1BSzlhd2ZobE9hajBjUWcxQXV6aW14blhzTVQvdkNVCm9Xa2xPZSt0TDA2OEd3LytIMFJQMTJ6N2t6VGJDbXZuRWU4bk1QSXNrU21NZTcvUnZLcUFuYTd0NHFDOVdyRXgKWlFZK0NlOVhrTnI3RGJaZnprTmpqUFV2OEozdHN2dzY5Zm1HcVBEWVpHMm1HQjRzeGFyNy9mZG4rbGd3MDVsRwpBbEhhaWpXTlVGTWtmcXgvUzZnampEL0NPZVpRcVltM1hLY3RCWkFSUjNJRUR0RWNEUkRzNllvczhCUTBzbS9ECkhnNG1XWWR4Y283WlpnUzF6ZWlhdkNwRWxDaXEzWnB5c0EvS1NLS056Y1RIRk5acXBxYWFNdVQvaTBuaUI1MmEKZmpFSDUzdk1wYXhUK1IvcFIxQ1NMREZ6VDI3OTFMaEprZkJWVWwzQkdnOVY2VCt6bkl1Nkk1aVU1RlZZbVl3UApubS9EYitncE9JalEyU0w5Wm1nVkNvdjFvNTBTU3ZLN1RYSFVIaXFoZXlucDAyR1ZYYXpFT295ODBWcG04dW9JCklvS2NFOS9IUldOT0F5Uk41cWtYRDBnalpJOWM1aUtjV25kQ1B1cnBFZENNZUdiems1cWRUNGhZSFFsM2RKeW8KNHJoZm9MN051R1VUa0o1ZCtGdFBwaXRISkNEVGdUb05LTVNGcTcxbzRoMXkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV1d0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktVd2dnU2hBZ0VBQW9JQkFRRGZyVm9ZWkxmK1VEekMKL0Q0L29FVWFndVoyY2k2RWM1Mi9zVHBPenpHZHhjSm1odWZLSmx6aFpzMzIzdmhHWVd1RVQydmo0clZ4Si9uSAp3dlkyRnhEZXJYT1Z5MVVmU2ZsZ1JMV2lDWVgxNzF0UDU1SVIrTldtSkhBUUV0aXg3a3NzV0lNRXd5eHR1ZlMxCkxrUkRyaFNGNW1CU0lYbm51czFzRU5jdUVmZVZ1SVUyclB5RlRxVzVKcHZ0dHhWQWsreE8vKzk3UTE4QWxvc3gKdTBidG9HYjR2NXZoVUZBUVgyamk1Yk0wLzA3S2VkTUczbkI2bktFNW16R0FZbC9lUXoxekpJSUlmZDA3NTZWYQpDbVdsV2w5emJjaFEyelg2dHYrbVRvR2VHREhsOTZCQzBXMVdJbkRKRXNtQTYxWnFITWJ3TllEd0N0Y0hIVXpECmEvWUhBTHFCQWdNQkFBRUNnZjhQVjFhUUwxaEVteTl0amc1WkR0SFFxRGFJZGZ0UUhwVmdCY0ZtMVVyZzBnSzAKWjZHa204V0REZ2ZqYlNlTlZ1RXhlRnFqV2RwWDkyeFhHbGNJdUV4SFgvZStsTXNRdnBWUllMcGhQblRuQU1YVgp0MG9rN1NYTFBVRll4OC8vcUcvWkZHTzA0UHJYNmFMRFNuZ0NBYXhxOFpNbFpFUkMyaUJKaTAwVXhGNHNKR25WCklJeHFLeVRnbWpBcFV6c1BDWXF5ci9aNmJTRkVnbVNxNzJobmt2Rm1PV3NYcldzRlZLY05iUUplWTRMMEJuWUsKWk5xODNmemt6ekpxbVh3OEVncWFPNlhWMmJtVmYrM1hSL3ViMDRGeXRya0Y5bE1JU0pWOUhkQmtRaGV2VzZhVApoSG5pblV6VkgyUlh4M1piYWw3ck1ZL0lOMWNmaFVlWm9BZDQ0ZGtDZ1lFQTh1bHorbXpPWXg0QVNnWUo0eVNICkdmc1VQeXpJUU9vaEVJQmtxMWNrUHJlbVhsdEdoWTcrRmtYYmh5cnVuOTRIREJJZWR0RWt0YjlZSDkvT0xNME8KK056TlhTYnlaQ3ZTa1U1ZlNNeUpDc3E3L01JTlVpNUxGN1FLQVF1ZXk3WDgvUGU3NHhFQ29hN1AvSlJkTnYrdgpjUnkyUEZOSlNGbTJGdkxGeDA4TDFWa0NnWUVBNjdxV3Q5dENpQ24zRXVPM2hEeFptZ3VSWU55Tk9TWkowdjR5Cm9zaXZ0WFl2ZGVYWGxGZWFWSEJtZG5vZUYrVTZ6TXpUQUY0d09jNmZpenpCMjlGVkxjWjJmTE5tWXc0RmxENFEKR0wzMHVJckQ5WXJraGZWZm85TW9aVHo2cWJnc0xQQnNZTXljelpFOThyb3dmZVp2MkUzQ1lRaDhOL1lGbXRmOQptWTViNFdrQ2dZQkQyc2pHQkl6bWpTUGhpYXhMWWhISFJTYlR1dXU1am0xc0VhR05aMHM5cGNsNGhDREFBRUNqCjhpR3ZzV04xRHUyREJyQ3gyaHhhRkxoR054dDkwazVEWUZLUm1lYU42dHZvTVM5V3c2UG9ldGRtZE1LSjJWcXEKcFdWQ0EzLzVRYjRJNEI4QS8raHZSOGpic29vVGFmc1ZLc01ST09hNHFpNitYRlM1SnpDVUNRS0JnRmZLN2tjYgpTZlFjYlFDRC90MG8vTlg2YVBLQ01iYVBJLytJM0tMenl6engvMHNSaHZDZ2o4SFMrdFkxTlBBQlY1emV5OWJmClBXYktKWEZkOTNVK3lWSjdEN1h4dXJnNWlLcGxVdWxrRmJpRk5lWkZERWMzMDU3WURidG1zcFJ6RzBEQmFodkQKR01NV3pOT1J0RzJ2WFFoYUxZS2wvbDE1S3kwNE5DTDBlaFBCQW9HQkFMK2RmaVFmUjhYVGMwMzF6R0xwQm1kdQpOMU81aDM1cUdIT0s4M25MY1VQSnhzc0JTSGVxNTZEVHBic3VMRjY0V1c2bk5KSWRwckdRLzNpOFZldzhDa05ZCmtXY2ZGWkdUTGM4b2g4Tkw4bWpiZGtITCs3M3ZuVy9FbEZRRDlWZTE3WWN0cEZKbUcwcVBSaFVWQzhkSG5RYnAKWXRMTTQxb01qQlQ3NUdjRjBZZ2wKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= diff --git a/test/kubernetes/e2e/features/services/tlsroute/types.go b/test/kubernetes/e2e/features/services/tlsroute/types.go new file mode 100644 index 00000000000..62c8b95ca88 --- /dev/null +++ b/test/kubernetes/e2e/features/services/tlsroute/types.go @@ -0,0 +1,163 @@ +package tlsroute + +import ( + "fmt" + "net/http" + "path/filepath" + "time" + + testmatchers "github.com/solo-io/gloo/test/gomega/matchers" + + "github.com/solo-io/skv2/codegen/util" + + "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // Constants used by TestConfigureTLSRouteBackingDestinationsWithSingleService + singleSvcNsName = "single-tls-route" + singleSvcGatewayName = "single-tls-gateway" + singleSvcListenerName443 = "listener-443" + singleSvcName = "single-svc" + singleSvcTLSRouteName = "single-tls-route" + + // Constants used by TestConfigureTLSRouteBackingDestinationsWithMultiServices + multiSvcNsName = "multi-tls-route" + multiSvcGatewayName = "multi-tls-gateway" + multiSvcListenerName6443 = "listener-6443" + multiSvcListenerName8443 = "listener-8443" + multiSvc1Name = "multi-svc-1" + multiSvc2Name = "multi-svc-2" + multiSvcTLSRouteName1 = "tls-route-1" + multiSvcTLSRouteName2 = "tls-route-2" + + // Constants for CrossNamespaceTLSRouteWithReferenceGrant + crossNsTestName = "CrossNamespaceTLSRouteWithReferenceGrant" + crossNsClientName = "cross-namespace-allowed-client-ns" + crossNsBackendNsName = "cross-namespace-allowed-backend-ns" + crossNsGatewayName = "gateway" + crossNsListenerName = "listener-8443" + crossNsBackendSvcName = "backend-svc" + crossNsTLSRouteName = "tls-route" + crossNsReferenceGrantName = "reference-grant" + + // Constants for CrossNamespaceTLSRouteWithoutReferenceGrant + crossNsNoRefGrantTestName = "CrossNamespaceTLSRouteWithoutReferenceGrant" + crossNsNoRefGrantClientNsName = "client-ns-no-refgrant" + crossNsNoRefGrantBackendNsName = "backend-ns-no-refgrant" + crossNsNoRefGrantGatewayName = "gateway" + crossNsNoRefGrantListenerName = "listener-8443" + crossNsNoRefGrantBackendSvcName = "backend-svc" + crossNsNoRefGrantTLSRouteName = "tls-route" +) + +var ( + // Variables used by TestConfigureTCPRouteBackingDestinationsWithSingleService + multiSvcNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "multi-ns.yaml") + multiSvcGatewayAndClientManifest = filepath.Join(util.MustGetThisDir(), "testdata", "multi-listener-gateway-and-client.yaml") + multiSvcBackendManifest = filepath.Join(util.MustGetThisDir(), "testdata", "multi-backend-service.yaml") + multiSvcTlsRouteManifest = filepath.Join(util.MustGetThisDir(), "testdata", "multi-tlsroute.yaml") + + // Variables used by TestConfigureTCPRouteBackingDestinationsWithMultiServices + singleSvcNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "single-ns.yaml") + singleSvcGatewayAndClientManifest = filepath.Join(util.MustGetThisDir(), "testdata", "single-listener-gateway-and-client.yaml") + singleSvcBackendManifest = filepath.Join(util.MustGetThisDir(), "testdata", "single-backend-service.yaml") + singleSvcTLSRouteManifest = filepath.Join(util.MustGetThisDir(), "testdata", "single-tlsroute.yaml") + singleSecretManifest = filepath.Join(util.MustGetThisDir(), "testdata", "tls-secret.yaml") + + // Manifests for CrossNamespaceTLSRouteWithReferenceGrant + crossNsClientNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-client-ns.yaml") + crossNsBackendNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-backend-ns.yaml") + crossNsGatewayManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-gateway-and-client.yaml") + crossNsBackendSvcManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-backend-service.yaml") + crossNsTLSRouteManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-tlsroute.yaml") + crossNsRefGrantManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-referencegrant.yaml") + + // Manifests for CrossNamespaceTCPRouteWithoutReferenceGrant + crossNsNoRefGrantClientNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-no-refgrant-client-ns.yaml") + crossNsNoRefGrantBackendNsManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-no-refgrant-backend-ns.yaml") + crossNsNoRefGrantGatewayManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-no-refgrant-gateway-and-client.yaml") + crossNsNoRefGrantBackendSvcManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-no-refgrant-backend-service.yaml") + crossNsNoRefGrantTLSRouteManifest = filepath.Join(util.MustGetThisDir(), "testdata", "cross-ns-no-refgrant-tlsroute.yaml") + + // Assertion test timers + ctxTimeout = 5 * time.Minute + timeout = 60 * time.Second + + // Proxy resources to be translated + singleSvcNS = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: singleSvcNsName, + }, + } + + singleGlooProxy = metav1.ObjectMeta{ + Name: "gloo-proxy-single-tls-gateway", + Namespace: singleSvcNsName, + } + singleSvcProxyDeployment = &appsv1.Deployment{ObjectMeta: singleGlooProxy} + singleSvcProxyService = &corev1.Service{ObjectMeta: singleGlooProxy} + + multiSvcNS = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: multiSvcNsName, + }, + } + + multiGlooProxy = metav1.ObjectMeta{ + Name: "gloo-proxy-multi-tls-gateway", + Namespace: multiSvcNsName, + } + multiProxyDeployment = &appsv1.Deployment{ObjectMeta: multiGlooProxy} + multiProxyService = &corev1.Service{ObjectMeta: multiGlooProxy} + + // Expected curl responses from tests + expectedSingleSvcResp = &testmatchers.HttpResponse{ + StatusCode: http.StatusOK, + Body: gomega.SatisfyAll( + gomega.MatchRegexp(fmt.Sprintf(`"namespace"\s*:\s*"%s"`, singleSvcNsName)), + gomega.MatchRegexp(`"service"\s*:\s*"single-svc"`), + ), + } + + crossNsGlooProxy = metav1.ObjectMeta{ + Name: "gloo-proxy-gateway", + Namespace: crossNsClientName, + } + crossNsProxyDeployment = &appsv1.Deployment{ObjectMeta: crossNsGlooProxy} + crossNsProxyService = &corev1.Service{ObjectMeta: crossNsGlooProxy} + + crossNsNoRefGrantGlooProxy = metav1.ObjectMeta{ + Name: "gloo-proxy-gateway", + Namespace: crossNsNoRefGrantClientNsName, + } + crossNsNoRefGrantProxyDeployment = &appsv1.Deployment{ObjectMeta: crossNsNoRefGrantGlooProxy} + crossNsNoRefGrantProxyService = &corev1.Service{ObjectMeta: crossNsNoRefGrantGlooProxy} + + expectedMultiSvc1Resp = &testmatchers.HttpResponse{ + StatusCode: http.StatusOK, + Body: gomega.SatisfyAll( + gomega.MatchRegexp(fmt.Sprintf(`"namespace"\s*:\s*"%s"`, multiSvcNsName)), + gomega.MatchRegexp(fmt.Sprintf(`"service"\s*:\s*"%s"`, multiSvc1Name)), + ), + } + + expectedMultiSvc2Resp = &testmatchers.HttpResponse{ + StatusCode: http.StatusOK, + Body: gomega.SatisfyAll( + gomega.MatchRegexp(fmt.Sprintf(`"namespace"\s*:\s*"%s"`, multiSvcNsName)), + gomega.MatchRegexp(fmt.Sprintf(`"service"\s*:\s*"%s"`, multiSvc2Name)), + ), + } + + expectedCrossNsResp = &testmatchers.HttpResponse{ + StatusCode: http.StatusOK, + Body: gomega.SatisfyAll( + gomega.MatchRegexp(fmt.Sprintf(`"namespace"\s*:\s*"%s"`, crossNsBackendNsName)), + gomega.MatchRegexp(fmt.Sprintf(`"service"\s*:\s*"%s"`, crossNsBackendSvcName)), + ), + } +) diff --git a/test/kubernetes/e2e/tests/k8s_gw_tests.go b/test/kubernetes/e2e/tests/k8s_gw_tests.go index 7758cf1866d..97c19041dc8 100644 --- a/test/kubernetes/e2e/tests/k8s_gw_tests.go +++ b/test/kubernetes/e2e/tests/k8s_gw_tests.go @@ -16,6 +16,7 @@ import ( "github.com/solo-io/gloo/test/kubernetes/e2e/features/server_tls" "github.com/solo-io/gloo/test/kubernetes/e2e/features/services/httproute" "github.com/solo-io/gloo/test/kubernetes/e2e/features/services/tcproute" + "github.com/solo-io/gloo/test/kubernetes/e2e/features/services/tlsroute" "github.com/solo-io/gloo/test/kubernetes/e2e/features/tracing" "github.com/solo-io/gloo/test/kubernetes/e2e/features/upstreams" "github.com/solo-io/gloo/test/kubernetes/e2e/features/virtualhost_options" @@ -32,6 +33,7 @@ func KubeGatewaySuiteRunner() e2e.SuiteRunner { kubeGatewaySuiteRunner.Register("Upstreams", upstreams.NewTestingSuite) kubeGatewaySuiteRunner.Register("HTTPRouteServices", httproute.NewTestingSuite) kubeGatewaySuiteRunner.Register("TCPRouteServices", tcproute.NewTestingSuite) + kubeGatewaySuiteRunner.Register("TLSRouteServices", tlsroute.NewTestingSuite) kubeGatewaySuiteRunner.Register("HeadlessSvc", headless_svc.NewK8sGatewayHeadlessSvcSuite) kubeGatewaySuiteRunner.Register("PortRouting", port_routing.NewK8sGatewayTestingSuite) kubeGatewaySuiteRunner.Register("RouteDelegation", route_delegation.NewTestingSuite) diff --git a/test/kubernetes/testutils/assertions/status.go b/test/kubernetes/testutils/assertions/status.go index b356fe84d86..980f14f8e8c 100644 --- a/test/kubernetes/testutils/assertions/status.go +++ b/test/kubernetes/testutils/assertions/status.go @@ -245,6 +245,35 @@ func (p *Provider) EventuallyTCPRouteCondition( }, currentTimeout, pollingInterval).Should(gomega.Succeed()) } +// EventuallyTLSRouteCondition checks that provided TLSRoute condition is set to expect. +func (p *Provider) EventuallyTLSRouteCondition( + ctx context.Context, + routeName string, + routeNamespace string, + cond gwv1.RouteConditionType, + expect metav1.ConditionStatus, + timeout ...time.Duration, +) { + ginkgo.GinkgoHelper() + currentTimeout, pollingInterval := helper.GetTimeouts(timeout...) + p.Gomega.Eventually(func(g gomega.Gomega) { + route := &gwv1a2.TLSRoute{} + err := p.clusterContext.Client.Get(ctx, types.NamespacedName{Name: routeName, Namespace: routeNamespace}, route) + g.Expect(err).NotTo(gomega.HaveOccurred(), "failed to get TLSRoute %s/%s", routeNamespace, routeName) + + var conditionFound bool + for _, parentStatus := range route.Status.Parents { + condition := getConditionByType(parentStatus.Conditions, string(cond)) + if condition != nil && condition.Status == expect { + conditionFound = true + break + } + } + g.Expect(conditionFound).To(gomega.BeTrue(), fmt.Sprintf("%v condition is not %v for any parent of TLSRoute %s/%s", + cond, expect, routeNamespace, routeName)) + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} + // Helper function to retrieve a condition by type from a list of conditions. func getConditionByType(conditions []metav1.Condition, conditionType string) *metav1.Condition { for _, condition := range conditions {