...

Source file src/sigs.k8s.io/gateway-api/apis/v1alpha2/validation/grpcroute_test.go

Documentation: sigs.k8s.io/gateway-api/apis/v1alpha2/validation

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

View as plain text