This is an automated email from the ASF dual-hosted git repository.
alinsran pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 51077751 feat(gateway-api): support GRPCRoute (#2570)
51077751 is described below
commit 51077751aab206f8719031dab7bc6dd75b959941
Author: AlinsRan <[email protected]>
AuthorDate: Tue Sep 23 16:03:16 2025 +0800
feat(gateway-api): support GRPCRoute (#2570)
---
Makefile | 2 +-
api/adc/types.go | 26 +-
config/rbac/role.yaml | 21 +-
docs/en/latest/concepts/gateway-api.md | 2 +-
go.mod | 2 +-
internal/adc/translator/grpcroute.go | 372 +++++++++++++++++++++
internal/adc/translator/httproute.go | 74 ++--
internal/controller/context.go | 4 +-
...route_controller.go => grpcroute_controller.go} | 372 ++++++++-------------
internal/controller/httproute_controller.go | 6 +-
internal/controller/indexer/grpcroute.go | 106 ++++++
internal/controller/indexer/indexer.go | 1 +
internal/controller/utils.go | 9 +-
internal/manager/controllers.go | 17 +-
internal/provider/apisix/provider.go | 5 +-
internal/provider/apisix/status.go | 35 ++
internal/provider/provider.go | 1 +
internal/types/k8s.go | 8 +-
test/e2e/framework/grpc.go | 58 ++++
test/e2e/framework/manifests/grpc-backend.yaml | 63 ++++
test/e2e/framework/manifests/ingress.yaml | 86 +----
test/e2e/gatewayapi/grpcroute.go | 289 ++++++++++++++++
test/e2e/scaffold/grpc.go | 159 +++++++++
23 files changed, 1337 insertions(+), 381 deletions(-)
diff --git a/Makefile b/Makefile
index 59f1d7b6..e82b876a 100644
--- a/Makefile
+++ b/Makefile
@@ -55,7 +55,7 @@ GATEAY_API_VERSION ?= v1.3.0
SUPPORTED_EXTENDED_FEATURES =
"HTTPRouteDestinationPortMatching,HTTPRouteMethodMatching,HTTPRoutePortRedirect,HTTPRouteRequestMirror,HTTPRouteSchemeRedirect,GatewayAddressEmpty,HTTPRouteResponseHeaderModification,GatewayPort8080"
CONFORMANCE_TEST_REPORT_OUTPUT ?=
$(DIR)/apisix-ingress-controller-conformance-report.yaml
##
https://github.com/kubernetes-sigs/gateway-api/blob/v1.3.0/conformance/utils/suite/profiles.go
-CONFORMANCE_PROFILES ?= GATEWAY-HTTP
+CONFORMANCE_PROFILES ?= GATEWAY-HTTP,GATEWAY-GRPC
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is
set)
ifeq (,$(shell go env GOBIN))
diff --git a/api/adc/types.go b/api/adc/types.go
index 2133d731..5a8062db 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -344,10 +344,6 @@ const (
type Scheme string
-const (
- SchemeHTTP = "http"
-)
-
type UpstreamType string
const (
@@ -519,11 +515,13 @@ func ComposeServiceNameWithRule(namespace, name string,
rule string) string {
return buf.String()
}
-func ComposeServiceNameWithStream(namespace, name string, rule string) string {
- // FIXME Use sync.Pool to reuse this buffer if the upstream
- // name composing code path is hot.
+func ComposeGRPCServiceNameWithRule(namespace, name string, rule string)
string {
+ return ComposeServicesNameWithScheme(namespace, name, rule, "grpc")
+}
+
+func ComposeServicesNameWithScheme(namespace, name string, rule string, scheme
string) string {
var p []byte
- plen := len(namespace) + len(name) + 6
+ plen := len(namespace) + len(name) + len(rule) + len(scheme) + 3
p = make([]byte, 0, plen)
buf := bytes.NewBuffer(p)
@@ -532,11 +530,16 @@ func ComposeServiceNameWithStream(namespace, name string,
rule string) string {
buf.WriteString(name)
buf.WriteByte('_')
buf.WriteString(rule)
- buf.WriteString("_stream")
+ buf.WriteByte('_')
+ buf.WriteString(scheme)
return buf.String()
}
+func ComposeServiceNameWithStream(namespace, name string, rule string) string {
+ return ComposeServicesNameWithScheme(namespace, name, rule, "stream")
+}
+
func ComposeConsumerName(namespace, name string) string {
// FIXME Use sync.Pool to reuse this buffer if the upstream
// name composing code path is hot.
@@ -569,9 +572,8 @@ func NewDefaultUpstream() *Upstream {
"managed-by": "apisix-ingress-controller",
},
},
- Nodes: make(UpstreamNodes, 0),
- Scheme: SchemeHTTP,
- Type: Roundrobin,
+ Nodes: make(UpstreamNodes, 0),
+ Type: Roundrobin,
}
}
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 388c7cd2..3a69f5ec 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -79,7 +79,6 @@ rules:
- gateway.networking.k8s.io
resources:
- gatewayclasses
- - gateways
verbs:
- get
- list
@@ -90,6 +89,7 @@ rules:
resources:
- gatewayclasses/status
- gateways/status
+ - grpcroutes/status
- httproutes/status
- referencegrants/status
verbs:
@@ -98,35 +98,22 @@ rules:
- apiGroups:
- gateway.networking.k8s.io
resources:
+ - gateways
+ - grpcroutes
- httproutes
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - gateway.networking.k8s.io
- resources:
- referencegrants
verbs:
- - list
- - update
- - watch
-- apiGroups:
- - networking.k8s.io
- resources:
- - ingressclasses
- verbs:
- get
- list
- watch
- apiGroups:
- networking.k8s.io
resources:
+ - ingressclasses
- ingresses
verbs:
- get
- list
- - update
- watch
- apiGroups:
- networking.k8s.io
diff --git a/docs/en/latest/concepts/gateway-api.md
b/docs/en/latest/concepts/gateway-api.md
index ce431a71..c0fcbb75 100644
--- a/docs/en/latest/concepts/gateway-api.md
+++ b/docs/en/latest/concepts/gateway-api.md
@@ -48,7 +48,7 @@ By supporting Gateway API, the APISIX Ingress controller can
realize richer func
| GatewayClass | Supported | N/A | Not
supported | v1 |
| Gateway | Partially supported | Partially supported | Not
supported | v1 |
| HTTPRoute | Supported | Partially supported | Not
supported | v1 |
-| GRPCRoute | Not supported | Not supported | Not
supported | v1 |
+| GRPCRoute | Supported | Supported | Not
supported | v1 |
| ReferenceGrant | Supported | Not supported | Not
supported | v1beta1 |
| TLSRoute | Not supported | Not supported | Not
supported | v1alpha2 |
| TCPRoute | Not supported | Not supported | Not
supported | v1alpha2 |
diff --git a/go.mod b/go.mod
index 81e8b7aa..6dbbaa30 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
+ google.golang.org/grpc v1.71.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.32.3
k8s.io/apiextensions-apiserver v0.32.3
@@ -195,7 +196,6 @@ require (
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api
v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc
v0.0.0-20250115164207-1a7da9e5054f // indirect
- google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
diff --git a/internal/adc/translator/grpcroute.go
b/internal/adc/translator/grpcroute.go
new file mode 100644
index 00000000..d24fc8bc
--- /dev/null
+++ b/internal/adc/translator/grpcroute.go
@@ -0,0 +1,372 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "cmp"
+ "fmt"
+ "strings"
+
+ "k8s.io/utils/ptr"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+ apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/internal/controller/label"
+ "github.com/apache/apisix-ingress-controller/internal/id"
+ "github.com/apache/apisix-ingress-controller/internal/provider"
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func (t *Translator) fillPluginsFromGRPCRouteFilters(
+ plugins adctypes.Plugins,
+ namespace string,
+ filters []gatewayv1.GRPCRouteFilter,
+ tctx *provider.TranslateContext,
+) {
+ for _, filter := range filters {
+ switch filter.Type {
+ case gatewayv1.GRPCRouteFilterRequestHeaderModifier:
+ t.fillPluginFromHTTPRequestHeaderFilter(plugins,
filter.RequestHeaderModifier)
+ case gatewayv1.GRPCRouteFilterRequestMirror:
+ t.fillPluginFromHTTPRequestMirrorFilter(plugins,
namespace, filter.RequestMirror, apiv2.SchemeGRPC)
+ case gatewayv1.GRPCRouteFilterResponseHeaderModifier:
+ t.fillPluginFromHTTPResponseHeaderFilter(plugins,
filter.ResponseHeaderModifier)
+ case gatewayv1.GRPCRouteFilterExtensionRef:
+ t.fillPluginFromExtensionRef(plugins, namespace,
filter.ExtensionRef, tctx)
+ }
+ }
+}
+
+func calculateGRPCRoutePriority(match *gatewayv1.GRPCRouteMatch, ruleIndex
int, hosts []string) uint64 {
+ const (
+ // PreciseHostnameShiftBits assigns bit 31-38 for the length of
hostname(max length=253).
+ // which has 8 bits, so the max length of hostname is 2^8-1 =
255.
+ PreciseHostnameShiftBits = 31
+
+ // HostnameLengthShiftBits assigns bits 23-30 for the length of
hostname(max length=253).
+ // which has 8 bits, so the max length of hostname is 2^8-1 =
255.
+ HostnameLengthShiftBits = 23
+
+ // ServiceMatchShiftBits assigns bits 19-22 for the length of
service name.
+ ServiceMatchShiftBits = 19
+
+ // MethodMatchShiftBits assigns bits 15-18 for the length of
method name.
+ MethodMatchShiftBits = 15
+
+ // HeaderNumberShiftBits assign bits 10-14 to number of
headers. (max number of headers = 16)
+ HeaderNumberShiftBits = 10
+
+ // RuleIndexShiftBits assigns bits 5-9 to rule index. (max
number of rules = 16)
+ RuleIndexShiftBits = 5
+ )
+
+ var (
+ priority uint64 = 0
+ // Handle hostname priority
+ // 1. Non-wildcard hostname priority
+ // 2. Hostname length priority
+ maxNonWildcardLength = 0
+ maxHostnameLength = 0
+ )
+
+ for _, host := range hosts {
+ isNonWildcard := !strings.Contains(host, "*")
+
+ if isNonWildcard && len(host) > maxNonWildcardLength {
+ maxNonWildcardLength = len(host)
+ }
+
+ if len(host) > maxHostnameLength {
+ maxHostnameLength = len(host)
+ }
+ }
+
+ // If there is a non-wildcard hostname, set the
PreciseHostnameShiftBits bit
+ if maxNonWildcardLength > 0 {
+ priority |= (uint64(maxNonWildcardLength) <<
PreciseHostnameShiftBits)
+ }
+
+ if maxHostnameLength > 0 {
+ priority |= (uint64(maxHostnameLength) <<
HostnameLengthShiftBits)
+ }
+
+ // Service and Method matching - this is the key difference from
HTTPRoute
+ serviceLength := 0
+ methodLength := 0
+
+ if match.Method != nil {
+ // Service matching
+ if match.Method.Service != nil {
+ serviceLength = len(*match.Method.Service)
+ priority |= (uint64(serviceLength) <<
ServiceMatchShiftBits)
+ }
+
+ // Method matching
+ if match.Method.Method != nil {
+ methodLength = len(*match.Method.Method)
+ priority |= (uint64(methodLength) <<
MethodMatchShiftBits)
+ }
+ }
+
+ // HeaderNumberShiftBits - GRPCRoute also supports header matching
+ headerCount := 0
+ if match.Headers != nil {
+ headerCount = len(match.Headers)
+ }
+ priority |= (uint64(headerCount) << HeaderNumberShiftBits)
+
+ // RuleIndexShiftBits - lower index has higher priority
+ // We invert the index so that rule 0 gets highest priority (16), rule
1 gets 15, etc.
+ index := 16 - ruleIndex
+ if index < 0 {
+ index = 0
+ }
+ if index > 16 {
+ index = 16
+ }
+ priority |= (uint64(index) << RuleIndexShiftBits)
+
+ return priority
+}
+
+func (t *Translator) TranslateGRPCRoute(tctx *provider.TranslateContext,
grpcRoute *gatewayv1.GRPCRoute) (*TranslateResult, error) {
+ result := &TranslateResult{}
+
+ hosts := make([]string, 0, len(grpcRoute.Spec.Hostnames))
+ for _, hostname := range grpcRoute.Spec.Hostnames {
+ hosts = append(hosts, string(hostname))
+ }
+
+ for _, listener := range tctx.Listeners {
+ if listener.Hostname != nil {
+ hosts = append(hosts, string(*listener.Hostname))
+ }
+ }
+
+ rules := grpcRoute.Spec.Rules
+
+ labels := label.GenLabel(grpcRoute)
+
+ for ruleIndex, rule := range rules {
+ service := adctypes.NewDefaultService()
+ service.Labels = labels
+
+ service.Name =
adctypes.ComposeGRPCServiceNameWithRule(grpcRoute.Namespace, grpcRoute.Name,
fmt.Sprintf("%d", ruleIndex))
+ service.ID = id.GenID(service.Name)
+ service.Hosts = hosts
+
+ var (
+ upstreams = make([]*adctypes.Upstream, 0)
+ weightedUpstreams =
make([]adctypes.TrafficSplitConfigRuleWeightedUpstream, 0)
+ backendErr error
+ )
+
+ for _, backend := range rule.BackendRefs {
+ if backend.Namespace == nil {
+ namespace :=
gatewayv1.Namespace(grpcRoute.Namespace)
+ backend.Namespace = &namespace
+ }
+ upstream := adctypes.NewDefaultUpstream()
+ upNodes, err := t.translateBackendRef(tctx,
backend.BackendRef, DefaultEndpointFilter)
+ if err != nil {
+ backendErr = err
+ continue
+ }
+ if len(upNodes) == 0 {
+ continue
+ }
+
+
t.AttachBackendTrafficPolicyToUpstream(backend.BackendRef,
tctx.BackendTrafficPolicies, upstream)
+ upstream.Nodes = upNodes
+
+ var (
+ kind string
+ port int32
+ )
+ if backend.Kind == nil {
+ kind = internaltypes.KindService
+ } else {
+ kind = string(*backend.Kind)
+ }
+ if backend.Port != nil {
+ port = int32(*backend.Port)
+ }
+ namespace := string(*backend.Namespace)
+ name := string(backend.Name)
+ upstreamName :=
adctypes.ComposeUpstreamNameForBackendRef(kind, namespace, name, port)
+ upstream.Name = upstreamName
+ upstream.ID = id.GenID(upstreamName)
+ upstream.Scheme = cmp.Or(upstream.Scheme,
apiv2.SchemeGRPC)
+ upstreams = append(upstreams, upstream)
+ }
+
+ // Handle multiple backends with traffic-split plugin
+ if len(upstreams) == 0 {
+ // Create a default upstream if no valid backends
+ upstream := adctypes.NewDefaultUpstream()
+ upstream.Scheme = apiv2.SchemeGRPC
+ service.Upstream = upstream
+ } else {
+ // Multiple backends - use traffic-split plugin
+ service.Upstream = upstreams[0]
+ // remove the id and name of the service.upstream, adc
schema does not need id and name for it
+ service.Upstream.ID = ""
+ service.Upstream.Name = ""
+
+ upstreams = upstreams[1:]
+
+ if len(upstreams) > 0 {
+ service.Upstreams = upstreams
+
+ // Set weight in traffic-split for the default
upstream
+ weight := apiv2.DefaultWeight
+ if rule.BackendRefs[0].Weight != nil {
+ weight =
int(*rule.BackendRefs[0].Weight)
+ }
+ weightedUpstreams = append(weightedUpstreams,
adctypes.TrafficSplitConfigRuleWeightedUpstream{
+ Weight: weight,
+ })
+
+ // Set other upstreams in traffic-split using
upstream_id
+ for i, upstream := range upstreams {
+ weight := apiv2.DefaultWeight
+ // get weight from the backend refs
starting from the second backend
+ if i+1 < len(rule.BackendRefs) &&
rule.BackendRefs[i+1].Weight != nil {
+ weight =
int(*rule.BackendRefs[i+1].Weight)
+ }
+ weightedUpstreams =
append(weightedUpstreams, adctypes.TrafficSplitConfigRuleWeightedUpstream{
+ UpstreamID: upstream.ID,
+ Weight: weight,
+ })
+ }
+
+ if len(weightedUpstreams) > 0 {
+ if service.Plugins == nil {
+ service.Plugins =
make(map[string]any)
+ }
+ service.Plugins["traffic-split"] =
&adctypes.TrafficSplitConfig{
+ Rules:
[]adctypes.TrafficSplitConfigRule{
+ {
+
WeightedUpstreams: weightedUpstreams,
+ },
+ },
+ }
+ }
+ }
+ }
+
+ if backendErr != nil && (service.Upstream == nil ||
len(service.Upstream.Nodes) == 0) {
+ if service.Plugins == nil {
+ service.Plugins = make(map[string]any)
+ }
+ service.Plugins["fault-injection"] = map[string]any{
+ "abort": map[string]any{
+ "http_status": 500,
+ "body": "No existing backendRef
provided",
+ },
+ }
+ }
+
+ t.fillPluginsFromGRPCRouteFilters(service.Plugins,
grpcRoute.GetNamespace(), rule.Filters, tctx)
+
+ matches := rule.Matches
+ if len(matches) == 0 {
+ matches = []gatewayv1.GRPCRouteMatch{{}}
+ }
+
+ routes := []*adctypes.Route{}
+ for j, match := range matches {
+ route, err := t.translateGatewayGRPCRouteMatch(&match)
+ if err != nil {
+ return nil, err
+ }
+
+ name := adctypes.ComposeRouteName(grpcRoute.Namespace,
grpcRoute.Name, fmt.Sprintf("%d-%d", ruleIndex, j))
+ route.Name = name
+ route.ID = id.GenID(name)
+ route.Labels = labels
+
+ // Set the route priority
+ priority := calculateGRPCRoutePriority(&match,
ruleIndex, hosts)
+ route.Priority = ptr.To(int64(priority))
+
+ routes = append(routes, route)
+ }
+ service.Routes = routes
+
+ result.Services = append(result.Services, service)
+ }
+
+ return result, nil
+}
+
+func (t *Translator) translateGatewayGRPCRouteMatch(match
*gatewayv1.GRPCRouteMatch) (*adctypes.Route, error) {
+ route := &adctypes.Route{}
+
+ var (
+ service string
+ method string
+ )
+ if match.Method != nil {
+ service = ptr.Deref(match.Method.Service, "")
+ method = ptr.Deref(match.Method.Method, "")
+ matchType := ptr.Deref(match.Method.Type,
gatewayv1.GRPCMethodMatchExact)
+ if matchType == gatewayv1.GRPCMethodMatchExact &&
+ service == "" && method == "" {
+ return nil, fmt.Errorf("service and method cannot both
be empty for exact match type")
+ }
+ }
+
+ uri := t.translateGRPCURI(service, method)
+ route.Uris = append(route.Uris, uri)
+
+ if match.Headers != nil {
+ for _, header := range match.Headers {
+ this, err :=
t.translateGRPCRouteHeaderMatchToVars(header)
+ if err != nil {
+ return nil, err
+ }
+ route.Vars = append(route.Vars, this)
+ }
+ }
+ return route, nil
+}
+
+func (t *Translator) translateGRPCURI(service, method string) string {
+ var uri string
+ if service == "" {
+ uri = "/*"
+ } else {
+ uri = fmt.Sprintf("/%s", service)
+ }
+ if method != "" {
+ uri = uri + fmt.Sprintf("/%s", method)
+ } else if service != "" {
+ uri = uri + "/*"
+ }
+ return uri
+}
+
+func (t *Translator) translateGRPCRouteHeaderMatchToVars(header
gatewayv1.GRPCHeaderMatch) ([]adctypes.StringOrSlice, error) {
+ var matchType string
+ if header.Type != nil {
+ matchType = string(*header.Type)
+ }
+ return HeaderMatchToVars(matchType, string(header.Name), header.Value)
+}
diff --git a/internal/adc/translator/httproute.go
b/internal/adc/translator/httproute.go
index b33de0e4..d88b32cf 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -54,7 +54,7 @@ func (t *Translator) fillPluginsFromHTTPRouteFilters(
case gatewayv1.HTTPRouteFilterRequestRedirect:
t.fillPluginFromHTTPRequestRedirectFilter(plugins,
filter.RequestRedirect)
case gatewayv1.HTTPRouteFilterRequestMirror:
- t.fillPluginFromHTTPRequestMirrorFilter(plugins,
namespace, filter.RequestMirror)
+ t.fillPluginFromHTTPRequestMirrorFilter(plugins,
namespace, filter.RequestMirror, apiv2.SchemeHTTP)
case gatewayv1.HTTPRouteFilterURLRewrite:
t.fillPluginFromURLRewriteFilter(plugins,
filter.URLRewrite, matches)
case gatewayv1.HTTPRouteFilterResponseHeaderModifier:
@@ -232,7 +232,7 @@ func (t *Translator)
fillPluginFromHTTPResponseHeaderFilter(plugins adctypes.Plu
plugin.Headers.Remove = append(plugin.Headers.Remove,
respHeaderModifier.Remove...)
}
-func (t *Translator) fillPluginFromHTTPRequestMirrorFilter(plugins
adctypes.Plugins, namespace string, reqMirror
*gatewayv1.HTTPRequestMirrorFilter) {
+func (t *Translator) fillPluginFromHTTPRequestMirrorFilter(plugins
adctypes.Plugins, namespace string, reqMirror
*gatewayv1.HTTPRequestMirrorFilter, scheme string) {
pluginName := adctypes.PluginProxyMirror
obj := plugins[pluginName]
@@ -255,7 +255,7 @@ func (t *Translator)
fillPluginFromHTTPRequestMirrorFilter(plugins adctypes.Plug
ns = string(*reqMirror.BackendRef.Namespace)
}
- host := fmt.Sprintf("http://%s.%s.svc.cluster.local:%d",
reqMirror.BackendRef.Name, ns, port)
+ host := fmt.Sprintf("%s://%s.%s.svc.cluster.local:%d", scheme,
reqMirror.BackendRef.Name, ns, port)
plugin.Host = host
}
@@ -722,36 +722,10 @@ func (t *Translator) translateGatewayHTTPRouteMatch(match
*gatewayv1.HTTPRouteMa
if len(match.Headers) > 0 {
for _, header := range match.Headers {
- name := strings.ToLower(string(header.Name))
- name = strings.ReplaceAll(name, "-", "_")
-
- var this []adctypes.StringOrSlice
- this = append(this, adctypes.StringOrSlice{
- StrVal: "http_" + name,
- })
-
- matchType := gatewayv1.HeaderMatchExact
- if header.Type != nil {
- matchType = *header.Type
- }
-
- switch matchType {
- case gatewayv1.HeaderMatchExact:
- this = append(this, adctypes.StringOrSlice{
- StrVal: "==",
- })
- case gatewayv1.HeaderMatchRegularExpression:
- this = append(this, adctypes.StringOrSlice{
- StrVal: "~~",
- })
- default:
- return nil, errors.New("unknown header match
type " + string(matchType))
+ this, err :=
t.translateHTTPRouteHeaderMatchToVars(header)
+ if err != nil {
+ return nil, err
}
-
- this = append(this, adctypes.StringOrSlice{
- StrVal: header.Value,
- })
-
route.Vars = append(route.Vars, this)
}
}
@@ -797,3 +771,39 @@ func (t *Translator) translateGatewayHTTPRouteMatch(match
*gatewayv1.HTTPRouteMa
return route, nil
}
+
+func HeaderMatchToVars(matchType, name, value string)
([]adctypes.StringOrSlice, error) {
+ name = strings.ToLower(name)
+ name = strings.ReplaceAll(name, "-", "_")
+
+ var this []adctypes.StringOrSlice
+ this = append(this, adctypes.StringOrSlice{
+ StrVal: "http_" + name,
+ })
+
+ switch matchType {
+ case string(gatewayv1.HeaderMatchExact):
+ this = append(this, adctypes.StringOrSlice{
+ StrVal: "==",
+ })
+ case string(gatewayv1.HeaderMatchRegularExpression):
+ this = append(this, adctypes.StringOrSlice{
+ StrVal: "~~",
+ })
+ default:
+ return nil, errors.New("unknown header match type " + matchType)
+ }
+
+ this = append(this, adctypes.StringOrSlice{
+ StrVal: value,
+ })
+ return this, nil
+}
+
+func (t *Translator) translateHTTPRouteHeaderMatchToVars(header
gatewayv1.HTTPHeaderMatch) ([]adctypes.StringOrSlice, error) {
+ var matchType string
+ if header.Type != nil {
+ matchType = string(*header.Type)
+ }
+ return HeaderMatchToVars(matchType, string(header.Name), header.Value)
+}
diff --git a/internal/controller/context.go b/internal/controller/context.go
index f5d85194..5398f044 100644
--- a/internal/controller/context.go
+++ b/internal/controller/context.go
@@ -26,5 +26,7 @@ type RouteParentRefContext struct {
Gateway *gatewayv1.Gateway
ListenerName string
- Conditions []metav1.Condition
+ Listener *gatewayv1.Listener
+
+ Conditions []metav1.Condition
}
diff --git a/internal/controller/httproute_controller.go
b/internal/controller/grpcroute_controller.go
similarity index 58%
copy from internal/controller/httproute_controller.go
copy to internal/controller/grpcroute_controller.go
index 6fe87881..138011a9 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -22,17 +22,12 @@ import (
"context"
"fmt"
- "github.com/api7/gopkg/pkg/log"
"github.com/go-logr/logr"
- "github.com/pkg/errors"
- "go.uber.org/zap"
- "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
k8stypes "k8s.io/apimachinery/pkg/types"
- "k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -42,7 +37,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
- "sigs.k8s.io/gateway-api/apis/v1alpha2"
"sigs.k8s.io/gateway-api/apis/v1beta1"
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
@@ -54,8 +48,8 @@ import (
"github.com/apache/apisix-ingress-controller/internal/utils"
)
-// HTTPRouteReconciler reconciles a GatewayClass object.
-type HTTPRouteReconciler struct { //nolint:revive
+// GRPCRouteReconciler reconciles a GatewayClass object.
+type GRPCRouteReconciler struct { //nolint:revive
client.Client
Scheme *runtime.Scheme
@@ -70,20 +64,20 @@ type HTTPRouteReconciler struct { //nolint:revive
}
// SetupWithManager sets up the controller with the Manager.
-func (r *HTTPRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
+func (r *GRPCRouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.genericEvent = make(chan event.GenericEvent, 100)
bdr := ctrl.NewControllerManagedBy(mgr).
- For(&gatewayv1.HTTPRoute{}).
+ For(&gatewayv1.GRPCRoute{}).
WithEventFilter(predicate.GenerationChangedPredicate{}).
Watches(&discoveryv1.EndpointSlice{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByServiceBef),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesByServiceRef),
).
Watches(&v1alpha1.PluginConfig{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByExtensionRef),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesByExtensionRef),
).
Watches(&gatewayv1.Gateway{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForGateway),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForGateway),
builder.WithPredicates(
predicate.Funcs{
GenericFunc: func(e event.GenericEvent)
bool {
@@ -102,53 +96,74 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
),
).
Watches(&v1alpha1.BackendTrafficPolicy{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForBackendTrafficPolicy),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForBackendTrafficPolicy),
builder.WithPredicates(
BackendTrafficPolicyPredicateFunc(r.genericEvent),
),
).
- Watches(&v1alpha1.HTTPRoutePolicy{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRouteByHTTPRoutePolicy),
-
builder.WithPredicates(httpRoutePolicyPredicateFuncs(r.genericEvent)),
- ).
Watches(&v1alpha1.GatewayProxy{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForGatewayProxy),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForGatewayProxy),
).
WatchesRawSource(
source.Channel(
r.genericEvent,
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRouteForGenericEvent),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRouteForGenericEvent),
),
)
if GetEnableReferenceGrant() {
bdr.Watches(&v1beta1.ReferenceGrant{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForReferenceGrant),
-
builder.WithPredicates(referenceGrantPredicates(KindHTTPRoute)),
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForReferenceGrant),
+
builder.WithPredicates(referenceGrantPredicates(KindGRPCRoute)),
)
}
return bdr.Complete(r)
}
-func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request)
(ctrl.Result, error) {
- defer r.Readier.Done(&gatewayv1.HTTPRoute{}, req.NamespacedName)
- hr := new(gatewayv1.HTTPRoute)
- if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
+func (r *GRPCRouteReconciler) listGRPCRoutesByExtensionRef(ctx
context.Context, obj client.Object) []reconcile.Request {
+ pluginconfig, ok := obj.(*v1alpha1.PluginConfig)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to PluginConfig")
+ return nil
+ }
+ namespace := pluginconfig.GetNamespace()
+ name := pluginconfig.GetName()
+
+ grList := &gatewayv1.GRPCRouteList{}
+ if err := r.List(ctx, grList, client.MatchingFields{
+ indexer.ExtensionRef: indexer.GenIndexKey(namespace, name),
+ }); err != nil {
+ r.Log.Error(err, "failed to list grpcroutes by extension
reference", "extension", name)
+ return nil
+ }
+ requests := make([]reconcile.Request, 0, len(grList.Items))
+ for _, gr := range grList.Items {
+ requests = append(requests, reconcile.Request{
+ NamespacedName: client.ObjectKey{
+ Namespace: gr.Namespace,
+ Name: gr.Name,
+ },
+ })
+ }
+ return requests
+}
+
+func (r *GRPCRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request)
(ctrl.Result, error) {
+ defer r.Readier.Done(&gatewayv1.GRPCRoute{}, req.NamespacedName)
+ gr := new(gatewayv1.GRPCRoute)
+ if err := r.Get(ctx, req.NamespacedName, gr); err != nil {
if client.IgnoreNotFound(err) == nil {
- if err := r.updateHTTPRoutePolicyStatusOnDeleting(ctx,
req.NamespacedName); err != nil {
- return ctrl.Result{}, err
- }
- hr.Namespace = req.Namespace
- hr.Name = req.Name
+ gr.Namespace = req.Namespace
+ gr.Name = req.Name
- hr.TypeMeta = metav1.TypeMeta{
- Kind: KindHTTPRoute,
+ gr.TypeMeta = metav1.TypeMeta{
+ Kind: KindGRPCRoute,
APIVersion: gatewayv1.GroupVersion.String(),
}
- if err := r.Provider.Delete(ctx, hr); err != nil {
- r.Log.Error(err, "failed to delete httproute",
"httproute", hr)
+ if err := r.Provider.Delete(ctx, gr); err != nil {
+ r.Log.Error(err, "failed to delete grpcroute",
"grpcroute", gr)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
@@ -167,7 +182,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
msg: "Route is accepted",
}
- gateways, err := ParseRouteParentRefs(ctx, r.Client, hr,
hr.Spec.ParentRefs)
+ gateways, err := ParseRouteParentRefs(ctx, r.Client, gr,
gr.Spec.ParentRefs)
if err != nil {
return ctrl.Result{}, err
}
@@ -178,17 +193,20 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
tctx := provider.NewDefaultTranslateContext(ctx)
- tctx.RouteParentRefs = hr.Spec.ParentRefs
- rk := utils.NamespacedNameKind(hr)
+ tctx.RouteParentRefs = gr.Spec.ParentRefs
+ rk := utils.NamespacedNameKind(gr)
for _, gateway := range gateways {
if err := ProcessGatewayProxy(r.Client, r.Log, tctx,
gateway.Gateway, rk); err != nil {
acceptStatus.status = false
acceptStatus.msg = err.Error()
}
+ if gateway.Listener != nil {
+ tctx.Listeners = append(tctx.Listeners,
*gateway.Listener)
+ }
}
var backendRefErr error
- if err := r.processHTTPRoute(tctx, hr); err != nil {
+ if err := r.processGRPCRoute(tctx, gr); err != nil {
// When encountering a backend reference error, it should not
affect the acceptance status
if types.IsSomeReasonError(err,
gatewayv1.RouteReasonInvalidKind) {
backendRefErr = err
@@ -198,61 +216,46 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
}
}
- if err := r.processHTTPRoutePolicies(tctx, hr); err != nil {
- acceptStatus.status = false
- acceptStatus.msg = err.Error()
- }
-
// Store the backend reference error for later use.
// If the backend reference error is because of an invalid kind, use
this error first
- if err := r.processHTTPRouteBackendRefs(tctx, req.NamespacedName); err
!= nil && backendRefErr == nil {
+ if err := r.processGRPCRouteBackendRefs(tctx, req.NamespacedName); err
!= nil && backendRefErr == nil {
backendRefErr = err
}
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
- filteredHTTPRoute, err := filterHostnames(gateways, hr.DeepCopy())
- if err != nil {
- acceptStatus.status = false
- acceptStatus.msg = err.Error()
- }
-
// TODO: diff the old and new status
- hr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0,
len(gateways))
+ gr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0,
len(gateways))
for _, gateway := range gateways {
parentStatus := gatewayv1.RouteParentStatus{}
SetRouteParentRef(&parentStatus, gateway.Gateway.Name,
gateway.Gateway.Namespace)
for _, condition := range gateway.Conditions {
parentStatus.Conditions =
MergeCondition(parentStatus.Conditions, condition)
}
- SetRouteConditionAccepted(&parentStatus, hr.GetGeneration(),
acceptStatus.status, acceptStatus.msg)
- SetRouteConditionResolvedRefs(&parentStatus,
hr.GetGeneration(), backendRefErr)
+ SetRouteConditionAccepted(&parentStatus, gr.GetGeneration(),
acceptStatus.status, acceptStatus.msg)
+ SetRouteConditionResolvedRefs(&parentStatus,
gr.GetGeneration(), backendRefErr)
- hr.Status.Parents = append(hr.Status.Parents, parentStatus)
+ gr.Status.Parents = append(gr.Status.Parents, parentStatus)
}
r.Updater.Update(status.Update{
- NamespacedName: utils.NamespacedName(hr),
- Resource: &gatewayv1.HTTPRoute{},
+ NamespacedName: utils.NamespacedName(gr),
+ Resource: &gatewayv1.GRPCRoute{},
Mutator: status.MutatorFunc(func(obj client.Object)
client.Object {
- h, ok := obj.(*gatewayv1.HTTPRoute)
+ h, ok := obj.(*gatewayv1.GRPCRoute)
if !ok {
err := fmt.Errorf("unsupported object type %T",
obj)
panic(err)
}
hCopy := h.DeepCopy()
- hCopy.Status = hr.Status
+ hCopy.Status = gr.Status
return hCopy
}),
})
UpdateStatus(r.Updater, r.Log, tctx)
if isRouteAccepted(gateways) && err == nil {
- routeToUpdate := hr
- if filteredHTTPRoute != nil {
- log.Debugw("filteredHTTPRoute",
zap.Any("filteredHTTPRoute", filteredHTTPRoute))
- routeToUpdate = filteredHTTPRoute
- }
+ routeToUpdate := gr
if err := r.Provider.Update(ctx, tctx, routeToUpdate); err !=
nil {
return ctrl.Result{}, err
}
@@ -260,7 +263,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
return ctrl.Result{}, nil
}
-func (r *HTTPRouteReconciler) listHTTPRoutesByServiceBef(ctx context.Context,
obj client.Object) []reconcile.Request {
+func (r *GRPCRouteReconciler) listGRPCRoutesByServiceRef(ctx context.Context,
obj client.Object) []reconcile.Request {
endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to EndpointSlice")
@@ -269,61 +272,33 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesByServiceBef(ctx context.Context, ob
namespace := endpointSlice.GetNamespace()
serviceName := endpointSlice.Labels[discoveryv1.LabelServiceName]
- hrList := &gatewayv1.HTTPRouteList{}
- if err := r.List(ctx, hrList, client.MatchingFields{
+ gList := &gatewayv1.GRPCRouteList{}
+ if err := r.List(ctx, gList, client.MatchingFields{
indexer.ServiceIndexRef: indexer.GenIndexKey(namespace,
serviceName),
}); err != nil {
- r.Log.Error(err, "failed to list httproutes by service",
"service", serviceName)
- return nil
- }
- requests := make([]reconcile.Request, 0, len(hrList.Items))
- for _, hr := range hrList.Items {
- requests = append(requests, reconcile.Request{
- NamespacedName: client.ObjectKey{
- Namespace: hr.Namespace,
- Name: hr.Name,
- },
- })
- }
- return requests
-}
-
-func (r *HTTPRouteReconciler) listHTTPRoutesByExtensionRef(ctx
context.Context, obj client.Object) []reconcile.Request {
- pluginconfig, ok := obj.(*v1alpha1.PluginConfig)
- if !ok {
- r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to EndpointSlice")
- return nil
- }
- namespace := pluginconfig.GetNamespace()
- name := pluginconfig.GetName()
-
- hrList := &gatewayv1.HTTPRouteList{}
- if err := r.List(ctx, hrList, client.MatchingFields{
- indexer.ExtensionRef: indexer.GenIndexKey(namespace, name),
- }); err != nil {
- r.Log.Error(err, "failed to list httproutes by extension
reference", "extension", name)
+ r.Log.Error(err, "failed to list grpcroutes by service",
"service", serviceName)
return nil
}
- requests := make([]reconcile.Request, 0, len(hrList.Items))
- for _, hr := range hrList.Items {
+ requests := make([]reconcile.Request, 0, len(gList.Items))
+ for _, gr := range gList.Items {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
- Namespace: hr.Namespace,
- Name: hr.Name,
+ Namespace: gr.Namespace,
+ Name: gr.Name,
},
})
}
return requests
}
-func (r *HTTPRouteReconciler) listHTTPRoutesForBackendTrafficPolicy(ctx
context.Context, obj client.Object) []reconcile.Request {
+func (r *GRPCRouteReconciler) listGRPCRoutesForBackendTrafficPolicy(ctx
context.Context, obj client.Object) []reconcile.Request {
policy, ok := obj.(*v1alpha1.BackendTrafficPolicy)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to BackendTrafficPolicy")
return nil
}
- httprouteList := []gatewayv1.HTTPRoute{}
+ grpcRouteList := []gatewayv1.GRPCRoute{}
for _, targetRef := range policy.Spec.TargetRefs {
service := &corev1.Service{}
if err := r.Get(ctx, client.ObjectKey{
@@ -335,28 +310,28 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesForBackendTrafficPolicy(ctx context.
}
continue
}
- hrList := &gatewayv1.HTTPRouteList{}
- if err := r.List(ctx, hrList, client.MatchingFields{
+ grList := &gatewayv1.GRPCRouteList{}
+ if err := r.List(ctx, grList, client.MatchingFields{
indexer.ServiceIndexRef:
indexer.GenIndexKey(policy.Namespace, string(targetRef.Name)),
}); err != nil {
- r.Log.Error(err, "failed to list httproutes by service
reference", "service", targetRef.Name)
+ r.Log.Error(err, "failed to list grpcroutes by service
reference", "service", targetRef.Name)
return nil
}
- httprouteList = append(httprouteList, hrList.Items...)
+ grpcRouteList = append(grpcRouteList, grList.Items...)
}
var namespacedNameMap = make(map[k8stypes.NamespacedName]struct{})
- requests := make([]reconcile.Request, 0, len(httprouteList))
- for _, hr := range httprouteList {
+ requests := make([]reconcile.Request, 0, len(grpcRouteList))
+ for _, gr := range grpcRouteList {
key := k8stypes.NamespacedName{
- Namespace: hr.Namespace,
- Name: hr.Name,
+ Namespace: gr.Namespace,
+ Name: gr.Name,
}
if _, ok := namespacedNameMap[key]; !ok {
namespacedNameMap[key] = struct{}{}
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
- Namespace: hr.Namespace,
- Name: hr.Name,
+ Namespace: gr.Namespace,
+ Name: gr.Name,
},
})
}
@@ -364,99 +339,52 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesForBackendTrafficPolicy(ctx context.
return requests
}
-func (r *HTTPRouteReconciler) listHTTPRoutesForGateway(ctx context.Context,
obj client.Object) []reconcile.Request {
+func (r *GRPCRouteReconciler) listGRPCRoutesForGateway(ctx context.Context,
obj client.Object) []reconcile.Request {
gateway, ok := obj.(*gatewayv1.Gateway)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to Gateway")
}
- hrList := &gatewayv1.HTTPRouteList{}
- if err := r.List(ctx, hrList, client.MatchingFields{
+ grList := &gatewayv1.GRPCRouteList{}
+ if err := r.List(ctx, grList, client.MatchingFields{
indexer.ParentRefs: indexer.GenIndexKey(gateway.Namespace,
gateway.Name),
}); err != nil {
- r.Log.Error(err, "failed to list httproutes by gateway",
"gateway", gateway.Name)
+ r.Log.Error(err, "failed to list grpcroutes by gateway",
"gateway", gateway.Name)
return nil
}
- requests := make([]reconcile.Request, 0, len(hrList.Items))
- for _, hr := range hrList.Items {
+ requests := make([]reconcile.Request, 0, len(grList.Items))
+ for _, gr := range grList.Items {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
- Namespace: hr.Namespace,
- Name: hr.Name,
+ Namespace: gr.Namespace,
+ Name: gr.Name,
},
})
}
return requests
}
-func (r *HTTPRouteReconciler) listHTTPRouteByHTTPRoutePolicy(ctx
context.Context, obj client.Object) (requests []reconcile.Request) {
- httpRoutePolicy, ok := obj.(*v1alpha1.HTTPRoutePolicy)
- if !ok {
- r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to HTTPRoutePolicy")
- return nil
- }
-
- var keys = make(map[k8stypes.NamespacedName]struct{})
- for _, ref := range httpRoutePolicy.Spec.TargetRefs {
- if ref.Kind != types.KindHTTPRoute {
- continue
- }
- key := k8stypes.NamespacedName{
- Namespace: obj.GetNamespace(),
- Name: string(ref.Name),
- }
- if _, ok := keys[key]; ok {
- continue
- }
-
- var httpRoute gatewayv1.HTTPRoute
- if err := r.Get(ctx, key, &httpRoute); err != nil {
- r.Log.Error(err, "failed to get HTTPRoute by
HTTPRoutePolicy targetRef", "namespace", key.Namespace, "name", key.Name)
- continue
- }
- if ref.SectionName != nil {
- var matchRuleName bool
- for _, rule := range httpRoute.Spec.Rules {
- if rule.Name != nil && *rule.Name ==
*ref.SectionName {
- matchRuleName = true
- break
- }
- }
- if !matchRuleName {
- r.Log.Error(errors.Errorf("failed to get
HTTPRoute rule by HTTPRoutePolicy targetRef"), "namespace", key.Namespace,
"name", key.Name, "sectionName", *ref.SectionName)
- continue
- }
- }
- keys[key] = struct{}{}
- requests = append(requests, reconcile.Request{NamespacedName:
key})
- }
-
- return requests
-}
-
-func (r *HTTPRouteReconciler) listHTTPRouteForGenericEvent(ctx
context.Context, obj client.Object) (requests []reconcile.Request) {
+func (r *GRPCRouteReconciler) listGRPCRouteForGenericEvent(ctx
context.Context, obj client.Object) (requests []reconcile.Request) {
switch obj.(type) {
case *v1alpha1.BackendTrafficPolicy:
- return r.listHTTPRoutesForBackendTrafficPolicy(ctx, obj)
- case *v1alpha1.HTTPRoutePolicy:
- return r.listHTTPRouteByHTTPRoutePolicy(ctx, obj)
+ return r.listGRPCRoutesForBackendTrafficPolicy(ctx, obj)
default:
- r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to BackendTrafficPolicy or HTTPRoutePolicy")
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to BackendTrafficPolicy")
return nil
}
}
-func (r *HTTPRouteReconciler) processHTTPRouteBackendRefs(tctx
*provider.TranslateContext, hrNN k8stypes.NamespacedName) error {
+func (r *GRPCRouteReconciler) processGRPCRouteBackendRefs(tctx
*provider.TranslateContext, grNN k8stypes.NamespacedName) error {
var terr error
for _, backend := range tctx.BackendRefs {
targetNN := k8stypes.NamespacedName{
- Namespace: hrNN.Namespace,
+ Namespace: grNN.Namespace,
Name: string(backend.Name),
}
if backend.Namespace != nil {
targetNN.Namespace = string(*backend.Namespace)
}
- if backend.Kind != nil && *backend.Kind != types.KindService {
+ if backend.Kind != nil && *backend.Kind != "Service" {
terr = types.NewInvalidKindError(*backend.Kind)
continue
}
@@ -478,14 +406,14 @@ func (r *HTTPRouteReconciler)
processHTTPRouteBackendRefs(tctx *provider.Transla
continue
}
- // if cross namespaces between HTTPRoute and referenced
Service, check ReferenceGrant
- if hrNN.Namespace != targetNN.Namespace {
+ // if cross namespaces between GRPCRoute and referenced
Service, check ReferenceGrant
+ if grNN.Namespace != targetNN.Namespace {
if permitted := checkReferenceGrant(tctx,
r.Client,
v1beta1.ReferenceGrantFrom{
Group: gatewayv1.GroupName,
- Kind: KindHTTPRoute,
- Namespace:
v1beta1.Namespace(hrNN.Namespace),
+ Kind: KindGRPCRoute,
+ Namespace:
v1beta1.Namespace(grNN.Namespace),
},
gatewayv1.ObjectReference{
Group: corev1.GroupName,
@@ -496,7 +424,7 @@ func (r *HTTPRouteReconciler)
processHTTPRouteBackendRefs(tctx *provider.Transla
); !permitted {
terr = types.ReasonError{
Reason:
string(v1beta1.RouteReasonRefNotPermitted),
- Message: fmt.Sprintf("%s is in a
different namespace than the HTTPRoute %s and no ReferenceGrant allowing
reference is configured", targetNN, hrNN),
+ Message: fmt.Sprintf("%s is in a
different namespace than the GRPCRoute %s and no ReferenceGrant allowing
reference is configured", targetNN, grNN),
}
continue
}
@@ -537,37 +465,37 @@ func (r *HTTPRouteReconciler)
processHTTPRouteBackendRefs(tctx *provider.Transla
return terr
}
-func (r *HTTPRouteReconciler) processHTTPRoute(tctx
*provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error {
+func (r *GRPCRouteReconciler) processGRPCRoute(tctx
*provider.TranslateContext, grpcroute *gatewayv1.GRPCRoute) error {
var terror error
- for _, rule := range httpRoute.Spec.Rules {
+ for _, rule := range grpcroute.Spec.Rules {
for _, filter := range rule.Filters {
- if filter.Type != gatewayv1.HTTPRouteFilterExtensionRef
|| filter.ExtensionRef == nil {
+ if filter.Type != gatewayv1.GRPCRouteFilterExtensionRef
|| filter.ExtensionRef == nil {
continue
}
- if filter.ExtensionRef.Kind == types.KindPluginConfig {
+ if filter.ExtensionRef.Kind == "PluginConfig" {
pluginconfig := new(v1alpha1.PluginConfig)
if err := r.Get(context.Background(),
client.ObjectKey{
- Namespace: httpRoute.GetNamespace(),
+ Namespace: grpcroute.GetNamespace(),
Name:
string(filter.ExtensionRef.Name),
}, pluginconfig); err != nil {
terror = err
continue
}
tctx.PluginConfigs[k8stypes.NamespacedName{
- Namespace: httpRoute.GetNamespace(),
+ Namespace: grpcroute.GetNamespace(),
Name:
string(filter.ExtensionRef.Name),
}] = pluginconfig
}
}
for _, backend := range rule.BackendRefs {
- if backend.Kind != nil && *backend.Kind !=
types.KindService {
+ if backend.Kind != nil && *backend.Kind != "Service" {
terror =
types.NewInvalidKindError(*backend.Kind)
continue
}
tctx.BackendRefs = append(tctx.BackendRefs,
gatewayv1.BackendRef{
BackendObjectReference:
gatewayv1.BackendObjectReference{
Name: backend.Name,
- Namespace: cmp.Or(backend.Namespace,
(*gatewayv1.Namespace)(&httpRoute.Namespace)),
+ Namespace: cmp.Or(backend.Namespace,
(*gatewayv1.Namespace)(&grpcroute.Namespace)),
Port: backend.Port,
},
})
@@ -577,40 +505,8 @@ func (r *HTTPRouteReconciler) processHTTPRoute(tctx
*provider.TranslateContext,
return terror
}
-func httpRoutePolicyPredicateFuncs(channel chan event.GenericEvent)
predicate.Predicate {
- return predicate.Funcs{
- CreateFunc: func(e event.CreateEvent) bool {
- return true
- },
- DeleteFunc: func(e event.DeleteEvent) bool {
- return true
- },
- UpdateFunc: func(e event.UpdateEvent) bool {
- oldPolicy, ok0 :=
e.ObjectOld.(*v1alpha1.HTTPRoutePolicy)
- newPolicy, ok1 :=
e.ObjectNew.(*v1alpha1.HTTPRoutePolicy)
- if !ok0 || !ok1 {
- return false
- }
- discardsRefs :=
slices.DeleteFunc(oldPolicy.Spec.TargetRefs, func(oldRef
v1alpha2.LocalPolicyTargetReferenceWithSectionName) bool {
- return
slices.ContainsFunc(newPolicy.Spec.TargetRefs, func(newRef
v1alpha2.LocalPolicyTargetReferenceWithSectionName) bool {
- return
oldRef.LocalPolicyTargetReference == newRef.LocalPolicyTargetReference &&
ptr.Equal(oldRef.SectionName, newRef.SectionName)
- })
- })
- if len(discardsRefs) > 0 {
- dump := oldPolicy.DeepCopy()
- dump.Spec.TargetRefs = discardsRefs
- channel <- event.GenericEvent{Object: dump}
- }
- return true
- },
- GenericFunc: func(e event.GenericEvent) bool {
- return false
- },
- }
-}
-
-// listHTTPRoutesForGatewayProxy list all HTTPRoute resources that are
affected by a given GatewayProxy
-func (r *HTTPRouteReconciler) listHTTPRoutesForGatewayProxy(ctx
context.Context, obj client.Object) []reconcile.Request {
+// listGRPCRoutesForGatewayProxy list all GRPCRoute resources that are
affected by a given GatewayProxy
+func (r *GRPCRouteReconciler) listGRPCRoutesForGatewayProxy(ctx
context.Context, obj client.Object) []reconcile.Request {
gatewayProxy, ok := obj.(*v1alpha1.GatewayProxy)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to GatewayProxy")
@@ -631,21 +527,21 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesForGatewayProxy(ctx context.Context,
var requests []reconcile.Request
- // for each gateway, find all HTTPRoute resources that reference it
+ // for each gateway, find all GRPCRoute resources that reference it
for _, gateway := range gatewayList.Items {
- httpRouteList := &gatewayv1.HTTPRouteList{}
- if err := r.List(ctx, httpRouteList, client.MatchingFields{
+ grpcRouteList := &gatewayv1.GRPCRouteList{}
+ if err := r.List(ctx, grpcRouteList, client.MatchingFields{
indexer.ParentRefs:
indexer.GenIndexKey(gateway.Namespace, gateway.Name),
}); err != nil {
- r.Log.Error(err, "failed to list httproutes for
gateway", "gateway", gateway.Name)
+ r.Log.Error(err, "failed to list grpcroutes for
gateway", "gateway", gateway.Name)
continue
}
- for _, httpRoute := range httpRouteList.Items {
+ for _, grpcRoute := range grpcRouteList.Items {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
- Namespace: httpRoute.Namespace,
- Name: httpRoute.Name,
+ Namespace: grpcRoute.Namespace,
+ Name: grpcRoute.Name,
},
})
}
@@ -654,31 +550,31 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesForGatewayProxy(ctx context.Context,
return requests
}
-func (r *HTTPRouteReconciler) listHTTPRoutesForReferenceGrant(ctx
context.Context, obj client.Object) (requests []reconcile.Request) {
+func (r *GRPCRouteReconciler) listGRPCRoutesForReferenceGrant(ctx
context.Context, obj client.Object) (requests []reconcile.Request) {
grant, ok := obj.(*v1beta1.ReferenceGrant)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to ReferenceGrant")
return nil
}
- var httpRouteList gatewayv1.HTTPRouteList
- if err := r.List(ctx, &httpRouteList); err != nil {
- r.Log.Error(err, "failed to list httproutes for reference
ReferenceGrant", "ReferenceGrant", k8stypes.NamespacedName{Namespace:
obj.GetNamespace(), Name: obj.GetName()})
+ var grpcRouteList gatewayv1.GRPCRouteList
+ if err := r.List(ctx, &grpcRouteList); err != nil {
+ r.Log.Error(err, "failed to list grpcroutes for reference
ReferenceGrant", "ReferenceGrant", k8stypes.NamespacedName{Namespace:
obj.GetNamespace(), Name: obj.GetName()})
return nil
}
- for _, httpRoute := range httpRouteList.Items {
- hr := v1beta1.ReferenceGrantFrom{
+ for _, grpcRoute := range grpcRouteList.Items {
+ gr := v1beta1.ReferenceGrantFrom{
Group: gatewayv1.GroupName,
- Kind: KindHTTPRoute,
- Namespace: v1beta1.Namespace(httpRoute.GetNamespace()),
+ Kind: KindGRPCRoute,
+ Namespace: v1beta1.Namespace(grpcRoute.GetNamespace()),
}
for _, from := range grant.Spec.From {
- if from == hr {
+ if from == gr {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKey{
- Namespace:
httpRoute.GetNamespace(),
- Name: httpRoute.GetName(),
+ Namespace:
grpcRoute.GetNamespace(),
+ Name: grpcRoute.GetName(),
},
})
}
diff --git a/internal/controller/httproute_controller.go
b/internal/controller/httproute_controller.go
index 6fe87881..8cd7b507 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -77,7 +77,7 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
For(&gatewayv1.HTTPRoute{}).
WithEventFilter(predicate.GenerationChangedPredicate{}).
Watches(&discoveryv1.EndpointSlice{},
-
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByServiceBef),
+
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByServiceRef),
).
Watches(&v1alpha1.PluginConfig{},
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByExtensionRef),
@@ -260,7 +260,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx
context.Context, req ctrl.Request) (
return ctrl.Result{}, nil
}
-func (r *HTTPRouteReconciler) listHTTPRoutesByServiceBef(ctx context.Context,
obj client.Object) []reconcile.Request {
+func (r *HTTPRouteReconciler) listHTTPRoutesByServiceRef(ctx context.Context,
obj client.Object) []reconcile.Request {
endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
if !ok {
r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to EndpointSlice")
@@ -291,7 +291,7 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesByServiceBef(ctx context.Context, ob
func (r *HTTPRouteReconciler) listHTTPRoutesByExtensionRef(ctx
context.Context, obj client.Object) []reconcile.Request {
pluginconfig, ok := obj.(*v1alpha1.PluginConfig)
if !ok {
- r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to EndpointSlice")
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to PluginConfig")
return nil
}
namespace := pluginconfig.GetNamespace()
diff --git a/internal/controller/indexer/grpcroute.go
b/internal/controller/indexer/grpcroute.go
new file mode 100644
index 00000000..656acf68
--- /dev/null
+++ b/internal/controller/indexer/grpcroute.go
@@ -0,0 +1,106 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package indexer
+
+import (
+ "context"
+
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func setupGRPCRouteIndexer(mgr ctrl.Manager) error {
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &gatewayv1.GRPCRoute{},
+ ParentRefs,
+ GRPCRouteParentRefsIndexFunc,
+ ); err != nil {
+ return err
+ }
+
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &gatewayv1.GRPCRoute{},
+ ExtensionRef,
+ GRPCRouteExtensionIndexFunc,
+ ); err != nil {
+ return err
+ }
+
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &gatewayv1.GRPCRoute{},
+ ServiceIndexRef,
+ GRPCRouteServiceIndexFunc,
+ ); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GRPCRouteParentRefsIndexFunc(rawObj client.Object) []string {
+ gr := rawObj.(*gatewayv1.GRPCRoute)
+ keys := make([]string, 0, len(gr.Spec.ParentRefs))
+ for _, ref := range gr.Spec.ParentRefs {
+ ns := gr.GetNamespace()
+ if ref.Namespace != nil {
+ ns = string(*ref.Namespace)
+ }
+ keys = append(keys, GenIndexKey(ns, string(ref.Name)))
+ }
+ return keys
+}
+
+func GRPCRouteServiceIndexFunc(rawObj client.Object) []string {
+ gr := rawObj.(*gatewayv1.GRPCRoute)
+ keys := make([]string, 0, len(gr.Spec.Rules))
+ for _, rule := range gr.Spec.Rules {
+ for _, backend := range rule.BackendRefs {
+ namespace := gr.GetNamespace()
+ if backend.Kind != nil && *backend.Kind !=
internaltypes.KindService {
+ continue
+ }
+ if backend.Namespace != nil {
+ namespace = string(*backend.Namespace)
+ }
+ keys = append(keys, GenIndexKey(namespace,
string(backend.Name)))
+ }
+ }
+ return keys
+}
+
+func GRPCRouteExtensionIndexFunc(rawObj client.Object) []string {
+ gr := rawObj.(*gatewayv1.GRPCRoute)
+ keys := make([]string, 0, len(gr.Spec.Rules))
+ for _, rule := range gr.Spec.Rules {
+ for _, filter := range rule.Filters {
+ if filter.Type != gatewayv1.GRPCRouteFilterExtensionRef
|| filter.ExtensionRef == nil {
+ continue
+ }
+ if filter.ExtensionRef.Kind ==
internaltypes.KindPluginConfig {
+ keys = append(keys,
GenIndexKey(gr.GetNamespace(), string(filter.ExtensionRef.Name)))
+ }
+ }
+ }
+ return keys
+}
diff --git a/internal/controller/indexer/indexer.go
b/internal/controller/indexer/indexer.go
index e89a3934..a63cef6f 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -55,6 +55,7 @@ func SetupIndexer(mgr ctrl.Manager) error {
for _, setup := range []func(ctrl.Manager) error{
setupGatewayIndexer,
setupHTTPRouteIndexer,
+ setupGRPCRouteIndexer,
setupIngressIndexer,
setupConsumerIndexer,
setupBackendTrafficPolicyIndexer,
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index 3d0c5018..3b2e7d5b 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -60,6 +60,7 @@ import (
const (
KindGateway = "Gateway"
KindHTTPRoute = "HTTPRoute"
+ KindGRPCRoute = "GRPCRoute"
KindGatewayClass = "GatewayClass"
KindIngress = "Ingress"
KindIngressClass = "IngressClass"
@@ -356,6 +357,7 @@ func ParseRouteParentRefs(
matched := false
reason := gatewayv1.RouteReasonNoMatchingParent
var listenerName string
+ var matchedListener gatewayv1.Listener
for _, listener := range gateway.Spec.Listeners {
if parentRef.SectionName != nil {
@@ -397,6 +399,7 @@ func ParseRouteParentRefs(
// TODO: check if the listener status is programmed
matched = true
+ matchedListener = listener
break
}
@@ -404,6 +407,7 @@ func ParseRouteParentRefs(
gateways = append(gateways, RouteParentRefContext{
Gateway: &gateway,
ListenerName: listenerName,
+ Listener: &matchedListener,
Conditions: []metav1.Condition{{
Type:
string(gatewayv1.RouteConditionAccepted),
Status:
metav1.ConditionTrue,
@@ -415,6 +419,7 @@ func ParseRouteParentRefs(
gateways = append(gateways, RouteParentRefContext{
Gateway: &gateway,
ListenerName: listenerName,
+ Listener: &matchedListener,
Conditions: []metav1.Condition{{
Type:
string(gatewayv1.RouteConditionAccepted),
Status:
metav1.ConditionFalse,
@@ -488,6 +493,8 @@ func routeHostnamesIntersectsWithListenerHostname(route
client.Object, listener
switch r := route.(type) {
case *gatewayv1.HTTPRoute:
return listenerHostnameIntersectWithRouteHostnames(listener,
r.Spec.Hostnames)
+ case *gatewayv1.GRPCRoute:
+ return listenerHostnameIntersectWithRouteHostnames(listener,
r.Spec.Hostnames)
default:
return false
}
@@ -638,7 +645,7 @@ func isRouteNamespaceAllowed(
func routeMatchesListenerType(route client.Object, listener
gatewayv1.Listener) bool {
switch route.(type) {
- case *gatewayv1.HTTPRoute:
+ case *gatewayv1.HTTPRoute, *gatewayv1.GRPCRoute:
if listener.Protocol != gatewayv1.HTTPProtocolType &&
listener.Protocol != gatewayv1.HTTPSProtocolType {
return false
}
diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go
index 79886225..46b896f5 100644
--- a/internal/manager/controllers.go
+++ b/internal/manager/controllers.go
@@ -77,15 +77,17 @@ import (
// GatewayAPI
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses,verbs=get;list;watch;update
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses/status,verbs=get;update
-//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch;update
+//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways/status,verbs=get;update
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=get;list;watch
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes/status,verbs=get;update
-//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=referencegrants,verbs=list;watch;update
+//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=referencegrants,verbs=get;list;watch
//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=referencegrants/status,verbs=get;update
+//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes,verbs=get;list;watch
+//
+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=grpcroutes/status,verbs=get;update
// Networking
-//
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;update
+//
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch
//
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;update
//
+kubebuilder:rbac:groups=networking.k8s.io,resources=ingressclasses,verbs=get;list;watch
@@ -119,6 +121,14 @@ func setupControllers(ctx context.Context, mgr
manager.Manager, pro provider.Pro
Updater: updater,
Readier: readier,
},
+ &controller.GRPCRouteReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Log:
ctrl.LoggerFrom(ctx).WithName("controllers").WithName(types.KindGRPCRoute),
+ Provider: pro,
+ Updater: updater,
+ Readier: readier,
+ },
&controller.IngressReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
@@ -200,6 +210,7 @@ func registerReadinessGVK(c client.Client, readier
readiness.ReadinessManager) {
{
GVKs: []schema.GroupVersionKind{
types.GvkOf(&gatewayv1.HTTPRoute{}),
+ types.GvkOf(&gatewayv1.GRPCRoute{}),
},
},
{
diff --git a/internal/provider/apisix/provider.go
b/internal/provider/apisix/provider.go
index 01f5f4e3..f37fbe9e 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -108,6 +108,9 @@ func (d *apisixProvider) Update(ctx context.Context, tctx
*provider.TranslateCon
case *gatewayv1.HTTPRoute:
result, err = d.translator.TranslateHTTPRoute(tctx,
t.DeepCopy())
resourceTypes = append(resourceTypes, adctypes.TypeService)
+ case *gatewayv1.GRPCRoute:
+ result, err = d.translator.TranslateGRPCRoute(tctx,
t.DeepCopy())
+ resourceTypes = append(resourceTypes, adctypes.TypeService)
case *gatewayv1.Gateway:
result, err = d.translator.TranslateGateway(tctx, t.DeepCopy())
resourceTypes = append(resourceTypes, adctypes.TypeGlobalRule,
adctypes.TypeSSL, adctypes.TypePluginMetadata)
@@ -178,7 +181,7 @@ func (d *apisixProvider) Delete(ctx context.Context, obj
client.Object) error {
var resourceTypes []string
var labels map[string]string
switch obj.(type) {
- case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute:
+ case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute, *gatewayv1.GRPCRoute:
resourceTypes = append(resourceTypes, adctypes.TypeService)
labels = label.GenLabel(obj)
case *gatewayv1.Gateway:
diff --git a/internal/provider/apisix/status.go
b/internal/provider/apisix/status.go
index a254b815..01768f9e 100644
--- a/internal/provider/apisix/status.go
+++ b/internal/provider/apisix/status.go
@@ -144,6 +144,41 @@ func (d *apisixProvider) updateStatus(nnk
types.NamespacedNameKind, condition me
return cp
}),
})
+ case types.KindGRPCRoute:
+ parentRefs :=
d.client.ConfigManager.GetConfigRefsByResourceKey(nnk)
+ log.Debugw("updating GRPCRoute status", zap.Any("parentRefs",
parentRefs))
+ gatewayRefs := map[types.NamespacedNameKind]struct{}{}
+ for _, parentRef := range parentRefs {
+ if parentRef.Kind == types.KindGateway {
+ gatewayRefs[parentRef] = struct{}{}
+ }
+ }
+ d.updater.Update(status.Update{
+ NamespacedName: nnk.NamespacedName(),
+ Resource: &gatewayv1.GRPCRoute{},
+ Mutator: status.MutatorFunc(func(obj client.Object)
client.Object {
+ cp := obj.(*gatewayv1.GRPCRoute).DeepCopy()
+ gatewayNs := cp.GetNamespace()
+ for i, ref := range cp.Status.Parents {
+ ns := gatewayNs
+ if ref.ParentRef.Namespace != nil {
+ ns =
string(*ref.ParentRef.Namespace)
+ }
+ if ref.ParentRef.Kind == nil ||
*ref.ParentRef.Kind == types.KindGateway {
+ nnk := types.NamespacedNameKind{
+ Name:
string(ref.ParentRef.Name),
+ Namespace: ns,
+ Kind:
types.KindGateway,
+ }
+ if _, ok := gatewayRefs[nnk];
ok {
+ ref.Conditions =
cutils.MergeCondition(ref.Conditions, condition)
+ cp.Status.Parents[i] =
ref
+ }
+ }
+ }
+ return cp
+ }),
+ })
}
}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 3f0bcded..92ada510 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -46,6 +46,7 @@ type TranslateContext struct {
BackendRefs []gatewayv1.BackendRef
GatewayTLSConfig []gatewayv1.GatewayTLSConfig
Credentials []v1alpha1.Credential
+ Listeners []gatewayv1.Listener
EndpointSlices
map[k8stypes.NamespacedName][]discoveryv1.EndpointSlice
Secrets map[k8stypes.NamespacedName]*corev1.Secret
diff --git a/internal/types/k8s.go b/internal/types/k8s.go
index 3f50033a..92cafdc7 100644
--- a/internal/types/k8s.go
+++ b/internal/types/k8s.go
@@ -22,6 +22,7 @@ import (
netv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+ gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
"github.com/apache/apisix-ingress-controller/api/v1alpha1"
v2 "github.com/apache/apisix-ingress-controller/api/v2"
@@ -32,6 +33,7 @@ const DefaultIngressClassAnnotation =
"ingressclass.kubernetes.io/is-default-cla
const (
KindGateway = "Gateway"
KindHTTPRoute = "HTTPRoute"
+ KindGRPCRoute = "GRPCRoute"
KindGatewayClass = "GatewayClass"
KindIngress = "Ingress"
KindIngressClass = "IngressClass"
@@ -57,6 +59,8 @@ func KindOf(obj any) string {
return KindGateway
case *gatewayv1.HTTPRoute:
return KindHTTPRoute
+ case *gatewayv1.GRPCRoute:
+ return KindGRPCRoute
case *gatewayv1.GatewayClass:
return KindGatewayClass
case *netv1.Ingress:
@@ -97,8 +101,10 @@ func KindOf(obj any) string {
func GvkOf(obj any) schema.GroupVersionKind {
kind := KindOf(obj)
switch obj.(type) {
- case *gatewayv1.Gateway, *gatewayv1.HTTPRoute, *gatewayv1.GatewayClass:
+ case *gatewayv1.Gateway, *gatewayv1.HTTPRoute, *gatewayv1.GatewayClass,
*gatewayv1.GRPCRoute:
return gatewayv1.SchemeGroupVersion.WithKind(kind)
+ case *gatewayv1beta1.ReferenceGrant:
+ return gatewayv1beta1.SchemeGroupVersion.WithKind(kind)
case *netv1.Ingress, *netv1.IngressClass:
return netv1.SchemeGroupVersion.WithKind(kind)
case *corev1.Secret, *corev1.Service:
diff --git a/test/e2e/framework/grpc.go b/test/e2e/framework/grpc.go
new file mode 100644
index 00000000..a064a075
--- /dev/null
+++ b/test/e2e/framework/grpc.go
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package framework
+
+import (
+ "bytes"
+ _ "embed"
+ "text/template"
+ "time"
+
+ "github.com/gruntwork-io/terratest/modules/k8s"
+ "github.com/stretchr/testify/assert"
+)
+
+var (
+ //go:embed manifests/grpc-backend.yaml
+ _grpcBackendDeployment string
+ grpcBackendTpl *template.Template
+)
+
+type GRPCBackendOpts struct {
+ KubectlOptions *k8s.KubectlOptions
+}
+
+func init() {
+ tpl, err := template.New("grpc-backend").Parse(_grpcBackendDeployment)
+ if err != nil {
+ panic(err)
+ }
+ grpcBackendTpl = tpl
+}
+
+func (f *Framework) DeployGRPCBackend(opts GRPCBackendOpts) {
+ if opts.KubectlOptions == nil {
+ opts.KubectlOptions = f.kubectlOpts
+ }
+ buf := bytes.NewBuffer(nil)
+
+ err := grpcBackendTpl.Execute(buf, opts)
+ assert.Nil(f.GinkgoT, err, "rendering grpc backend spec")
+
+ k8s.KubectlApplyFromString(f.GinkgoT, opts.KubectlOptions, buf.String())
+
+ k8s.WaitUntilDeploymentAvailable(f.GinkgoT, opts.KubectlOptions,
"grpc-infra-backend-v1", 10, 10*time.Second)
+}
diff --git a/test/e2e/framework/manifests/grpc-backend.yaml
b/test/e2e/framework/manifests/grpc-backend.yaml
new file mode 100644
index 00000000..953c75b0
--- /dev/null
+++ b/test/e2e/framework/manifests/grpc-backend.yaml
@@ -0,0 +1,63 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+apiVersion: v1
+kind: Service
+metadata:
+ name: grpc-infra-backend-v1
+spec:
+ selector:
+ app: grpc-infra-backend-v1
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+ appProtocol: kubernetes.io/h2c
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: grpc-infra-backend-v1
+ labels:
+ app: grpc-infra-backend-v1
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: grpc-infra-backend-v1
+ template:
+ metadata:
+ labels:
+ app: grpc-infra-backend-v1
+ spec:
+ containers:
+ - name: grpc-infra-backend-v1
+ image:
gcr.io/k8s-staging-gateway-api/echo-basic:v20240412-v1.0.0-394-g40c666fd
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: GRPC_ECHO_SERVER
+ value: "1"
+ resources:
+ requests:
+ cpu: 10m
diff --git a/test/e2e/framework/manifests/ingress.yaml
b/test/e2e/framework/manifests/ingress.yaml
index 2347dae2..255a5d42 100644
--- a/test/e2e/framework/manifests/ingress.yaml
+++ b/test/e2e/framework/manifests/ingress.yaml
@@ -81,43 +81,9 @@ rules:
- ""
resources:
- namespaces
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - ""
- resources:
+ - pods
- secrets
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - ""
- resources:
- services
- - pods
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - coordination.k8s.io
- resources:
- - leases
- verbs:
- - create
- - delete
- - get
- - list
- - patch
- - update
- - watch
-- apiGroups:
- - discovery.k8s.io
- resources:
- - endpointslices
verbs:
- get
- list
@@ -156,82 +122,64 @@ rules:
- get
- update
- apiGroups:
- - gateway.networking.k8s.io
+ - coordination.k8s.io
resources:
- - gatewayclasses
+ - leases
verbs:
+ - create
+ - delete
- get
- list
+ - patch
- update
- watch
- apiGroups:
- - gateway.networking.k8s.io
- resources:
- - gatewayclasses/status
- verbs:
- - get
- - update
-- apiGroups:
- - gateway.networking.k8s.io
+ - discovery.k8s.io
resources:
- - gateways
+ - endpointslices
verbs:
- get
- list
- - update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
- - gateways/status
- verbs:
- - get
- - update
-- apiGroups:
- - gateway.networking.k8s.io
- resources:
- - httproutes
+ - gatewayclasses
verbs:
- get
- list
+ - update
- watch
- apiGroups:
- gateway.networking.k8s.io
resources:
+ - gatewayclasses/status
+ - gateways/status
+ - grpcroutes/status
- httproutes/status
+ - referencegrants/status
verbs:
- get
- update
- apiGroups:
- gateway.networking.k8s.io
resources:
+ - gateways
+ - grpcroutes
+ - httproutes
- referencegrants
verbs:
- get
- list
- watch
-- apiGroups:
- - gateway.networking.k8s.io
- resources:
- - referencegrants/status
- verbs:
- - get
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
- verbs:
- - get
- - list
- - watch
-- apiGroups:
- - networking.k8s.io
- resources:
- ingresses
verbs:
- get
- list
- - update
- watch
- apiGroups:
- networking.k8s.io
diff --git a/test/e2e/gatewayapi/grpcroute.go b/test/e2e/gatewayapi/grpcroute.go
new file mode 100644
index 00000000..e4485fe0
--- /dev/null
+++ b/test/e2e/gatewayapi/grpcroute.go
@@ -0,0 +1,289 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package gatewayapi
+
+import (
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "google.golang.org/grpc/metadata"
+ pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver"
+
+ "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test GRPCRoute", Label("networking.k8s.io", "grpcroute"),
func() {
+ s := scaffold.NewDefaultScaffold()
+
+ BeforeEach(func() {
+ By("deploy grpc backend")
+ s.DeployGRPCBackend()
+
+ By("create GatewayProxy")
+
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(),
"creating GatewayProxy")
+
+ By("create GatewayClass")
+
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(),
"creating GatewayClass")
+
+ s.RetryAssertion(func() string {
+ gcyaml, _ := s.GetResourceYaml("GatewayClass",
s.Namespace())
+ return gcyaml
+ }).Should(
+ And(
+ ContainSubstring(`status: "True"`),
+ ContainSubstring("message: the gatewayclass has
been accepted by the apisix-ingress-controller"),
+ ),
+ "check GatewayClass condition",
+ )
+
+ By("create Gateway")
+
Expect(s.CreateResourceFromString(s.GetGatewayYaml())).NotTo(HaveOccurred(),
"creating Gateway")
+
+ s.RetryAssertion(func() string {
+ gcyaml, _ := s.GetResourceYaml("Gateway", s.Namespace())
+ return gcyaml
+ }).Should(
+ And(
+ ContainSubstring(`status: "True"`),
+ ContainSubstring("message: the gateway has been
accepted by the apisix-ingress-controlle"),
+ ),
+ "check Gateway condition status",
+ )
+ })
+
+ Context("GRPCRoute Filters", func() {
+ var reqHeaderModifyWithAdd = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+ name: req-header-modify
+spec:
+ parentRefs:
+ - name: %s
+ rules:
+ - matches:
+ filters:
+ - type: RequestHeaderModifier
+ requestHeaderModifier:
+ add:
+ - name: X-Req-Add
+ value: "plugin-req-add"
+ set:
+ - name: X-Req-Set
+ value: "plugin-req-set"
+ remove:
+ - X-Req-Removed
+ backendRefs:
+ - name: grpc-infra-backend-v1
+ port: 8080
+`
+ var respHeaderModifyWithAdd = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+ name: resp-header-modify
+spec:
+ parentRefs:
+ - name: %s
+ rules:
+ - matches:
+ filters:
+ - type: ResponseHeaderModifier
+ responseHeaderModifier:
+ add:
+ - name: X-Resp-Add
+ value: "plugin-resp-add"
+ backendRefs:
+ - name: grpc-infra-backend-v1
+ port: 8080
+`
+ It("GRPCRoute RequestHeaderModifier", func() {
+ By("create GRPCRoute")
+ s.ResourceApplied("GRPCRoute", "req-header-modify",
fmt.Sprintf(reqHeaderModifyWithAdd, s.Namespace()), 1)
+
+ testCases := []scaffold.ExpectedResponse{
+ {
+ EchoRequest: &pb.EchoRequest{},
+ },
+ {
+ EchoRequest: &pb.EchoRequest{},
+ Headers: map[string]string{
+ "X-Req-Add": "plugin-req-add",
+ },
+ },
+ {
+ EchoRequest: &pb.EchoRequest{},
+ RequestMetadata:
&scaffold.RequestMetadata{
+ Metadata: map[string]string{
+ "X-Req-Set": "test-set",
+ },
+ },
+ Headers: map[string]string{
+ "X-Req-Set": "plugin-req-set",
+ },
+ },
+ {
+ EchoRequest: &pb.EchoRequest{},
+ RequestMetadata:
&scaffold.RequestMetadata{
+ Metadata: map[string]string{
+ "X-Req-Removed":
"to-be-removed",
+ },
+ },
+ Headers: map[string]string{
+ "X-Req-Removed": "",
+ },
+ },
+ }
+
+ for i := range testCases {
+ tc := testCases[i]
+ s.RetryAssertion(func() error {
+ return s.RequestEchoBackend(tc)
+ }).ShouldNot(HaveOccurred(), "request grpc
backend")
+ }
+ })
+
+ It("GRPCRoute ResponseHeaderModifier", func() {
+ By("create GRPCRoute")
+ s.ResourceApplied("GRPCRoute", "resp-header-modify",
fmt.Sprintf(respHeaderModifyWithAdd, s.Namespace()), 1)
+
+ testCases := []scaffold.ExpectedResponse{
+ {
+ EchoRequest: &pb.EchoRequest{},
+ },
+ {
+ EchoRequest: &pb.EchoRequest{},
+ EchoResponse: scaffold.EchoResponse{
+ Headers: &metadata.MD{
+ "X-Resp-Add":
[]string{"plugin-resp-add"},
+ },
+ },
+ },
+ }
+
+ for i := range testCases {
+ tc := testCases[i]
+ s.RetryAssertion(func() error {
+ return s.RequestEchoBackend(tc)
+ }).ShouldNot(HaveOccurred(), "request grpc
backend")
+ }
+ })
+
+ It("GRPCRoute ExtensionRef", func() {
+ var rewritePlugin = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: PluginConfig
+metadata:
+ name: rewrite
+spec:
+ plugins:
+ - name: proxy-rewrite
+ config:
+ headers:
+ add:
+ x-req-add: "plugin-req-add"
+`
+ var rewritePluginUpdate = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: PluginConfig
+metadata:
+ name: rewrite
+spec:
+ plugins:
+ - name: proxy-rewrite
+ config:
+ headers:
+ add:
+ x-req-add: "plugin-req-add-v2"
+`
+ var extensionRefRewritePlugin = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: GRPCRoute
+metadata:
+ name: rewrite
+spec:
+ parentRefs:
+ - name: %s
+ rules:
+ - matches:
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: apisix.apache.org
+ kind: PluginConfig
+ name: rewrite
+ backendRefs:
+ - name: grpc-infra-backend-v1
+ port: 8080
+`
+
Expect(s.CreateResourceFromString(rewritePlugin)).NotTo(HaveOccurred(),
"creating PluginConfig")
+ s.ResourceApplied("GRPCRoute", "rewrite",
fmt.Sprintf(extensionRefRewritePlugin, s.Namespace()), 1)
+
+ testCases := []struct {
+ scaffold.ExpectedResponse
+ Helper func()
+ }{
+ {
+ ExpectedResponse:
scaffold.ExpectedResponse{
+ EchoRequest: &pb.EchoRequest{},
+ },
+ },
+ {
+ ExpectedResponse:
scaffold.ExpectedResponse{
+ EchoRequest: &pb.EchoRequest{},
+ Headers: map[string]string{
+ "x-req-add":
"plugin-req-add",
+ },
+ },
+ },
+ {
+ ExpectedResponse:
scaffold.ExpectedResponse{
+ EchoRequest: &pb.EchoRequest{},
+ Headers: map[string]string{
+ "x-req-add":
"plugin-req-add-v2",
+ },
+ },
+ Helper: func() {
+
Expect(s.CreateResourceFromString(rewritePluginUpdate)).NotTo(HaveOccurred(),
"updating PluginConfig")
+ },
+ },
+ }
+
+ for i := range testCases {
+ if testCases[i].Helper != nil {
+ testCases[i].Helper()
+ }
+ tc := testCases[i].ExpectedResponse
+ s.RetryAssertion(func() error {
+ return s.RequestEchoBackend(tc)
+ }).ShouldNot(HaveOccurred(), "request grpc
backend")
+ }
+ })
+
+ // TODO: add GRPCRoute RequestMirror test
+ /*
+ It("GRPCRoute RequestMirror", func() {})
+ */
+ })
+
+ // TODO: add BackendTrafficPolicy test
+ /*
+ Context("GRPCRoute With BackendTrafficPolicy", func() {})
+ */
+})
diff --git a/test/e2e/scaffold/grpc.go b/test/e2e/scaffold/grpc.go
new file mode 100644
index 00000000..00a694da
--- /dev/null
+++ b/test/e2e/scaffold/grpc.go
@@ -0,0 +1,159 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package scaffold
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/apache/apisix-ingress-controller/test/e2e/framework"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+ "google.golang.org/grpc/status"
+ pb "sigs.k8s.io/gateway-api/conformance/echo-basic/grpcechoserver"
+)
+
+type RequestMetadata struct {
+ // The :authority pseudoheader to set on the outgoing request.
+ Authority string
+
+ // Outgoing metadata pairs to add to the request.
+ Metadata map[string]string
+}
+
+type ExpectedResponse struct {
+ EchoRequest *pb.EchoRequest
+ EchoTwoRequest *pb.EchoRequest
+ EchoThreeRequest *pb.EchoRequest
+
+ RequestMetadata *RequestMetadata
+
+ Headers map[string]string
+
+ EchoResponse EchoResponse
+}
+
+type EchoResponse struct {
+ Code codes.Code
+ Headers *metadata.MD
+ Trailers *metadata.MD
+ Response *pb.EchoResponse
+}
+
+func (s *Scaffold) DeployGRPCBackend() {
+ s.Framework.DeployGRPCBackend(framework.GRPCBackendOpts{
+ KubectlOptions: s.kubectlOptions,
+ })
+}
+
+func (s *Scaffold) RequestEchoBackend(exp ExpectedResponse) error {
+ endpoint := s.apisixTunnels.HTTP.Endpoint()
+
+ endpoint = strings.Replace(endpoint, "localhost", "127.0.0.1", 1)
+
+ dialOpts :=
[]grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
+ if exp.RequestMetadata != nil && exp.RequestMetadata.Authority != "" {
+ dialOpts = append(dialOpts,
grpc.WithAuthority(exp.RequestMetadata.Authority))
+ }
+ conn, err := grpc.NewClient(endpoint, dialOpts...)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = conn.Close() }()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+
+ if exp.RequestMetadata != nil && len(exp.RequestMetadata.Metadata) > 0 {
+ ctx = metadata.NewOutgoingContext(ctx,
metadata.New(exp.RequestMetadata.Metadata))
+ }
+
+ var (
+ resp = &EchoResponse{
+ Headers: &metadata.MD{},
+ Trailers: &metadata.MD{},
+ }
+ )
+
+ client := pb.NewGrpcEchoClient(conn)
+ switch {
+ case exp.EchoRequest != nil:
+ resp.Response, err = client.Echo(ctx, exp.EchoRequest,
grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers))
+ case exp.EchoTwoRequest != nil:
+ resp.Response, err = client.EchoTwo(ctx, exp.EchoTwoRequest,
grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers))
+ case exp.EchoThreeRequest != nil:
+ resp.Response, err = client.EchoThree(ctx,
exp.EchoThreeRequest, grpc.Header(resp.Headers), grpc.Trailer(resp.Trailers))
+ }
+ if err != nil {
+ resp.Code = status.Code(err)
+ fmt.Printf("RPC finished with error: %v\n", err)
+ } else {
+ resp.Code = codes.OK
+ }
+ if err := expectEchoResponses(&exp, resp); err != nil {
+ return err
+ }
+ return nil
+}
+
+func expectEchoResponses(expected *ExpectedResponse, actual *EchoResponse)
error {
+ if expected.EchoResponse.Code != actual.Code {
+ return fmt.Errorf("expected status code to be %s (%d), but got
%s (%d)",
+ expected.EchoResponse.Code.String(),
+ expected.EchoResponse.Code,
+ actual.Code.String(),
+ actual.Code,
+ )
+ }
+ if expected.EchoResponse.Headers != nil {
+ for key, values := range *expected.EchoResponse.Headers {
+ actualValues := actual.Headers.Get(key)
+ if len(values) != len(actualValues) {
+ return fmt.Errorf("expected header %q to have
%d values, but got %d", key, len(values), len(actualValues))
+ }
+ for i, v := range values {
+ if actualValues[i] != v {
+ return fmt.Errorf("expected header %q
to have value %q, but got %q", key, v, actualValues[i])
+ }
+ }
+ }
+ }
+ if len(expected.Headers) > 0 {
+ msgHeaders := actual.Response.GetAssertions().GetHeaders()
+
+ kv := make(map[string]string)
+ for _, header := range msgHeaders {
+ kv[header.GetKey()] = header.GetValue()
+ }
+ for key, value := range expected.Headers {
+ actualValue, ok := kv[strings.ToLower(key)]
+ if !ok {
+ if value != "" {
+ return fmt.Errorf("expected header %q
to be present, but not found", key)
+ }
+ continue
+ }
+ if actualValue != value {
+ return fmt.Errorf("expected header %q to be %q,
but got %q", key, value, actualValue)
+ }
+ }
+ }
+ return nil
+}