diff --git a/apis/v1alpha2/grpcroute_types.go b/apis/v1alpha2/grpcroute_types.go index 9c397be9cb..9e77debc22 100644 --- a/apis/v1alpha2/grpcroute_types.go +++ b/apis/v1alpha2/grpcroute_types.go @@ -147,7 +147,6 @@ type GRPCRouteSpec struct { // // +optional // +kubebuilder:validation:MaxItems=16 - // +kubebuilder:default={{matches: {{method: {type: "Exact"}}}}} Rules []GRPCRouteRule `json:"rules,omitempty"` } @@ -313,6 +312,10 @@ type GRPCRouteMatch struct { // request service and/or method. // // At least one of Service and Method MUST be a non-empty string. +// +// +kubebuilder:validation:XValidation:message="One or both of 'service' or 'method' must be specified",rule="has(self.type) ? has(self.service) || has(self.method) : true" +// +kubebuilder:validation:XValidation:message="service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)",rule="(!has(self.type) || self.type == 'Exact') && has(self.service) ? self.service.matches(r\"\"\"^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$\"\"\"): true" +// +kubebuilder:validation:XValidation:message="method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)",rule="(!has(self.type) || self.type == 'Exact') && has(self.method) ? self.method.matches(r\"\"\"^[A-Za-z_][A-Za-z_0-9]*$\"\"\"): true" type GRPCMethodMatch struct { // Type specifies how to match against the service and/or method. // Support: Core (Exact with service and method specified) diff --git a/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml b/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml index 1544a2f944..bac8979e3f 100644 --- a/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml +++ b/config/crd/experimental/gateway.networking.k8s.io_grpcroutes.yaml @@ -293,10 +293,6 @@ spec: && ((!has(p1.port) && !has(p2.port)) || (!has(p1.port) && p2.port == 0) || (p1.port == 0 && !has(p2.port)) || (p1.port == p2.port)))) rules: - default: - - matches: - - method: - type: Exact description: Rules are a list of GRPC matchers, filters and actions. items: description: GRPCRouteRule defines the semantics for matching a @@ -1398,6 +1394,21 @@ spec: - RegularExpression type: string type: object + x-kubernetes-validations: + - message: One or both of 'service' or 'method' must be + specified + rule: 'has(self.type) ? has(self.service) || has(self.method) + : true' + - message: service must only contain valid characters + (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.service) ? self.service.matches(r"""^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$"""): + true' + - message: method must only contain valid characters (matching + ^[A-Za-z_][A-Za-z_0-9]*$) + rule: '(!has(self.type) || self.type == ''Exact'') && + has(self.method) ? self.method.matches(r"""^[A-Za-z_][A-Za-z_0-9]*$"""): + true' type: object maxItems: 8 type: array diff --git a/pkg/test/cel/grpcroute_test.go b/pkg/test/cel/grpcroute_test.go index 936a13a983..a418456773 100644 --- a/pkg/test/cel/grpcroute_test.go +++ b/pkg/test/cel/grpcroute_test.go @@ -22,9 +22,10 @@ package main import ( "context" "fmt" - "testing" "strings" + "testing" "time" + gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -175,7 +176,7 @@ func TestGRPCRouteRule(t *testing.T) { Matches: []gatewayv1a2.GRPCRouteMatch{ { Method: &gatewayv1a2.GRPCMethodMatch{ - Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), + Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), Service: ptrTo("helloworld.Greeter"), }, }, @@ -201,7 +202,7 @@ func TestGRPCRouteRule(t *testing.T) { Matches: []gatewayv1a2.GRPCRouteMatch{ { Method: &gatewayv1a2.GRPCMethodMatch{ - Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), + Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), Method: ptrTo("SayHello"), }, }, @@ -228,7 +229,7 @@ func TestGRPCRouteRule(t *testing.T) { Matches: []gatewayv1a2.GRPCRouteMatch{ { Method: &gatewayv1a2.GRPCMethodMatch{ - Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), + Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")), Service: ptrTo("helloworld.Greeter"), }, }, @@ -297,6 +298,121 @@ func TestGRPCRouteRule(t *testing.T) { } } +func TestGRPCMethodMatch(t *testing.T) { + tests := []struct { + name string + method gatewayv1a2.GRPCMethodMatch + wantErrors []string + }{ + { + name: "valid GRPCRoute with 1 service in GRPCMethodMatch field", + method: gatewayv1a2.GRPCMethodMatch{ + Service: ptrTo("foo.Test.Example"), + }, + }, + { + name: "valid GRPCRoute with 1 method in GRPCMethodMatch field", + method: gatewayv1a2.GRPCMethodMatch{ + Method: ptrTo("Login"), + }, + }, + { + name: "invalid GRPCRoute missing service or method in GRPCMethodMatch field", + method: gatewayv1a2.GRPCMethodMatch{ + Service: nil, + Method: nil, + }, + wantErrors: []string{"One or both of 'service' or 'method"}, + }, + { + name: "GRPCRoute uses regex in service and method with undefined match type", + method: gatewayv1a2.GRPCMethodMatch{ + Service: ptrTo(".*"), + Method: ptrTo(".*"), + }, + 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]*$)"}, + }, + { + name: "GRPCRoute uses regex in service and method with match type Exact", + method: gatewayv1a2.GRPCMethodMatch{ + Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact), + Service: ptrTo(".*"), + Method: ptrTo(".*"), + }, + 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]*$)"}, + }, + { + name: "GRPCRoute uses regex in method with undefined match type", + method: gatewayv1a2.GRPCMethodMatch{ + Method: ptrTo(".*"), + }, + wantErrors: []string{"method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"}, + }, + { + name: "GRPCRoute uses regex in service with match type Exact", + method: gatewayv1a2.GRPCMethodMatch{ + Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact), + Service: ptrTo(".*"), + }, + wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)"}, + }, + { + name: "GRPCRoute uses regex in service and method with match type RegularExpression", + method: gatewayv1a2.GRPCMethodMatch{ + Type: ptrTo(gatewayv1a2.GRPCMethodMatchRegularExpression), + Service: ptrTo(".*"), + Method: ptrTo(".*"), + }, + }, + { + name: "GRPCRoute uses valid service and method with undefined match type", + method: gatewayv1a2.GRPCMethodMatch{ + Service: ptrTo("foo.Test.Example"), + Method: ptrTo("Login"), + }, + }, + { + name: "GRPCRoute uses valid service and method with match type Exact", + method: gatewayv1a2.GRPCMethodMatch{ + Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact), + Service: ptrTo("foo.Test.Example"), + Method: ptrTo("Login"), + }, + }, + { + name: "GRPCRoute uses a valid service with a leading dot when match type is Exact", + method: gatewayv1a2.GRPCMethodMatch{ + Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact), + Service: ptrTo(".foo.Test.Example"), + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + route := gatewayv1a2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()), + Namespace: metav1.NamespaceDefault, + }, + Spec: gatewayv1a2.GRPCRouteSpec{ + Rules: []gatewayv1a2.GRPCRouteRule{ + { + Matches: []gatewayv1a2.GRPCRouteMatch{ + { + Method: &tc.method, + }, + }, + }, + }, + }, + } + validateGRPCRoute(t, &route, tc.wantErrors) + }) + } +} + func validateGRPCRoute(t *testing.T, route *gatewayv1a2.GRPCRoute, wantErrors []string) { t.Helper()