sigs.k8s.io/gateway-api@v1.0.0/apis/v1alpha2/validation/grpcroute_test.go (about) 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: ®ex, 99 Method: ®ex, 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: ®ex, 128 Method: ®ex, 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: ®ex, 158 Method: ®ex, 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 }