diff --git a/internal/gatewayapi/address.go b/internal/gatewayapi/address.go index 998fbe248db..54907df0b12 100644 --- a/internal/gatewayapi/address.go +++ b/internal/gatewayapi/address.go @@ -18,7 +18,7 @@ type AddressesTranslator interface { func (t *Translator) ProcessAddresses(gateways []*GatewayContext, xdsIR XdsIRMap, infraIR InfraIRMap, resources *Resources) { for _, gateway := range gateways { // Infra IR already exist - irKey := irStringKey(gateway.Gateway.Namespace, gateway.Gateway.Name) + irKey := t.getIRKey(gateway.Gateway) gwInfraIR := infraIR[irKey] var ipAddr []string diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 685373cb8a7..7a79002b29b 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -283,6 +283,12 @@ func GatewayOwnerLabels(namespace, name string) map[string]string { } } +// GatewayClassOwnerLabel returns the GatewayCLass Owner label using +// the provided name as the value. +func GatewayClassOwnerLabel(name string) map[string]string { + return map[string]string{OwningGatewayClassLabel: name} +} + // servicePortToContainerPort translates a service port into an ephemeral // container port. func servicePortToContainerPort(servicePort int32) int32 { @@ -444,6 +450,10 @@ func irTLSListenerConfigName(secret *v1.Secret) string { return fmt.Sprintf("%s-%s", secret.Namespace, secret.Name) } +func isMergeGatewaysEnabled(resources *Resources) bool { + return resources.EnvoyProxy != nil && resources.EnvoyProxy.Spec.MergeGateways != nil && *resources.EnvoyProxy.Spec.MergeGateways +} + func protocolSliceToStringSlice(protocols []gwapiv1.ProtocolType) []string { var protocolStrings []string for _, protocol := range protocols { diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go index bee77dbdd6b..8549728f92d 100644 --- a/internal/gatewayapi/listener.go +++ b/internal/gatewayapi/listener.go @@ -27,31 +27,25 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap t.validateConflictedLayer7Listeners(gateways) t.validateConflictedLayer4Listeners(gateways, gwapiv1.TCPProtocolType, gwapiv1.TLSProtocolType) t.validateConflictedLayer4Listeners(gateways, gwapiv1.UDPProtocolType) + if t.MergeGateways { + t.validateConflictedMergedListeners(gateways) + } // Iterate through all listeners to validate spec // and compute status for each, and add valid ones // to the Xds IR. for _, gateway := range gateways { - // init IR per gateway - irKey := irStringKey(gateway.Gateway.Namespace, gateway.Gateway.Name) - gwXdsIR := &ir.Xds{} - gwInfraIR := ir.NewInfra() - gwInfraIR.Proxy.Name = irKey - gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayOwnerLabels(gateway.Namespace, gateway.Name) - if resources.EnvoyProxy != nil { - gwInfraIR.Proxy.Config = resources.EnvoyProxy - } - - // save the IR references in the map before the translation starts - xdsIR[irKey] = gwXdsIR - infraIR[irKey] = gwInfraIR - // Infra IR proxy ports must be unique. var foundPorts []*protocolPort + irKey := t.getIRKey(gateway.Gateway) - gwXdsIR.AccessLog = processAccessLog(gwInfraIR.Proxy.Config) - gwXdsIR.Tracing = processTracing(gateway.Gateway, gwInfraIR.Proxy.Config) - gwXdsIR.Metrics = processMetrics(gwInfraIR.Proxy.Config) + if resources.EnvoyProxy != nil { + infraIR[irKey].Proxy.Config = resources.EnvoyProxy + } + + xdsIR[irKey].AccessLog = processAccessLog(infraIR[irKey].Proxy.Config) + xdsIR[irKey].Tracing = processTracing(gateway.Gateway, infraIR[irKey].Proxy.Config) + xdsIR[irKey].Metrics = processMetrics(infraIR[irKey].Proxy.Config) for _, listener := range gateway.listeners { // Process protocol & supported kinds @@ -119,7 +113,7 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap // see more https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/gwapiv1.Listener. irListener.Hostnames = append(irListener.Hostnames, "*") } - gwXdsIR.HTTP = append(gwXdsIR.HTTP, irListener) + xdsIR[irKey].HTTP = append(xdsIR[irKey].HTTP, irListener) } // Add the listener to the Infra IR. Infra IR ports must have a unique port number per layer-4 protocol @@ -139,14 +133,20 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap case gwapiv1.UDPProtocolType: proto = ir.UDPProtocolType } + + infraPortName := string(listener.Name) + if t.MergeGateways { + infraPortName = irHTTPListenerName(listener) + } + infraPort := ir.ListenerPort{ - Name: string(listener.Name), + Name: infraPortName, Protocol: proto, ServicePort: servicePort.port, ContainerPort: containerPort, } // Only 1 listener is supported. - gwInfraIR.Proxy.Listeners[0].Ports = append(gwInfraIR.Proxy.Listeners[0].Ports, infraPort) + infraIR[irKey].Proxy.Listeners[0].Ports = append(infraIR[irKey].Proxy.Listeners[0].Ports, infraPort) } } } diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go index 3a019499c72..fe0ec085def 100644 --- a/internal/gatewayapi/route.go +++ b/internal/gatewayapi/route.go @@ -572,8 +572,7 @@ func (t *Translator) processHTTPRouteParentRefListener(route RouteContext, route perHostRoutes = append(perHostRoutes, hostRoute) } } - - irKey := irStringKey(listener.gateway.Namespace, listener.gateway.Name) + irKey := t.getIRKey(listener.gateway) irListener := xdsIR[irKey].GetHTTPListener(irHTTPListenerName(listener)) if irListener != nil { if GetRouteType(route) == KindGRPCRoute { @@ -669,7 +668,8 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour hasHostnameIntersection = true - irKey := irStringKey(listener.gateway.Namespace, listener.gateway.Name) + irKey := t.getIRKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) // Create the TCP Listener while parsing the TLSRoute since // the listener directly links to a routeDestination. @@ -807,7 +807,9 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour continue } accepted = true - irKey := irStringKey(listener.gateway.Namespace, listener.gateway.Name) + + irKey := t.getIRKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) // Create the UDP Listener while parsing the UDPRoute since // the listener directly links to a routeDestination. @@ -942,7 +944,8 @@ func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resour continue } accepted = true - irKey := irStringKey(listener.gateway.Namespace, listener.gateway.Name) + irKey := t.getIRKey(listener.gateway) + containerPort := servicePortToContainerPort(int32(listener.Port)) // Create the TCP Listener while parsing the TCPRoute since // the listener directly links to a routeDestination. diff --git a/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.in.yaml b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.in.yaml new file mode 100644 index 00000000000..a109f785281 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.in.yaml @@ -0,0 +1,34 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + - name: udp + port: 80 + protocol: UDP diff --git a/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.out.yaml b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.out.yaml new file mode 100755 index 00000000000..93775aaf5f7 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-invalid-multiple-gateways.out.yaml @@ -0,0 +1,143 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - name: http + port: 80 + protocol: HTTP + - name: udp + port: 80 + protocol: UDP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Port, protocol and hostname tuple must be unique for every listener + reason: HostnameConflict + status: "True" + type: Conflicted + - lastTransitionTime: null + message: Listener is invalid, see other Conditions for details. + reason: Invalid + status: "False" + type: Programmed + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: udp + supportedKinds: + - group: gateway.networking.k8s.io + kind: UDPRoute +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + telemetry: {} + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + - containerPort: 10080 + name: envoy-gateway/gateway-2/udp + protocol: UDP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.in.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.in.yaml new file mode 100644 index 00000000000..aeca411f058 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.in.yaml @@ -0,0 +1,100 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + hostname: "*.envoyproxy.io" + allowedRoutes: + namespaces: + from: All +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-2 + port: 8888 + protocol: HTTP + - name: http-3 + hostname: example.com + port: 8888 + protocol: HTTP + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 + filters: + - type: ExtensionRef + extensionRef: + group: gateway.envoyproxy.io + kind: AuthenticationFilter + name: test +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-2 + spec: + hostnames: + - example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + sectionName: http-3 + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-2 + port: 8080 +authenticationFilters: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: AuthenticationFilter + metadata: + namespace: default + name: test + spec: + type: JWT + jwtProviders: + - name: test + issuer: https://www.test.local + remoteJWKS: + uri: https://test.local/jwt/public-key/jwks.json diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.out.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.out.yaml new file mode 100755 index 00000000000..aebb2aff709 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways-multiple-routes.out.yaml @@ -0,0 +1,289 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: All + hostname: '*.envoyproxy.io' + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - name: http-2 + port: 8888 + protocol: HTTP + - allowedRoutes: + namespaces: + from: All + hostname: example.com + name: http-3 + port: 8888 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-3 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + filters: + - extensionRef: + group: gateway.envoyproxy.io + kind: AuthenticationFilter + name: test + type: ExtensionRef + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-2 + namespace: default + spec: + hostnames: + - example.com + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + sectionName: http-3 + rules: + - backendRefs: + - name: service-2 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http-3 +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + telemetry: {} + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + - containerPort: 8888 + name: envoy-gateway/gateway-2/http-2 + protocol: HTTP + servicePort: 8888 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*.envoyproxy.io' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: gateway.envoyproxy.io + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + requestAuthentication: + jwt: + providers: + - issuer: https://www.test.local + name: test + remoteJWKS: + uri: https://test.local/jwt/public-key/jwks.json + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http-2 + port: 8888 + - address: 0.0.0.0 + hostnames: + - example.com + isHTTP2: false + name: envoy-gateway/gateway-2/http-3 + port: 8888 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-2/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: example.com + name: httproute/default/httproute-2/rule/0/match/0/example_com + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways.in.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways.in.yaml new file mode 100644 index 00000000000..aad24f222ea --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways.in.yaml @@ -0,0 +1,41 @@ +envoyproxy: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + namespace: envoy-gateway-system + name: test + spec: + mergeGateways: true +gateways: + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http-2 + port: 8888 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: http-3 + hostname: example.com + port: 8888 + protocol: HTTP diff --git a/internal/gatewayapi/testdata/merge-valid-multiple-gateways.out.yaml b/internal/gatewayapi/testdata/merge-valid-multiple-gateways.out.yaml new file mode 100755 index 00000000000..bd4578160a6 --- /dev/null +++ b/internal/gatewayapi/testdata/merge-valid-multiple-gateways.out.yaml @@ -0,0 +1,164 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1beta1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + infrastructure: {} + listeners: + - allowedRoutes: + namespaces: + from: Same + name: http-2 + port: 8888 + protocol: HTTP + - hostname: example.com + name: http-3 + port: 8888 + protocol: HTTP + status: + listeners: + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-2 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute + - attachedRoutes: 0 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http-3 + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +infraIR: + envoy-gateway-class: + proxy: + config: + apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: EnvoyProxy + metadata: + creationTimestamp: null + name: test + namespace: envoy-gateway-system + spec: + logging: {} + mergeGateways: true + telemetry: {} + status: {} + listeners: + - address: "" + ports: + - containerPort: 10080 + name: envoy-gateway/gateway-1/http + protocol: HTTP + servicePort: 80 + - containerPort: 8888 + name: envoy-gateway/gateway-2/http-2 + protocol: HTTP + servicePort: 8888 + metadata: + labels: + gateway.envoyproxy.io/owning-gatewayclass: envoy-gateway-class + name: envoy-gateway-class +xdsIR: + envoy-gateway-class: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + port: 10080 + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http-2 + port: 8888 + - address: 0.0.0.0 + hostnames: + - example.com + isHTTP2: false + name: envoy-gateway/gateway-2/http-3 + port: 8888 diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go index c449321c548..35bb6cc3fec 100644 --- a/internal/gatewayapi/translator.go +++ b/internal/gatewayapi/translator.go @@ -10,6 +10,7 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" ) const ( @@ -31,6 +32,7 @@ const ( // The value should be the namespace of the accepted Envoy Gateway. OwningGatewayNamespaceLabel = "gateway.envoyproxy.io/owning-gateway-namespace" + OwningGatewayClassLabel = "gateway.envoyproxy.io/owning-gatewayclass" // OwningGatewayNameLabel is the owner reference label used for managed infra. // The value should be the name of the accepted Envoy Gateway. OwningGatewayNameLabel = "gateway.envoyproxy.io/owning-gateway-name" @@ -73,6 +75,10 @@ type Translator struct { // instead. EndpointRoutingDisabled bool + // MergeGateways is true when all Gateway Listeners + // should be merged under the parent GatewayClass. + MergeGateways bool + // ExtensionGroupKinds stores the group/kind for all resources // introduced by an Extension so that the translator can // store referenced resources in the IR for later use. @@ -123,12 +129,12 @@ func newTranslateResult(gateways []*GatewayContext, } func (t *Translator) Translate(resources *Resources) *TranslateResult { - xdsIR := make(XdsIRMap) - infraIR := make(InfraIRMap) - // Get Gateways belonging to our GatewayClass. gateways := t.GetRelevantGateways(resources.Gateways) + // Build IR maps. + xdsIR, infraIR := t.InitIRs(gateways, resources) + // Process all Listeners for all relevant Gateways. t.ProcessListeners(gateways, xdsIR, infraIR, resources) @@ -184,3 +190,40 @@ func (t *Translator) GetRelevantGateways(gateways []*gwapiv1.Gateway) []*Gateway return relevant } + +// InitIRs checks if mergeGateways is enabled in EnvoyProxy config and initializes XdsIR and InfraIR maps with adequate keys. +func (t *Translator) InitIRs(gateways []*GatewayContext, resources *Resources) (map[string]*ir.Xds, map[string]*ir.Infra) { + xdsIR := make(XdsIRMap) + infraIR := make(InfraIRMap) + + var irKey string + for _, gateway := range gateways { + gwXdsIR := &ir.Xds{} + gwInfraIR := ir.NewInfra() + if isMergeGatewaysEnabled(resources) { + t.MergeGateways = true + irKey = string(t.GatewayClassName) + gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayClassOwnerLabel(string(t.GatewayClassName)) + } else { + irKey = irStringKey(gateway.Gateway.Namespace, gateway.Gateway.Name) + gwInfraIR.Proxy.GetProxyMetadata().Labels = GatewayOwnerLabels(gateway.Namespace, gateway.Name) + } + + gwInfraIR.Proxy.Name = irKey + // save the IR references in the map before the translation starts + xdsIR[irKey] = gwXdsIR + infraIR[irKey] = gwInfraIR + } + + return xdsIR, infraIR +} + +// XdsIR and InfraIR map keys by default are {GatewayNamespace}/{GatewayName}, but if mergeGateways is set, they are merged under {GatewayClassName} key. +func (t *Translator) getIRKey(gateway *gwapiv1.Gateway) string { + irKey := irStringKey(gateway.Namespace, gateway.Name) + if t.MergeGateways { + return string(t.GatewayClassName) + } + + return irKey +} diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go index a6d18487e76..1ada34033a0 100644 --- a/internal/gatewayapi/validate.go +++ b/internal/gatewayapi/validate.go @@ -533,6 +533,29 @@ type portListeners struct { hostnames map[string]int } +// Port, protocol and hostname tuple should be unique across all listeners on merged Gateways. +func (t *Translator) validateConflictedMergedListeners(gateways []*GatewayContext) { + listenerSets := sets.Set[string]{} + for _, gateway := range gateways { + for _, listener := range gateway.listeners { + hostname := new(gwapiv1.Hostname) + if listener.Hostname != nil { + hostname = listener.Hostname + } + portProtocolHostname := fmt.Sprintf("%s:%s:%d", listener.Protocol, *hostname, listener.Port) + if listenerSets.Has(portProtocolHostname) { + listener.SetCondition( + gwapiv1.ListenerConditionConflicted, + metav1.ConditionTrue, + gwapiv1.ListenerReasonHostnameConflict, + "Port, protocol and hostname tuple must be unique for every listener", + ) + } + listenerSets.Insert(portProtocolHostname) + } + } +} + func (t *Translator) validateConflictedLayer7Listeners(gateways []*GatewayContext) { // Iterate through all layer-7 (HTTP, HTTPS, TLS) listeners and collect info about protocols // and hostnames per port. diff --git a/internal/infrastructure/kubernetes/proxy/resource.go b/internal/infrastructure/kubernetes/proxy/resource.go index 6f9b451432c..08106c458f3 100644 --- a/internal/infrastructure/kubernetes/proxy/resource.go +++ b/internal/infrastructure/kubernetes/proxy/resource.go @@ -102,7 +102,7 @@ func expectedProxyContainers(infra *ir.ProxyInfra, deploymentConfig *egv1a1.Kube return nil, fmt.Errorf("invalid protocol %q", p.Protocol) } port := corev1.ContainerPort{ - Name: p.Name, + Name: providerutils.ExpectedContainerPortHashedName(p.Name), ContainerPort: p.ContainerPort, Protocol: protocol, } diff --git a/internal/infrastructure/kubernetes/proxy/resource_provider.go b/internal/infrastructure/kubernetes/proxy/resource_provider.go index 8dab7122855..efcc1e6a1e1 100644 --- a/internal/infrastructure/kubernetes/proxy/resource_provider.go +++ b/internal/infrastructure/kubernetes/proxy/resource_provider.go @@ -8,6 +8,7 @@ package proxy import ( "fmt" "strconv" + "strings" "golang.org/x/exp/maps" appsv1 "k8s.io/api/apps/v1" @@ -45,7 +46,7 @@ func (r *ResourceRender) Name() string { func (r *ResourceRender) ServiceAccount() (*corev1.ServiceAccount, error) { // Set the labels based on the owning gateway name. labels := envoyLabels(r.infra.GetProxyMetadata().Labels) - if len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 { + if (len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0) && len(labels[gatewayapi.OwningGatewayClassLabel]) == 0 { return nil, fmt.Errorf("missing owning gateway labels") } @@ -72,8 +73,10 @@ func (r *ResourceRender) Service() (*corev1.Service, error) { if port.Protocol == ir.UDPProtocolType { protocol = corev1.ProtocolUDP } + // Listeners on merged gateways will have a port name {GatewayNamespace}/{GatewayName}/{ListenerName}. + portName := strings.ReplaceAll(port.Name, "/", "-") p := corev1.ServicePort{ - Name: port.Name, + Name: portName, Protocol: protocol, Port: port.ServicePort, TargetPort: target, @@ -84,7 +87,7 @@ func (r *ResourceRender) Service() (*corev1.Service, error) { // Set the labels based on the owning gatewayclass name. labels := envoyLabels(r.infra.GetProxyMetadata().Labels) - if len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 { + if (len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0) && len(labels[gatewayapi.OwningGatewayClassLabel]) == 0 { return nil, fmt.Errorf("missing owning gateway labels") } @@ -123,7 +126,7 @@ func (r *ResourceRender) Service() (*corev1.Service, error) { func (r *ResourceRender) ConfigMap() (*corev1.ConfigMap, error) { // Set the labels based on the owning gateway name. labels := envoyLabels(r.infra.GetProxyMetadata().Labels) - if len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 { + if (len(labels[gatewayapi.OwningGatewayNameLabel]) == 0 || len(labels[gatewayapi.OwningGatewayNamespaceLabel]) == 0) && len(labels[gatewayapi.OwningGatewayClassLabel]) == 0 { return nil, fmt.Errorf("missing owning gateway labels") } @@ -169,7 +172,7 @@ func (r *ResourceRender) Deployment() (*appsv1.Deployment, error) { // Set the labels based on the owning gateway name. labels := r.infra.GetProxyMetadata().Labels dpLabels := envoyLabels(labels) - if len(dpLabels[gatewayapi.OwningGatewayNamespaceLabel]) == 0 || len(dpLabels[gatewayapi.OwningGatewayNameLabel]) == 0 { + if (len(dpLabels[gatewayapi.OwningGatewayNameLabel]) == 0 || len(dpLabels[gatewayapi.OwningGatewayNamespaceLabel]) == 0) && len(dpLabels[gatewayapi.OwningGatewayClassLabel]) == 0 { return nil, fmt.Errorf("missing owning gateway labels") } diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go index 4517f1e7522..6798aa026d6 100644 --- a/internal/provider/kubernetes/controller.go +++ b/internal/provider/kubernetes/controller.go @@ -70,6 +70,7 @@ type gatewayAPIReconciler struct { namespace string namespaceLabels []string envoyGateway *egv1a1.EnvoyGateway + mergeGateways bool resources *message.ProviderResources extGVKs []schema.GroupVersionKind @@ -334,6 +335,10 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques } } + if resourceTree.EnvoyProxy != nil && resourceTree.EnvoyProxy.Spec.MergeGateways != nil { + r.mergeGateways = *resourceTree.EnvoyProxy.Spec.MergeGateways + } + if err := r.gatewayClassUpdater(ctx, acceptedGC, true, string(gwapiv1.GatewayClassReasonAccepted), status.MsgValidGatewayClass); err != nil { r.log.Error(err, "unable to update GatewayClass status") return reconcile.Result{}, err diff --git a/internal/provider/kubernetes/helpers.go b/internal/provider/kubernetes/helpers.go index 40bcc964536..04bf83fecba 100644 --- a/internal/provider/kubernetes/helpers.go +++ b/internal/provider/kubernetes/helpers.go @@ -17,9 +17,8 @@ import ( mcsapi "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/envoyproxy/gateway/internal/envoygateway/config" "github.com/envoyproxy/gateway/internal/gatewayapi" - "github.com/envoyproxy/gateway/internal/provider/utils" + "github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/proxy" ) const ( @@ -197,14 +196,15 @@ func refsSecret(ref *gwapiv1.SecretObjectReference) bool { (ref.Kind == nil || *ref.Kind == gatewayapi.KindSecret) } -func infraServiceName(gateway *gwapiv1.Gateway) string { - infraName := utils.GetHashedName(fmt.Sprintf("%s/%s", gateway.Namespace, gateway.Name)) - return fmt.Sprintf("%s-%s", config.EnvoyPrefix, infraName) -} - -func infraDeploymentName(gateway *gwapiv1.Gateway) string { - infraName := utils.GetHashedName(fmt.Sprintf("%s/%s", gateway.Namespace, gateway.Name)) - return fmt.Sprintf("%s-%s", config.EnvoyPrefix, infraName) +// infraName returns expected name for the EnvoyProxy infra resources. +// By default it returns hashed string from {GatewayNamespace}/{GatewayName}, +// but if mergeGateways is set, it will return hashed string of {GatewayClassName}. +func infraName(gateway *gwapiv1.Gateway, merged bool) string { + if merged { + return proxy.ExpectedResourceHashedName(string(gateway.Spec.GatewayClassName)) + } + infraName := fmt.Sprintf("%s/%s", gateway.Namespace, gateway.Name) + return proxy.ExpectedResourceHashedName(infraName) } // validateBackendRef validates that ref is a reference to a local Service. diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go index 6ef95122ecf..b24872447d4 100644 --- a/internal/provider/kubernetes/predicates.go +++ b/internal/provider/kubernetes/predicates.go @@ -168,14 +168,26 @@ func (r *gatewayAPIReconciler) validateServiceForReconcile(obj client.Object) bo r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) return false } + labels := svc.GetLabels() // Check if the Service belongs to a Gateway, if so, update the Gateway status. - gtw := r.findOwningGateway(ctx, svc.GetLabels()) + gtw := r.findOwningGateway(ctx, labels) if gtw != nil { r.statusUpdateForGateway(ctx, gtw) return false } + // Only merged gateways will have this label, update status of all Gateways under found GatewayClass. + gclass, ok := labels[gatewayapi.OwningGatewayClassLabel] + if ok { + res, _ := r.resources.GatewayAPIResources.Load(gclass) + for _, gw := range res.Gateways { + gw := gw + r.statusUpdateForGateway(ctx, gw) + } + return false + } + nsName := utils.NamespacedName(svc) return r.isRouteReferencingBackend(&nsName) } @@ -286,17 +298,29 @@ func (r *gatewayAPIReconciler) validateDeploymentForReconcile(obj client.Object) r.log.Info("unexpected object type, bypassing reconciliation", "object", obj) return false } + labels := deployment.GetLabels() // Only deployments in the configured namespace should be reconciled. if deployment.Namespace == r.namespace { // Check if the deployment belongs to a Gateway, if so, update the Gateway status. - gtw := r.findOwningGateway(ctx, deployment.GetLabels()) + gtw := r.findOwningGateway(ctx, labels) if gtw != nil { r.statusUpdateForGateway(ctx, gtw) return false } } + // Only merged gateways will have this label, update status of all Gateways under found GatewayClass. + gclass, ok := labels[gatewayapi.OwningGatewayClassLabel] + if ok { + res, _ := r.resources.GatewayAPIResources.Load(gclass) + for _, gtw := range res.Gateways { + gtw := gtw + r.statusUpdateForGateway(ctx, gtw) + } + return false + } + // There is no need to reconcile the Deployment any further. return false } @@ -377,7 +401,7 @@ func (r *gatewayAPIReconciler) filterHTTPRoutesByNamespaceLabels(httpRoutes []gw func (r *gatewayAPIReconciler) envoyDeploymentForGateway(ctx context.Context, gateway *gwapiv1.Gateway) (*appsv1.Deployment, error) { key := types.NamespacedName{ Namespace: r.namespace, - Name: infraDeploymentName(gateway), + Name: infraName(gateway, r.mergeGateways), } deployment := new(appsv1.Deployment) if err := r.client.Get(ctx, key, deployment); err != nil { @@ -393,7 +417,7 @@ func (r *gatewayAPIReconciler) envoyDeploymentForGateway(ctx context.Context, ga func (r *gatewayAPIReconciler) envoyServiceForGateway(ctx context.Context, gateway *gwapiv1.Gateway) (*corev1.Service, error) { key := types.NamespacedName{ Namespace: r.namespace, - Name: infraServiceName(gateway), + Name: infraName(gateway, r.mergeGateways), } svc := new(corev1.Service) if err := r.client.Get(ctx, key, svc); err != nil { diff --git a/internal/provider/kubernetes/predicates_test.go b/internal/provider/kubernetes/predicates_test.go index 71d9dfd60c2..2bd311e95ee 100644 --- a/internal/provider/kubernetes/predicates_test.go +++ b/internal/provider/kubernetes/predicates_test.go @@ -334,7 +334,7 @@ func TestValidateServiceForReconcile(t *testing.T) { configs: []client.Object{ test.GetGatewayClass("test-gc", v1alpha1.GatewayControllerName), sampleGateway, - test.GetGatewayDeployment(types.NamespacedName{Name: infraDeploymentName(sampleGateway)}, nil), + test.GetGatewayDeployment(types.NamespacedName{Name: infraName(sampleGateway, false)}, nil), }, service: test.GetService(types.NamespacedName{Name: "service"}, map[string]string{ gatewayapi.OwningGatewayNameLabel: "scheduled-status-test", diff --git a/internal/provider/utils/utils.go b/internal/provider/utils/utils.go index 984a4e90d01..0417b892cfa 100644 --- a/internal/provider/utils/utils.go +++ b/internal/provider/utils/utils.go @@ -25,11 +25,7 @@ func NamespacedName(obj client.Object) types.NamespacedName { // GetHashedName returns a partially hashed name for the string including up to 48 characters of the original name before the hash. // Input `nsName` should be formatted as `{Namespace}/{ResourceName}`. func GetHashedName(nsName string) string { - - h := sha256.New() // Using sha256 instead of sha1 due to Blocklisted import crypto/sha1: weak cryptographic primitive (gosec) - h.Write([]byte(nsName)) - hashedName := strings.ToLower(fmt.Sprintf("%x", h.Sum(nil))) - + hashedName := HashString(nsName) // replace `/` with `-` to create a valid K8s resource name resourceName := strings.ReplaceAll(nsName, "/", "-") @@ -38,3 +34,24 @@ func GetHashedName(nsName string) string { } return fmt.Sprintf("%s-%s", resourceName, hashedName[0:8]) } + +func HashString(str string) string { + h := sha256.New() // Using sha256 instead of sha1 due to Blocklisted import crypto/sha1: weak cryptographic primitive (gosec) + h.Write([]byte(str)) + return strings.ToLower(fmt.Sprintf("%x", h.Sum(nil))) +} + +// ExpectedContainerPortHashedName returns expected container port name with max length of 15 characters. +// If mergedGateways is enabled or listener port name is larger than 15 characters it will return partially hashed name. +// Listeners on merged gateways have a infraIR port name {GatewayNamespace}/{GatewayName}/{ListenerName}. +func ExpectedContainerPortHashedName(name string) string { + if len(name) > 15 { + hashedName := HashString(name) + // replace `/` with `-` to create a valid K8s resource name + resourceName := strings.ReplaceAll(name, "/", "-") + listenerName := string(resourceName[2]) + + return fmt.Sprintf("%s-%s", listenerName, hashedName[0:14-len(listenerName)]) + } + return name +}