diff --git a/apis/v1alpha2/gatewayclass_types.go b/apis/v1alpha2/gatewayclass_types.go index 69d012d123..fe33b8ecd4 100644 --- a/apis/v1alpha2/gatewayclass_types.go +++ b/apis/v1alpha2/gatewayclass_types.go @@ -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. diff --git a/apis/v1alpha2/grpcroute_types.go b/apis/v1alpha2/grpcroute_types.go index dc5f558d99..f98a03aa3e 100644 --- a/apis/v1alpha2/grpcroute_types.go +++ b/apis/v1alpha2/grpcroute_types.go @@ -18,6 +18,8 @@ package v1alpha2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "sigs.k8s.io/gateway-api/apis/v1beta1" ) // +genclient @@ -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 { @@ -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 @@ -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 @@ -321,12 +321,8 @@ 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 @@ -334,12 +330,8 @@ type GRPCMethodMatch struct { // // 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"` } @@ -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 @@ -513,7 +502,6 @@ type GRPCRouteFilter struct { // Support: Extended // // +optional - // ResponseHeaderModifier *HTTPHeaderFilter `json:"responseHeaderModifier,omitempty"` // RequestMirror defines a schema for a filter that mirrors requests. @@ -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 diff --git a/apis/v1alpha2/httproute_types.go b/apis/v1alpha2/httproute_types.go index ac1e009cbb..8a32a075d8 100644 --- a/apis/v1alpha2/httproute_types.go +++ b/apis/v1alpha2/httproute_types.go @@ -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 // HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request // headers. diff --git a/apis/v1alpha2/tcproute_types.go b/apis/v1alpha2/tcproute_types.go index f60677c1a7..fe927ab8d4 100644 --- a/apis/v1alpha2/tcproute_types.go +++ b/apis/v1alpha2/tcproute_types.go @@ -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 diff --git a/apis/v1alpha2/tlsroute_types.go b/apis/v1alpha2/tlsroute_types.go index e5a4955354..afe34d82d6 100644 --- a/apis/v1alpha2/tlsroute_types.go +++ b/apis/v1alpha2/tlsroute_types.go @@ -112,6 +112,8 @@ type TLSRouteRule struct { // // Support: Core for Kubernetes Service // + // Support: Extended for Kubernetes ServiceImport + // // Support: Implementation-specific for any other resource // // Support for weight: Extended diff --git a/apis/v1alpha2/udproute_types.go b/apis/v1alpha2/udproute_types.go index eaa4f5c21a..9e3770c293 100644 --- a/apis/v1alpha2/udproute_types.go +++ b/apis/v1alpha2/udproute_types.go @@ -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 diff --git a/apis/v1alpha2/validation/grpcroute.go b/apis/v1alpha2/validation/grpcroute.go index 74709b8de9..62ed859235 100644 --- a/apis/v1alpha2/validation/grpcroute.go +++ b/apis/v1alpha2/validation/grpcroute.go @@ -17,7 +17,9 @@ limitations under the License. package validation import ( + "fmt" "net/http" + "regexp" "strings" "k8s.io/apimachinery/pkg/util/validation/field" @@ -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. @@ -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 { + 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"))...) diff --git a/apis/v1alpha2/validation/grpcroute_test.go b/apis/v1alpha2/validation/grpcroute_test.go index 67e8f5743a..9055864b3f 100644 --- a/apis/v1alpha2/validation/grpcroute_test.go +++ b/apis/v1alpha2/validation/grpcroute_test.go @@ -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 @@ -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: ®ex, + Method: ®ex, + }, + }, + }, + }, + }, + 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: ®ex, + Method: ®ex, + 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: ®ex, + Method: ®ex, + 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{ diff --git a/apis/v1beta1/gateway_types.go b/apis/v1beta1/gateway_types.go index 8f4d3ff70d..305bff37c7 100644 --- a/apis/v1beta1/gateway_types.go +++ b/apis/v1beta1/gateway_types.go @@ -41,7 +41,7 @@ type Gateway struct { // Status defines the current state of Gateway. // - // +kubebuilder:default={conditions: {{type: "Accepted", status: "Unknown", reason:"NotReconciled", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}} + // +kubebuilder:default={conditions: {{type: "Accepted", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"},{type: "Programmed", status: "Unknown", reason:"Pending", message:"Waiting for controller", lastTransitionTime: "1970-01-01T00:00:00Z"}}} Status GatewayStatus `json:"status,omitempty"` } @@ -488,6 +488,7 @@ type GatewayStatus struct { // Known condition types are: // // * "Accepted" + // * "Programmed" // * "Ready" // // +optional @@ -535,6 +536,8 @@ const ( // // * "Invalid" // * "Pending" + // * "NoResources" + // * "AddressNotAssigned" // // Possible reasons for this condition to be Unknown are: // @@ -549,9 +552,20 @@ const ( // true. GatewayReasonProgrammed GatewayConditionReason = "Programmed" - // This reason is used with the "Programmed" condition when the Listener is + // This reason is used with the "Programmed" and "Accepted" conditions when the Gateway is // syntactically or semantically invalid. GatewayReasonInvalid GatewayConditionReason = "Invalid" + + // This reason is used with the "Programmed" condition when the + // Gateway is not scheduled because insufficient infrastructure + // resources are available. + GatewayReasonNoResources GatewayConditionReason = "NoResources" + + // This reason is used with the "Programmed" condition when none of the requested + // addresses have been assigned to the Gateway. This reason can be used to + // express a range of circumstances, including (but not limited to) IPAM + // address exhaustion, address not yet allocated, or a named address not being found. + GatewayReasonAddressNotAssigned GatewayConditionReason = "AddressNotAssigned" ) const ( @@ -563,11 +577,14 @@ const ( // Possible reasons for this condition to be True are: // // * "Accepted" + // * "ListenersNotValid" // // Possible reasons for this condition to be False are: // + // * "Invalid" // * "NotReconciled" - // * "NoResources" + // * "UnsupportedAddress" + // * "ListenersNotValid" // // Possible reasons for this condition to be Unknown are: // @@ -578,76 +595,65 @@ const ( // interoperability. GatewayConditionAccepted GatewayConditionType = "Accepted" - // Deprecated: use "Accepted" instead. - GatewayConditionScheduled GatewayConditionType = "Scheduled" - // This reason is used with the "Accepted" condition when the condition is // True. GatewayReasonAccepted GatewayConditionReason = "Accepted" + // This reason is used with the "Accepted" condition when one or + // more Listeners have an invalid or unsupported configuration + // and cannot be configured on the Gateway. + // This can be the reason when "Accepted" is "True" or "False", depending on whether + // the listener being invalid causes the entire Gateway to not be accepted. + GatewayReasonListenersNotValid GatewayConditionReason = "ListenersNotValid" + + // This reason is used with the "Accepted" and "Programmed" + // conditions when the status is "Unknown" and no controller has reconciled + // the Gateway. + GatewayReasonPending GatewayConditionReason = "Pending" + + // This reason is used with the "Accepted" condition when the Gateway could not be configured + // because the requested address is not supported. This reason could be used in a number of + // instances, including: + // + // * The address is already in use. + // * The type of address is not supported by the implementation. + GatewaReasonUnsupportedAddress GatewayConditionReason = "UnsupportedAddress" +) + +const ( + // Deprecated: use "Accepted" instead. + GatewayConditionScheduled GatewayConditionType = "Scheduled" + // This reason is used with the "Scheduled" condition when the condition is // True. // // Deprecated: use the "Accepted" condition with reason "Accepted" instead. GatewayReasonScheduled GatewayConditionReason = "Scheduled" - // This reason is used with the "Accepted", "Programmed" and "Ready" - // conditions when the status is "Unknown" and no controller has reconciled - // the Gateway. - GatewayReasonPending GatewayConditionReason = "Pending" - // Deprecated: Use "Pending" instead. GatewayReasonNotReconciled GatewayConditionReason = "NotReconciled" - - // This reason is used with the "Accepted" condition when the - // Gateway is not scheduled because insufficient infrastructure - // resources are available. - GatewayReasonNoResources GatewayConditionReason = "NoResources" ) const ( - // Ready is an optional Condition that has Extended support. When it's set, - // the condition indicates whether the Gateway has been completely configured - // and traffic is ready to flow through the data plane immediately. + // "Ready" is a condition type reserved for future use. It should not be used by implementations. // - // If both the "ListenersNotValid" and "ListenersNotReady" - // reasons are true, the Gateway controller should prefer the - // "ListenersNotValid" reason. + // If used in the future, "Ready" will represent the final state where all configuration is confirmed good + // _and has completely propagated to the data plane_. That is, it is a _guarantee_ that, as soon as something + // sees the Condition as `true`, then connections will be correctly routed _immediately_. // - // Possible reasons for this condition to be true are: + // This is a very strong guarantee, and to date no implementation has satisfied it enough to implement it. + // This reservation can be discussed in the future if necessary. // - // * "Ready" - // - // Possible reasons for this condition to be False are: - // - // * "ListenersNotValid" - // * "ListenersNotReady" - // * "AddressNotAssigned" - // - // Controllers may raise this condition with other reasons, - // but should prefer to use the reasons listed above to improve - // interoperability. + // Note: This condition is not really "deprecated", but rather "reserved"; however, deprecated triggers Go linters + // to alert about usage. + // Deprecated: Ready is reserved for future use GatewayConditionReady GatewayConditionType = "Ready" - // This reason is used with the "Ready" condition when the condition is - // true. + // Deprecated: Ready is reserved for future use GatewayReasonReady GatewayConditionReason = "Ready" - // This reason is used with the "Ready" condition when one or - // more Listeners have an invalid or unsupported configuration - // and cannot be configured on the Gateway. - GatewayReasonListenersNotValid GatewayConditionReason = "ListenersNotValid" - - // This reason is used with the "Ready" condition when one or - // more Listeners are not ready to serve traffic. + // Deprecated: Ready is reserved for future use GatewayReasonListenersNotReady GatewayConditionReason = "ListenersNotReady" - - // This reason is used with the "Ready" condition when none of the requested - // addresses have been assigned to the Gateway. This reason can be used to - // express a range of circumstances, including (but not limited to) IPAM - // address exhaustion, invalid or unsupported address requests, or a named - // address not being found. - GatewayReasonAddressNotAssigned GatewayConditionReason = "AddressNotAssigned" ) // ListenerStatus is the status associated with a Listener. @@ -744,7 +750,6 @@ const ( // // * "PortUnavailable" // * "UnsupportedProtocol" - // * "UnsupportedAddress" // // Possible reasons for this condition to be Unknown are: // @@ -780,14 +785,6 @@ const ( // Listener could not be attached to be Gateway because its // protocol type is not supported. ListenerReasonUnsupportedProtocol ListenerConditionReason = "UnsupportedProtocol" - - // This reason is used with the "Accepted" condition when the Listener could - // not be attached to the Gateway because the requested address is not - // supported. This reason could be used in a number of instances, including: - // - // * The address is already in use. - // * The type of address is not supported by the implementation. - ListenerReasonUnsupportedAddress ListenerConditionReason = "UnsupportedAddress" ) const ( @@ -869,34 +866,6 @@ const ( // This reason is used with the "Programmed" condition when the condition is // true. ListenerReasonProgrammed ListenerConditionReason = "Programmed" -) - -const ( - // Ready is an optional Condition that has Extended support. When it's set, - // the condition indicates whether the Listener has been configured on the - // Gateway and traffic is ready to flow through the data plane immediately. - // - // Possible reasons for this condition to be True are: - // - // * "Ready" - // - // Possible reasons for this condition to be False are: - // - // * "Invalid" - // * "Pending" - // - // Possible reasons for this condition to be Unknown are: - // - // * "Pending" - // - // Controllers may raise this condition with other reasons, - // but should prefer to use the reasons listed above to improve - // interoperability. - ListenerConditionReady ListenerConditionType = "Ready" - - // This reason is used with the "Ready" condition when the condition is - // true. - ListenerReasonReady ListenerConditionReason = "Ready" // This reason is used with the "Ready" and "Programmed" conditions when the // Listener is syntactically or semantically invalid. @@ -907,3 +876,22 @@ const ( // online and ready to accept client traffic. ListenerReasonPending ListenerConditionReason = "Pending" ) + +const ( + // "Ready" is a condition type reserved for future use. It should not be used by implementations. + // Note: This condition is not really "deprecated", but rather "reserved"; however, deprecated triggers Go linters + // to alert about usage. + // + // If used in the future, "Ready" will represent the final state where all configuration is confirmed good + // _and has completely propagated to the data plane_. That is, it is a _guarantee_ that, as soon as something + // sees the Condition as `true`, then connections will be correctly routed _immediately_. + // + // This is a very strong guarantee, and to date no implementation has satisfied it enough to implement it. + // This reservation can be discussed in the future if necessary. + // + // Deprecated: Ready is reserved for future use + ListenerConditionReady ListenerConditionType = "Ready" + + // Deprecated: Ready is reserved for future use + ListenerReasonReady ListenerConditionReason = "Ready" +) diff --git a/apis/v1beta1/gatewayclass_types.go b/apis/v1beta1/gatewayclass_types.go index 9507e5a529..f20487bfa5 100644 --- a/apis/v1beta1/gatewayclass_types.go +++ b/apis/v1beta1/gatewayclass_types.go @@ -42,7 +42,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. diff --git a/apis/v1beta1/httproute_types.go b/apis/v1beta1/httproute_types.go index 1dc7e2ede0..96f057465d 100644 --- a/apis/v1beta1/httproute_types.go +++ b/apis/v1beta1/httproute_types.go @@ -156,10 +156,13 @@ type HTTPRouteRule struct { // ties. Across all rules specified on applicable Routes, precedence must be // given to the match with the largest number of: // - // * Characters in a matching path. + // * Characters in a matching "Exact" path match + // * Characters in a matching "Prefix" path match // * Header matches. // * Query param matches. // + // Note: The precedence of RegularExpression path matches are implementation-specific. + // // If ties still exist across multiple Routes, matching precedence MUST be // determined in order of the following criteria, continuing on ties: // @@ -232,6 +235,8 @@ type HTTPRouteRule struct { // // Support: Core for Kubernetes Service // + // Support: Extended for Kubernetes ServiceImport + // // Support: Implementation-specific for any other resource // // Support for weight: Core @@ -343,12 +348,8 @@ const ( // // - ":method" - ":" is an invalid character. This means that HTTP/2 pseudo // 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|~]+$` -type HTTPHeaderName string +// - "/invalid" - "/ " is an invalid character +type HTTPHeaderName HeaderName // HTTPHeaderMatch describes how to select a HTTP route by matching HTTP request // headers. @@ -449,10 +450,7 @@ type HTTPQueryParamMatch struct { // // Users SHOULD NOT route traffic based on repeated query params to guard // themselves against potential differences in the implementations. - // - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=256 - Name string `json:"name"` + Name HTTPHeaderName `json:"name"` // Value is the value of HTTP query param to be matched. // @@ -587,8 +585,7 @@ type HTTPRouteFilter struct { // Reason of `UnsupportedValue`. // // +unionDiscriminator - // +kubebuilder:validation:Enum=RequestHeaderModifier;RequestMirror;RequestRedirect;ExtensionRef - // + // +kubebuilder:validation:Enum=RequestHeaderModifier;ResponseHeaderModifier;RequestMirror;RequestRedirect;URLRewrite;ExtensionRef Type HTTPRouteFilterType `json:"type"` // RequestHeaderModifier defines a schema for a filter that modifies request @@ -605,7 +602,6 @@ type HTTPRouteFilter struct { // Support: Extended // // +optional - // ResponseHeaderModifier *HTTPHeaderFilter `json:"responseHeaderModifier,omitempty"` // RequestMirror defines a schema for a filter that mirrors requests. @@ -629,7 +625,6 @@ type HTTPRouteFilter struct { // // Support: Extended // - // // +optional URLRewrite *HTTPURLRewriteFilter `json:"urlRewrite,omitempty"` @@ -662,7 +657,6 @@ const ( // Support in HTTPRouteRule: Extended // // Support in HTTPBackendRef: Extended - // HTTPRouteFilterResponseHeaderModifier HTTPRouteFilterType = "ResponseHeaderModifier" // HTTPRouteFilterRequestRedirect can be used to redirect a request to @@ -683,8 +677,6 @@ const ( // Support in HTTPRouteRule: Extended // // Support in HTTPBackendRef: Extended - // - // HTTPRouteFilterURLRewrite HTTPRouteFilterType = "URLRewrite" // HTTPRouteFilterRequestMirror can be used to mirror HTTP requests to a @@ -821,7 +813,6 @@ const ( ) // HTTPPathModifier defines configuration for path modifiers. -// type HTTPPathModifier struct { // Type defines the type of path modifier. Additional types may be // added in a future release of the API. @@ -833,14 +824,12 @@ type HTTPPathModifier struct { // Accepted Condition for the Route to `status: False`, with a // Reason of `UnsupportedValue`. // - // // +kubebuilder:validation:Enum=ReplaceFullPath;ReplacePrefixMatch Type HTTPPathModifierType `json:"type"` // ReplaceFullPath specifies the value with which to replace the full path // of a request during a rewrite or redirect. // - // // +kubebuilder:validation:MaxLength=1024 // +optional ReplaceFullPath *string `json:"replaceFullPath,omitempty"` @@ -855,7 +844,6 @@ type HTTPPathModifier struct { // ignored. For example, the paths `/abc`, `/abc/`, and `/abc/def` would all // match the prefix `/abc`, but the path `/abcd` would not. // - // // +kubebuilder:validation:MaxLength=1024 // +optional ReplacePrefixMatch *string `json:"replacePrefixMatch,omitempty"` @@ -882,7 +870,7 @@ type HTTPRequestRedirectFilter struct { // Hostname is the hostname to be used in the value of the `Location` // header in the response. - // When empty, the hostname of the request is used. + // When empty, the hostname in the `Host` header of the request is used. // // Support: Core // @@ -895,13 +883,21 @@ type HTTPRequestRedirectFilter struct { // // Support: Extended // - // // +optional Path *HTTPPathModifier `json:"path,omitempty"` // Port is the port to be used in the value of the `Location` // header in the response. - // When empty, port (if specified) of the request is used. + // + // When empty, the Gateway Listener port is used. + // + // Implementations SHOULD NOT add the port number in the 'Location' + // header in the following cases: + // + // * A Location header that will use HTTP (whether that is determined via + // the Listener protocol or the Scheme field) _and_ use port 80. + // * A Location header that will use HTTPS (whether that is determined via + // the Listener protocol or the Scheme field) _and_ use port 443. // // Support: Extended // @@ -930,15 +926,12 @@ type HTTPRequestRedirectFilter struct { // MUST NOT be used on the same Route rule as a HTTPRequestRedirect filter. // // Support: Extended -// -// type HTTPURLRewriteFilter struct { // Hostname is the value to be used to replace the Host header value during // forwarding. // // Support: Extended // - // // +optional Hostname *PreciseHostname `json:"hostname,omitempty"` @@ -946,7 +939,6 @@ type HTTPURLRewriteFilter struct { // // Support: Extended // - // // +optional Path *HTTPPathModifier `json:"path,omitempty"` } diff --git a/apis/v1beta1/object_reference_types.go b/apis/v1beta1/object_reference_types.go index f631e1ec70..ac92d292d8 100644 --- a/apis/v1beta1/object_reference_types.go +++ b/apis/v1beta1/object_reference_types.go @@ -53,7 +53,7 @@ type SecretObjectReference struct { // +kubebuilder:default="" Group *Group `json:"group"` - // Kind is kind of the referent. For example "HTTPRoute" or "Service". + // Kind is kind of the referent. For example "Secret". // // +optional // +kubebuilder:default=Secret @@ -99,9 +99,21 @@ type BackendObjectReference struct { // +kubebuilder:default="" Group *Group `json:"group,omitempty"` - // Kind is kind of the referent. For example "HTTPRoute" or "Service". + // Kind is the Kubernetes resource kind of the referent. For example + // "Service". + // // Defaults to "Service" when not specified. // + // ExternalName services can refer to CNAME DNS records that may live + // outside of the cluster and as such are difficult to reason about in + // terms of conformance. They also may not be safe to forward to (see + // CVE-2021-25740 for more information). Implementations SHOULD NOT + // support ExternalName Services. + // + // Support: Core (Services with a type other than ExternalName) + // + // Support: Implementation-specific (Services with type ExternalName) + // // +optional // +kubebuilder:default=Service Kind *Kind `json:"kind,omitempty"` diff --git a/apis/v1beta1/shared_types.go b/apis/v1beta1/shared_types.go index 2cd09f0832..b6ca27f6c6 100644 --- a/apis/v1beta1/shared_types.go +++ b/apis/v1beta1/shared_types.go @@ -546,6 +546,14 @@ type AnnotationValue string // +kubebuilder:validation:Pattern=`^Hostname|IPAddress|NamedAddress|[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$` type AddressType string +// HeaderName is the name of a header or query parameter. +// +// +kubebuilder:validation:MinLength=1 +// +kubebuilder:validation:MaxLength=256 +// +kubebuilder:validation:Pattern=`^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$` +// +k8s:deepcopy-gen=false +type HeaderName string + const ( // A textual representation of a numeric IP address. IPv4 // addresses must be in dotted-decimal form. IPv6 addresses diff --git a/apis/v1beta1/validation/gateway.go b/apis/v1beta1/validation/gateway.go index 60d7f55305..cef9f7387d 100644 --- a/apis/v1beta1/validation/gateway.go +++ b/apis/v1beta1/validation/gateway.go @@ -19,6 +19,7 @@ package validation import ( "fmt" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -69,6 +70,8 @@ func validateGatewayListeners(listeners []gatewayv1b1.Listener, path *field.Path errs = append(errs, ValidateListenerTLSConfig(listeners, path)...) errs = append(errs, validateListenerHostname(listeners, path)...) errs = append(errs, ValidateTLSCertificateRefs(listeners, path)...) + errs = append(errs, ValidateListenerNames(listeners, path)...) + errs = append(errs, validateHostnameProtocolPort(listeners, path)...) return errs } @@ -118,3 +121,39 @@ func ValidateTLSCertificateRefs(listeners []gatewayv1b1.Listener, path *field.Pa } return errs } + +// ValidateListenerNames validates the names of the listeners +// must be unique within the Gateway +func ValidateListenerNames(listeners []gatewayv1b1.Listener, path *field.Path) field.ErrorList { + var errs field.ErrorList + nameMap := make(map[gatewayv1b1.SectionName]struct{}, len(listeners)) + for i, c := range listeners { + if _, found := nameMap[c.Name]; found { + errs = append(errs, field.Duplicate(path.Index(i).Child("name"), fmt.Sprintln("must be unique within the Gateway"))) + } + nameMap[c.Name] = struct{}{} + } + return errs +} + +// validateHostnameProtocolPort validates that the combination of port, protocol, and hostname are +// unique for each listener. +func validateHostnameProtocolPort(listeners []gatewayv1b1.Listener, path *field.Path) field.ErrorList { + var errs field.ErrorList + hostnameProtocolPortSets := sets.Set[string]{} + for i, listener := range listeners { + hostname := new(gatewayv1b1.Hostname) + if listener.Hostname != nil { + hostname = listener.Hostname + } + protocol := listener.Protocol + port := listener.Port + hostnameProtocolPort := fmt.Sprintf("%s:%s:%d", *hostname, protocol, port) + if hostnameProtocolPortSets.Has(hostnameProtocolPort) { + errs = append(errs, field.Duplicate(path.Index(i), fmt.Sprintln("combination of port, protocol, and hostname must be unique for each listener"))) + } else { + hostnameProtocolPortSets.Insert(hostnameProtocolPort) + } + } + return errs +} diff --git a/apis/v1beta1/validation/gateway_test.go b/apis/v1beta1/validation/gateway_test.go index dfb3a1877c..746e6a16d6 100644 --- a/apis/v1beta1/validation/gateway_test.go +++ b/apis/v1beta1/validation/gateway_test.go @@ -134,6 +134,119 @@ func TestValidateGateway(t *testing.T) { }, expectErrsOnFields: []string{"spec.listeners[0].tls.certificateRefs"}, }, + "names are not unique within the Gateway": { + mutate: func(gw *gatewayv1b1.Gateway) { + hostnameFoo := gatewayv1b1.Hostname("foo.com") + hostnameBar := gatewayv1b1.Hostname("bar.com") + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Hostname = &hostnameFoo + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "foo", + Hostname: &hostnameBar, + }, + ) + }, + expectErrsOnFields: []string{"spec.listeners[1].name"}, + }, + "combination of port, protocol, and hostname are not unique for each listener": { + mutate: func(gw *gatewayv1b1.Gateway) { + hostnameFoo := gatewayv1b1.Hostname("foo.com") + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Hostname = &hostnameFoo + gw.Spec.Listeners[0].Protocol = gatewayv1b1.HTTPProtocolType + gw.Spec.Listeners[0].Port = 80 + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "bar", + Hostname: &hostnameFoo, + Protocol: gatewayv1b1.HTTPProtocolType, + Port: 80, + }, + ) + }, + expectErrsOnFields: []string{"spec.listeners[1]"}, + }, + "combination of port and protocol are not unique for each listenr when hostnames not set": { + mutate: func(gw *gatewayv1b1.Gateway) { + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Protocol = gatewayv1b1.HTTPProtocolType + gw.Spec.Listeners[0].Port = 80 + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "bar", + Protocol: gatewayv1b1.HTTPProtocolType, + Port: 80, + }, + ) + }, + expectErrsOnFields: []string{"spec.listeners[1]"}, + }, + "port is unique when protocol and hostname are the same": { + mutate: func(gw *gatewayv1b1.Gateway) { + hostnameFoo := gatewayv1b1.Hostname("foo.com") + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Hostname = &hostnameFoo + gw.Spec.Listeners[0].Protocol = gatewayv1b1.HTTPProtocolType + gw.Spec.Listeners[0].Port = 80 + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "bar", + Hostname: &hostnameFoo, + Protocol: gatewayv1b1.HTTPProtocolType, + Port: 8080, + }, + ) + }, + expectErrsOnFields: nil, + }, + "hostname is unique when protocol and port are the same": { + mutate: func(gw *gatewayv1b1.Gateway) { + hostnameFoo := gatewayv1b1.Hostname("foo.com") + hostnameBar := gatewayv1b1.Hostname("bar.com") + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Hostname = &hostnameFoo + gw.Spec.Listeners[0].Protocol = gatewayv1b1.HTTPProtocolType + gw.Spec.Listeners[0].Port = 80 + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "bar", + Hostname: &hostnameBar, + Protocol: gatewayv1b1.HTTPProtocolType, + Port: 80, + }, + ) + }, + expectErrsOnFields: nil, + }, + "protocol is unique when port and hostname are the same": { + mutate: func(gw *gatewayv1b1.Gateway) { + hostnameFoo := gatewayv1b1.Hostname("foo.com") + tlsConfigFoo := tlsConfig + tlsModeFoo := gatewayv1b1.TLSModeType("Terminate") + tlsConfigFoo.Mode = &tlsModeFoo + tlsConfigFoo.CertificateRefs = []gatewayv1b1.SecretObjectReference{ + { + Name: "FooCertificateRefs", + }, + } + gw.Spec.Listeners[0].Name = "foo" + gw.Spec.Listeners[0].Hostname = &hostnameFoo + gw.Spec.Listeners[0].Protocol = gatewayv1b1.HTTPSProtocolType + gw.Spec.Listeners[0].Port = 8000 + gw.Spec.Listeners[0].TLS = &tlsConfigFoo + gw.Spec.Listeners = append(gw.Spec.Listeners, + gatewayv1b1.Listener{ + Name: "bar", + Hostname: &hostnameFoo, + Protocol: gatewayv1b1.TLSProtocolType, + Port: 8000, + TLS: &tlsConfigFoo, + }, + ) + }, + expectErrsOnFields: nil, + }, } for name, tc := range testCases {