Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apis/v1alpha2/gatewayclass_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import (
// If implementations choose to propagate GatewayClass changes to existing
// Gateways, that MUST be clearly documented by the implementation.
//
// Whenever one or more Gateways are using a GatewayClass, implementations MUST
// Whenever one or more Gateways are using a GatewayClass, implementations SHOULD
// add the `gateway-exists-finalizer.gateway.networking.k8s.io` finalizer on the
// associated GatewayClass. This ensures that a GatewayClass associated with a
// Gateway is not deleted while in use.
Expand Down
22 changes: 6 additions & 16 deletions apis/v1alpha2/grpcroute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package v1alpha2

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"sigs.k8s.io/gateway-api/apis/v1beta1"
)

// +genclient
Expand Down Expand Up @@ -149,7 +151,7 @@ type GRPCRouteSpec struct {
Rules []GRPCRouteRule `json:"rules,omitempty"`
}

// GRPCRouteRule defines the semantics for matching an gRPC request based on
// GRPCRouteRule defines the semantics for matching a gRPC request based on
// conditions (matches), processing it (filters), and forwarding the request to
// an API object (backendRefs).
type GRPCRouteRule struct {
Expand Down Expand Up @@ -205,7 +207,6 @@ type GRPCRouteRule struct {
//
// +optional
// +kubebuilder:validation:MaxItems=8
// +kubebuilder:default={{method: {type: "Exact"}}}
Matches []GRPCRouteMatch `json:"matches,omitempty"`

// Filters define the filters that are applied to requests that match
Expand Down Expand Up @@ -286,7 +287,6 @@ type GRPCRouteMatch struct {
// not specified, all services and methods will match.
//
// +optional
// +kubebuilder:default={type: "Exact"}
Method *GRPCMethodMatch `json:"method,omitempty"`

// Headers specifies gRPC request header matchers. Multiple match values are
Expand Down Expand Up @@ -321,25 +321,17 @@ type GRPCMethodMatch struct {
//
// At least one of Service and Method MUST be a non-empty string.
//
// A GRPC Service must be a valid Protobuf Type Name
// (https://protobuf.com/docs/language-spec#type-references).
//
// +optional
// +kubebuilder:validation:MaxLength=1024
// +kubebuilder:validation:Pattern=`^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$`
Service *string `json:"service,omitempty"`

// Value of the method to match against. If left empty or omitted, will
// match all services.
//
// At least one of Service and Method MUST be a non-empty string.
//
// A GRPC Method must be a valid Protobuf Method
// (https://protobuf.com/docs/language-spec#methods).
//
// +optional
// +kubebuilder:validation:MaxLength=1024
// +kubebuilder:validation:Pattern=`^[A-Za-z_][A-Za-z_0-9]*$`
Method *string `json:"method,omitempty"`
}

Expand Down Expand Up @@ -419,10 +411,7 @@ const (
GRPCHeaderMatchRegularExpression GRPCHeaderMatchType = "RegularExpression"
)

// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=256
// +kubebuilder:validation:Pattern=`^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$`
type GRPCHeaderName string
type GRPCHeaderName v1beta1.HeaderName

// GRPCRouteFilterType identifies a type of GRPCRoute filter.
type GRPCRouteFilterType string
Expand Down Expand Up @@ -513,7 +502,6 @@ type GRPCRouteFilter struct {
// Support: Extended
//
// +optional
// <gateway:experimental>
ResponseHeaderModifier *HTTPHeaderFilter `json:"responseHeaderModifier,omitempty"`

// RequestMirror defines a schema for a filter that mirrors requests.
Expand Down Expand Up @@ -562,6 +550,8 @@ type GRPCBackendRef struct {
//
// Support: Core for Kubernetes Service
//
// Support: Extended for Kubernetes ServiceImport
//
// Support: Implementation-specific for any other resource
//
// Support for weight: Core
Expand Down
6 changes: 1 addition & 5 deletions apis/v1alpha2/httproute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,8 @@ type HeaderMatchType = v1beta1.HeaderMatchType
// headers are not currently supported by this type.
//
// * "/invalid" - "/" is an invalid character
//
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=256
// +kubebuilder:validation:Pattern=`^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$`
// +k8s:deepcopy-gen=false
type HTTPHeaderName = v1beta1.HTTPHeaderName
type HTTPHeaderName = v1beta1.HeaderName
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we drop HTTP from this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like because of the above change to make the GRPC variant type GRPCHeaderName v1beta1.HeaderName instead of its own string alias?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this was trying to ensure that our types and validation were consistent since by definition a GRPC Header name is going to have the same validation and constraints as HTTP.


// HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request
// headers.
Expand Down
2 changes: 2 additions & 0 deletions apis/v1alpha2/tcproute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type TCPRouteRule struct {
//
// Support: Core for Kubernetes Service
//
// Support: Extended for Kubernetes ServiceImport
//
// Support: Implementation-specific for any other resource
//
// Support for weight: Extended
Expand Down
2 changes: 2 additions & 0 deletions apis/v1alpha2/tlsroute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ type TLSRouteRule struct {
//
// Support: Core for Kubernetes Service
//
// Support: Extended for Kubernetes ServiceImport

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: While we all understand what ServiceImport is, a new reader may not. So better to call out that these are objects.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment applies in a lot of places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, are you saying that "Extended for Kubernetes ServiceImport" should be replaced with "Extended for Kubernetes ServiceImport Objects", would the same be true for "Core for Kubernetes Service Objects"? I'm not that familiar with the proper terminology here, but would "Resource(s)" work as well?

//
// Support: Implementation-specific for any other resource
//
// Support for weight: Extended
Expand Down
3 changes: 3 additions & 0 deletions apis/v1alpha2/udproute_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ type UDPRouteRule struct {
// the packets, then 80% of packets must be dropped instead.
//
// Support: Core for Kubernetes Service
//
// Support: Extended for Kubernetes ServiceImport
//
// Support: Implementation-specific for any other resource
//
// Support for weight: Extended
Expand Down
27 changes: 23 additions & 4 deletions apis/v1alpha2/validation/grpcroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ limitations under the License.
package validation

import (
"fmt"
"net/http"
"regexp"
"strings"

"k8s.io/apimachinery/pkg/util/validation/field"
Expand All @@ -31,6 +33,10 @@ var (
repeatableGRPCRouteFilters = []gatewayv1a2.GRPCRouteFilterType{
gatewayv1a2.GRPCRouteFilterExtensionRef,
}
validServiceName = `^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$`
validServiceNameRegex = regexp.MustCompile(validServiceName)
validMethodName = `^[A-Za-z_][A-Za-z_0-9]*$`
validMethodNameRegex = regexp.MustCompile(validMethodName)
)

// ValidateGRPCRoute validates GRPCRoute according to the Gateway API specification.
Expand Down Expand Up @@ -63,13 +69,26 @@ func validateGRPCRouteRules(rules []gatewayv1a2.GRPCRouteRule, path *field.Path)
return errs
}

// validateRuleMatches validates that at least one of the fields Service or Method of
// GRPCMethodMatch to be specified
// validateRuleMatches validates GRPCMethodMatch
func validateRuleMatches(matches []gatewayv1a2.GRPCRouteMatch, path *field.Path) field.ErrorList {
var errs field.ErrorList
for i, m := range matches {
if m.Method != nil && m.Method.Service == nil && m.Method.Method == nil {
errs = append(errs, field.Required(path.Index(i).Child("method"), "one or both of `service` or `method` must be specified"))
if m.Method != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can m.Method == nil if so, is it a valid object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's optional, but there is defaulting involved so it seems unlikely/impossible that this would ever be empty.

Copy link

@thockin thockin May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often add the else ... field.Required() to these - it has helped prevent tests that passed whjen they shouldn't.

if m.Method.Service == nil && m.Method.Method == nil {
errs = append(errs, field.Required(path.Index(i).Child("method"), "one or both of `service` or `method` must be specified"))
}
// GRPCRoute method matcher admits two types: Exact and RegularExpression.
// If not specified, the match will be treated as type Exact (also the default value for this field).
if m.Method.Type == nil || *m.Method.Type == gatewayv1a2.GRPCMethodMatchExact {
if m.Method.Service != nil && !validServiceNameRegex.MatchString(*m.Method.Service) {
errs = append(errs, field.Invalid(path.Index(i).Child("method"), *m.Method.Service,
fmt.Sprintf("must only contain valid characters (matching %s)", validServiceName)))
}
if m.Method.Method != nil && !validMethodNameRegex.MatchString(*m.Method.Method) {
errs = append(errs, field.Invalid(path.Index(i).Child("method"), *m.Method.Method,
fmt.Sprintf("must only contain valid characters (matching %s)", validMethodName)))
}
}
}
if m.Headers != nil {
errs = append(errs, validateGRPCHeaderMatches(m.Headers, path.Index(i).Child("headers"))...)
Expand Down
114 changes: 112 additions & 2 deletions apis/v1alpha2/validation/grpcroute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ import (
func TestValidateGRPCRoute(t *testing.T) {
t.Parallel()

service := "foo"
method := "login"
service := "foo.Test.Example"
method := "Login"
regex := ".*"

tests := []struct {
name string
Expand Down Expand Up @@ -87,6 +88,115 @@ func TestValidateGRPCRoute(t *testing.T) {
},
},
},
{
name: "GRPCRoute use regex in service and method with undefined match type",
rules: []gatewayv1a2.GRPCRouteRule{
{
Matches: []gatewayv1a2.GRPCRouteMatch{
{
Method: &gatewayv1a2.GRPCMethodMatch{
Service: &regex,
Method: &regex,
},
},
},
},
},
errs: field.ErrorList{
{
Type: field.ErrorTypeInvalid,
BadValue: regex,
Field: "spec.rules[0].matches[0].method",
Detail: `must only contain valid characters (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$)`,
},
{
Type: field.ErrorTypeInvalid,
BadValue: regex,
Field: "spec.rules[0].matches[0].method",
Detail: `must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)`,
},
},
},
{
name: "GRPCRoute use regex in service and method with match type Exact",
rules: []gatewayv1a2.GRPCRouteRule{
{
Matches: []gatewayv1a2.GRPCRouteMatch{
{
Method: &gatewayv1a2.GRPCMethodMatch{
Service: &regex,
Method: &regex,
Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
},
},
},
},
},
errs: field.ErrorList{
{
Type: field.ErrorTypeInvalid,
BadValue: regex,
Field: "spec.rules[0].matches[0].method",
Detail: `must only contain valid characters (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$)`,
},
{
Type: field.ErrorTypeInvalid,
BadValue: regex,
Field: "spec.rules[0].matches[0].method",
Detail: `must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)`,
},
},
},
{
name: "GRPCRoute use regex in service and method with match type RegularExpression",
rules: []gatewayv1a2.GRPCRouteRule{
{
Matches: []gatewayv1a2.GRPCRouteMatch{
{
Method: &gatewayv1a2.GRPCMethodMatch{
Service: &regex,
Method: &regex,
Type: ptrTo(gatewayv1a2.GRPCMethodMatchRegularExpression),
},
},
},
},
},
errs: field.ErrorList{},
},
{
name: "GRPCRoute use valid service and method with undefined match type",
rules: []gatewayv1a2.GRPCRouteRule{
{
Matches: []gatewayv1a2.GRPCRouteMatch{
{
Method: &gatewayv1a2.GRPCMethodMatch{
Service: &service,
Method: &method,
},
},
},
},
},
errs: field.ErrorList{},
},
{
name: "GRPCRoute use valid service and method with match type Exact",
rules: []gatewayv1a2.GRPCRouteRule{
{
Matches: []gatewayv1a2.GRPCRouteMatch{
{
Method: &gatewayv1a2.GRPCMethodMatch{
Service: &service,
Method: &method,
Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
},
},
},
},
},
errs: field.ErrorList{},
},
{
name: "GRPCRoute with duplicate ExtensionRef filters",
rules: []gatewayv1a2.GRPCRouteRule{
Expand Down
Loading