diff --git a/internal/cmd/server.go b/internal/cmd/server.go index 3a7bf62511..052d0229e4 100644 --- a/internal/cmd/server.go +++ b/internal/cmd/server.go @@ -153,6 +153,8 @@ func setupRunners(cfg *config.Server) error { pResources.Namespaces.Close() pResources.GatewayStatuses.Close() pResources.HTTPRouteStatuses.Close() + pResources.TLSRoutes.Close() + pResources.TLSRouteStatuses.Close() xdsIR.Close() infraIR.Close() xds.Close() diff --git a/internal/envoygateway/scheme.go b/internal/envoygateway/scheme.go index e4971c7c7e..31fc950c4c 100644 --- a/internal/envoygateway/scheme.go +++ b/internal/envoygateway/scheme.go @@ -3,7 +3,8 @@ package envoygateway import ( "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" ) @@ -28,6 +29,9 @@ func init() { if err := gwapiv1b1.AddToScheme(scheme); err != nil { panic(err) } + if err := gwapiv1a2.AddToScheme(scheme); err != nil { + panic(err) + } } // GetScheme returns a scheme with types supported by the Kubernetes provider. diff --git a/internal/gatewayapi/contexts.go b/internal/gatewayapi/contexts.go index 23523401c7..abfb0d130e 100644 --- a/internal/gatewayapi/contexts.go +++ b/internal/gatewayapi/contexts.go @@ -7,6 +7,8 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" egv1alpha1 "github.com/envoyproxy/gateway/api/config/v1alpha1" @@ -20,6 +22,10 @@ type GatewayContext struct { listeners map[v1beta1.SectionName]*ListenerContext } +// GetListenerContext returns the ListenerContext with listenerName. +// If the listener exists in the Gateway Spec but NOT yet in the GatewayContext, +// this creates a new ListenerContext for the listener and attaches it to the +// GatewayContext. func (g *GatewayContext) GetListenerContext(listenerName v1beta1.SectionName) *ListenerContext { if g.listeners == nil { g.listeners = make(map[v1beta1.SectionName]*ListenerContext) @@ -167,6 +173,30 @@ func (l *ListenerContext) SetTLSSecret(tlsSecret *v1.Secret) { l.tlsSecret = tlsSecret } +// RouteContext represents a generic Route object (HTTPRoute, TLSRoute, etc.) +// that can reference Gateway objects. +type RouteContext interface { + client.Object + + // GetRouteType returns the Kind of the Route object, HTTPRoute, + // TLSRoute, TCPRoute, UDPRoute etc. + GetRouteType() string + + // TODO: [v1alpha2-v1beta1] This should not be required once all Route + // objects being implemented are of type v1beta1. + // GetHostnames returns the hosts targeted by the Route object. + GetHostnames() []string + + // TODO: [v1alpha2-v1beta1] This should not be required once all Route + // objects being implemented are of type v1beta1. + // GetParentReferences returns the ParentReference of the Route object. + GetParentReferences() []v1beta1.ParentReference + + // GetRouteParentContext returns RouteParentContext by using the Route + // objects' ParentReference. + GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext +} + // HTTPRouteContext wraps an HTTPRoute and provides helper methods for // accessing the route's parents. type HTTPRouteContext struct { @@ -175,6 +205,22 @@ type HTTPRouteContext struct { parentRefs map[v1beta1.ParentReference]*RouteParentContext } +func (h *HTTPRouteContext) GetRouteType() string { + return KindHTTPRoute +} + +func (h *HTTPRouteContext) GetHostnames() []string { + hostnames := make([]string, len(h.Spec.Hostnames)) + for idx, s := range h.Spec.Hostnames { + hostnames[idx] = string(s) + } + return hostnames +} + +func (h *HTTPRouteContext) GetParentReferences() []v1beta1.ParentReference { + return h.Spec.ParentRefs +} + func (h *HTTPRouteContext) GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext { if h.parentRefs == nil { h.parentRefs = make(map[v1beta1.ParentReference]*RouteParentContext) @@ -215,20 +261,109 @@ func (h *HTTPRouteContext) GetRouteParentContext(forParentRef v1beta1.ParentRefe ctx := &RouteParentContext{ ParentReference: parentRef, - route: h.HTTPRoute, + httpRoute: h.HTTPRoute, routeParentStatusIdx: routeParentStatusIdx, } h.parentRefs[forParentRef] = ctx return ctx } +// TLSRouteContext wraps a TLSRoute and provides helper methods for +// accessing the route's parents. +type TLSRouteContext struct { + *v1alpha2.TLSRoute + + parentRefs map[v1beta1.ParentReference]*RouteParentContext +} + +func (t *TLSRouteContext) GetRouteType() string { + return KindTLSRoute +} + +func (t *TLSRouteContext) GetHostnames() []string { + hostnames := make([]string, len(t.Spec.Hostnames)) + for idx, s := range t.Spec.Hostnames { + hostnames[idx] = string(s) + } + return hostnames +} + +func (t *TLSRouteContext) GetParentReferences() []v1beta1.ParentReference { + parentReferences := make([]v1beta1.ParentReference, len(t.Spec.ParentRefs)) + for idx, p := range t.Spec.ParentRefs { + parentReferences[idx] = UpgradeParentReference(p) + } + return parentReferences +} + +func (t *TLSRouteContext) GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext { + if t.parentRefs == nil { + t.parentRefs = make(map[v1beta1.ParentReference]*RouteParentContext) + } + + if ctx := t.parentRefs[forParentRef]; ctx != nil { + return ctx + } + + var parentRef *v1beta1.ParentReference + for i, p := range t.Spec.ParentRefs { + p := UpgradeParentReference(p) + if reflect.DeepEqual(p, forParentRef) { + upgraded := UpgradeParentReference(t.Spec.ParentRefs[i]) + parentRef = &upgraded + break + } + } + if parentRef == nil { + panic("parentRef not found") + } + + routeParentStatusIdx := -1 + for i := range t.Status.Parents { + p := UpgradeParentReference(t.Status.Parents[i].ParentRef) + defaultNamespace := v1beta1.Namespace(metav1.NamespaceDefault) + if forParentRef.Namespace == nil { + forParentRef.Namespace = &defaultNamespace + } + if p.Namespace == nil { + p.Namespace = &defaultNamespace + } + if reflect.DeepEqual(p, forParentRef) { + routeParentStatusIdx = i + break + } + } + if routeParentStatusIdx == -1 { + rParentStatus := v1alpha2.RouteParentStatus{ + // TODO: get this value from the config + ControllerName: v1alpha2.GatewayController(egv1alpha1.GatewayControllerName), + ParentRef: DowngradeParentReference(forParentRef), + } + t.Status.Parents = append(t.Status.Parents, rParentStatus) + routeParentStatusIdx = len(t.Status.Parents) - 1 + } + + ctx := &RouteParentContext{ + ParentReference: parentRef, + + tlsRoute: t.TLSRoute, + routeParentStatusIdx: routeParentStatusIdx, + } + t.parentRefs[forParentRef] = ctx + return ctx +} + // RouteParentContext wraps a ParentReference and provides helper methods for // setting conditions and other status information on the associated -// HTTPRoute, etc. +// HTTPRoute, TLSRoute etc. type RouteParentContext struct { *v1beta1.ParentReference - route *v1beta1.HTTPRoute + // TODO: [v1alpha2-v1beta1] This can probably be replaced with + // a single field pointing to *v1beta1.RouteStatus. + httpRoute *v1beta1.HTTPRoute + tlsRoute *v1alpha2.TLSRoute + routeParentStatusIdx int listeners []*ListenerContext } @@ -237,43 +372,77 @@ func (r *RouteParentContext) SetListeners(listeners ...*ListenerContext) { r.listeners = append(r.listeners, listeners...) } -func (r *RouteParentContext) SetCondition(conditionType v1beta1.RouteConditionType, status metav1.ConditionStatus, reason v1beta1.RouteConditionReason, message string) { +func (r *RouteParentContext) SetCondition(route RouteContext, conditionType v1beta1.RouteConditionType, status metav1.ConditionStatus, reason v1beta1.RouteConditionReason, message string) { cond := metav1.Condition{ Type: string(conditionType), Status: status, Reason: string(reason), Message: message, - ObservedGeneration: r.route.Generation, + ObservedGeneration: route.GetGeneration(), LastTransitionTime: metav1.NewTime(time.Now()), } idx := -1 - for i, existing := range r.route.Status.Parents[r.routeParentStatusIdx].Conditions { - if existing.Type == cond.Type { - // return early if the condition is unchanged - if existing.Status == cond.Status && - existing.Reason == cond.Reason && - existing.Message == cond.Message { - return + switch route.GetRouteType() { + case KindHTTPRoute: + for i, existing := range r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions { + if existing.Type == cond.Type { + // return early if the condition is unchanged + if existing.Status == cond.Status && + existing.Reason == cond.Reason && + existing.Message == cond.Message { + return + } + idx = i + break } - idx = i - break } - } - if idx > -1 { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond - } else { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.route.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + if idx > -1 { + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond + } else { + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + } + case KindTLSRoute: + for i, existing := range r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions { + if existing.Type == cond.Type { + // return early if the condition is unchanged + if existing.Status == cond.Status && + existing.Reason == cond.Reason && + existing.Message == cond.Message { + return + } + idx = i + break + } + } + + if idx > -1 { + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions[idx] = cond + } else { + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions = append(r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions, cond) + } } } -func (r *RouteParentContext) ResetConditions() { - r.route.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) +func (r *RouteParentContext) ResetConditions(route RouteContext) { + switch route.GetRouteType() { + case KindHTTPRoute: + r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) + case KindTLSRoute: + r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions = make([]metav1.Condition, 0) + } } -func (r *RouteParentContext) IsAccepted() bool { - for _, cond := range r.route.Status.Parents[r.routeParentStatusIdx].Conditions { +func (r *RouteParentContext) IsAccepted(route RouteContext) bool { + var conditions []metav1.Condition + switch route.GetRouteType() { + case KindHTTPRoute: + conditions = r.httpRoute.Status.Parents[r.routeParentStatusIdx].Conditions + case KindTLSRoute: + conditions = r.tlsRoute.Status.Parents[r.routeParentStatusIdx].Conditions + } + for _, cond := range conditions { if cond.Type == string(v1beta1.RouteConditionAccepted) && cond.Status == metav1.ConditionTrue { return true } diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index f4313c93b2..36220fbbb8 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -27,6 +27,11 @@ func FromNamespacesPtr(fromNamespaces v1beta1.FromNamespaces) *v1beta1.FromNames return &fromNamespaces } +func SectionNamePtr(name string) *v1beta1.SectionName { + sectionName := v1beta1.SectionName(name) + return §ionName +} + func StringPtr(val string) *string { return &val } @@ -138,9 +143,9 @@ func HasReadyListener(listeners []*ListenerContext) bool { return false } -// ComputeHosts returns a list of the intersecting hostnames between the route +// computeHosts returns a list of the intersecting hostnames between the route // and the listener. -func ComputeHosts(routeHostnames []v1beta1.Hostname, listenerHostname *v1beta1.Hostname) []string { +func computeHosts(routeHostnames []string, listenerHostname *v1beta1.Hostname) []string { var listenerHostnameVal string if listenerHostname != nil { listenerHostnameVal = string(*listenerHostname) @@ -159,7 +164,7 @@ func ComputeHosts(routeHostnames []v1beta1.Hostname, listenerHostname *v1beta1.H var hostnames []string for i := range routeHostnames { - routeHostname := string(routeHostnames[i]) + routeHostname := routeHostnames[i] // TODO ensure routeHostname is a valid hostname diff --git a/internal/gatewayapi/helpers_v1alpha2.go b/internal/gatewayapi/helpers_v1alpha2.go new file mode 100644 index 0000000000..c4b2153f2a --- /dev/null +++ b/internal/gatewayapi/helpers_v1alpha2.go @@ -0,0 +1,136 @@ +// Portions of this code are based on code from Contour. + +package gatewayapi + +import ( + "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// TODO: [v1alpha2-v1beta1] +// This file can be removed once TLSRoute graduates to v1beta1. + +func GroupPtrV1Alpha2(group string) *v1alpha2.Group { + gwGroup := v1alpha2.Group(group) + return &gwGroup +} + +func KindPtrV1Alpha2(kind string) *v1alpha2.Kind { + gwKind := v1alpha2.Kind(kind) + return &gwKind +} + +func NamespacePtrV1Alpha2(namespace string) *v1alpha2.Namespace { + gwNamespace := v1alpha2.Namespace(namespace) + return &gwNamespace +} + +func SectionNamePtrV1Alpha2(sectionName string) *v1alpha2.SectionName { + gwSectionName := v1alpha2.SectionName(sectionName) + return &gwSectionName +} + +func PortNumPtrV1Alpha2(port int) *v1alpha2.PortNumber { + pn := v1alpha2.PortNumber(port) + return &pn +} + +func UpgradeParentReferences(old []v1alpha2.ParentReference) []v1beta1.ParentReference { + newParentReferences := make([]v1beta1.ParentReference, len(old)) + for i, o := range old { + newParentReferences[i] = UpgradeParentReference(o) + } + return newParentReferences +} + +// UpgradeParentReference converts v1alpha2.ParentReference to v1beta1.ParentReference +func UpgradeParentReference(old v1alpha2.ParentReference) v1beta1.ParentReference { + upgraded := v1beta1.ParentReference{} + + if old.Group != nil { + upgraded.Group = GroupPtr(string(*old.Group)) + } + + if old.Kind != nil { + upgraded.Kind = KindPtr(string(*old.Kind)) + } + + if old.Namespace != nil { + upgraded.Namespace = NamespacePtr(string(*old.Namespace)) + } + + upgraded.Name = v1beta1.ObjectName(old.Name) + + if old.SectionName != nil { + upgraded.SectionName = SectionNamePtr(string(*old.SectionName)) + } + + if old.Port != nil { + upgraded.Port = PortNumPtr(int32(*old.Port)) + } + + return upgraded +} + +func DowngradeParentReference(old v1beta1.ParentReference) v1alpha2.ParentReference { + downgraded := v1alpha2.ParentReference{} + + if old.Group != nil { + downgraded.Group = GroupPtrV1Alpha2(string(*old.Group)) + } + + if old.Kind != nil { + downgraded.Kind = KindPtrV1Alpha2(string(*old.Kind)) + } + + if old.Namespace != nil { + downgraded.Namespace = NamespacePtrV1Alpha2(string(*old.Namespace)) + } + + downgraded.Name = v1alpha2.ObjectName(old.Name) + + if old.SectionName != nil { + downgraded.SectionName = SectionNamePtrV1Alpha2(string(*old.SectionName)) + } + + if old.Port != nil { + downgraded.Port = PortNumPtrV1Alpha2(int(*old.Port)) + } + + return downgraded +} + +func UpgradeRouteParentStatuses(routeParentStatuses []v1alpha2.RouteParentStatus) []v1beta1.RouteParentStatus { + var res []v1beta1.RouteParentStatus + + for _, rps := range routeParentStatuses { + res = append(res, v1beta1.RouteParentStatus{ + ParentRef: UpgradeParentReference(rps.ParentRef), + ControllerName: v1beta1.GatewayController(rps.ControllerName), + Conditions: rps.Conditions, + }) + } + + return res +} + +func DowngradeRouteParentStatuses(routeParentStatuses []v1beta1.RouteParentStatus) []v1alpha2.RouteParentStatus { + var res []v1alpha2.RouteParentStatus + + for _, rps := range routeParentStatuses { + res = append(res, v1alpha2.RouteParentStatus{ + ParentRef: DowngradeParentReference(rps.ParentRef), + ControllerName: v1alpha2.GatewayController(rps.ControllerName), + Conditions: rps.Conditions, + }) + } + + return res +} + +func NamespaceDerefOrAlpha(namespace *v1alpha2.Namespace, defaultNamespace string) string { + if namespace != nil && *namespace != "" { + return string(*namespace) + } + return defaultNamespace +} diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go index 05c5929a7a..d4793d5ecd 100644 --- a/internal/gatewayapi/runner/runner.go +++ b/internal/gatewayapi/runner/runner.go @@ -44,6 +44,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { gatewayClassesCh := r.ProviderResources.GatewayClasses.Subscribe(ctx) gatewaysCh := r.ProviderResources.Gateways.Subscribe(ctx) httpRoutesCh := r.ProviderResources.HTTPRoutes.Subscribe(ctx) + tlsRoutesCh := r.ProviderResources.TLSRoutes.Subscribe(ctx) servicesCh := r.ProviderResources.Services.Subscribe(ctx) namespacesCh := r.ProviderResources.Namespaces.Subscribe(ctx) @@ -54,6 +55,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { case <-gatewayClassesCh: case <-gatewaysCh: case <-httpRoutesCh: + case <-tlsRoutesCh: case <-servicesCh: case <-namespacesCh: } @@ -61,6 +63,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { // Load all resources required for translation in.Gateways = r.ProviderResources.GetGateways() in.HTTPRoutes = r.ProviderResources.GetHTTPRoutes() + in.TLSRoutes = r.ProviderResources.GetTLSRoutes() in.Services = r.ProviderResources.GetServices() in.Namespaces = r.ProviderResources.GetNamespaces() gatewayClasses := r.ProviderResources.GetGatewayClasses() @@ -99,6 +102,7 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { newKeys = append(newKeys, key) } } + for key, val := range result.XdsIR { if err := val.Validate(); err != nil { r.Logger.Error(err, "unable to validate xds ir, skipped sending it") @@ -124,6 +128,10 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) { key := utils.NamespacedName(httpRoute) r.ProviderResources.HTTPRouteStatuses.Store(key, httpRoute) } + for _, tlsRoute := range result.TLSRoutes { + key := utils.NamespacedName(tlsRoute) + r.ProviderResources.TLSRouteStatuses.Store(key, tlsRoute) + } } } r.Logger.Info("shutting down") diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml new file mode 100644 index 0000000000..c5762afbaf --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.in.yaml @@ -0,0 +1,35 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + hostname: foo.com + protocol: TLS + port: 80 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + kinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml new file mode 100644 index 0000000000..59c61cc716 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-allowed-tls-route-kind.out.yaml @@ -0,0 +1,72 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 80 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + kinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + status: + listeners: + - name: tls + attachedRoutes: 0 + conditions: + - type: ResolvedRefs + status: "False" + reason: InvalidRouteKinds + message: "Kind is not supported, kind must be TLSRoute" + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml index c17a2ef492..a4793ddeff 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.in.yaml @@ -9,6 +9,7 @@ gateways: listeners: - name: tls protocol: HTTPS + hostname: foo.com port: 443 allowedRoutes: namespaces: diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml index 40a696060d..13c19dc555 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-invalid-tls-configuration-invalid-mode.out.yaml @@ -9,6 +9,7 @@ gateways: listeners: - name: tls protocol: HTTPS + hostname: foo.com port: 443 allowedRoutes: namespaces: diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml new file mode 100644 index 0000000000..bf254c658b --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.in.yaml @@ -0,0 +1,70 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 90 + hostname: foo.com + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + - name: tls-terminate + protocol: HTTPS + port: 443 + hostname: foo.com + tls: + mode: Terminate + certificateRefs: + - name: tls-secret-1 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-2 + port: 8080 +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +secrets: + - apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: tls-secret-1 + type: kubernetes.io/tls + data: + tls.crt: Zm9vCg== + tls.key: YmFyCg== diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml new file mode 100644 index 0000000000..f47a7e3a1f --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tls-terminate-and-passthrough.out.yaml @@ -0,0 +1,154 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls-passthrough + protocol: TLS + port: 90 + hostname: foo.com + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + - name: tls-terminate + protocol: HTTPS + port: 443 + hostname: foo.com + tls: + mode: Terminate + certificateRefs: + - name: tls-secret-1 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls-passthrough + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready + - name: tls-terminate + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-2 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +httpRoutes: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + http: + - name: envoy-gateway-gateway-1-tls-terminate + address: 0.0.0.0 + port: 10443 + hostnames: + - "foo.com" + tls: + serverCertificate: Zm9vCg== + privateKey: YmFyCg== + routes: + - name: default-httproute-1-rule-0-match-0-foo.com + pathMatch: + prefix: "/" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 + tcp: + - name: envoy-gateway-gateway-1-tls-passthrough + address: 0.0.0.0 + port: 10090 + tls: + snis: + - "foo.com" + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" + ports: + - name: tls-passthrough + protocol: "TLS" + servicePort: 90 + containerPort: 10090 + - name: tls-terminate + protocol: "HTTPS" + servicePort: 443 + containerPort: 10443 diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml index c13cd35b07..ecd190d5e1 100644 --- a/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml +++ b/internal/gatewayapi/testdata/gateway-with-listener-with-valid-tls-configuration.out.yaml @@ -81,8 +81,8 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 image: envoyproxy/envoy:v1.23-latest listeners: diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml new file mode 100644 index 0000000000..e3bfa50235 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.in.yaml @@ -0,0 +1,56 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: tls-1 + protocol: TLS + tls: + mode: Passthrough + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml new file mode 100644 index 0000000000..f0c6ae9bc5 --- /dev/null +++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-http-and-tlsroute-same-hostname-and-port.out.yaml @@ -0,0 +1,120 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-1 + protocol: HTTP + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + - name: tls-1 + protocol: TLS + tls: + mode: Passthrough + port: 80 + hostname: foo.com + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: http-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + conditions: + - type: Conflicted + status: "True" + reason: HostnameConflict + message: All listeners for a given port must use a unique hostname + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. + - name: tls-1 + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + conditions: + - type: Conflicted + status: "True" + reason: HostnameConflict + message: All listeners for a given port must use a unique hostname + - type: Ready + status: "False" + reason: Invalid + message: Listener is invalid, see other Conditions for details. +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +tlsRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref + +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml index 18fc9ff2c8..3218c2096e 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-bad-port.out.yaml @@ -79,8 +79,8 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 image: envoyproxy/envoy:v1.23-latest listeners: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml index a0b7631900..7b0ad031a9 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-invalid-kind.out.yaml @@ -80,8 +80,8 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 image: envoyproxy/envoy:v1.23-latest listeners: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml index 634ed1b1bc..5e41249f9d 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-port.out.yaml @@ -78,8 +78,8 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 image: envoyproxy/envoy:v1.23-latest listeners: diff --git a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml index 4dfdd8dd66..27f0f389c4 100644 --- a/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml +++ b/internal/gatewayapi/testdata/httproute-with-invalid-backend-ref-no-service.out.yaml @@ -79,8 +79,8 @@ infraIR: proxy: metadata: labels: - gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway name: envoy-gateway-gateway-1 image: envoyproxy/envoy:v1.23-latest listeners: diff --git a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml new file mode 100644 index 0000000000..71db6c0ece --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.in.yaml @@ -0,0 +1,32 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml new file mode 100644 index 0000000000..9a82becdbc --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-attaching-to-gateway.out.yaml @@ -0,0 +1,84 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls + address: 0.0.0.0 + port: 10090 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 90 + containerPort: 10090 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml new file mode 100644 index 0000000000..d8a62c2adf --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.in.yaml @@ -0,0 +1,31 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + tls: + mode: Terminate + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml new file mode 100644 index 0000000000..90c6250061 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-incorrect-mode.out.yaml @@ -0,0 +1,67 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + tls: + mode: Terminate + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: UnsupportedTLSMode + message: TLS Terminate mode is not supported, TLS mode must be Passthrough. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml new file mode 100644 index 0000000000..38395404cd --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.in.yaml @@ -0,0 +1,29 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml new file mode 100644 index 0000000000..dbc22d0a5d --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-not-attaching-to-gateway-with-no-mode.out.yaml @@ -0,0 +1,65 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: Invalid + message: Listener must have TLS set when protocol is TLS. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml new file mode 100644 index 0000000000..d18aca10b8 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.in.yaml @@ -0,0 +1,57 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 +services: + - apiVersion: v1 + kind: Service + metadata: + namespace: test-service-namespace + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 +referenceGrants: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: ReferenceGrant + metadata: + namespace: test-service-namespace + name: referencegrant-1 + spec: + from: + - group: gateway.networking.k8s.io + kind: TLSRoute + namespace: default + to: + - group: "" + kind: Service diff --git a/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml new file mode 100644 index 0000000000..9bc5dc2c00 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-backendref-in-other-namespace-allowed-by-refgrant.out.yaml @@ -0,0 +1,85 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + port: 90 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 1 + conditions: + - type: Ready + status: "True" + reason: Ready + message: Listener is ready +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "True" + reason: Accepted + message: Route is accepted +xdsIR: + envoy-gateway-gateway-1: + tcp: + - name: envoy-gateway-gateway-1-tls + address: 0.0.0.0 + port: 10090 + tls: + snis: + - foo.com + destinations: + - host: 7.7.7.7 + port: 8080 + weight: 1 +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" + ports: + - name: tls + protocol: "TLS" + servicePort: 90 + containerPort: 10090 diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml new file mode 100644 index 0000000000..81afa2331b --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.in.yaml @@ -0,0 +1,44 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + tls: + mode: Passthrough + certificateRefs: + - name: tls-secret-1 + port: 90 + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 +secrets: + - apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: tls-secret-1 + type: kubernetes.io/tls + data: + tls.crt: Zm9vCg== + tls.key: YmFyCg== diff --git a/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml new file mode 100644 index 0000000000..b8b1322044 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-listener-both-passthrough-and-cert-data.out.yaml @@ -0,0 +1,70 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + hostname: foo.com + tls: + mode: Passthrough + certificateRefs: + - name: tls-secret-1 + port: 90 + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: Invalid + message: Listener must not have TLS certificate refs set for TLS mode Passthrough +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.in.yaml b/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.in.yaml new file mode 100644 index 0000000000..ac35ef2672 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.in.yaml @@ -0,0 +1,52 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + # TODO: add test for partial wildcard + # - name: tls-1 + # protocol: TLS + # hostname: "*w.example.com" + # port: 90 + # tls: + # mode: Passthrough + # allowedRoutes: + # namespaces: + # from: All + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 +services: + - apiVersion: v1 + kind: Service + metadata: + namespace: default + name: service-1 + spec: + clusterIP: 7.7.7.7 + ports: + - port: 8080 diff --git a/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.out.yaml b/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.out.yaml new file mode 100644 index 0000000000..feb5f80714 --- /dev/null +++ b/internal/gatewayapi/testdata/tlsroute-with-partial-wildcard-hostname.out.yaml @@ -0,0 +1,68 @@ +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: tls + protocol: TLS + port: 91 + tls: + mode: Passthrough + allowedRoutes: + namespaces: + from: All + status: + listeners: + - name: tls + supportedKinds: + - group: gateway.networking.k8s.io + kind: TLSRoute + attachedRoutes: 0 + conditions: + - type: Ready + status: "False" + reason: Invalid + message: Hostname must not be empty with TLS mode Passthrough. +tlsRoutes: + - apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: TLSRoute + metadata: + namespace: default + name: tlsroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + rules: + - backendRefs: + - name: service-1 + namespace: test-service-namespace + port: 8080 + status: + parents: + - parentRef: + namespace: envoy-gateway + name: gateway-1 + controllerName: gateway.envoyproxy.io/gatewayclass-controller + conditions: + - type: Accepted + status: "False" + reason: NoReadyListeners + message: There are no ready listeners for this parent ref +xdsIR: + envoy-gateway-gateway-1: {} +infraIR: + envoy-gateway-gateway-1: + proxy: + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway-gateway-1 + image: envoyproxy/envoy:v1.23-latest + listeners: + - address: "" diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index edf0bd3dc5..71e3ef9a11 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -19,6 +19,7 @@ import ( const ( KindGateway = "Gateway" KindHTTPRoute = "HTTPRoute" + KindTLSRoute = "TLSRoute" KindService = "Service" KindSecret = "Secret" @@ -45,6 +46,7 @@ type InfraIRMap map[string]*ir.Infra type Resources struct { Gateways []*v1beta1.Gateway HTTPRoutes []*v1beta1.HTTPRoute + TLSRoutes []*v1alpha2.TLSRoute ReferenceGrants []*v1alpha2.ReferenceGrant Namespaces []*v1.Namespace Services []*v1.Service @@ -90,11 +92,14 @@ type Translator struct { type TranslateResult struct { Gateways []*v1beta1.Gateway HTTPRoutes []*v1beta1.HTTPRoute + TLSRoutes []*v1alpha2.TLSRoute XdsIR XdsIRMap InfraIR InfraIRMap } -func newTranslateResult(gateways []*GatewayContext, httpRoutes []*HTTPRouteContext, xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { +func newTranslateResult(gateways []*GatewayContext, + httpRoutes []*HTTPRouteContext, tlsRoutes []*TLSRouteContext, + xdsIR XdsIRMap, infraIR InfraIRMap) *TranslateResult { translateResult := &TranslateResult{ XdsIR: xdsIR, InfraIR: infraIR, @@ -106,6 +111,9 @@ func newTranslateResult(gateways []*GatewayContext, httpRoutes []*HTTPRouteConte for _, httpRoute := range httpRoutes { translateResult.HTTPRoutes = append(translateResult.HTTPRoutes, httpRoute.HTTPRoute) } + for _, tlsRoute := range tlsRoutes { + translateResult.TLSRoutes = append(translateResult.TLSRoutes, tlsRoute.TLSRoute) + } return translateResult } @@ -123,10 +131,13 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult { // Process all relevant HTTPRoutes. httpRoutes := t.ProcessHTTPRoutes(resources.HTTPRoutes, gateways, resources, xdsIR) + // Process all relevant TLSRoutes. + tlsRoutes := t.ProcessTLSRoutes(resources.TLSRoutes, gateways, resources, xdsIR) + // Sort xdsIR based on the Gateway API spec sortXdsIRMap(xdsIR) - return newTranslateResult(gateways, httpRoutes, xdsIR, infraIR) + return newTranslateResult(gateways, httpRoutes, tlsRoutes, xdsIR, infraIR) } func (t *Translator) GetRelevantGateways(gateways []*v1beta1.Gateway) []*GatewayContext { @@ -246,6 +257,33 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap for _, listener := range gateway.listeners { // Process protocol & supported kinds switch listener.Protocol { + case v1beta1.TLSProtocolType: + if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { + listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindTLSRoute}) + } else { + for _, kind := range listener.AllowedRoutes.Kinds { + if kind.Group != nil && string(*kind.Group) != v1beta1.GroupName { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), + ) + continue + } + + if kind.Kind != KindTLSRoute { + listener.SetCondition( + v1beta1.ListenerConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalidRouteKinds, + fmt.Sprintf("Kind is not supported, kind must be %s", KindTLSRoute), + ) + continue + } + listener.SetSupportedKinds(kind) + } + } case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: if listener.AllowedRoutes == nil || len(listener.AllowedRoutes.Kinds) == 0 { listener.SetSupportedKinds(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindHTTPRoute}) @@ -258,6 +296,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap v1beta1.ListenerReasonInvalidRouteKinds, fmt.Sprintf("Group is not supported, group must be %s", v1beta1.GroupName), ) + continue } if kind.Kind != KindHTTPRoute { @@ -267,7 +306,9 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap v1beta1.ListenerReasonInvalidRouteKinds, fmt.Sprintf("Kind is not supported, kind must be %s", KindHTTPRoute), ) + continue } + listener.SetSupportedKinds(kind) } } default: @@ -432,6 +473,49 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap } listener.SetTLSSecret(secret) + case v1beta1.TLSProtocolType: + if listener.TLS == nil { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + fmt.Sprintf("Listener must have TLS set when protocol is %s.", listener.Protocol), + ) + break + } + + if listener.TLS.Mode != nil && *listener.TLS.Mode != v1beta1.TLSModePassthrough { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + "UnsupportedTLSMode", + fmt.Sprintf("TLS %s mode is not supported, TLS mode must be Passthrough.", *listener.TLS.Mode), + ) + break + } + + // With TLS Passthrough, partial wildcards are not allowed in xDS config, so "*", "*w.abc.com" are + // invalid configurations. + // TODO: add regex match to detect partial wildcards like *w.abc.com + if listener.Hostname == nil || *listener.Hostname == "" { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Hostname must not be empty with TLS mode Passthrough.", + ) + break + } + + if len(listener.TLS.CertificateRefs) > 0 { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener must not have TLS certificate refs set for TLS mode Passthrough", + ) + break + } } lConditions := listener.GetConditions() @@ -459,31 +543,60 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap continue } + // Add the listener to the Xds IR. servicePort := int32(listener.Port) containerPort := servicePortToContainerPort(servicePort) - // Add the listener to the Xds IR. - irListener := &ir.HTTPListener{ - Name: irListenerName(listener), - Address: "0.0.0.0", - Port: uint32(containerPort), - TLS: irTLSConfig(listener.tlsSecret), - } - if listener.Hostname != nil { - irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) - } else { - // Hostname specifies the virtual hostname to match for protocol types that define this concept. - // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. - // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. - irListener.Hostnames = append(irListener.Hostnames, "*") + switch listener.Protocol { + case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType: + irListener := &ir.HTTPListener{ + Name: irListenerName(listener), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: irTLSConfig(listener.tlsSecret), + } + if listener.Hostname != nil { + irListener.Hostnames = append(irListener.Hostnames, string(*listener.Hostname)) + } else { + // Hostname specifies the virtual hostname to match for protocol types that define this concept. + // When unspecified, all hostnames are matched. This field is ignored for protocols that don’t require hostname based matching. + // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Listener. + irListener.Hostnames = append(irListener.Hostnames, "*") + } + gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) + case v1beta1.TLSProtocolType: + irListener := &ir.TCPListener{ + Name: irListenerName(listener), + Address: "0.0.0.0", + Port: uint32(containerPort), + TLS: &ir.TLSInspectorConfig{ + SNIs: []string{}, + }, + } + if listener.Hostname == nil || *listener.Hostname == "" { + listener.SetCondition( + v1beta1.ListenerConditionReady, + metav1.ConditionFalse, + v1beta1.ListenerReasonInvalid, + "Listener is invalid, see other Conditions for details.", + ) + } + if listener.Hostname != nil && *listener.Hostname != "" { + irListener.TLS.SNIs = append(irListener.TLS.SNIs, string(*listener.Hostname)) + } + gwXdsIR.TCP = append(gwXdsIR.TCP, irListener) } - gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) // Add the listener to the Infra IR. Infra IR ports must have a unique port number. if !slices.Contains(foundPorts, servicePort) { foundPorts = append(foundPorts, servicePort) - proto := ir.HTTPProtocolType - if listener.Protocol == v1beta1.HTTPSProtocolType { + var proto ir.ProtocolType + switch listener.Protocol { + case v1beta1.HTTPProtocolType: + proto = ir.HTTPProtocolType + case v1beta1.HTTPSProtocolType: proto = ir.HTTPSProtocolType + case v1beta1.TLSProtocolType: + proto = ir.TLSProtocolType } infraPort := ir.ListenerPort{ Name: string(listener.Name), @@ -525,7 +638,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Group != nil && *backendRef.Group != "" { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonInvalidKind, @@ -535,7 +648,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Kind != nil && *backendRef.Kind != KindService { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonInvalidKind, @@ -559,7 +672,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, }, resources.ReferenceGrants, ) { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonRefNotPermitted, @@ -570,7 +683,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if backendRef.Port == nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, "PortNotSpecified", @@ -581,7 +694,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, service := resources.GetService(NamespaceDerefOr(backendRef.Namespace, httpRoute.Namespace), string(backendRef.Name)) if service == nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonBackendNotFound, @@ -599,7 +712,7 @@ func buildRuleRouteDest(backendRef v1beta1.HTTPBackendRef, } if !portFound { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, "PortNotFound", @@ -628,42 +741,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways // Find out if this route attaches to one of our Gateway's listeners, // and if so, get the list of listeners that allow it to attach for each // parentRef. - var relevantRoute bool - for _, parentRef := range httpRoute.Spec.ParentRefs { - isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) - - // Parent ref is not to a Gateway that we control: skip it - if !isRelevantParentRef { - continue - } - relevantRoute = true - - parentRefCtx := httpRoute.GetRouteParentContext(parentRef) - // Reset conditions since they will be recomputed during translation - parentRefCtx.ResetConditions() - - if !HasReadyListener(selectedListeners) { - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionFalse, "NoReadyListeners", "There are no ready listeners for this parent ref") - continue - } - - var allowedListeners []*ListenerContext - for _, listener := range selectedListeners { - if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: KindHTTPRoute}) && listener.AllowsNamespace(resources.GetNamespace(httpRoute.Namespace)) { - allowedListeners = append(allowedListeners, listener) - } - } - - if len(allowedListeners) == 0 { - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonNotAllowedByListeners, "No listeners included by this parent ref allowed this attachment.") - continue - } - - parentRefCtx.SetListeners(allowedListeners...) - - parentRefCtx.SetCondition(v1beta1.RouteConditionAccepted, metav1.ConditionTrue, v1beta1.RouteReasonAccepted, "Route is accepted") - } - + relevantRoute := processAllowedListenersForParentRefs(httpRoute, gateways, resources) if !relevantRoute { continue } @@ -672,7 +750,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, parentRef := range httpRoute.parentRefs { // Skip parent refs that did not accept the route - if !parentRef.IsAccepted() { + if !parentRef.IsAccepted(httpRoute) { continue } @@ -700,7 +778,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways case v1beta1.HTTPRouteFilterRequestRedirect: // Can't have two redirects for the same route if redirectResponse != nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -722,7 +800,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways redir.Scheme = redirect.Scheme } else { errMsg := fmt.Sprintf("Scheme: %s is unsupported, only 'https' and 'http' are supported", *redirect.Scheme) - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -734,7 +812,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways if redirect.Hostname != nil { if err := isValidHostname(string(*redirect.Hostname)); err != nil { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -763,7 +841,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } default: errMsg := fmt.Sprintf("Redirect path type: %s is invalid, only \"ReplaceFullPath\" and \"ReplacePrefixMatch\" are supported", redirect.Path.Type) - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -780,7 +858,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways redir.StatusCode = &redirectCode } else { errMsg := fmt.Sprintf("Status code %d is invalid, only 302 and 301 are supported", redirectCode) - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -812,7 +890,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, addHeader := range headersToAdd { emptyFilterConfig = false if addHeader.Name == "" { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -823,7 +901,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names if strings.Contains(string(addHeader.Name), "/") || strings.Contains(string(addHeader.Name), ":") { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -842,7 +920,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } if !canAddHeader { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -869,7 +947,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways for _, setHeader := range headersToSet { if setHeader.Name == "" { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -879,7 +957,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } // Per Gateway API specification on HTTPHeaderName, : and / are invalid characters in header names if strings.Contains(string(setHeader.Name), "/") || strings.Contains(string(setHeader.Name), ":") { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -898,7 +976,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } if !canAddHeader { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -925,7 +1003,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } for _, removedHeader := range headersToRemove { if removedHeader == "" { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -942,7 +1020,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } if !canRemHeader { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -958,7 +1036,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways // Update the status if the filter failed to configure any valid headers to add/remove if len(addRequestHeaders) == 0 && len(removeRequestHeaders) == 0 && !emptyFilterConfig { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -969,7 +1047,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways // "If a reference to a custom filter type cannot be resolved, the filter MUST NOT be skipped. // Instead, requests that would have been processed by that filter MUST receive a HTTP error response." errMsg := fmt.Sprintf("Unknown custom filter type: %s", filter.Type) - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionResolvedRefs, metav1.ConditionFalse, v1beta1.RouteReasonUnsupportedValue, @@ -1028,7 +1106,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } for _, backendRef := range rule.BackendRefs { - destination, backendWeight := buildRuleRouteDest(backendRef, parentRef, httpRoute, resources) for _, route := range ruleRoutes { // If the route already has a direct response or redirect configured, then it was from a filter so skip @@ -1043,7 +1120,6 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } } } - } // If the route has no valid backends then just use a direct response and don't fuss with weighted responses @@ -1064,7 +1140,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways var hasHostnameIntersection bool for _, listener := range parentRef.listeners { - hosts := ComputeHosts(httpRoute.Spec.Hostnames, listener.Hostname) + hosts := computeHosts(httpRoute.GetHostnames(), listener.Hostname) if len(hosts) == 0 { continue } @@ -1104,7 +1180,7 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } irKey := irStringKey(listener.gateway) - irListener := xdsIR[irKey].GetListener(irListenerName(listener)) + irListener := xdsIR[irKey].GetHTTPListener(irListenerName(listener)) if irListener != nil { irListener.Routes = append(irListener.Routes, perHostRoutes...) } @@ -1117,14 +1193,14 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways } if !hasHostnameIntersection { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionAccepted, metav1.ConditionFalse, v1beta1.RouteReasonNoMatchingListenerHostname, "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", ) } else { - parentRef.SetCondition( + parentRef.SetCondition(httpRoute, v1beta1.RouteConditionAccepted, metav1.ConditionTrue, v1beta1.RouteReasonAccepted, @@ -1137,6 +1213,246 @@ func (t *Translator) ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways return relevantHTTPRoutes } +func (t *Translator) ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext { + var relevantTLSRoutes []*TLSRouteContext + + for _, t := range tlsRoutes { + if t == nil { + panic("received nil tlsroute") + } + tlsRoute := &TLSRouteContext{TLSRoute: t} + + // Find out if this route attaches to one of our Gateway's listeners, + // and if so, get the list of listeners that allow it to attach for each + // parentRef. + relevantRoute := processAllowedListenersForParentRefs(tlsRoute, gateways, resources) + if !relevantRoute { + continue + } + + relevantTLSRoutes = append(relevantTLSRoutes, tlsRoute) + + for _, parentRef := range tlsRoute.parentRefs { + // Skip parent refs that did not accept the route + if !parentRef.IsAccepted(tlsRoute) { + continue + } + + // Need to compute Route rules within the parentRef loop because + // any conditions that come out of it have to go on each RouteParentStatus, + // not on the Route as a whole. + var routeDestinations []*ir.RouteDestination + + // compute backends + for _, rule := range tlsRoute.Spec.Rules { + for _, backendRef := range rule.BackendRefs { + if backendRef.Group != nil && *backendRef.Group != "" { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Group is invalid, only the core API group (specified by omitting the group field or setting it to an empty string) is supported", + ) + continue + } + + if backendRef.Kind != nil && *backendRef.Kind != KindService { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonInvalidKind, + "Kind is invalid, only Service is supported", + ) + continue + } + + if backendRef.Namespace != nil && string(*backendRef.Namespace) != "" && string(*backendRef.Namespace) != tlsRoute.Namespace { + if !isValidCrossNamespaceRef( + crossNamespaceFrom{ + group: v1beta1.GroupName, + kind: KindTLSRoute, + namespace: tlsRoute.Namespace, + }, + crossNamespaceTo{ + group: "", + kind: KindService, + namespace: string(*backendRef.Namespace), + name: string(backendRef.Name), + }, + resources.ReferenceGrants, + ) { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonRefNotPermitted, + fmt.Sprintf("Backend ref to service %s/%s not permitted by any ReferenceGrant", *backendRef.Namespace, backendRef.Name), + ) + continue + } + } + + if backendRef.Port == nil { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotSpecified", + "A valid port number corresponding to a port on the Service must be specified", + ) + continue + } + + // TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when TLSRoute graduates to v1beta1. + serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, tlsRoute.Namespace) + service := resources.GetService(serviceNamespace, string(backendRef.Name)) + if service == nil { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + v1beta1.RouteReasonBackendNotFound, + fmt.Sprintf("Service %s/%s not found", serviceNamespace, string(backendRef.Name)), + ) + continue + } + + var portFound bool + for _, port := range service.Spec.Ports { + if port.Port == int32(*backendRef.Port) { + portFound = true + break + } + } + + if !portFound { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionResolvedRefs, + metav1.ConditionFalse, + "PortNotFound", + fmt.Sprintf("Port %d not found on service %s/%s", *backendRef.Port, serviceNamespace, string(backendRef.Name)), + ) + continue + } + + weight := uint32(1) + if backendRef.Weight != nil { + weight = uint32(*backendRef.Weight) + } + + routeDestinations = append(routeDestinations, &ir.RouteDestination{ + Host: service.Spec.ClusterIP, + Port: uint32(*backendRef.Port), + Weight: weight, + }) + } + + // TODO handle: + // - no valid backend refs + // - sum of weights for valid backend refs is 0 + // - returning 500's for invalid backend refs + // - etc. + } + + var hasHostnameIntersection bool + for _, listener := range parentRef.listeners { + hosts := computeHosts(tlsRoute.GetHostnames(), listener.Hostname) + if len(hosts) == 0 { + continue + } + hasHostnameIntersection = true + + irKey := irStringKey(listener.gateway) + irListener := xdsIR[irKey].GetTCPListener(irListenerName(listener)) + if irListener != nil { + irListener.Destinations = routeDestinations + } + // Theoretically there should only be one parent ref per + // Route that attaches to a given Listener, so fine to just increment here, but we + // might want to check to ensure we're not double-counting. + if len(routeDestinations) > 0 { + listener.IncrementAttachedRoutes() + } + } + + if !hasHostnameIntersection { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNoMatchingListenerHostname, + "There were no hostname intersections between the HTTPRoute and this parent ref's Listener(s).", + ) + } else { + parentRef.SetCondition(tlsRoute, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + } + } + + return relevantTLSRoutes +} + +// processAllowedListenersForParentRefs finds out if the route attaches to one of our +// Gateways' listeners, and if so, gets the list of listeners that allow it to +// attach for each parentRef. +func processAllowedListenersForParentRefs(routeContext RouteContext, gateways []*GatewayContext, resources *Resources) bool { + var relevantRoute bool + + for _, parentRef := range routeContext.GetParentReferences() { + isRelevantParentRef, selectedListeners := GetReferencedListeners(parentRef, gateways) + + // Parent ref is not to a Gateway that we control: skip it + if !isRelevantParentRef { + continue + } + relevantRoute = true + + parentRefCtx := routeContext.GetRouteParentContext(parentRef) + // Reset conditions since they will be recomputed during translation + parentRefCtx.ResetConditions(routeContext) + + if !HasReadyListener(selectedListeners) { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + "NoReadyListeners", + "There are no ready listeners for this parent ref", + ) + continue + } + + var allowedListeners []*ListenerContext + for _, listener := range selectedListeners { + acceptedKind := routeContext.GetRouteType() + if listener.AllowsKind(v1beta1.RouteGroupKind{Group: GroupPtr(v1beta1.GroupName), Kind: v1beta1.Kind(acceptedKind)}) && + listener.AllowsNamespace(resources.GetNamespace(routeContext.GetNamespace())) { + allowedListeners = append(allowedListeners, listener) + } + } + + if len(allowedListeners) == 0 { + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionFalse, + v1beta1.RouteReasonNotAllowedByListeners, + "No listeners included by this parent ref allowed this attachment.", + ) + continue + } + + parentRefCtx.SetListeners(allowedListeners...) + + parentRefCtx.SetCondition(routeContext, + v1beta1.RouteConditionAccepted, + metav1.ConditionTrue, + v1beta1.RouteReasonAccepted, + "Route is accepted", + ) + } + return relevantRoute +} + type crossNamespaceFrom struct { group string kind string @@ -1193,7 +1509,6 @@ func isValidCrossNamespaceRef(from crossNamespaceFrom, to crossNamespaceTo, refe // Checks if a hostname is valid according to RFC 1123 and gateway API's requirement that it not be an IP address func isValidHostname(hostname string) error { - if errs := validation.IsDNS1123Subdomain(hostname); errs != nil { return fmt.Errorf("hostname %q is invalid for a redirect filter: %v", hostname, errs) } @@ -1229,8 +1544,8 @@ func irListenerName(listener *ListenerContext) string { return fmt.Sprintf("%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name) } -func routeName(httpRoute *HTTPRouteContext, ruleIdx, matchIdx int) string { - return fmt.Sprintf("%s-%s-rule-%d-match-%d", httpRoute.Namespace, httpRoute.Name, ruleIdx, matchIdx) +func routeName(route RouteContext, ruleIdx, matchIdx int) string { + return fmt.Sprintf("%s-%s-rule-%d-match-%d", route.GetNamespace(), route.GetName(), ruleIdx, matchIdx) } func irTLSConfig(tlsSecret *v1.Secret) *ir.TLSListenerConfig { diff --git a/internal/ir/infra.go b/internal/ir/infra.go index 52e05ea7c3..99d91f103a 100644 --- a/internal/ir/infra.go +++ b/internal/ir/infra.go @@ -79,6 +79,9 @@ const ( // HTTPSProtocolType accepts HTTP/1.1 or HTTP/2 sessions over TLS. HTTPSProtocolType ProtocolType = "HTTPS" + + // Accepts TLS sessions over TCP. + TLSProtocolType ProtocolType = "TLS" ) // NewInfra returns a new Infra with default parameters. diff --git a/internal/ir/xds.go b/internal/ir/xds.go index d03de20b28..9224762d59 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -8,10 +8,11 @@ import ( ) var ( - ErrHTTPListenerNameEmpty = errors.New("field Name must be specified") - ErrHTTPListenerAddressInvalid = errors.New("field Address must be a valid IP address") - ErrHTTPListenerPortInvalid = errors.New("field Port specified is invalid") + ErrListenerNameEmpty = errors.New("field Name must be specified") + ErrListenerAddressInvalid = errors.New("field Address must be a valid IP address") + ErrListenerPortInvalid = errors.New("field Port specified is invalid") ErrHTTPListenerHostnamesEmpty = errors.New("field Hostnames must be specified with at least a single hostname entry") + ErrTCPListenesSNIsEmpty = errors.New("field SNIs must be specified with at least a single server name entry") ErrTLSServerCertEmpty = errors.New("field ServerCertificate must be specified") ErrTLSPrivateKey = errors.New("field PrivateKey must be specified") ErrHTTPRouteNameEmpty = errors.New("field Name must be specified") @@ -35,6 +36,8 @@ var ( type Xds struct { // HTTP listeners exposed by the gateway. HTTP []*HTTPListener + // TCP Listeners exposed by the gateway. + TCP []*TCPListener } // Validate the fields within the Xds structure. @@ -48,6 +51,24 @@ func (x Xds) Validate() error { return errs } +func (x Xds) GetHTTPListener(name string) *HTTPListener { + for _, listener := range x.HTTP { + if listener.Name == name { + return listener + } + } + return nil +} + +func (x Xds) GetTCPListener(name string) *TCPListener { + for _, listener := range x.TCP { + if listener.Name == name { + return listener + } + } + return nil +} + // HTTPListener holds the listener configuration. // +k8s:deepcopy-gen=true type HTTPListener struct { @@ -68,26 +89,17 @@ type HTTPListener struct { Routes []*HTTPRoute } -func (x Xds) GetListener(name string) *HTTPListener { - for _, listener := range x.HTTP { - if listener.Name == name { - return listener - } - } - return nil -} - // Validate the fields within the HTTPListener structure func (h HTTPListener) Validate() error { var errs error if h.Name == "" { - errs = multierror.Append(errs, ErrHTTPListenerNameEmpty) + errs = multierror.Append(errs, ErrListenerNameEmpty) } if ip := net.ParseIP(h.Address); ip == nil { - errs = multierror.Append(errs, ErrHTTPListenerAddressInvalid) + errs = multierror.Append(errs, ErrListenerAddressInvalid) } if h.Port == 0 { - errs = multierror.Append(errs, ErrHTTPListenerPortInvalid) + errs = multierror.Append(errs, ErrListenerPortInvalid) } if len(h.Hostnames) == 0 { errs = multierror.Append(errs, ErrHTTPListenerHostnamesEmpty) @@ -234,6 +246,20 @@ type RouteDestination struct { Weight uint32 } +// Validate the fields within the RouteDestination structure +func (r RouteDestination) Validate() error { + var errs error + // Only support IP hosts for now + if ip := net.ParseIP(r.Host); ip == nil { + errs = multierror.Append(errs, ErrRouteDestinationHostInvalid) + } + if r.Port == 0 { + errs = multierror.Append(errs, ErrRouteDestinationPortInvalid) + } + + return errs +} + // Add header configures a headder to be added to a request. // +k8s:deepcopy-gen=true type AddHeader struct { @@ -336,20 +362,6 @@ func (r HTTPPathModifier) Validate() error { return errs } -// Validate the fields within the RouteDestination structure -func (r RouteDestination) Validate() error { - var errs error - // Only support IP hosts for now - if ip := net.ParseIP(r.Host); ip == nil { - errs = multierror.Append(errs, ErrRouteDestinationHostInvalid) - } - if r.Port == 0 { - errs = multierror.Append(errs, ErrRouteDestinationPortInvalid) - } - - return errs -} - // StringMatch holds the various match conditions. // Only one of Exact, Prefix or SafeRegex can be set. // +k8s:deepcopy-gen=true @@ -384,3 +396,63 @@ func (s StringMatch) Validate() error { return errs } + +// TCPListener holds the TCP listener configuration. +// +k8s:deepcopy-gen=true +type TCPListener struct { + // Name of the TCPListener + Name string + // Address that the listener should listen on. + Address string + // Port on which the service can be expected to be accessed by clients. + Port uint32 + // TLS information required for TLS Passthrough, If provided, incoming + // connections' server names are inspected and routed to backends accordingly. + TLS *TLSInspectorConfig + // Destinations associated with TCP traffic to the service. + Destinations []*RouteDestination +} + +// Validate the fields within the TCPListener structure +func (h TCPListener) Validate() error { + var errs error + if h.Name == "" { + errs = multierror.Append(errs, ErrListenerNameEmpty) + } + if ip := net.ParseIP(h.Address); ip == nil { + errs = multierror.Append(errs, ErrListenerAddressInvalid) + } + if h.Port == 0 { + errs = multierror.Append(errs, ErrListenerPortInvalid) + } + if h.TLS != nil { + if err := h.TLS.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + for _, route := range h.Destinations { + if err := route.Validate(); err != nil { + errs = multierror.Append(errs, err) + } + } + return errs +} + +// TLSInspectorConfig holds the configuration required for inspecting TLS +// passthrough connections. +// +k8s:deepcopy-gen=true +type TLSInspectorConfig struct { + // Server names that are compared against the server names of a new connection. + // Wildcard hosts are supported in the prefix form. Partial wildcards are not + // supported, and values like *w.example.com are invalid. + // SNIs are used only in case of TLS Passthrough. + SNIs []string +} + +func (t TLSInspectorConfig) Validate() error { + var errs error + if len(t.SNIs) == 0 { + errs = multierror.Append(errs, ErrTCPListenesSNIsEmpty) + } + return errs +} diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index da45b040cd..537a01aa0d 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -45,6 +45,34 @@ var ( Routes: []*HTTPRoute{&weightedInvalidBackendsHTTPRoute}, } + // TCPListener + happyTCPListenerTLSPassthrough = TCPListener{ + Name: "happy", + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidNameTCPListenerTLSPassthrough = TCPListener{ + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidAddrTCPListenerTLSPassthrough = TCPListener{ + Name: "invalid-addr", + Address: "1.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{"example.com"}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + invalidSNITCPListenerTLSPassthrough = TCPListener{ + Address: "0.0.0.0", + Port: 80, + TLS: &TLSInspectorConfig{SNIs: []string{}}, + Destinations: []*RouteDestination{&happyRouteDestination}, + } + // HTTPRoute happyHTTPRoute = HTTPRoute{ Name: "happy", @@ -244,12 +272,19 @@ func TestValidateXds(t *testing.T) { }, want: nil, }, + { + name: "happy tls", + input: Xds{ + TCP: []*TCPListener{&happyTCPListenerTLSPassthrough}, + }, + want: nil, + }, { name: "invalid listener", input: Xds{ HTTP: []*HTTPListener{&happyHTTPListener, &invalidAddrHTTPListener, &invalidRouteMatchHTTPListener}, }, - want: []error{ErrHTTPListenerAddressInvalid, ErrHTTPRouteMatchEmpty}, + want: []error{ErrListenerAddressInvalid, ErrHTTPRouteMatchEmpty}, }, { name: "invalid backend", @@ -300,12 +335,12 @@ func TestValidateHTTPListener(t *testing.T) { Hostnames: []string{"example.com"}, Routes: []*HTTPRoute{&happyHTTPRoute}, }, - want: []error{ErrHTTPListenerNameEmpty}, + want: []error{ErrListenerNameEmpty}, }, { name: "invalid addr", input: invalidAddrHTTPListener, - want: []error{ErrHTTPListenerAddressInvalid}, + want: []error{ErrListenerAddressInvalid}, }, { name: "invalid port and hostnames", @@ -314,7 +349,7 @@ func TestValidateHTTPListener(t *testing.T) { Address: "1.0.0", Routes: []*HTTPRoute{&happyHTTPRoute}, }, - want: []error{ErrHTTPListenerPortInvalid, ErrHTTPListenerHostnamesEmpty}, + want: []error{ErrListenerPortInvalid, ErrHTTPListenerHostnamesEmpty}, }, { name: "invalid route match", @@ -337,6 +372,48 @@ func TestValidateHTTPListener(t *testing.T) { } } +func TestValidateTCPListener(t *testing.T) { + tests := []struct { + name string + input TCPListener + want []error + }{ + { + name: "tls passthrough happy", + input: happyTCPListenerTLSPassthrough, + want: nil, + }, + { + name: "tls passthrough invalid name", + input: invalidNameTCPListenerTLSPassthrough, + want: []error{ErrListenerNameEmpty}, + }, + { + name: "tls passthrough invalid addr", + input: invalidAddrTCPListenerTLSPassthrough, + want: []error{ErrListenerAddressInvalid}, + }, + { + name: "tls passthrough empty SNIs", + input: invalidSNITCPListenerTLSPassthrough, + want: []error{ErrTCPListenesSNIsEmpty}, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + if test.want == nil { + require.NoError(t, test.input.Validate()) + } else { + got := test.input.Validate() + for _, w := range test.want { + assert.ErrorContains(t, got, w.Error()) + } + } + }) + } +} + func TestValidateTLSListenerConfig(t *testing.T) { tests := []struct { name string @@ -376,7 +453,6 @@ func TestValidateTLSListenerConfig(t *testing.T) { } }) } - } func TestValidateHTTPRoute(t *testing.T) { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index b765727d61..16c3477e5b 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -358,6 +358,57 @@ func (in *StringMatch) DeepCopy() *StringMatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPListener) DeepCopyInto(out *TCPListener) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSInspectorConfig) + (*in).DeepCopyInto(*out) + } + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make([]*RouteDestination, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RouteDestination) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPListener. +func (in *TCPListener) DeepCopy() *TCPListener { + if in == nil { + return nil + } + out := new(TCPListener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSInspectorConfig) DeepCopyInto(out *TLSInspectorConfig) { + *out = *in + if in.SNIs != nil { + in, out := &in.SNIs, &out.SNIs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSInspectorConfig. +func (in *TLSInspectorConfig) DeepCopy() *TLSInspectorConfig { + if in == nil { + return nil + } + out := new(TLSInspectorConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSListenerConfig) DeepCopyInto(out *TLSListenerConfig) { *out = *in @@ -397,6 +448,17 @@ func (in *Xds) DeepCopyInto(out *Xds) { } } } + if in.TCP != nil { + in, out := &in.TCP, &out.TCP + *out = make([]*TCPListener, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(TCPListener) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Xds. diff --git a/internal/message/types.go b/internal/message/types.go index 6d7ec1a64d..c3f5675559 100644 --- a/internal/message/types.go +++ b/internal/message/types.go @@ -4,6 +4,7 @@ import ( "github.com/telepresenceio/watchable" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/internal/ir" @@ -15,11 +16,13 @@ type ProviderResources struct { GatewayClasses watchable.Map[string, *gwapiv1b1.GatewayClass] Gateways watchable.Map[types.NamespacedName, *gwapiv1b1.Gateway] HTTPRoutes watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute] + TLSRoutes watchable.Map[types.NamespacedName, *gwapiv1a2.TLSRoute] Namespaces watchable.Map[string, *corev1.Namespace] Services watchable.Map[types.NamespacedName, *corev1.Service] GatewayStatuses watchable.Map[types.NamespacedName, *gwapiv1b1.Gateway] HTTPRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute] + TLSRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1a2.TLSRoute] } func (p *ProviderResources) GetGatewayClasses() []*gwapiv1b1.GatewayClass { @@ -56,6 +59,17 @@ func (p *ProviderResources) GetHTTPRoutes() []*gwapiv1b1.HTTPRoute { return res } +func (p *ProviderResources) GetTLSRoutes() []*gwapiv1a2.TLSRoute { + if p.TLSRoutes.Len() == 0 { + return nil + } + res := make([]*gwapiv1a2.TLSRoute, 0, p.TLSRoutes.Len()) + for _, v := range p.TLSRoutes.LoadAll() { + res = append(res, v) + } + return res +} + func (p *ProviderResources) GetNamespaces() []*corev1.Namespace { if p.Namespaces.Len() == 0 { return nil diff --git a/internal/message/types_test.go b/internal/message/types_test.go index 36e1649547..4b2faa409b 100644 --- a/internal/message/types_test.go +++ b/internal/message/types_test.go @@ -7,6 +7,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -34,6 +35,12 @@ func TestProviderResources(t *testing.T) { Namespace: "test", }, } + t1 := &gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute1", + Namespace: "test", + }, + } s1 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service1", @@ -46,6 +53,7 @@ func TestProviderResources(t *testing.T) { assert.Nil(t, resources.GetGatewayClasses()) assert.Nil(t, resources.GetGateways()) assert.Nil(t, resources.GetHTTPRoutes()) + assert.Nil(t, resources.GetTLSRoutes()) assert.Nil(t, resources.GetServices()) // Add resources @@ -64,6 +72,12 @@ func TestProviderResources(t *testing.T) { } resources.HTTPRoutes.Store(r1Key, r1) + t1Key := types.NamespacedName{ + Namespace: t1.GetNamespace(), + Name: t1.GetName(), + } + resources.TLSRoutes.Store(t1Key, t1) + s1Key := types.NamespacedName{ Namespace: s1.GetNamespace(), Name: s1.GetName(), @@ -83,6 +97,9 @@ func TestProviderResources(t *testing.T) { hrs := resources.GetHTTPRoutes() assert.Equal(t, len(hrs), 1) + trs := resources.GetTLSRoutes() + assert.Equal(t, len(trs), 1) + svcs := resources.GetServices() assert.Equal(t, len(svcs), 1) @@ -110,6 +127,12 @@ func TestProviderResources(t *testing.T) { Namespace: "test", }, } + t2 := &gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute2", + Namespace: "test", + }, + } s2 := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "service2", @@ -131,6 +154,12 @@ func TestProviderResources(t *testing.T) { } resources.HTTPRoutes.Store(r2Key, r2) + t2Key := types.NamespacedName{ + Namespace: t2.GetNamespace(), + Name: t2.GetName(), + } + resources.TLSRoutes.Store(t2Key, t2) + s2Key := types.NamespacedName{ Namespace: s2.GetNamespace(), Name: s2.GetName(), @@ -150,6 +179,9 @@ func TestProviderResources(t *testing.T) { hrs = resources.GetHTTPRoutes() assert.ElementsMatch(t, hrs, []*gwapiv1b1.HTTPRoute{r1, r2}) + trs = resources.GetTLSRoutes() + assert.ElementsMatch(t, trs, []*gwapiv1a2.TLSRoute{t1, t2}) + svcs = resources.GetServices() assert.ElementsMatch(t, svcs, []*corev1.Service{s1, s2}) } diff --git a/internal/provider/kubernetes/config/rbac/role.yaml b/internal/provider/kubernetes/config/rbac/role.yaml index ba7e139e0d..66a48f7f1d 100644 --- a/internal/provider/kubernetes/config/rbac/role.yaml +++ b/internal/provider/kubernetes/config/rbac/role.yaml @@ -31,6 +31,7 @@ rules: - httproutes - referencegrants - referencepolicies + - tlsroutes verbs: - get - list @@ -42,5 +43,6 @@ rules: - gatewayclasses/status - gateways/status - httproutes/status + - tlsroutes/status verbs: - update diff --git a/internal/provider/kubernetes/helpers.go b/internal/provider/kubernetes/helpers.go new file mode 100644 index 0000000000..8ca309cecf --- /dev/null +++ b/internal/provider/kubernetes/helpers.go @@ -0,0 +1,76 @@ +package kubernetes + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// validateParentRefs validates the provided routeParentReferences, returning the +// referenced Gateways managed by Envoy Gateway. The only supported parentRef +// is a Gateway. +func validateParentRefs(ctx context.Context, client client.Client, namespace string, + gatewayClassController gwapiv1b1.GatewayController, + routeParentReferences []gwapiv1b1.ParentReference) ([]gwapiv1b1.Gateway, error) { + + var ret []gwapiv1b1.Gateway + for i := range routeParentReferences { + ref := routeParentReferences[i] + if ref.Kind != nil && *ref.Kind != "Gateway" { + return nil, fmt.Errorf("invalid Kind %q", *ref.Kind) + } + if ref.Group != nil && *ref.Group != gwapiv1b1.GroupName { + return nil, fmt.Errorf("invalid Group %q", *ref.Group) + } + + // Ensure the referenced Gateway exists, using the route's namespace unless + // specified by the parentRef. + ns := namespace + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + gwKey := types.NamespacedName{ + Namespace: ns, + Name: string(ref.Name), + } + + gw := new(gwapiv1b1.Gateway) + if err := client.Get(ctx, gwKey, gw); err != nil { + return nil, fmt.Errorf("failed to get gateway %s/%s: %v", gwKey.Namespace, gwKey.Name, err) + } + + gcKey := types.NamespacedName{Name: string(gw.Spec.GatewayClassName)} + gc := new(gwapiv1b1.GatewayClass) + if err := client.Get(ctx, gcKey, gc); err != nil { + return nil, fmt.Errorf("failed to get gatewayclass %s: %v", gcKey.Name, err) + } + if gc.Spec.ControllerName == gatewayClassController { + ret = append(ret, *gw) + } + } + + return ret, nil +} + +// isRoutePresentInNamespace checks if any kind of Routes - HTTPRoute, TLSRoute +// exists in the namespace ns. +func isRoutePresentInNamespace(ctx context.Context, c client.Client, ns string) (bool, error) { + tlsRouteList := &gwapiv1a2.TLSRouteList{} + if err := c.List(ctx, tlsRouteList, &client.ListOptions{Namespace: ns}); err != nil { + return false, fmt.Errorf("error listing tlsroutes") + } + + httpRouteList := &gwapiv1b1.HTTPRouteList{} + if err := c.List(ctx, httpRouteList, &client.ListOptions{Namespace: ns}); err != nil { + return false, fmt.Errorf("error listing httproutes") + } + + if len(tlsRouteList.Items)+len(httpRouteList.Items) > 0 { + return true, nil + } + return false, nil +} diff --git a/internal/provider/kubernetes/httproute.go b/internal/provider/kubernetes/httproute.go index 88eb98a591..3a3798e5a1 100644 --- a/internal/provider/kubernetes/httproute.go +++ b/internal/provider/kubernetes/httproute.go @@ -129,7 +129,7 @@ func (r *httpRouteReconciler) getHTTPRoutesForGateway(obj client.Object) []recon requests := []reconcile.Request{} for i := range routes.Items { route := routes.Items[i] - gateways, err := r.validateParentRefs(ctx, &route) + gateways, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, route.Spec.ParentRefs) if err != nil { r.log.Info("invalid parentRefs for httproute, bypassing reconciliation", "object", obj) continue @@ -195,7 +195,7 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R } // Validate the route. - gws, err := r.validateParentRefs(ctx, &route) + gws, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, route.Spec.ParentRefs) if err != nil { // Remove the route from the watchable map since it's invalid. r.resources.HTTPRoutes.Delete(routeKey) @@ -273,12 +273,12 @@ func (r *httpRouteReconciler) Reconcile(ctx context.Context, request reconcile.R log.Info("deleted httproute from resource map") // Delete the Namespace and Service from the resource maps if no other - // routes exist in the namespace. - routeList = &gwapiv1b1.HTTPRouteList{} - if err := r.client.List(ctx, routeList, &client.ListOptions{Namespace: request.Namespace}); err != nil { - return reconcile.Result{}, fmt.Errorf("error listing httproutes") + // routes (TLSRoute or HTTPRoute) exist in the namespace. + found, err := isRoutePresentInNamespace(ctx, r.client, request.NamespacedName.Namespace) + if err != nil { + return reconcile.Result{}, err } - if len(routeList.Items) == 0 { + if !found { r.resources.Namespaces.Delete(request.Namespace) log.Info("deleted namespace from resource map") r.resources.Services.Delete(request.NamespacedName) @@ -344,46 +344,3 @@ func (r *httpRouteReconciler) subscribeAndUpdateStatus(ctx context.Context) { } r.log.Info("status subscriber shutting down") } - -// validateParentRefs validates parentRefs for the provided route, returning the referenced Gateways -// managed by Envoy Gateway. The only supported parentRef is a Gateway. -func (r *httpRouteReconciler) validateParentRefs(ctx context.Context, route *gwapiv1b1.HTTPRoute) ([]gwapiv1b1.Gateway, error) { - if route == nil { - return nil, fmt.Errorf("httproute is nil") - } - - var ret []gwapiv1b1.Gateway - for i := range route.Spec.ParentRefs { - ref := route.Spec.ParentRefs[i] - if ref.Kind != nil && *ref.Kind != "Gateway" { - return nil, fmt.Errorf("invalid Kind %q", *ref.Kind) - } - if ref.Group != nil && *ref.Group != gwapiv1b1.GroupName { - return nil, fmt.Errorf("invalid Group %q", *ref.Group) - } - // Ensure the referenced Gateway exists, using the route's namespace unless - // specified by the parentRef. - ns := route.Namespace - if ref.Namespace != nil { - ns = string(*ref.Namespace) - } - gwKey := types.NamespacedName{ - Namespace: ns, - Name: string(ref.Name), - } - gw := new(gwapiv1b1.Gateway) - if err := r.client.Get(ctx, gwKey, gw); err != nil { - return nil, fmt.Errorf("failed to get gateway %s/%s: %v", gwKey.Namespace, gwKey.Name, err) - } - gcKey := types.NamespacedName{Name: string(gw.Spec.GatewayClassName)} - gc := new(gwapiv1b1.GatewayClass) - if err := r.client.Get(ctx, gcKey, gc); err != nil { - return nil, fmt.Errorf("failed to get gatewayclass %s: %v", gcKey.Name, err) - } - if gc.Spec.ControllerName == r.classController { - ret = append(ret, *gw) - } - } - - return ret, nil -} diff --git a/internal/provider/kubernetes/httproute_test.go b/internal/provider/kubernetes/httproute_test.go index 6fd509c78b..c504d31215 100644 --- a/internal/provider/kubernetes/httproute_test.go +++ b/internal/provider/kubernetes/httproute_test.go @@ -732,7 +732,7 @@ func TestValidateParentRefs(t *testing.T) { objs = append(objs, tc.gateways[i]) } r.client = fakeclient.NewClientBuilder().WithScheme(envoygateway.GetScheme()).WithObjects(objs...).Build() - gws, err := r.validateParentRefs(ctx, tc.route) + gws, err := validateParentRefs(ctx, r.client, tc.route.Namespace, r.classController, tc.route.Spec.ParentRefs) if tc.expected { require.NoError(t, err) } else { diff --git a/internal/provider/kubernetes/kubernetes.go b/internal/provider/kubernetes/kubernetes.go index 1383078572..127febf547 100644 --- a/internal/provider/kubernetes/kubernetes.go +++ b/internal/provider/kubernetes/kubernetes.go @@ -50,9 +50,13 @@ func New(cfg *rest.Config, svr *config.Server, resources *message.ProviderResour if err := newGatewayController(mgr, svr, updateHandler.Writer(), resources); err != nil { return nil, fmt.Errorf("failed to create gateway controller: %w", err) } + if err := newHTTPRouteController(mgr, svr, updateHandler.Writer(), resources); err != nil { return nil, fmt.Errorf("failed to create httproute controller: %w", err) } + if err := newTLSRouteController(mgr, svr, updateHandler.Writer(), resources); err != nil { + return nil, fmt.Errorf("failed to create tlsroute controller: %w", err) + } return &Provider{ manager: mgr, diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go index fec19e3d45..5a7dc33693 100644 --- a/internal/provider/kubernetes/kubernetes_test.go +++ b/internal/provider/kubernetes/kubernetes_test.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/envoyproxy/gateway/api/config/v1alpha1" @@ -62,6 +63,7 @@ func TestProvider(t *testing.T) { "gatewayclass accepted status": testGatewayClassAcceptedStatus, "gateway scheduled status": testGatewayScheduledStatus, "httproute": testHTTPRoute, + "tlsroute": testTLSRoute, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { @@ -83,17 +85,40 @@ func startEnv() (*envtest.Environment, *rest.Config, error) { return env, cfg, nil } -func testGatewayClassController(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { - cli := provider.manager.GetClient() - - gc := &gwapiv1b1.GatewayClass{ +func getGatewayClass(name string) *gwapiv1b1.GatewayClass { + return &gwapiv1b1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-gc-controllername", + Name: name, }, Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: v1alpha1.GatewayControllerName, + ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), + }, + } +} + +func getService(name, namespace string, ports map[string]int32) *corev1.Service { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{}, }, } + for name, port := range ports { + service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{ + Name: name, + Port: port, + }) + } + return service +} + +func testGatewayClassController(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("test-gc-controllername") require.NoError(t, cli.Create(ctx, gc)) defer func() { @@ -109,14 +134,7 @@ func testGatewayClassController(ctx context.Context, t *testing.T, provider *Pro func testGatewayClassAcceptedStatus(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-gc-accepted-status", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: v1alpha1.GatewayControllerName, - }, - } + gc := getGatewayClass("test-gc-accepted-status") require.NoError(t, cli.Create(ctx, gc)) defer func() { @@ -145,14 +163,7 @@ func testGatewayClassAcceptedStatus(ctx context.Context, t *testing.T, provider func testGatewayScheduledStatus(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "gc-scheduled-status-test", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), - }, - } + gc := getGatewayClass("gc-scheduled-status-test") require.NoError(t, cli.Create(ctx, gc)) // Ensure the GatewayClass reports "Ready". @@ -241,14 +252,7 @@ func testGatewayScheduledStatus(ctx context.Context, t *testing.T, provider *Pro func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { cli := provider.manager.GetClient() - gc := &gwapiv1b1.GatewayClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: "httproute-test", - }, - Spec: gwapiv1b1.GatewayClassSpec{ - ControllerName: gwapiv1b1.GatewayController(v1alpha1.GatewayControllerName), - }, - } + gc := getGatewayClass("httproute-test") require.NoError(t, cli.Create(ctx, gc)) // Ensure the GatewayClass reports ready. @@ -296,24 +300,10 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour require.NoError(t, cli.Delete(ctx, gw)) }() - svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: ns.Name, - Name: "test", - }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - { - Name: "http", - Port: 80, - }, - { - Name: "https", - Port: 443, - }, - }, - }, - } + svc := getService("test", ns.Name, map[string]int32{ + "http": 80, + "https": 443, + }) require.NoError(t, cli.Create(ctx, svc)) @@ -579,3 +569,123 @@ func testHTTPRoute(ctx context.Context, t *testing.T, provider *Provider, resour }) } } + +func testTLSRoute(ctx context.Context, t *testing.T, provider *Provider, resources *message.ProviderResources) { + cli := provider.manager.GetClient() + + gc := getGatewayClass("tlsroute-test") + require.NoError(t, cli.Create(ctx, gc)) + + defer func() { + require.NoError(t, cli.Delete(ctx, gc)) + }() + + // Create the namespace for the Gateway under test. + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "tlsroute-test"}} + require.NoError(t, cli.Create(ctx, ns)) + + gw := &gwapiv1b1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1b1.GatewaySpec{ + GatewayClassName: gwapiv1b1.ObjectName(gc.Name), + Listeners: []gwapiv1b1.Listener{ + { + Name: "test", + Port: gwapiv1b1.PortNumber(int32(8080)), + Protocol: gwapiv1b1.TLSProtocolType, + }, + }, + }, + } + require.NoError(t, cli.Create(ctx, gw)) + + defer func() { + require.NoError(t, cli.Delete(ctx, gw)) + }() + + svc := getService("test", ns.Name, map[string]int32{ + "tls": 90, + }) + + require.NoError(t, cli.Create(ctx, svc)) + + defer func() { + require.NoError(t, cli.Delete(ctx, svc)) + }() + + var testCases = []struct { + name string + route gwapiv1a2.TLSRoute + }{ + { + name: "tlsroute", + route: gwapiv1a2.TLSRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tlsroute-test", + Namespace: ns.Name, + }, + Spec: gwapiv1a2.TLSRouteSpec{ + CommonRouteSpec: gwapiv1a2.CommonRouteSpec{ + ParentRefs: []gwapiv1a2.ParentReference{ + { + Name: gwapiv1a2.ObjectName(gw.Name), + }, + }, + }, + Hostnames: []gwapiv1a2.Hostname{"test.hostname.local"}, + Rules: []gwapiv1a2.TLSRouteRule{ + { + BackendRefs: []gwapiv1a2.BackendRef{ + { + BackendObjectReference: gwapiv1a2.BackendObjectReference{ + Name: "test", + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.NoError(t, cli.Create(ctx, &testCase.route)) + defer func() { + require.NoError(t, cli.Delete(ctx, &testCase.route)) + }() + + require.Eventually(t, func() bool { + return resources.TLSRoutes.Len() == 1 + }, defaultWait, defaultTick) + + // Ensure the test TLSRoute in the TLSRoute resources is as expected. + key := types.NamespacedName{ + Namespace: testCase.route.Namespace, + Name: testCase.route.Name, + } + require.Eventually(t, func() bool { + return cli.Get(ctx, key, &testCase.route) == nil + }, defaultWait, defaultTick) + troutes, _ := resources.TLSRoutes.Load(key) + assert.Equal(t, &testCase.route, troutes) + + // Ensure the TLSRoute Namespace is in the Namespace resource map. + require.Eventually(t, func() bool { + _, ok := resources.Namespaces.Load(testCase.route.Namespace) + return ok + }, defaultWait, defaultTick) + + // Ensure the Service is in the resource map. + svcKey := utils.NamespacedName(svc) + require.Eventually(t, func() bool { + _, ok := resources.Services.Load(svcKey) + return ok + }, defaultWait, defaultTick) + }) + } +} diff --git a/internal/provider/kubernetes/rbac.go b/internal/provider/kubernetes/rbac.go index 32a72ee193..4015f40550 100644 --- a/internal/provider/kubernetes/rbac.go +++ b/internal/provider/kubernetes/rbac.go @@ -1,7 +1,7 @@ package kubernetes -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;referencepolicies;referencegrants,verbs=get;list;watch;update -// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status,verbs=update +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;tlsroutes;referencepolicies;referencegrants,verbs=get;list;watch;update +// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;tlsroutes/status,verbs=update // RBAC for watched resources of Gateway API controllers. // +kubebuilder:rbac:groups="",resources=secrets;services;namespaces,verbs=get;list;watch diff --git a/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml b/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml new file mode 100644 index 0000000000..fb30827b00 --- /dev/null +++ b/internal/provider/kubernetes/testdata/in/tlsroute-experimental-crd.yaml @@ -0,0 +1,542 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1086 + gateway.networking.k8s.io/bundle-version: v0.6.0-dev + gateway.networking.k8s.io/channel: experimental + creationTimestamp: null + name: tlsroutes.gateway.networking.k8s.io +spec: + group: gateway.networking.k8s.io + names: + categories: + - gateway-api + kind: TLSRoute + listKind: TLSRouteList + plural: tlsroutes + singular: tlsroute + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha2 + schema: + openAPIV3Schema: + description: "The TLSRoute resource is similar to TCPRoute, but can be configured + to match against TLS-specific metadata. This allows more flexibility in + matching streams for a given TLS listener. \n If you need to forward traffic + to a single target for a TLS listener, you could choose to use a TCPRoute + with a TLS listener." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec defines the desired state of TLSRoute. + properties: + hostnames: + description: "Hostnames defines a set of SNI names that should match + against the SNI attribute of TLS ClientHello message in TLS handshake. + This matches the RFC 1123 definition of a hostname with 2 notable + exceptions: \n 1. IPs are not allowed in SNI names per RFC 6066. + 2. A hostname may be prefixed with a wildcard label (`*.`). The + wildcard label must appear by itself as the first label. \n If + a hostname is specified by both the Listener and TLSRoute, there + must be at least one intersecting hostname for the TLSRoute to be + attached to the Listener. For example: \n * A Listener with `test.example.com` + as the hostname matches TLSRoutes that have either not specified + any hostnames, or have specified at least one of `test.example.com` + or `*.example.com`. * A Listener with `*.example.com` as the hostname + matches TLSRoutes that have either not specified any hostnames + or have specified at least one hostname that matches the Listener + hostname. For example, `test.example.com` and `*.example.com` + would both match. On the other hand, `example.com` and `test.example.net` + would not match. \n If both the Listener and TLSRoute have specified + hostnames, any TLSRoute hostnames that do not match the Listener + hostname MUST be ignored. For example, if a Listener specified `*.example.com`, + and the TLSRoute specified `test.example.com` and `test.example.net`, + `test.example.net` must not be considered for a match. \n If both + the Listener and TLSRoute have specified hostnames, and none match + with the criteria above, then the TLSRoute is not accepted. The + implementation must raise an 'Accepted' Condition with a status + of `False` in the corresponding RouteParentStatus. \n Support: Core" + items: + description: "Hostname is the fully qualified domain name of a network + host. This matches the RFC 1123 definition of a hostname with + 2 notable exceptions: \n 1. IPs are not allowed. 2. A hostname + may be prefixed with a wildcard label (`*.`). The wildcard label + must appear by itself as the first label. \n Hostname can be \"precise\" + which is a domain name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must consist + of lower case alphanumeric characters or '-', and must start and + end with an alphanumeric character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + maxItems: 16 + type: array + parentRefs: + description: "ParentRefs references the resources (usually Gateways) + that a Route wants to be attached to. Note that the referenced parent + resource needs to allow this for the attachment to be complete. + For Gateways, that means the Gateway needs to allow attachment from + Routes of this kind and namespace. \n The only kind of parent resource + with \"Core\" support is Gateway. This API may be extended in the + future to support additional kinds of parent resources such as one + of the route kinds. \n It is invalid to reference an identical parent + more than once. It is valid to reference multiple distinct sections + within the same parent resource, such as 2 Listeners within a Gateway. + \n It is possible to separately reference multiple distinct objects + that may be collapsed by an implementation. For example, some implementations + may choose to merge compatible Gateway Listeners together. If that + is the case, the list of routes attached to those resources should + also be merged." + items: + description: "ParentReference identifies an API object (usually + a Gateway) that can be considered a parent of this resource (usually + a route). The only kind of parent resource with \"Core\" support + is Gateway. This API may be extended in the future to support + additional kinds of parent resources, such as HTTPRoute. \n The + API object must be valid in the cluster; the Group and Kind must + be registered in the cluster for this reference to be valid." + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: Core + (Gateway) \n Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. When + unspecified, this refers to the local namespace of the Route. + \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. It + can be interpreted differently based on the type of parent + resource. \n When the parent resource is a Gateway, this targets + all listeners listening on the specified port that also support + this kind of Route(and select this Route). It's not recommended + to set `Port` unless the networking behaviors specified in + a Route must apply to a specific port as opposed to a listener(s) + whose port(s) may be changed. When both Port and SectionName + are specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY choose + to support other parent resources. Implementations supporting + other types of parent resources MUST clearly document how/if + Port is interpreted. \n For the purpose of status, an attachment + is considered successful as long as the parent resource accepts + it partially. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within the + target resource. In the following resources, SectionName is + interpreted as the following: \n * Gateway: Listener Name. + When both Port (experimental) and SectionName are specified, + the name and port of the selected listener must match both + specified values. \n Implementations MAY choose to support + attaching Routes to other resources. If that is the case, + they MUST clearly document how SectionName is interpreted. + \n When unspecified (empty string), this will reference the + entire resource. For the purpose of status, an attachment + is considered successful if at least one section in the parent + resource accepts it. For example, Gateway listeners can restrict + which Routes can attach to them by Route kind, namespace, + or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this + Route, the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + maxItems: 32 + type: array + rules: + description: Rules are a list of TLS matchers and actions. + items: + description: TLSRouteRule is the configuration for a given rule. + properties: + backendRefs: + description: "BackendRefs defines the backend(s) where matching + requests should be sent. If unspecified or invalid (refers + to a non-existent resource or a Service with no endpoints), + the rule performs no forwarding; if no filters are specified + that would result in a response being sent, the underlying + implementation must actively reject request attempts to this + backend, by rejecting the connection or returning a 500 status + code. Request rejections must respect weight; if an invalid + backend is requested to have 80% of requests, then 80% of + requests must be rejected instead. \n Support: Core for Kubernetes + Service \n Support: Custom for any other resource \n Support + for weight: Extended" + items: + description: "BackendRef defines how a Route should forward + a request to a Kubernetes resource. \n Note that when a + namespace is specified, a ReferenceGrant object is required + in the referent namespace to allow that namespace's owner + to accept the reference. See the ReferenceGrant documentation + for details." + properties: + group: + default: "" + description: Group is the group of the referent. For example, + "networking.k8s.io". When unspecified (empty string), + core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: Kind is kind of the referent. For example + "HTTPRoute" or "Service". Defaults to "Service" when + not specified. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the backend. + When unspecified, the local namespace is inferred. \n + Note that when a namespace is specified, a ReferenceGrant + object is required in the referent namespace to allow + that namespace's owner to accept the reference. See + the ReferenceGrant documentation for details. \n Support: + Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: Port specifies the destination port number + to use for this resource. Port is required when the + referent is a Kubernetes Service. In this case, the + port number is the service port number, not the target + port. For other resources, destination port might be + derived from the referent resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + weight: + default: 1 + description: "Weight specifies the proportion of requests + forwarded to the referenced backend. This is computed + as weight/(sum of all weights in this BackendRefs list). + For non-zero values, there may be some epsilon from + the exact proportion defined here depending on the precision + an implementation supports. Weight is not a percentage + and the sum of weights does not need to equal 100. \n + If only one backend is specified and it has a weight + greater than 0, 100% of the traffic is forwarded to + that backend. If weight is set to 0, no traffic should + be forwarded for this entry. If unspecified, weight + defaults to 1. \n Support for this field varies based + on the context where used." + format: int32 + maximum: 1000000 + minimum: 0 + type: integer + required: + - name + type: object + maxItems: 16 + minItems: 1 + type: array + type: object + maxItems: 16 + minItems: 1 + type: array + required: + - rules + type: object + status: + description: Status defines the current state of TLSRoute. + properties: + parents: + description: "Parents is a list of parent resources (usually Gateways) + that are associated with the route, and the status of the route + with respect to each parent. When this route attaches to a parent, + the controller that manages the parent must add an entry to this + list when the controller first sees the route and should update + the entry as appropriate when the route or gateway is modified. + \n Note that parent references that cannot be resolved by an implementation + of this API will not be added to this list. Implementations of this + API can only populate Route status for the Gateways/parent resources + they are responsible for. \n A maximum of 32 Gateways will be represented + in this list. An empty list means the route has not been attached + to any Gateway." + items: + description: RouteParentStatus describes the status of a route with + respect to an associated Parent. + properties: + conditions: + description: "Conditions describes the status of the route with + respect to the Gateway. Note that the route's availability + is also subject to the Gateway's own status conditions and + listener status. \n If the Route's ParentRef specifies an + existing Gateway that supports Routes of this kind AND that + Gateway's controller has sufficient access, then that Gateway's + controller MUST set the \"Accepted\" condition on the Route, + to indicate whether the route has been accepted or rejected + by the Gateway, and why. \n A Route MUST be considered \"Accepted\" + if at least one of the Route's rules is implemented by the + Gateway. \n There are a number of cases where the \"Accepted\" + condition may not be set due to lack of controller visibility, + that includes when: \n * The Route refers to a non-existent + parent. * The Route is of a type that the controller does + not support. * The Route is in a namespace the controller + does not have access to." + items: + description: "Condition contains details for one aspect of + the current state of this API Resource. --- This struct + is intended for direct use as an array at the field path + .status.conditions. For example, type FooStatus struct{ + \ // Represents the observations of a foo's current state. + \ // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type + \ // +patchStrategy=merge // +listType=map // + +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` + \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should + be when the underlying condition changed. If that is + not known, then using the time when the API field changed + is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, + if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the + current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier + indicating the reason for the condition's last transition. + Producers of specific condition types may define expected + values and meanings for this field, and whether the + values are considered a guaranteed API. The value should + be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across + resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability + to deconflict is important. The regex it matches is + (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: "ControllerName is a domain/path string that indicates + the name of the controller that wrote this status. This corresponds + with the controllerName field on GatewayClass. \n Example: + \"example.net/gateway-controller\". \n The format of this + field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid + Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + \n Controllers MUST populate this field when writing status. + Controllers should ensure that entries to status populated + with their ControllerName are cleaned up when they are no + longer necessary." + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + parentRef: + description: ParentRef corresponds with a ParentRef in the spec + that this RouteParentStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: "Group is the group of the referent. \n Support: + Core" + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: "Kind is kind of the referent. \n Support: + Core (Gateway) \n Support: Custom (Other Resources)" + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: "Name is the name of the referent. \n Support: + Core" + maxLength: 253 + minLength: 1 + type: string + namespace: + description: "Namespace is the namespace of the referent. + When unspecified, this refers to the local namespace of + the Route. \n Support: Core" + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: "Port is the network port this Route targets. + It can be interpreted differently based on the type of + parent resource. \n When the parent resource is a Gateway, + this targets all listeners listening on the specified + port that also support this kind of Route(and select this + Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to + a specific port as opposed to a listener(s) whose port(s) + may be changed. When both Port and SectionName are specified, + the name and port of the selected listener must match + both specified values. \n Implementations MAY choose to + support other parent resources. Implementations supporting + other types of parent resources MUST clearly document + how/if Port is interpreted. \n For the purpose of status, + an attachment is considered successful as long as the + parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them + by Route kind, namespace, or hostname. If 1 of 2 Gateway + listeners accept attachment from the referencing Route, + the Route MUST be considered successfully attached. If + no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Extended \n " + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: "SectionName is the name of a section within + the target resource. In the following resources, SectionName + is interpreted as the following: \n * Gateway: Listener + Name. When both Port (experimental) and SectionName are + specified, the name and port of the selected listener + must match both specified values. \n Implementations MAY + choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName + is interpreted. \n When unspecified (empty string), this + will reference the entire resource. For the purpose of + status, an attachment is considered successful if at least + one section in the parent resource accepts it. For example, + Gateway listeners can restrict which Routes can attach + to them by Route kind, namespace, or hostname. If 1 of + 2 Gateway listeners accept attachment from the referencing + Route, the Route MUST be considered successfully attached. + If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + \n Support: Core" + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + required: + - controllerName + - parentRef + type: object + maxItems: 32 + type: array + required: + - parents + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/internal/provider/kubernetes/tlsroute.go b/internal/provider/kubernetes/tlsroute.go new file mode 100644 index 0000000000..ff4296b53a --- /dev/null +++ b/internal/provider/kubernetes/tlsroute.go @@ -0,0 +1,326 @@ +// Portions of this code are based on code from Contour, available at: +// https://github.com/projectcontour/contour/blob/main/internal/controller/tlsroute.go + +package kubernetes + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/envoyproxy/gateway/internal/envoygateway/config" + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/message" + "github.com/envoyproxy/gateway/internal/provider/utils" + "github.com/envoyproxy/gateway/internal/status" +) + +const ( + serviceTLSRouteIndex = "serviceTLSRouteBackendRef" +) + +type tlsRouteReconciler struct { + client client.Client + log logr.Logger + statusUpdater status.Updater + classController gwapiv1b1.GatewayController + + resources *message.ProviderResources +} + +// newTLSRouteController creates the tlsroute controller from mgr. The controller will be pre-configured +// to watch for TLSRoute objects across all namespaces. +func newTLSRouteController(mgr manager.Manager, cfg *config.Server, su status.Updater, resources *message.ProviderResources) error { + r := &tlsRouteReconciler{ + client: mgr.GetClient(), + log: cfg.Logger, + classController: gwapiv1b1.GatewayController(cfg.EnvoyGateway.Gateway.ControllerName), + statusUpdater: su, + resources: resources, + } + + c, err := controller.New("tlsroute", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + r.log.Info("created tlsroute controller") + + if err := c.Watch( + &source.Kind{Type: &gwapiv1a2.TLSRoute{}}, + &handler.EnqueueRequestForObject{}, + ); err != nil { + return err + } + + // Subscribe to status updates + go r.subscribeAndUpdateStatus(context.Background()) + + // Add indexing on TLSRoute, for Service objects that are referenced in TLSRoute objects + // via `.spec.rules.backendRefs`. This helps in querying for TLSRoutes that are affected by + // a particular Service CRUD. + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &gwapiv1a2.TLSRoute{}, serviceTLSRouteIndex, func(rawObj client.Object) []string { + tlsRoute := rawObj.(*gwapiv1a2.TLSRoute) + var backendServices []string + for _, rule := range tlsRoute.Spec.Rules { + for _, backend := range rule.BackendRefs { + if string(*backend.Kind) == gatewayapi.KindService { + // If an explicit Service namespace is not provided, use the TLSRoute namespace to + // lookup the provided Service Name. + backendServices = append(backendServices, + types.NamespacedName{ + Namespace: gatewayapi.NamespaceDerefOrAlpha(backend.Namespace, tlsRoute.Namespace), + Name: string(backend.Name), + }.String(), + ) + } + } + } + return backendServices + }); err != nil { + return err + } + + // Watch Gateway CRUDs and reconcile affected TLSRoutes. + if err := c.Watch( + &source.Kind{Type: &gwapiv1b1.Gateway{}}, + handler.EnqueueRequestsFromMapFunc(r.getTLSRoutesForGateway), + ); err != nil { + return err + } + + // Watch Service CRUDs and reconcile affected TLSRoutes. + if err := c.Watch( + &source.Kind{Type: &corev1.Service{}}, + handler.EnqueueRequestsFromMapFunc(r.getTLSRoutesForService), + ); err != nil { + return err + } + + r.log.Info("watching tlsroute objects") + return nil +} + +// getTLSRoutesForGateway uses a Gateway obj to fetch TLSRoutes, iterating +// through them and creating a reconciliation request for each valid TLSRoute +// that references obj. +func (r *tlsRouteReconciler) getTLSRoutesForGateway(obj client.Object) []reconcile.Request { + ctx := context.Background() + + gw, ok := obj.(*gwapiv1b1.Gateway) + if !ok { + r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) + return []reconcile.Request{} + } + + routes := &gwapiv1a2.TLSRouteList{} + if err := r.client.List(ctx, routes); err != nil { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for i := range routes.Items { + route := routes.Items[i] + gateways, err := validateParentRefs(ctx, r.client, route.Namespace, r.classController, gatewayapi.UpgradeParentReferences(route.Spec.ParentRefs)) + if err != nil { + r.log.Info("invalid parentRefs for tlsroute, bypassing reconciliation", "object", obj) + continue + } + for j := range gateways { + if gateways[j].Namespace == gw.Namespace && gateways[j].Name == gw.Name { + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: route.Namespace, + Name: route.Name, + }, + } + requests = append(requests, req) + break + } + } + } + + return requests +} + +// getTLSRoutesForService uses a Service obj to fetch TLSRoutes that references +// the Service using `.spec.rules.backendRefs`. The affected TLSRoutes are then +// pushed for reconciliation. +func (r *tlsRouteReconciler) getTLSRoutesForService(obj client.Object) []reconcile.Request { + affectedTLSRouteList := &gwapiv1a2.TLSRouteList{} + + if err := r.client.List(context.Background(), affectedTLSRouteList, &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(serviceTLSRouteIndex, utils.NamespacedName(obj).String()), + }); err != nil { + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, len(affectedTLSRouteList.Items)) + for i, item := range affectedTLSRouteList.Items { + requests[i] = reconcile.Request{ + NamespacedName: utils.NamespacedName(item.DeepCopy()), + } + } + + return requests +} + +func (r *tlsRouteReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + log := r.log.WithValues("namespace", request.Namespace, "name", request.Name) + + log.Info("reconciling tlsroute") + + // Fetch all TLSRoutes from the cache. + routeList := &gwapiv1a2.TLSRouteList{} + if err := r.client.List(ctx, routeList); err != nil { + return reconcile.Result{}, fmt.Errorf("error listing tlsroutes") + } + + found := false + for i := range routeList.Items { + // See if this route from the list matched the reconciled route. + route := routeList.Items[i] + routeKey := utils.NamespacedName(&route) + if routeKey == request.NamespacedName { + found = true + } + + // Store the tlsroute in the resource map. + r.resources.TLSRoutes.Store(routeKey, &route) + log.Info("added tlsroute to resource map") + + // Get the route's namespace from the cache. + nsKey := types.NamespacedName{Name: route.Namespace} + ns := new(corev1.Namespace) + if err := r.client.Get(ctx, nsKey, ns); err != nil { + if errors.IsNotFound(err) { + // The route's namespace doesn't exist in the cache, so remove it from + // the namespace resource map if it exists. + if _, ok := r.resources.Namespaces.Load(nsKey.Name); ok { + r.resources.Namespaces.Delete(nsKey.Name) + log.Info("deleted namespace from resource map") + } + } + return reconcile.Result{}, fmt.Errorf("failed to get namespace %s", nsKey.Name) + } + + // The route's namespace exists, so add it to the resource map. + r.resources.Namespaces.Store(nsKey.Name, ns) + log.Info("added namespace to resource map") + + // Get the route's backendRefs from the cache. Note that a Service is the + // only supported kind. + for i := range route.Spec.Rules { + for j := range route.Spec.Rules[i].BackendRefs { + ref := route.Spec.Rules[i].BackendRefs[j] + if err := validateTLSRouteBackendRef(&ref); err != nil { + return reconcile.Result{}, fmt.Errorf("invalid backendRef: %w", err) + } + + // The backendRef is valid, so get the referenced service from the cache. + svcKey := types.NamespacedName{Namespace: route.Namespace, Name: string(ref.Name)} + svc := new(corev1.Service) + if err := r.client.Get(ctx, svcKey, svc); err != nil { + if errors.IsNotFound(err) { + // The ref's service doesn't exist in the cache, so remove it from + // the resource map if it exists. + if _, ok := r.resources.Services.Load(svcKey); ok { + r.resources.Services.Delete(svcKey) + log.Info("deleted service from resource map") + } + } + return reconcile.Result{}, fmt.Errorf("failed to get service %s/%s", + svcKey.Namespace, svcKey.Name) + } + + // The backendRef Service exists, so add it to the resource map. + r.resources.Services.Store(svcKey, svc) + log.Info("added service to resource map") + } + } + } + + if !found { + // Delete the tlsroute from the resource map. + r.resources.TLSRoutes.Delete(request.NamespacedName) + log.Info("deleted tlsroute from resource map") + + // Delete the Namespace and Service from the resource maps if no other + // routes (TLSRoute or HTTPRoute) exist in the namespace. + found, err := isRoutePresentInNamespace(ctx, r.client, request.NamespacedName.Namespace) + if err != nil { + return reconcile.Result{}, err + } + if !found { + r.resources.Namespaces.Delete(request.Namespace) + log.Info("deleted namespace from resource map") + r.resources.Services.Delete(request.NamespacedName) + log.Info("deleted service from resource map") + } + } + + log.Info("reconciled tlsroute") + + return reconcile.Result{}, nil +} + +// validateTLSRouteBackendRef validates that ref is a reference to a local Service. +func validateTLSRouteBackendRef(ref *gwapiv1a2.BackendRef) error { + switch { + case ref == nil: + return nil + case ref.Group != nil && *ref.Group != corev1.GroupName: + return fmt.Errorf("invalid group; must be nil or empty string") + case ref.Kind != nil && *ref.Kind != gatewayapi.KindService: + return fmt.Errorf("invalid kind %q; must be %q", + *ref.BackendObjectReference.Kind, gatewayapi.KindService) + case ref.Namespace != nil: + return fmt.Errorf("invalid namespace; must be nil") + } + + return nil +} + +// subscribeAndUpdateStatus subscribes to tlsroute status updates and writes it into the +// Kubernetes API Server +func (r *tlsRouteReconciler) subscribeAndUpdateStatus(ctx context.Context) { + // Subscribe to resources + for snapshot := range r.resources.TLSRouteStatuses.Subscribe(ctx) { + r.log.Info("received a status notification") + updates := snapshot.Updates + for _, update := range updates { + // skip delete updates. + if update.Delete { + continue + } + key := update.Key + val := update.Value + r.statusUpdater.Send(status.Update{ + NamespacedName: key, + Resource: new(gwapiv1a2.TLSRoute), + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + t, ok := obj.(*gwapiv1a2.TLSRoute) + if !ok { + panic(fmt.Sprintf("unsupported object type %T", obj)) + } + tCopy := t.DeepCopy() + tCopy.Status.Parents = val.Status.Parents + return tCopy + }), + }) + } + } + r.log.Info("status subscriber shutting down") +} diff --git a/internal/status/status.go b/internal/status/status.go index c58a488c0b..373a19b394 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -13,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -143,6 +144,7 @@ func (u *UpdateWriter) Send(update Update) { // GatewayClasses // Gateway // HTTPRoute +// TLSRoute func isStatusEqual(objA, objB interface{}) bool { opts := cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime", "ObservedGeneration") switch a := objA.(type) { @@ -164,6 +166,12 @@ func isStatusEqual(objA, objB interface{}) bool { return true } } + case *gwapiv1a2.TLSRoute: + if b, ok := objB.(*gwapiv1a2.TLSRoute); ok { + if cmp.Equal(a.Status, b.Status, opts) { + return true + } + } } return false } diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go index 2a1dff9b30..b98d5dfe80 100644 --- a/internal/xds/translator/cluster.go +++ b/internal/xds/translator/cluster.go @@ -12,18 +12,18 @@ import ( "github.com/envoyproxy/gateway/internal/ir" ) -func buildXdsCluster(httpRoute *ir.HTTPRoute) (*cluster.Cluster, error) { +func buildXdsCluster(routeName string, destinations []*ir.RouteDestination) (*cluster.Cluster, error) { localities := make([]*endpoint.LocalityLbEndpoints, 0, 1) locality := &endpoint.LocalityLbEndpoints{ Locality: &core.Locality{}, - LbEndpoints: buildXdsEndpoints(httpRoute.Destinations), + LbEndpoints: buildXdsEndpoints(destinations), Priority: 0, // Each locality gets the same weight 1. There is a single locality // per priority, so the weight value does not really matter, but some // load balancers need the value to be set. LoadBalancingWeight: &wrapperspb.UInt32Value{Value: 1}} localities = append(localities, locality) - clusterName := getXdsClusterName(httpRoute.Name) + clusterName := getXdsClusterName(routeName) return &cluster.Cluster{ Name: clusterName, ConnectTimeout: durationpb.New(5 * time.Second), diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go index baca1aae39..e3ea4c82fe 100644 --- a/internal/xds/translator/listener.go +++ b/internal/xds/translator/listener.go @@ -6,7 +6,9 @@ import ( core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" + tls_inspector "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3" tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/types/known/anypb" @@ -71,6 +73,74 @@ func buildXdsListener(httpListener *ir.HTTPListener) (*listener.Listener, error) }, nil } +func buildXdsTCPListener(clusterName string, tcpListener *ir.TCPListener) (*listener.Listener, error) { + if tcpListener == nil { + return nil, errors.New("http listener is nil") + } + + statPrefix := "tcp" + if tcpListener.TLS != nil { + statPrefix = "passthrough" + } + mgr := &tcp.TcpProxy{ + StatPrefix: statPrefix, + ClusterSpecifier: &tcp.TcpProxy_Cluster{ + Cluster: clusterName, + }, + } + mgrAny, err := anypb.New(mgr) + if err != nil { + return nil, err + } + + filterChain := &listener.FilterChain{ + Filters: []*listener.Filter{{ + Name: wellknown.TCPProxy, + ConfigType: &listener.Filter_TypedConfig{ + TypedConfig: mgrAny, + }, + }}, + } + if tcpListener.TLS != nil { + filterChain.FilterChainMatch = &listener.FilterChainMatch{ + ServerNames: tcpListener.TLS.SNIs, + } + } + + xdsListener := &listener.Listener{ + Name: getXdsListenerName(tcpListener.Name, tcpListener.Port), + Address: &core.Address{ + Address: &core.Address_SocketAddress{ + SocketAddress: &core.SocketAddress{ + Protocol: core.SocketAddress_TCP, + Address: tcpListener.Address, + PortSpecifier: &core.SocketAddress_PortValue{ + PortValue: tcpListener.Port, + }, + }, + }, + }, + FilterChains: []*listener.FilterChain{filterChain}, + } + + if tcpListener.TLS != nil { + tlsInspector := &tls_inspector.TlsInspector{} + tlsInspectorAny, err := anypb.New(tlsInspector) + if err != nil { + return nil, err + } + + xdsListener.ListenerFilters = []*listener.ListenerFilter{{ + Name: wellknown.TlsInspector, + ConfigType: &listener.ListenerFilter_TypedConfig{ + TypedConfig: tlsInspectorAny, + }, + }} + } + + return xdsListener, nil +} + func buildXdsDownstreamTLSSocket(listenerName string, tlsConfig *ir.TLSListenerConfig) (*core.TransportSocket, error) { tlsCtx := &tls.DownstreamTlsContext{ @@ -113,5 +183,4 @@ func buildXdsDownstreamTLSSecret(listenerName string, }, }, }, nil - } diff --git a/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml b/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml new file mode 100644 index 0000000000..c7f5963306 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/tls-route-passthrough.yaml @@ -0,0 +1,12 @@ +tcp: +- name: "tls-passthrough" + address: "0.0.0.0" + port: 10080 + tls: + snis: + - foo.com + destinations: + - host: "1.2.3.4" + port: 50000 + - host: "5.6.7.8" + port: 50001 diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml new file mode 100644 index 0000000000..95ed98685b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.clusters.yaml @@ -0,0 +1,23 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 5s + dnsLookupFamily: V4_ONLY + loadAssignment: + clusterName: cluster_tls-passthrough + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + - endpoint: + address: + socketAddress: + address: 5.6.7.8 + portValue: 50001 + loadBalancingWeight: 1 + locality: {} + name: cluster_tls-passthrough + outlierDetection: {} + type: STATIC diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml new file mode 100644 index 0000000000..b52cb5d8d2 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.listeners.yaml @@ -0,0 +1,19 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + filterChains: + - filterChainMatch: + serverNames: + - foo.com + filters: + - name: envoy.filters.network.tcp_proxy + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: cluster_tls-passthrough + statPrefix: passthrough + listenerFilters: + - name: envoy.filters.listener.tls_inspector + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector + name: listener_tls-passthrough_10080 diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.routes.yaml @@ -0,0 +1 @@ +[] diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index 3c6bf26147..434b0e62e8 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -64,7 +64,7 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { if len(httpRoute.Destinations) == 0 && httpRoute.BackendWeights.Invalid > 0 { continue } - xdsCluster, err := buildXdsCluster(httpRoute) + xdsCluster, err := buildXdsCluster(httpRoute.Name, httpRoute.Destinations) if err != nil { return nil, multierror.Append(err, errors.New("error building xds cluster")) } @@ -81,6 +81,22 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) { tCtx.AddXdsResource(resource.RouteType, xdsRouteCfg) } + for _, tcpListener := range ir.TCP { + // 1:1 between IR TCPListener and xDS Cluster + xdsCluster, err := buildXdsCluster(tcpListener.Name, tcpListener.Destinations) + if err != nil { + return nil, multierror.Append(err, errors.New("error building xds cluster")) + } + tCtx.AddXdsResource(resource.ClusterType, xdsCluster) + + // 1:1 between IR TCPListener and xDS Listener + xdsListener, err := buildXdsTCPListener(xdsCluster.Name, tcpListener) + if err != nil { + return nil, multierror.Append(err, errors.New("error building xds listener")) + } + + tCtx.AddXdsResource(resource.ListenerType, xdsListener) + } return tCtx, nil } diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 90cea49670..0102b6d84f 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -50,6 +50,9 @@ func TestTranslate(t *testing.T) { name: "simple-tls", requireSecrets: true, }, + { + name: "tls-route-passthrough", + }, } for _, tc := range testCases {