sigs.k8s.io/gateway-api@v1.0.0/pkg/test/cel/grpcroute_test.go (about) 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 }