Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TCPRoute support from Gateway API #1103

Merged
merged 2 commits into from
Mar 29, 2024
Merged
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
95 changes: 84 additions & 11 deletions docs/content/en/docs/configuration/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ description: >

The following steps configure the Kubernetes cluster and HAProxy Ingress to read and parse Gateway API resources:

* Manually install the Gateway API CRDs from the standard channel. See the Gateway API [documentation](https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api)
* ... or simply `kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml`
* Manually install the Gateway API CRDs from the experimental channel - HAProxy Ingress supports TCPRoute which is not included in the standard channel. See the Gateway API [documentation](https://gateway-api.sigs.k8s.io/guides/#installing-gateway-api)
* ... or simply `kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml`
* `v1.0.0` is just a reference for a fresh new deployment, Gateway API `v0.4.0` or any newer versions are supported.
* Start (or restart) the controller

Expand All @@ -25,9 +25,9 @@ Gateway API `v1alpha2`, `v1beta1` and `v1` specs are partially implemented in v0

* Target Services can be annotated with [Backend or Path scoped]({{% relref "keys#scope" %}}) configuration keys, this will continue to be supported.
* Gateway API resources doesn't support annotations, this is planned to continue to be unsupported. Extensions to the Gateway API spec will be added in the extension points of the API.
* Only the `GatewayClass`, `Gateway` and `HTTPRoute` resource definitions are implemented.
* Only the `GatewayClass`, `Gateway`, `TCPRoute` and `HTTPRoute` resource definitions are implemented.
* The controller doesn't implement partial parsing yet for Gateway API resources, changes should be a bit slow on clusters with thousands of Ingress, Gateway API resources or Services.
* Gateway's Listener Port and Protocol are not implemented - Port uses the global [bind-port]({{% relref "keys#bind-port" %}}) configuration and Protocol is based on the presence or absence of the TLS attribute.
* Gateway's Listener Port and Protocol are implemented for TCPRoute, but they are not implemented for HTTPRoute - for HTTP workloads, Port uses the global [bind-port]({{% relref "keys#bind-port" %}}) configuration and Protocol is based on the presence or absence of the TLS attribute.
* Gateway's Addresses is not implemented - binding addresses use the global [bind-ip-addr]({{% relref "keys#bind-ip-addr" %}}) configuration.
* Gateway's Hostname only supports empty/absence of Hostname or a single `*`, any other string will override the HTTPRoute Hostnames configuration without any merging.
* HTTPRoute's Rules and BackendRefs don't support Filters.
Expand All @@ -51,14 +51,13 @@ Add the following steps to the [Getting Started guide]({{% relref "/docs/getting
[Manually install](https://gateway-api.sigs.k8s.io/v1alpha2/guides/getting-started/#installing-gateway-api-crds-manually) the Gateway API CRDs:

```
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml
```

Add the following deployment and service if echoserver isn't running yet:
Restart HAProxy Ingress so it can find the just installed APIs:

```
kubectl --namespace default create deployment echoserver --image k8s.gcr.io/echoserver:1.3
kubectl --namespace default expose deployment echoserver --port=8080
kubectl --namespace ingress-controller delete pod -lapp.kubernetes.io/name=haproxy-ingress
```

A GatewayClass enables Gateways to be read and parsed by HAProxy Ingress. Create a GatewayClass with the following content:
Expand All @@ -72,7 +71,16 @@ spec:
controllerName: haproxy-ingress.github.io/controller
```

Gateways create listeners and allow to configure hostnames. Create a Gateway with the following content:
### Deploy HTTP workload

Add the following deployment and service if echoserver isn't running yet:

```
kubectl --namespace default create deployment echoserver --image k8s.gcr.io/echoserver:1.3
kubectl --namespace default expose deployment echoserver --port=8080
```

Gateways create listeners and allow to configure hostnames for HTTP workloads. Create a Gateway with the following content:

Note: port and protocol attributes [have some limitations](#conformance).

Expand All @@ -96,8 +104,6 @@ HTTPRoutes configure the hostnames and target services. Create a HTTPRoute with
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
labels:
gateway: echo
name: echoserver
namespace: default
spec:
Expand All @@ -117,3 +123,70 @@ Send a request to our just configured route:
curl http://echoserver-from-gateway.local
wget -qO- http://echoserver-from-gateway.local
```

### Deploy TCP workload

Add the following deployment and service:

```
kubectl --namespace default create deployment redis --image docker.io/redis
kubectl --namespace default expose deployment redis --port=6379
```

A new port need to be added if HAProxy Ingress is not configured in the host network. If so, add the following snippet in `values.yaml` and apply it using Helm:

```
controller:
...
service:
...
extraPorts:
- port: 6379
targetPort: 6379
```

Gateways create listeners and allow to configure the listening port for TCP workloads. Create a Gateway with the following content:

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: redis
namespace: default
spec:
gatewayClassName: haproxy
listeners:
- name: redis-gw
port: 6379
protocol: TCP
```

TCPRoutes configure the target services. Create a TCPRoute with the following content:

```yaml
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: redis
namespace: default
spec:
parentRefs:
- name: redis
rules:
- backendRefs:
- name: redis
port: 6379
```

Send a ping to the Redis server using `curl`. Change `192.168.106.2` below to the IP address of HAProxy Ingress:

```
curl -v telnet://192.168.106.2:6379
* Trying 192.168.106.2:6379...
* Connected to 192.168.106.2 (192.168.106.2) port 6379
ping
+PONG
^C
```

Type `ping` and see a `+PONG` response. Press `^C` to close the connection.
16 changes: 15 additions & 1 deletion pkg/controller/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,10 @@ func CreateWithConfig(ctx context.Context, restConfig *rest.Config, opt *Options
configLog.Info("watching for Gateway API resources - --watch-gateway is true")
}

var hasGatewayV1, hasGatewayB1, hasGatewayA2 bool
var hasGatewayV1, hasGatewayB1, hasGatewayA2, hasTCPRouteA2 bool
if opt.WatchGateway {
gwapis := []string{"gatewayclass", "gateway", "httproute"}
tcpapis := []string{"tcproute"}

gwV1 := configHasAPI(clientGateway.Discovery(), gatewayv1.GroupVersion, gwapis...)
if gwV1 {
Expand All @@ -221,9 +222,20 @@ func CreateWithConfig(ctx context.Context, restConfig *rest.Config, opt *Options

// only one GatewayClass/Gateway/HTTPRoute version should be enabled at the same time,
// otherwise we'd be retrieving the same duplicated resource from distinct api endpoints.
gw := gwV1 || gwB1 || gwA2
hasGatewayV1 = gwV1
hasGatewayB1 = gwB1 && !hasGatewayV1
hasGatewayA2 = gwA2 && !hasGatewayB1

tcpA2 := configHasAPI(clientGateway.Discovery(), gatewayv1alpha2.GroupVersion, tcpapis...)
if tcpA2 {
configLog.Info("found custom resource definition for TCPRoute API v1alpha2")
}

// TODO: cannot enable TCPRoute without Gateway and GatewayClass, but currently HTTPRoute
// discovery is coupled and its CRD should be installed as well, even if not used.
// We should use a distinct flag for HTTPRoute.
hasTCPRouteA2 = tcpA2 && gw
}

if opt.EnableEndpointSlicesAPI {
Expand Down Expand Up @@ -475,6 +487,7 @@ func CreateWithConfig(ctx context.Context, restConfig *rest.Config, opt *Options
HasGatewayA2: hasGatewayA2,
HasGatewayB1: hasGatewayB1,
HasGatewayV1: hasGatewayV1,
HasTCPRouteA2: hasTCPRouteA2,
HealthzAddr: healthz,
HealthzURL: opt.HealthzURL,
IngressClass: opt.IngressClass,
Expand Down Expand Up @@ -657,6 +670,7 @@ type Config struct {
HasGatewayA2 bool
HasGatewayB1 bool
HasGatewayV1 bool
HasTCPRouteA2 bool
HealthzAddr string
HealthzURL string
IngressClass string
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/config/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (o *Options) AddFlags(fs *flag.FlagSet) {
"processes.",
)

flag.StringVar(&o.MasterSocket, "master-socket", o.MasterSocket, ""+
fs.StringVar(&o.MasterSocket, "master-socket", o.MasterSocket, ""+
"Defines the master CLI unix socket of an external HAProxy running in "+
"master-worker mode. Defaults to use the embedded HAProxy if not declared.",
)
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/legacy/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ func (c *k8scache) hasGateway() bool {
var errGatewayA2Disabled = fmt.Errorf("gateway API v1alpha2 wasn't initialized")
var errGatewayB1Disabled = fmt.Errorf("legacy controller does not support Gateway API v1beta1")
var errGatewayV1Disabled = fmt.Errorf("legacy controller does not support Gateway API v1")
var errTCPRouteA2Disabled = fmt.Errorf("legacy controller does not support TCPRoute API")

func (c *k8scache) GetGatewayA2(namespace, name string) (*gatewayv1alpha2.Gateway, error) {
if !c.hasGateway() {
Expand Down Expand Up @@ -272,6 +273,10 @@ func (c *k8scache) GetHTTPRouteList() ([]*gatewayv1.HTTPRoute, error) {
return nil, errGatewayV1Disabled
}

func (c *k8scache) GetTCPRouteList() ([]*gatewayv1alpha2.TCPRoute, error) {
return nil, errTCPRouteA2Disabled
}

func (c *k8scache) GetService(defaultNamespace, serviceName string) (*api.Service, error) {
namespace, name, err := c.buildResourceName(defaultNamespace, "service", serviceName, c.dynamicConfig.CrossNamespaceServices)
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions pkg/controller/reconciler/watchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ func (w *watchers) getHandlers() []*hdlr {
if w.cfg.HasGatewayV1 {
handlers = append(handlers, w.handlersGatewayv1()...)
}
if w.cfg.HasTCPRouteA2 {
handlers = append(handlers, w.handlersTCPRoutev1alpha2()...)
}
for _, h := range handlers {
h.w = w
}
Expand Down Expand Up @@ -476,6 +479,19 @@ func (w *watchers) handlersGatewayv1() []*hdlr {
}
}

func (w *watchers) handlersTCPRoutev1alpha2() []*hdlr {
return []*hdlr{
{
typ: &gatewayv1alpha2.TCPRoute{},
res: types.ResourceTCPRoute,
full: true,
pr: []predicate.Predicate{
predicate.GenerationChangedPredicate{},
},
},
}
}

type hdlr struct {
w *watchers
typ client.Object
Expand Down
17 changes: 17 additions & 0 deletions pkg/controller/services/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type c struct {
var errGatewayA2Disabled = fmt.Errorf("gateway API v1alpha2 wasn't initialized")
var errGatewayB1Disabled = fmt.Errorf("gateway API v1beta1 wasn't initialized")
var errGatewayV1Disabled = fmt.Errorf("gateway API v1 wasn't initialized")
var errTCPRouteA2Disabled = fmt.Errorf("TCPRoute API v1alpha2 wasn't initialized")

func (c *c) get(key string, obj client.Object) error {
ns, n, err := cache.SplitMetaNamespaceKey(key)
Expand Down Expand Up @@ -381,6 +382,22 @@ func (c *c) GetHTTPRouteList() ([]*gatewayv1.HTTPRoute, error) {
return rlist, nil
}

func (c *c) GetTCPRouteList() ([]*gatewayv1alpha2.TCPRoute, error) {
if !c.config.HasTCPRouteA2 {
return nil, errTCPRouteA2Disabled
}
list := gatewayv1alpha2.TCPRouteList{}
err := c.client.List(c.ctx, &list)
if err != nil {
return nil, err
}
rlist := make([]*gatewayv1alpha2.TCPRoute, len(list.Items))
for i := range list.Items {
rlist[i] = &list.Items[i]
}
return rlist, nil
}

func (c *c) GetService(defaultNamespace, serviceName string) (*api.Service, error) {
namespace, name, err := buildResourceName(defaultNamespace, "service", serviceName, c.dynconfig.CrossNamespaceServices)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/controller/services/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func (s *Services) setup(ctx context.Context) error {
HasGatewayA2: cfg.HasGatewayA2,
HasGatewayB1: cfg.HasGatewayB1,
HasGatewayV1: cfg.HasGatewayV1,
HasTCPRouteA2: cfg.HasTCPRouteA2,
EnableEPSlices: cfg.EnableEndpointSliceAPI,
}
instance := haproxy.CreateInstance(s.legacylogger.new("haproxy"), instanceOptions)
Expand Down
Loading