Skip to content

Commit

Permalink
Issue 30 tcp http support (#31)
Browse files Browse the repository at this point in the history
* BorderClient basics

* Documentation updates

* About to apply a broad rename

* Rename to support library differences

* Add HTTP Border Client

* Add TCP Border Client

* Use pointers

* Synchronizer uses BorderClient

* Translator extracts client type from Annotations, default when not found

* Copy Paste issue, TCP Border Client need to use the *Stream methods

* Fix stupid mistake in the refactor

* Put an interface between NKL and the NGINX Plus client Upstream objects
  • Loading branch information
ciroque authored Apr 10, 2023
1 parent c6b0849 commit eedb6de
Show file tree
Hide file tree
Showing 24 changed files with 753 additions and 124 deletions.
9 changes: 5 additions & 4 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ stateDiagram-v2
Handler --> Translator
Translator --> Handler
Handler --> Synchronizer : "nkl-synchronizer queue"
Synchronizer --> NGINXPlusLB1
Synchronizer --> NGINXPlusLB2
Synchronizer --> NGINXPlusLB...
Synchronizer --> NGINXPlusLBn
Synchronizer --> BorderClient : "HttpBorderClient | TcpBorderClient"
BorderClient --> NGINXPlusLB1
BorderClient --> NGINXPlusLB2
BorderClient --> NGINXPlusLB...
BorderClient --> NGINXPlusLBn
```

### Settings
Expand Down
43 changes: 43 additions & 0 deletions internal/application/application_common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"errors"
"github.com/nginxinc/kubernetes-nginx-ingress/internal/core"
"github.com/nginxinc/kubernetes-nginx-ingress/test/mocks"
)

const (
deletedEventType = core.Deleted
createEventType = core.Created
upstreamName = "upstreamName"
server = "server"
)

func buildTerrorizingBorderClient(clientType string) (Interface, *mocks.MockNginxClient, error) {
nginxClient := mocks.NewErroringMockClient(errors.New(`something went horribly horribly wrong`))
bc, err := NewBorderClient(clientType, nginxClient)

return bc, nginxClient, err
}

func buildBorderClient(clientType string) (Interface, *mocks.MockNginxClient, error) {
nginxClient := mocks.NewMockNginxClient()
bc, err := NewBorderClient(clientType, nginxClient)

return bc, nginxClient, err
}

func buildServerUpdateEvent(eventType core.EventType, clientType string) *core.ServerUpdateEvent {
upstreamServers := core.UpstreamServers{
{
Host: server,
},
}

return core.NewServerUpdateEvent(eventType, upstreamName, clientType, upstreamServers)
}
11 changes: 11 additions & 0 deletions internal/application/application_constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright (c) 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

const (
ClientTypeTcp = "tcp"
ClientTypeHttp = "http"
)
37 changes: 37 additions & 0 deletions internal/application/border_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"fmt"
"github.com/nginxinc/kubernetes-nginx-ingress/internal/core"
"github.com/sirupsen/logrus"
)

type Interface interface {
Update(*core.ServerUpdateEvent) error
Delete(*core.ServerUpdateEvent) error
}

type BorderClient struct {
}

// NewBorderClient Returns a NullBorderClient if the type is unknown, this avoids panics due to nil pointer dereferences.
func NewBorderClient(clientType string, borderClient interface{}) (Interface, error) {
logrus.Debugf(`NewBorderClient for type: %s`, clientType)

switch clientType {
case "tcp":
return NewTcpBorderClient(borderClient)

case "http":
return NewHttpBorderClient(borderClient)

default:
borderClient, _ := NewNullBorderClient()
return borderClient, fmt.Errorf(`unknown border client type: %s`, clientType)
}
}
52 changes: 52 additions & 0 deletions internal/application/border_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"github.com/nginxinc/kubernetes-nginx-ingress/test/mocks"
"testing"
)

func TestBorderClient_CreatesHttpBorderClient(t *testing.T) {
borderClient := mocks.MockNginxClient{}
client, err := NewBorderClient("http", borderClient)
if err != nil {
t.Errorf(`error creating border client: %v`, err)
}

if _, ok := client.(*HttpBorderClient); !ok {
t.Errorf(`expected client to be of type HttpBorderClient`)
}
}

func TestBorderClient_CreatesTcpBorderClient(t *testing.T) {
borderClient := mocks.MockNginxClient{}
client, err := NewBorderClient("tcp", borderClient)
if err != nil {
t.Errorf(`error creating border client: %v`, err)
}

if _, ok := client.(*TcpBorderClient); !ok {
t.Errorf(`expected client to be of type TcpBorderClient`)
}
}

func TestBorderClient_UnknownClientType(t *testing.T) {
unknownClientType := "unknown"
borderClient := mocks.MockNginxClient{}
client, err := NewBorderClient(unknownClientType, borderClient)
if err == nil {
t.Errorf(`expected error creating border client`)
}

if err.Error() != `unknown border client type: unknown` {
t.Errorf(`expected error to be 'unknown border client type: unknown', got: %v`, err)
}

if _, ok := client.(*NullBorderClient); !ok {
t.Errorf(`expected client to be of type NullBorderClient`)
}
}
19 changes: 19 additions & 0 deletions internal/application/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

/*
Package application includes support for applying updates to the Border servers.
"Border TcpServers" are the servers that are exposed to the outside world and direct traffic into the cluster.
At this time the only supported Border TcpServers are NGINX Plus servers. The BorderClient module defines
an interface that can be implemented to support other Border Server types.
- HttpBorderClient: updates NGINX Plus servers using HTTP Upstream methods on the NGINX Plus API.
- TcpBorderClient: updates NGINX Plus servers using Stream Upstream methods on the NGINX Plus API.
Selection of the appropriate client is based on the Annotations present on the NodePort Service definition.
*/

package application
63 changes: 63 additions & 0 deletions internal/application/http_border_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"fmt"
"github.com/nginxinc/kubernetes-nginx-ingress/internal/core"
nginxClient "github.com/nginxinc/nginx-plus-go-client/client"
)

type HttpBorderClient struct {
BorderClient
nginxClient NginxClientInterface
}

func NewHttpBorderClient(client interface{}) (Interface, error) {
ngxClient, ok := client.(NginxClientInterface)
if !ok {
return nil, fmt.Errorf(`expected a NginxClientInterface, got a %v`, client)
}

return &HttpBorderClient{
nginxClient: ngxClient,
}, nil
}

func (hbc *HttpBorderClient) Update(event *core.ServerUpdateEvent) error {
httpUpstreamServers := asNginxHttpUpstreamServers(event.UpstreamServers)
_, _, _, err := hbc.nginxClient.UpdateHTTPServers(event.UpstreamName, httpUpstreamServers)
if err != nil {
return fmt.Errorf(`error occurred updating the nginx+ upstream server: %w`, err)
}

return nil
}

func (hbc *HttpBorderClient) Delete(event *core.ServerUpdateEvent) error {
err := hbc.nginxClient.DeleteHTTPServer(event.UpstreamName, event.UpstreamServers[0].Host)
if err != nil {
return fmt.Errorf(`error occurred deleting the nginx+ upstream server: %w`, err)
}

return nil
}

func asNginxHttpUpstreamServer(server *core.UpstreamServer) nginxClient.UpstreamServer {
return nginxClient.UpstreamServer{
Server: server.Host,
}
}

func asNginxHttpUpstreamServers(servers core.UpstreamServers) []nginxClient.UpstreamServer {
var upstreamServers []nginxClient.UpstreamServer

for _, server := range servers {
upstreamServers = append(upstreamServers, asNginxHttpUpstreamServer(server))
}

return upstreamServers
}
80 changes: 80 additions & 0 deletions internal/application/http_border_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"testing"
)

func TestHttpBorderClient_Delete(t *testing.T) {
event := buildServerUpdateEvent(deletedEventType, ClientTypeHttp)
borderClient, nginxClient, err := buildBorderClient(ClientTypeHttp)
if err != nil {
t.Fatalf(`error occurred creating a new border client: %v`, err)
}

err = borderClient.Delete(event)
if err != nil {
t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err)
}

if !nginxClient.CalledFunctions["DeleteHTTPServer"] {
t.Fatalf(`expected DeleteHTTPServer to be called`)
}
}

func TestHttpBorderClient_Update(t *testing.T) {
event := buildServerUpdateEvent(createEventType, ClientTypeHttp)
borderClient, nginxClient, err := buildBorderClient(ClientTypeHttp)
if err != nil {
t.Fatalf(`error occurred creating a new border client: %v`, err)
}

err = borderClient.Update(event)
if err != nil {
t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err)
}

if !nginxClient.CalledFunctions["UpdateHTTPServers"] {
t.Fatalf(`expected UpdateHTTPServers to be called`)
}
}

func TestHttpBorderClient_BadNginxClient(t *testing.T) {
var emptyInterface interface{}
_, err := NewBorderClient(ClientTypeHttp, emptyInterface)
if err == nil {
t.Fatalf(`expected an error to occur when creating a new border client`)
}
}

func TestHttpBorderClient_DeleteReturnsError(t *testing.T) {
event := buildServerUpdateEvent(deletedEventType, ClientTypeHttp)
borderClient, _, err := buildTerrorizingBorderClient(ClientTypeHttp)
if err != nil {
t.Fatalf(`error occurred creating a new border client: %v`, err)
}

err = borderClient.Delete(event)

if err == nil {
t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`)
}
}

