...

Source file src/sigs.k8s.io/gateway-api/pkg/test/cel/grpcroute_test.go

Documentation: sigs.k8s.io/gateway-api/pkg/test/cel

     1  //go:build experimental
     2  // +build experimental
     3  
     4  /*
     5  Copyright 2023 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package main
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
    30  
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  )
    33  
    34  func TestGRPCRouteFilter(t *testing.T) {
    35  	tests := []struct {
    36  		name        string
    37  		wantErrors  []string
    38  		routeFilter gatewayv1a2.GRPCRouteFilter
    39  	}{
    40  		{
    41  			name: "valid GRPCRouteFilterRequestHeaderModifier route filter",
    42  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    43  				Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
    44  				RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
    45  					Set:    []gatewayv1a2.HTTPHeader{{Name: "name", Value: "foo"}},
    46  					Add:    []gatewayv1a2.HTTPHeader{{Name: "add", Value: "foo"}},
    47  					Remove: []string{"remove"},
    48  				},
    49  			},
    50  		},
    51  		{
    52  			name: "invalid GRPCRouteFilterRequestHeaderModifier type filter with non-matching field",
    53  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    54  				Type:          gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
    55  				RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
    56  			},
    57  			wantErrors: []string{"filter.requestHeaderModifier must be specified for RequestHeaderModifier filter.type", "filter.requestMirror must be nil if the filter.type is not RequestMirror"},
    58  		},
    59  		{
    60  			name: "invalid GRPCRouteFilterRequestHeaderModifier type filter with empty value field",
    61  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    62  				Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
    63  			},
    64  			wantErrors: []string{"filter.requestHeaderModifier must be specified for RequestHeaderModifier filter.type"},
    65  		},
    66  		{
    67  			name: "valid GRPCRouteFilterResponseHeaderModifier route filter",
    68  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    69  				Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
    70  				ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
    71  					Set:    []gatewayv1a2.HTTPHeader{{Name: "name", Value: "foo"}},
    72  					Add:    []gatewayv1a2.HTTPHeader{{Name: "add", Value: "foo"}},
    73  					Remove: []string{"remove"},
    74  				},
    75  			},
    76  		},
    77  		{
    78  			name: "invalid GRPCRouteFilterResponseHeaderModifier type filter with non-matching field",
    79  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    80  				Type:          gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
    81  				RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
    82  			},
    83  			wantErrors: []string{"filter.responseHeaderModifier must be specified for ResponseHeaderModifier filter.type", "filter.requestMirror must be nil if the filter.type is not RequestMirror"},
    84  		},
    85  		{
    86  			name: "invalid GRPCRouteFilterResponseHeaderModifier type filter with empty value field",
    87  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    88  				Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
    89  			},
    90  			wantErrors: []string{"filter.responseHeaderModifier must be specified for ResponseHeaderModifier filter.type"},
    91  		},
    92  		{
    93  			name: "valid GRPCRouteFilterRequestMirror route filter",
    94  			routeFilter: gatewayv1a2.GRPCRouteFilter{
    95  				Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
    96  				RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{BackendRef: gatewayv1a2.BackendObjectReference{
    97  					Group:     ptrTo(gatewayv1a2.Group("group")),
    98  					Kind:      ptrTo(gatewayv1a2.Kind("kind")),
    99  					Name:      "name",
   100  					Namespace: ptrTo(gatewayv1a2.Namespace("ns")),
   101  					Port:      ptrTo(gatewayv1a2.PortNumber(22)),
   102  				}},
   103  			},
   104  		},
   105  		{
   106  			name: "invalid GRPCRouteFilterRequestMirror type filter with non-matching field",
   107  			routeFilter: gatewayv1a2.GRPCRouteFilter{
   108  				Type:                  gatewayv1a2.GRPCRouteFilterRequestMirror,
   109  				RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{},
   110  			},
   111  			wantErrors: []string{"filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier", "filter.requestMirror must be specified for RequestMirror filter.type"},
   112  		},
   113  		{
   114  			name: "invalid GRPCRouteFilterRequestMirror type filter with empty value field",
   115  			routeFilter: gatewayv1a2.GRPCRouteFilter{
   116  				Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
   117  			},
   118  			wantErrors: []string{"filter.requestMirror must be specified for RequestMirror filter.type"},
   119  		},
   120  		{
   121  			name: "valid GRPCRouteFilterExtensionRef filter",
   122  			routeFilter: gatewayv1a2.GRPCRouteFilter{
   123  				Type: gatewayv1a2.GRPCRouteFilterExtensionRef,
   124  				ExtensionRef: &gatewayv1a2.LocalObjectReference{
   125  					Group: "group",
   126  					Kind:  "kind",
   127  					Name:  "name",
   128  				},
   129  			},
   130  		},
   131  		{
   132  			name: "invalid GRPCRouteFilterExtensionRef type filter with non-matching field",
   133  			routeFilter: gatewayv1a2.GRPCRouteFilter{
   134  				Type:          gatewayv1a2.GRPCRouteFilterExtensionRef,
   135  				RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
   136  			},
   137  			wantErrors: []string{"filter.requestMirror must be nil if the filter.type is not RequestMirror", "filter.extensionRef must be specified for ExtensionRef filter.type"},
   138  		},
   139  		{
   140  			name: "invalid GRPCRouteFilterExtensionRef type filter with empty value field",
   141  			routeFilter: gatewayv1a2.GRPCRouteFilter{
   142  				Type: gatewayv1a2.GRPCRouteFilterExtensionRef,
   143  			},
   144  			wantErrors: []string{"filter.extensionRef must be specified for ExtensionRef filter.type"},
   145  		},
   146  	}
   147  	for _, tc := range tests {
   148  		t.Run(tc.name, func(t *testing.T) {
   149  			route := &gatewayv1a2.GRPCRoute{
   150  				ObjectMeta: metav1.ObjectMeta{
   151  					Name:      fmt.Sprintf("foo-%v", time.Now().UnixNano()),
   152  					Namespace: metav1.NamespaceDefault,
   153  				},
   154  				Spec: gatewayv1a2.GRPCRouteSpec{
   155  					Rules: []gatewayv1a2.GRPCRouteRule{{
   156  						Filters: []gatewayv1a2.GRPCRouteFilter{tc.routeFilter},
   157  					}},
   158  				},
   159  			}
   160  			validateGRPCRoute(t, route, tc.wantErrors)
   161  		})
   162  	}
   163  }
   164  
   165  func TestGRPCRouteRule(t *testing.T) {
   166  	testService := gatewayv1a2.ObjectName("test-service")
   167  	tests := []struct {
   168  		name       string
   169  		wantErrors []string
   170  		rules      []gatewayv1a2.GRPCRouteRule
   171  	}{
   172  		{
   173  			name: "valid GRPCRoute with no filters",
   174  			rules: []gatewayv1a2.GRPCRouteRule{
   175  				{
   176  					Matches: []gatewayv1a2.GRPCRouteMatch{
   177  						{
   178  							Method: &gatewayv1a2.GRPCMethodMatch{
   179  								Type:    ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
   180  								Service: ptrTo("helloworld.Greeter"),
   181  							},
   182  						},
   183  					},
   184  					BackendRefs: []gatewayv1a2.GRPCBackendRef{
   185  						{
   186  							BackendRef: gatewayv1a2.BackendRef{
   187  								BackendObjectReference: gatewayv1a2.BackendObjectReference{
   188  									Name: testService,
   189  									Port: ptrTo(gatewayv1a2.PortNumber(8080)),
   190  								},
   191  								Weight: ptrTo(int32(100)),
   192  							},
   193  						},
   194  					},
   195  				},
   196  			},
   197  		},
   198  		{
   199  			name: "valid GRPCRoute with only Method specified",
   200  			rules: []gatewayv1a2.GRPCRouteRule{
   201  				{
   202  					Matches: []gatewayv1a2.GRPCRouteMatch{
   203  						{
   204  							Method: &gatewayv1a2.GRPCMethodMatch{
   205  								Type:   ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
   206  								Method: ptrTo("SayHello"),
   207  							},
   208  						},
   209  					},
   210  					BackendRefs: []gatewayv1a2.GRPCBackendRef{
   211  						{
   212  							BackendRef: gatewayv1a2.BackendRef{
   213  								BackendObjectReference: gatewayv1a2.BackendObjectReference{
   214  									Name: testService,
   215  									Port: ptrTo(gatewayv1a2.PortNumber(8080)),
   216  								},
   217  								Weight: ptrTo(int32(100)),
   218  							},
   219  						},
   220  					},
   221  				},
   222  			},
   223  		},
   224  		{
   225  			name:       "invalid because multiple filters are repeated",
   226  			wantErrors: []string{"RequestHeaderModifier filter cannot be repeated", "ResponseHeaderModifier filter cannot be repeated"},
   227  			rules: []gatewayv1a2.GRPCRouteRule{
   228  				{
   229  					Matches: []gatewayv1a2.GRPCRouteMatch{
   230  						{
   231  							Method: &gatewayv1a2.GRPCMethodMatch{
   232  								Type:    ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
   233  								Service: ptrTo("helloworld.Greeter"),
   234  							},
   235  						},
   236  					},
   237  					Filters: []gatewayv1a2.GRPCRouteFilter{
   238  						{
   239  							Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
   240  							RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
   241  								Set: []gatewayv1a2.HTTPHeader{
   242  									{
   243  										Name:  "special-header",
   244  										Value: "foo",
   245  									},
   246  								},
   247  							},
   248  						},
   249  						{
   250  							Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
   251  							RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
   252  								Add: []gatewayv1a2.HTTPHeader{
   253  									{
   254  										Name:  "my-header",
   255  										Value: "bar",
   256  									},
   257  								},
   258  							},
   259  						},
   260  						{
   261  							Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
   262  							ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
   263  								Set: []gatewayv1a2.HTTPHeader{
   264  									{
   265  										Name:  "special-header",
   266  										Value: "foo",
   267  									},
   268  								},
   269  							},
   270  						},
   271  						{
   272  							Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
   273  							ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
   274  								Add: []gatewayv1a2.HTTPHeader{
   275  									{
   276  										Name:  "my-header",
   277  										Value: "bar",
   278  									},
   279  								},
   280  							},
   281  						},
   282  					},
   283  				},
   284  			},
   285  		},
   286  	}
   287  	for _, tc := range tests {
   288  		t.Run(tc.name, func(t *testing.T) {
   289  			route := &gatewayv1a2.GRPCRoute{
   290  				ObjectMeta: metav1.ObjectMeta{
   291  					Name:      fmt.Sprintf("foo-%v", time.Now().UnixNano()),
   292  					Namespace: metav1.NamespaceDefault,
   293  				},
   294  				Spec: gatewayv1a2.GRPCRouteSpec{Rules: tc.rules},
   295  			}
   296  			validateGRPCRoute(t, route, tc.wantErrors)
   297  		})
   298  	}
   299  }
   300  
   301  func TestGRPCMethodMatch(t *testing.T) {
   302  	tests := []struct {
   303  		name       string
   304  		method     gatewayv1a2.GRPCMethodMatch
   305  		wantErrors []string
   306  	}{
   307  		{
   308  			name: "valid GRPCRoute with 1 service in GRPCMethodMatch field",
   309  			method: gatewayv1a2.GRPCMethodMatch{
   310  				Service: ptrTo("foo.Test.Example"),
   311  			},
   312  		},
   313  		{
   314  			name: "valid GRPCRoute with 1 method in GRPCMethodMatch field",
   315  			method: gatewayv1a2.GRPCMethodMatch{
   316  				Method: ptrTo("Login"),
   317  			},
   318  		},
   319  		{
   320  			name: "invalid GRPCRoute missing service or method in GRPCMethodMatch field",
   321  			method: gatewayv1a2.GRPCMethodMatch{
   322  				Service: nil,
   323  				Method:  nil,
   324  			},
   325  			wantErrors: []string{"One or both of 'service' or 'method"},
   326  		},
   327  		{
   328  			name: "GRPCRoute uses regex in service and method with undefined match type",
   329  			method: gatewayv1a2.GRPCMethodMatch{
   330  				Service: ptrTo(".*"),
   331  				Method:  ptrTo(".*"),
   332  			},
   333  			wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)", "method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
   334  		},
   335  		{
   336  			name: "GRPCRoute uses regex in service and method with match type Exact",
   337  			method: gatewayv1a2.GRPCMethodMatch{
   338  				Type:    ptrTo(gatewayv1a2.GRPCMethodMatchExact),
   339  				Service: ptrTo(".*"),
   340  				Method:  ptrTo(".*"),
   341  			},
   342  			wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)", "method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
   343  		},
   344  		{
   345  			name: "GRPCRoute uses regex in method with undefined match type",
   346  			method: gatewayv1a2.GRPCMethodMatch{
   347  				Method: ptrTo(".*"),
   348  			},
   349  			wantErrors: []string{"method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
   350  		},
   351  		{
   352  			name: "GRPCRoute uses regex in service with match type Exact",
   353  			method: gatewayv1a2.GRPCMethodMatch{
   354  				Type:    ptrTo(gatewayv1a2.GRPCMethodMatchExact),
   355  				Service: ptrTo(".*"),
   356  			},
   357  			wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)"},
   358  		},
   359  		{
   360  			name: "GRPCRoute uses regex in service and method with match type RegularExpression",
   361  			method: gatewayv1a2.GRPCMethodMatch{
   362  				Type:    ptrTo(gatewayv1a2.GRPCMethodMatchRegularExpression),
   363  				Service: ptrTo(".*"),
   364  				Method:  ptrTo(".*"),
   365  			},
   366  		},
   367  		{
   368  			name: "GRPCRoute uses valid service and method with undefined match type",
   369  			method: gatewayv1a2.GRPCMethodMatch{
   370  				Service: ptrTo("foo.Test.Example"),
   371  				Method:  ptrTo("Login"),
   372  			},
   373  		},
   374  		{
   375  			name: "GRPCRoute uses valid service and method with match type Exact",
   376  			method: gatewayv1a2.GRPCMethodMatch{
   377  				Type:    ptrTo(gatewayv1a2.GRPCMethodMatchExact),
   378  				Service: ptrTo("foo.Test.Example"),
   379  				Method:  ptrTo("Login"),
   380  			},
   381  		},
   382  		{
   383  			name: "GRPCRoute uses a valid service with a leading dot when match type is Exact",
   384  			method: gatewayv1a2.GRPCMethodMatch{
   385  				Type:    ptrTo(gatewayv1a2.GRPCMethodMatchExact),
   386  				Service: ptrTo(".foo.Test.Example"),
   387  			},
   388  		},
   389  	}
   390  
   391  	for _, tc := range tests {
   392  		tc := tc
   393  		t.Run(tc.name, func(t *testing.T) {
   394  			route := gatewayv1a2.GRPCRoute{
   395  				ObjectMeta: metav1.ObjectMeta{
   396  					Name:      fmt.Sprintf("foo-%v", time.Now().UnixNano()),
   397  					Namespace: metav1.NamespaceDefault,
   398  				},
   399  				Spec: gatewayv1a2.GRPCRouteSpec{
   400  					Rules: []gatewayv1a2.GRPCRouteRule{
   401  						{
   402  							Matches: []gatewayv1a2.GRPCRouteMatch{
   403  								{
   404  									Method: &tc.method,
   405  								},
   406  							},
   407  						},
   408  					},
   409  				},
   410  			}
   411  			validateGRPCRoute(t, &route, tc.wantErrors)
   412  		})
   413  	}
   414  }
   415  
   416  func validateGRPCRoute(t *testing.T, route *gatewayv1a2.GRPCRoute, wantErrors []string) {
   417  	t.Helper()
   418  
   419  	ctx := context.Background()
   420  	err := k8sClient.Create(ctx, route)
   421  
   422  	if (len(wantErrors) != 0) != (err != nil) {
   423  		t.Fatalf("Unexpected response while creating GRPCRoute %q; got err=\n%v\n;want error=%v", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, wantErrors)
   424  	}
   425  
   426  	var missingErrorStrings []string
   427  	for _, wantError := range wantErrors {
   428  		if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) {
   429  			missingErrorStrings = append(missingErrorStrings, wantError)
   430  		}
   431  	}
   432  	if len(missingErrorStrings) != 0 {
   433  		t.Errorf("Unexpected response while creating GRPCRoute %q; got err=\n%v\n;missing strings within error=%q", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, missingErrorStrings)
   434  	}
   435  }
   436  

View as plain text