Skip to content

Commit

Permalink
manager/client unit testing
Browse files Browse the repository at this point in the history
  • Loading branch information
rizzza committed Mar 29, 2023
1 parent 0afe2bc commit 2101a3b
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 38 deletions.
23 changes: 6 additions & 17 deletions internal/lbapi/lbapi.go → internal/lbapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ const (
apiVersion = "v1"
)

// HTTPClient interface
type HTTPClient interface {
Do(req *retryablehttp.Request) (*http.Response, error)
}

type Client struct {
client *retryablehttp.Client
client HTTPClient
baseURL string
}

Expand All @@ -31,25 +36,9 @@ func NewClient(url string, opts ...func(*Client)) *Client {
client: retryCli,
}

for _, opt := range opts {
opt(c)
}

return c
}

func WithRetries(r int) func(*Client) {
return func(c *Client) {
c.client.RetryMax = r
}
}

func WithTimeout(timeout time.Duration) func(*Client) {
return func(c *Client) {
c.client.HTTPClient.Timeout = timeout
}
}

func (c Client) GetLoadBalancer(ctx context.Context, id string) (*LoadBalancer, error) {
lb := &LoadBalancer{}
url := fmt.Sprintf("%s/%s/loadbalancers/%s", c.baseURL, apiVersion, id)
Expand Down
162 changes: 162 additions & 0 deletions internal/lbapi/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package lbapi

import (
"context"
"io"
"net/http"
"strings"
"testing"

"github.com/hashicorp/go-retryablehttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.infratographer.com/loadbalancer-manager-haproxy/internal/lbapi/mock"
)

func newLBAPIMock(respJSON string, respCode int) *mock.HTTPClient {
mockCli := &mock.HTTPClient{}
mockCli.DoFunc = func(*retryablehttp.Request) (*http.Response, error) {
json := respJSON

r := io.NopCloser(strings.NewReader(json))
return &http.Response{
StatusCode: respCode,
Body: r,
}, nil
}

return mockCli
}

func TestGetLoadBalancer(t *testing.T) {
t.Run("GET v1/loadbalancers/:id", func(t *testing.T) {
t.Parallel()
respJSON := `{
"id": "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f",
"ports": [
{
"address_family": "ipv4",
"id": "16dd23d7-d3ab-42c8-a645-3169f2659a0b",
"name": "ssh-service",
"port": 22,
"pools": [
"49faa4a3-8d0b-4a7a-8bb9-7ed1b5995e49"
]
}
]
}`
cli := Client{
baseURL: "test.url",
client: newLBAPIMock(respJSON, http.StatusOK),
}

lb, err := cli.GetLoadBalancer(context.Background(), "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f")
require.Nil(t, err)

assert.NotNil(t, lb)
assert.Equal(t, "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f", lb.ID)
assert.Len(t, lb.Ports, 1)
assert.Equal(t, "ipv4", lb.Ports[0].AddressFamily)
assert.Equal(t, "16dd23d7-d3ab-42c8-a645-3169f2659a0b", lb.Ports[0].ID)
assert.Equal(t, "ssh-service", lb.Ports[0].Name)
assert.Equal(t, int64(22), lb.Ports[0].Port)
assert.Len(t, lb.Ports[0].Pools, 1)
assert.Equal(t, "49faa4a3-8d0b-4a7a-8bb9-7ed1b5995e49", lb.Ports[0].Pools[0])
})

negativeTests := []struct {
name string
respJSON string
respCode int
expectedFailure error
}{
{"GET v1/loadbalancers/:id - 401", "", http.StatusUnauthorized, ErrLBHTTPUnauthorized},
{"GET v1/loadbalancers/:id - 500", "", http.StatusInternalServerError, ErrLBHTTPError},
{"GET v1/loadbalancers/:id - other error", "", http.StatusBadRequest, ErrLBHTTPError},
}

for _, tt := range negativeTests {
// go vet
tt := tt

t.Run(tt.name, func(t *testing.T) {
t.Parallel()

cli := Client{
baseURL: "test.url",
client: newLBAPIMock(tt.respJSON, tt.respCode),
}

lb, err := cli.GetLoadBalancer(context.Background(), "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f")
require.NotNil(t, err)
assert.Nil(t, lb)
assert.ErrorIs(t, err, tt.expectedFailure)
})
}
}

func TestGetPool(t *testing.T) {
t.Run("GET v1/loadbalancers/pools/:id", func(t *testing.T) {
t.Parallel()
respJSON := `{
"id": "49faa4a3-8d0b-4a7a-8bb9-7ed1b5995e49",
"name": "ssh-service-a",
"origins": [
{
"id": "c0a80101-0000-0000-0000-000000000001",
"name": "svr1-2222",
"origin_target": "1.2.3.4",
"origin_disabled": false,
"port": 2222
}
]
}`
cli := Client{
baseURL: "test.url",
client: newLBAPIMock(respJSON, http.StatusOK),
}

pool, err := cli.GetPool(context.Background(), "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f")
require.Nil(t, err)

assert.NotNil(t, pool)
assert.Equal(t, "49faa4a3-8d0b-4a7a-8bb9-7ed1b5995e49", pool.ID)
assert.Equal(t, "ssh-service-a", pool.Name)
require.Len(t, pool.Origins, 1)
assert.Equal(t, "c0a80101-0000-0000-0000-000000000001", pool.Origins[0].ID)
assert.Equal(t, "svr1-2222", pool.Origins[0].Name)
assert.Equal(t, "1.2.3.4", pool.Origins[0].IPAddress)
assert.Equal(t, false, pool.Origins[0].Disabled)
assert.Equal(t, int64(2222), pool.Origins[0].Port)
})

negativeTests := []struct {
name string
respJSON string
respCode int
expectedFailure error
}{
{"GET v1/loadbalancers/pools/:id - 401", "", http.StatusUnauthorized, ErrLBHTTPUnauthorized},
{"GET v1/loadbalancers/pools/:id - 500", "", http.StatusInternalServerError, ErrLBHTTPError},
{"GET v1/loadbalancers/pools/:id - other error", "", http.StatusBadRequest, ErrLBHTTPError},
}

for _, tt := range negativeTests {
// go vet
tt := tt

t.Run(tt.name, func(t *testing.T) {
t.Parallel()

cli := Client{
baseURL: "test.url",
client: newLBAPIMock(tt.respJSON, tt.respCode),
}

lb, err := cli.GetLoadBalancer(context.Background(), "58622a8d-54a2-4b0c-8b5f-8de7dff29f6f")
require.NotNil(t, err)
assert.Nil(t, lb)
assert.ErrorIs(t, err, tt.expectedFailure)
})
}
}
16 changes: 16 additions & 0 deletions internal/lbapi/mock/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mock

import (
"net/http"

"github.com/hashicorp/go-retryablehttp"
)

// HTTPClient is the mock http client
type HTTPClient struct {
DoFunc func(req *retryablehttp.Request) (*http.Response, error)
}

func (c *HTTPClient) Do(req *retryablehttp.Request) (*http.Response, error) {
return c.DoFunc(req)
}
46 changes: 27 additions & 19 deletions internal/pkg/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ type lbAPI interface {
GetPool(ctx context.Context, id string) (*lbapi.Pool, error)
}

type dataPlaneAPI interface {
PostConfig(ctx context.Context, config string) error
ApiIsReady(ctx context.Context) bool
}

// ManagerConfig contains configuration and client connections
type ManagerConfig struct {
Context context.Context
Logger *zap.SugaredLogger
NatsConn *nats.Conn
DataPlaneClient dataPlaneAPI
LBClient lbAPI

// primarily for testing
currentConfig string
}

// Run subscribes to a NATS subject and updates the haproxy config via dataplaneapi
Expand All @@ -49,7 +57,7 @@ func (m *ManagerConfig) Run() error {
}

// use desired config on start
if err := m.updateConfigToLatest(); err != nil {
if err := m.updateConfigToLatest(viper.GetString("haproxy.config.base")); err != nil {
m.Logger.Errorw("failed to initialize the config", zap.Error(err))
}

Expand Down Expand Up @@ -104,7 +112,7 @@ func (m ManagerConfig) processMsg(msg *pubsub.Message) error {
}

lbID := urn.ResourceID.String()
if err = m.updateConfigToLatest(lbID); err != nil {
if err = m.updateConfigToLatest(viper.GetString("haproxy.config.base"), lbID); err != nil {
m.Logger.Errorw("failed to update haproxy config", zap.String("loadbalancer.id", lbID), zap.Error(err))
return err
}
Expand All @@ -113,32 +121,30 @@ func (m ManagerConfig) processMsg(msg *pubsub.Message) error {
}

// updateConfigToLatest update the haproxy cfg to either baseline or one requested from lbapi with optional lbID param
func (m ManagerConfig) updateConfigToLatest(lbID ...string) error {
func (m *ManagerConfig) updateConfigToLatest(baseCfgPath string, lbID ...string) error {
if len(lbID) > 1 {
return fmt.Errorf("optional lbID param must be not set or set to a singular loadbalancer ID")
}

m.Logger.Info("updating the config")

// load base config
cfg, err := parser.New(options.Path(viper.GetString("haproxy.config.base")), options.NoNamedDefaultsFrom)
cfg, err := parser.New(options.Path(baseCfgPath), options.NoNamedDefaultsFrom)
if err != nil {
m.Logger.Fatalw("failed to load haproxy base config", "error", err)
}

lbAPIConfigured := len(viper.GetString("loadbalancerapi.url")) > 0
if !lbAPIConfigured {
m.Logger.Warn("loadbalancerapi.url is not configured: defaulting to base haproxy config")
}

if len(lbID) == 1 && lbAPIConfigured {
if len(lbID) == 1 {
// requested a lb id, query lbapi
// get desired state
lb, err := m.LBClient.GetLoadBalancer(m.Context, lbID[0])
if err != nil {
return err
}

// query each pool and copy the origins into our lb datastructure
for _, port := range lb.Ports {
// query each pool and store the origins
for i, port := range lb.Ports {
for _, poolID := range port.Pools {
// query poolID
p, err := m.LBClient.GetPool(m.Context, poolID)
if err != nil {
return err
Expand All @@ -149,7 +155,8 @@ func (m ManagerConfig) updateConfigToLatest(lbID ...string) error {
Name: p.Name,
Origins: p.Origins,
}
port.PoolData = append(port.PoolData, data)

lb.Ports[i].PoolData = append(port.PoolData, data)
}
}

Expand All @@ -166,6 +173,7 @@ func (m ManagerConfig) updateConfigToLatest(lbID ...string) error {
}

m.Logger.Info("config successfully updated")
m.currentConfig = cfg.String() // primarily for testing

return nil
}
Expand All @@ -189,22 +197,22 @@ func mergeConfig(cfg parser.Parser, lb *lbapi.LoadBalancer) (parser.Parser, erro
for _, p := range lb.Ports {
// create port
if err := cfg.SectionsCreate(parser.Frontends, p.Name); err != nil {
return nil, fmt.Errorf("failed to create frontend section with label %q: %w", p.Name, err)
return nil, fmt.Errorf("failed to create frontend section with label %q: %v", p.Name, err)
}

if err := cfg.Insert(parser.Frontends, p.Name, "bind", types.Bind{
Path: fmt.Sprintf("%s@:%d", p.AddressFamily, p.Port)}); err != nil {
return nil, fmt.Errorf("failed to create frontend attr bind: %w", err)
return nil, fmt.Errorf("failed to create frontend attr bind: %v", err)
}

// map frontend to backend
if err := cfg.Set(parser.Frontends, p.Name, "use_backend", types.UseBackend{Name: p.Name}); err != nil {
return nil, fmt.Errorf("failed to create frontend attr use_backend: %w", err)
return nil, fmt.Errorf("failed to create frontend attr use_backend: %v", err)
}

// create backend
if err := cfg.SectionsCreate(parser.Backends, p.Name); err != nil {
return nil, fmt.Errorf("failed to create section backend with label %q': %w", p.Name, err)
return nil, fmt.Errorf("failed to create section backend with label %q': %v", p.Name, err)
}

for _, pool := range p.PoolData {
Expand All @@ -221,7 +229,7 @@ func mergeConfig(cfg parser.Parser, lb *lbapi.LoadBalancer) (parser.Parser, erro
}

if err := cfg.Set(parser.Backends, p.Name, "server", srvr); err != nil {
return nil, fmt.Errorf("failed to add backend %q attr server: %w", p.Name, err)
return nil, fmt.Errorf("failed to add backend %q attr server: %v", p.Name, err)
}
}
}
Expand Down
Loading

0 comments on commit 2101a3b

Please sign in to comment.