|
| 1 | +# GEP-718: Rework forwardTo segment in routes |
| 2 | + |
| 3 | +Issue URL: https://github.com/kubernetes-sigs/gateway-api/issues/718 |
| 4 | + |
| 5 | +Status: implementable |
| 6 | + |
| 7 | +## Motivation |
| 8 | + |
| 9 | +### Complications due to the `serviceName` shortcut |
| 10 | + |
| 11 | +Within `RouteForwardTo` (and `HTTPRouteForwardTo` by extension) there exists |
| 12 | +two mechanisms to specify the upstream network endpoint where the traffic should |
| 13 | +be forwarded to: |
| 14 | +- `ServiceName`: the name of the Kubernetes Service |
| 15 | +- `BackendRef`: a LocalObjectReference to a resource, this is an extension point in the API |
| 16 | + |
| 17 | +While working on [#685](https://github.com/kubernetes-sigs/gateway-api/pull/685), |
| 18 | +it came to light that: |
| 19 | +1. `BackendRef` itself could be used to point to a Kubernetes Service |
| 20 | +2. With [GEP-709](https://github.com/kubernetes-sigs/gateway-api/pull/711), |
| 21 | +there will be a need to introduce a new `namespace` field which will enable use-cases to forward |
| 22 | +traffic to a services and backends located in different namespaces |
| 23 | + |
| 24 | +While (1) can be fixed with more validation and documentation, it only solves for |
| 25 | +the specific case. (2) requires adding two fields: `serviceNamespace` |
| 26 | +and `BackendRef.Namespace` - something we would like to avoid since the `serviceName` |
| 27 | +field was introduced only as a shortcut and adding more service-specific fields |
| 28 | +defeats the original purpose. |
| 29 | + |
| 30 | +These problems are in addition to the original problem that |
| 31 | +[#685](https://github.com/kubernetes-sigs/gateway-api/pull/685) attempts to solve: |
| 32 | +clarifying port requirements when a port is required or not. |
| 33 | +We have been updating documentation to tackle such corner-cases but that |
| 34 | +continues to result in more and more elaborations. Some excerpts from our |
| 35 | +existing documentation: |
| 36 | +``` |
| 37 | + // ServiceName refers to the name of the Service to mirror matched requests |
| 38 | + // to. When specified, this takes the place of BackendRef. If both |
| 39 | + // BackendRef and ServiceName are specified, ServiceName will be given |
| 40 | + // precedence. |
| 41 | +
|
| 42 | + // BackendRef is a local object reference to mirror matched requests to. If |
| 43 | + // both BackendRef and ServiceName are specified, ServiceName will be given |
| 44 | + // precedence. |
| 45 | +
|
| 46 | + // Weight specifies the proportion of HTTP requests forwarded to the backend |
| 47 | + // referenced by the ServiceName or BackendRef field. This is computed as |
| 48 | +
|
| 49 | + // Port specifies the destination port number to use for the |
| 50 | + // backend referenced by the ServiceName or BackendRef field. |
| 51 | + // If unspecified, the destination port in the request is used |
| 52 | + // when forwarding to a backendRef or serviceName. |
| 53 | +``` |
| 54 | + |
| 55 | +### Simplifying `RouteForwardTo` |
| 56 | + |
| 57 | +RouteForwardTo is not the best of the names. Using a noun instead of a verb |
| 58 | +would help with in-person communication and documentation. |
| 59 | +Since we are already considering dropping the special case of `service` as a |
| 60 | +backend, we have an opportunity to further simplify `RouteForwardTo` for better |
| 61 | +UX. |
| 62 | + |
| 63 | +## Proposal |
| 64 | + |
| 65 | +- Remove the `ServiceName` field from `RouteForwardTo` |
| 66 | +- Introduce `Service` as a default for `BackendRef.Kind` |
| 67 | +- Rename `RouteForwardTo.BackendRef` to `RouteForwardTo.TargetRef` |
| 68 | +- Rename `RouteForwardTo` to `Backend` |
| 69 | +- Rename `Route.Spec.Rules[].ForwardTo[]` to `Route.Spec.Rules[].Backends[]` in |
| 70 | + all routes |
| 71 | +- Apply the same to HTTP types and filters |
| 72 | + |
| 73 | +## API |
| 74 | + |
| 75 | +The updated `Backend` (formerly `RouteForwardTo`) struct will be: |
| 76 | +(HTTP version has been omitted for brevity) |
| 77 | + |
| 78 | +```go |
| 79 | +// Backend defines how and where a request should be forwarded from the Gateway. |
| 80 | +type Backend struct { |
| 81 | + // TargetRef is a reference to the resource to forward matched requests to. |
| 82 | + // |
| 83 | + // If the referent cannot be found, the rule is not included in the route. |
| 84 | + // The controller should raise the "ResolvedRefs" condition on the Gateway |
| 85 | + // with the "DegradedRoutes" reason. The gateway status for this route should |
| 86 | + // be updated with a condition that describes the error more specifically. |
| 87 | + // |
| 88 | + // The protocol to use is defined using AppProtocol field (introduced in |
| 89 | + // Kubernetes 1.18) in the Service resource. In the absence of the |
| 90 | + // AppProtocol field a `gateway.networking.k8s.io/app-protocol` annotation on the |
| 91 | + // BackendPolicy resource may be used to define the protocol. If the |
| 92 | + // AppProtocol field is available, this annotation should not be used. The |
| 93 | + // AppProtocol field, when populated, takes precedence over the annotation |
| 94 | + // in the BackendPolicy resource. For custom backends, it is encouraged to |
| 95 | + // add a semantically-equivalent field in the Custom Resource Definition. |
| 96 | + // |
| 97 | + // Support: Core (Kubernetes Service) |
| 98 | + // Support: Custom (any other resource) |
| 99 | + // |
| 100 | + // +optional |
| 101 | + // +kubebuilder:default={kind: "service"} |
| 102 | + TargetRef *LocalObjectReference `json:"targetRef,omitempty"` |
| 103 | + |
| 104 | + // Port specifies the destination port number to use for the |
| 105 | + // backend referenced by the TargetRef field. |
| 106 | + // This field is required when the target is a Kubernetes Service. |
| 107 | + // |
| 108 | + // Support: Core |
| 109 | + // |
| 110 | + // +optional |
| 111 | + Port *PortNumber `json:"port,omitempty"` |
| 112 | + |
| 113 | + // Weight specifies the proportion of HTTP requests forwarded to the backend |
| 114 | + // referenced by the TargetRef field. This is computed as |
| 115 | + // weight/(sum of all weights in this Backends list). For non-zero values, |
| 116 | + // there may be some epsilon from the exact proportion defined here |
| 117 | + // depending on the precision an implementation supports. Weight is not a |
| 118 | + // percentage and the sum of weights does not need to equal 100. |
| 119 | + // |
| 120 | + // If only one backend is specified and it has a weight greater than 0, 100% |
| 121 | + // of the traffic is forwarded to that backend. If weight is set to 0, no |
| 122 | + // traffic should be forwarded for this entry. If unspecified, weight |
| 123 | + // defaults to 1. |
| 124 | + // |
| 125 | + // Support: Extended |
| 126 | + // |
| 127 | + // +optional |
| 128 | + // +kubebuilder:default=1 |
| 129 | + // +kubebuilder:validation:Minimum=0 |
| 130 | + // +kubebuilder:validation:Maximum=1000000 |
| 131 | + Weight *int32 `json:"weight,omitempty"` |
| 132 | +} |
| 133 | + |
| 134 | +``` |
| 135 | + |
| 136 | +A summarized diff for the changes being proposed: |
| 137 | + |
| 138 | +```patch |
| 139 | +diff --git a/apis/v1alpha2/shared_types.go b/apis/v1alpha2/shared_types.go |
| 140 | +index 1215300..c5afddb 100644 |
| 141 | +--- a/apis/v1alpha2/shared_types.go |
| 142 | ++++ b/apis/v1alpha2/shared_types.go |
| 143 | +@@ -94,12 +94,9 @@ type GatewayReference struct { |
| 144 | + Namespace string `json:"namespace"` |
| 145 | + } |
| 146 | + |
| 147 | +-// RouteForwardTo defines how a Route should forward a request. |
| 148 | +-type RouteForwardTo struct { |
| 149 | +- // ServiceName refers to the name of the Service to forward matched requests |
| 150 | +- // to. When specified, this takes the place of BackendRef. If both |
| 151 | +- // BackendRef and ServiceName are specified, ServiceName will be given |
| 152 | +- // precedence. |
| 153 | ++// Backend defines how and where a request should be forwarded from the Gateway. |
| 154 | ++type Backend struct { |
| 155 | ++ // TargetRef is a reference to the resource to forward matched requests to. |
| 156 | + // |
| 157 | + // If the referent cannot be found, the rule is not included in the route. |
| 158 | + // The controller should raise the "ResolvedRefs" condition on the Gateway |
| 159 | +@@ -115,30 +112,16 @@ type RouteForwardTo struct { |
| 160 | + // in the BackendPolicy resource. For custom backends, it is encouraged to |
| 161 | + // add a semantically-equivalent field in the Custom Resource Definition. |
| 162 | + // |
| 163 | +- // Support: Core |
| 164 | +- // |
| 165 | +- // +optional |
| 166 | +- // +kubebuilder:validation:MaxLength=253 |
| 167 | +- ServiceName *string `json:"serviceName,omitempty"` |
| 168 | +- |
| 169 | +- // BackendRef is a reference to a backend to forward matched requests to. If |
| 170 | +- // both BackendRef and ServiceName are specified, ServiceName will be given |
| 171 | +- // precedence. |
| 172 | +- // |
| 173 | +- // If the referent cannot be found, the rule is not included in the route. |
| 174 | +- // The controller should raise the "ResolvedRefs" condition on the Gateway |
| 175 | +- // with the "DegradedRoutes" reason. The gateway status for this route should |
| 176 | +- // be updated with a condition that describes the error more specifically. |
| 177 | +- // |
| 178 | +- // Support: Custom |
| 179 | ++ // Support: Core (Kubernetes Service) |
| 180 | ++ // Support: Custom (any other resource) |
| 181 | + // |
| 182 | + // +optional |
| 183 | +- BackendRef *LocalObjectReference `json:"backendRef,omitempty"` |
| 184 | ++ // +kubebuilder:default={kind: "service"} |
| 185 | ++ TargetRef *LocalObjectReference `json:"targetRef,omitempty"` |
| 186 | + |
| 187 | + // Port specifies the destination port number to use for the |
| 188 | +- // backend referenced by the ServiceName or BackendRef field. |
| 189 | +- // If unspecified, the destination port in the request is used |
| 190 | +- // when forwarding to a backendRef or serviceName. |
| 191 | ++ // backend referenced by the TargetRef field. |
| 192 | ++ // This field is required when the target is a Kubernetes Service. |
| 193 | + // |
| 194 | + // Support: Core |
| 195 | + // |
| 196 | +@@ -146,8 +129,8 @@ type RouteForwardTo struct { |
| 197 | + Port *PortNumber `json:"port,omitempty"` |
| 198 | + |
| 199 | + // Weight specifies the proportion of HTTP requests forwarded to the backend |
| 200 | +- // referenced by the ServiceName or BackendRef field. This is computed as |
| 201 | +- // weight/(sum of all weights in this ForwardTo list). For non-zero values, |
| 202 | ++ // referenced by the TargetRef field. This is computed as |
| 203 | ++ // weight/(sum of all weights in this Backends list). For non-zero values, |
| 204 | + // there may be some epsilon from the exact proportion defined here |
| 205 | + // depending on the precision an implementation supports. Weight is not a |
| 206 | + // percentage and the sum of weights does not need to equal 100. |
| 207 | +``` |
| 208 | + |
| 209 | +## Examples |
| 210 | + |
| 211 | + |
| 212 | +For Kubernetes Services, an updated `forwardTo` section will read as follows: |
| 213 | + |
| 214 | +```yaml |
| 215 | +... |
| 216 | +backends: |
| 217 | +- targetRef: |
| 218 | + name: foo-service-v1 |
| 219 | + port: 80 |
| 220 | + weight: 80 |
| 221 | +- targetRef: |
| 222 | + name: foo-service-canary |
| 223 | + port: 80 |
| 224 | + weight: 20 |
| 225 | +... |
| 226 | +``` |
| 227 | + |
| 228 | +Here, the `kind` field is omitted as it will be injected as a default. |
| 229 | + |
| 230 | +For custom backends, the API will look like the following: |
| 231 | + |
| 232 | +```yaml |
| 233 | +... |
| 234 | +backends: |
| 235 | +- targetRef: |
| 236 | + name: foo-v1 |
| 237 | + kind: server |
| 238 | + group: networking.acme.io |
| 239 | + port: 80 |
| 240 | + weight: 80 |
| 241 | +- targetRef: |
| 242 | + name: foo-v1-canary |
| 243 | + kind: server |
| 244 | + group: networking.acme.io |
| 245 | + port: 80 |
| 246 | + weight: 20 |
| 247 | +... |
| 248 | +``` |
| 249 | + |
| 250 | +For completeness, here is an example of HTTPRoute: |
| 251 | + |
| 252 | +```yaml |
| 253 | +kind: HTTPRoute |
| 254 | +apiVersion: gateway.networking.k8s.io/v1alpha2 |
| 255 | +metadata: |
| 256 | + name: bar-route |
| 257 | + labels: |
| 258 | + gateway: prod-web-gw |
| 259 | +spec: |
| 260 | + hostnames: |
| 261 | + - "bar.example.com" |
| 262 | + rules: |
| 263 | + - matches: |
| 264 | + - headers: |
| 265 | + type: Exact |
| 266 | + values: |
| 267 | + env: canary |
| 268 | + backends: |
| 269 | + - targetRef: |
| 270 | + name: foo-service-v1 |
| 271 | + port: 80 |
| 272 | + weight: 80 |
| 273 | + - targetRef: |
| 274 | + name: foo-service-canary |
| 275 | + port: 80 |
| 276 | + weight: 20 |
| 277 | +``` |
| 278 | +
|
| 279 | +## Alternatives considered |
| 280 | +
|
| 281 | +### Rename serviceName to backendName |
| 282 | +
|
| 283 | +As suggested in [this comment](https://github.com/kubernetes-sigs/gateway-api/pull/685#discussion_r667285837), |
| 284 | +we could buy the best of both the worlds by introducing `backendName`: |
| 285 | + |
| 286 | +```yaml |
| 287 | +forwardTo: |
| 288 | +- backendName: foo |
| 289 | + port: 80 |
| 290 | + kind: <defaults to Service> |
| 291 | +``` |
| 292 | + |
| 293 | +While this saves one line of YAML (`backendRef:`), it could be potentially |
| 294 | +violating the `Naming of the reference field` |
| 295 | +[API convention][api-convetions]. |
| 296 | +Most of our object references are of the form `XRef`. |
| 297 | + |
| 298 | +### Flatten `TargetRef` into `Backend` itself |
| 299 | + |
| 300 | +A further simplification is possible in this case if the `TargetRef` struct |
| 301 | +is embedded directly inside `Backend`: |
| 302 | + |
| 303 | +```yaml |
| 304 | +... |
| 305 | +backends: |
| 306 | +- name: foo-service-v1 |
| 307 | + port: 80 |
| 308 | + weight: 80 |
| 309 | +- name: foo-service-canary |
| 310 | + port: 80 |
| 311 | + weight: 20 |
| 312 | +... |
| 313 | +``` |
| 314 | + |
| 315 | +This result in saving of one line of YAML per backend. |
| 316 | +This has been avoided for the following reasons: |
| 317 | +- Separate the fields that are present for object referencing purposes from |
| 318 | + fields that are specific to Gateway API and influence routing behavior. |
| 319 | +- [API conventions][api-conventions] are unclear about folding such fields |
| 320 | + directly within a struct that contains other information. Upstream APIs |
| 321 | + seem to separate out references from other fields via a `fooRef` struct. |
| 322 | +- If we were to do this, this would be a new pattern within this API as there |
| 323 | + is no other place where such a flattening is performed. Future maintainers |
| 324 | + could reference this pattern and further worsen the API experience. |
| 325 | + |
| 326 | +## Out of scope |
| 327 | + |
| 328 | +N/A |
| 329 | + |
| 330 | +## References |
| 331 | + |
| 332 | +Existing documentation: |
| 333 | +- [RouteForwardTo](https://github.com/kubernetes-sigs/gateway-api/blob/a9d45b51c396fbed022204af0185b00a4ac7e282/apis/v1alpha2/shared_types.go#L97-L167) |
| 334 | +- [HTTPRouteForwardTo](https://github.com/kubernetes-sigs/gateway-api/blob/a9d45b51c396fbed022204af0185b00a4ac7e282/apis/v1alpha2/httproute_types.go#L731-L813) |
| 335 | +- [#685](https://github.com/kubernetes-sigs/gateway-api/pull/685) |
| 336 | + - [Comment thread that spawned this GEP](https://github.com/kubernetes-sigs/gateway-api/pull/685#discussion_r640204333) |
| 337 | +- [#578](https://github.com/kubernetes-sigs/gateway-api/issues/578) |
| 338 | + |
| 339 | +[api-conventions]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#naming-of-the-reference-field |
0 commit comments