Skip to content

Commit

Permalink
Merge pull request #1103 from jcmoraisjr/jm-tcproute
Browse files Browse the repository at this point in the history
Add TCPRoute support from Gateway API
  • Loading branch information
jcmoraisjr authored Mar 29, 2024
2 parents 9116dc9 + d91f395 commit 7efd263
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 75 deletions.
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

0 comments on commit 7efd263

Please sign in to comment.