func TestHttpBorderClient_UpdateReturnsError(t *testing.T) {
event := buildServerUpdateEvent(createEventType, ClientTypeHttp)
borderClient, _, err := buildTerrorizingBorderClient(ClientTypeHttp)
if err != nil {
t.Fatalf(`error occurred creating a new border client: %v`, err)
}

err = borderClient.Update(event)

if err == nil {
t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`)
}
}
16 changes: 16 additions & 0 deletions internal/application/nginx_client_interface.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import nginxClient "github.com/nginxinc/nginx-plus-go-client/client"

type NginxClientInterface interface {
DeleteStreamServer(upstream string, server string) error
UpdateStreamServers(upstream string, servers []nginxClient.StreamUpstreamServer) ([]nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, error)

DeleteHTTPServer(upstream string, server string) error
UpdateHTTPServers(upstream string, servers []nginxClient.UpstreamServer) ([]nginxClient.UpstreamServer, []nginxClient.UpstreamServer, []nginxClient.UpstreamServer, error)
}
28 changes: 28 additions & 0 deletions internal/application/null_border_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import (
"github.com/nginxinc/kubernetes-nginx-ingress/internal/core"
"github.com/sirupsen/logrus"
)

type NullBorderClient struct {
}

func NewNullBorderClient() (Interface, error) {
return &NullBorderClient{}, nil
}

func (nbc *NullBorderClient) Update(_ *core.ServerUpdateEvent) error {
logrus.Warn("NullBorderClient.Update called")
return nil
}

func (nbc *NullBorderClient) Delete(_ *core.ServerUpdateEvent) error {
logrus.Warn("NullBorderClient.Delete called")
return nil
}
24 changes: 24 additions & 0 deletions internal/application/null_border_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 F5 Inc. All rights reserved.
* Use of this source code is governed by the Apache License that can be found in the LICENSE file.
*/

package application

import "testing"

func TestNullBorderClient_Delete(t *testing.T) {
client := NullBorderClient{}
err := client.Delete(nil)
if err != nil {
t.Errorf(`expected no error deleting border client, got: %v`, err)
}
}

func TestNullBorderClient_Update(t *testing.T) {
client := NullBorderClient{}
err := client.Update(nil)
if err != nil {
t.Errorf(`expected no error updating border client, got: %v`, err)
}
}
Loading

0 comments on commit eedb6de

Please sign in to comment.