Skip to content

Commit

Permalink
feat: root listeners methods (#101)
Browse files Browse the repository at this point in the history
* test: enable kong admin url env override
* feat: expose raw http requests to kong
* feat: expose raw JSON kong root data
* feat: add client.Listeners method to gather proxy and stream listeners
  • Loading branch information
shaneutt authored Nov 5, 2021
1 parent 28f0f1b commit ad2aa69
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 10 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/blang/semver/v4 v4.0.0
github.com/google/go-querystring v1.1.0
github.com/google/uuid v1.3.0
github.com/mitchellh/mapstructure v1.4.2
github.com/stretchr/testify v1.7.0
k8s.io/code-generator v0.22.3
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
54 changes: 44 additions & 10 deletions kong/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ func NewClient(baseURL *string, client *http.Client) (*Client, error) {
var rootURL string
if baseURL != nil {
rootURL = *baseURL
} else if urlFromEnv := os.Getenv("KONG_ADMIN_URL"); urlFromEnv != "" {
rootURL = urlFromEnv
} else {
rootURL = defaultBaseURL
}
Expand Down Expand Up @@ -189,9 +191,9 @@ func (c *Client) workspacedBaseURL(workspace string) string {
return c.defaultRootURL
}

// Do executes a HTTP request and returns a response
func (c *Client) Do(ctx context.Context, req *http.Request,
v interface{}) (*Response, error) {
// DoRAW executes an HTTP request and returns an http.Response
// the caller is responsible for closing the response body.
func (c *Client) DoRAW(ctx context.Context, req *http.Request) (*http.Response, error) {
var err error
if req == nil {
return nil, fmt.Errorf("request cannot be nil")
Expand All @@ -213,6 +215,17 @@ func (c *Client) Do(ctx context.Context, req *http.Request,
return nil, fmt.Errorf("making HTTP request: %w", err)
}

return resp, err
}

// Do executes an HTTP request and returns a kong.Response
func (c *Client) Do(ctx context.Context, req *http.Request,
v interface{}) (*Response, error) {
resp, err := c.DoRAW(ctx, req)
if err != nil {
return nil, err
}

// log the response
err = c.logResponse(resp)
if err != nil {
Expand All @@ -225,13 +238,6 @@ func (c *Client) Do(ctx context.Context, req *http.Request,
if err = hasError(resp); err != nil {
return response, err
}
// Call Close on exit
defer func() {
e := resp.Body.Close()
if e != nil {
err = e
}
}()

// response
if v != nil {
Expand Down Expand Up @@ -322,3 +328,31 @@ func (c *Client) Root(ctx context.Context) (map[string]interface{}, error) {
}
return info, nil
}

// RootJSON returns the response of GET request on the root of the Admin API
// (GET / or /kong with a workspace) returning the raw JSON response data.
func (c *Client) RootJSON(ctx context.Context) ([]byte, error) {
endpoint := "/"
ws := c.Workspace()
if len(ws) > 0 {
endpoint = "/kong"
}

req, err := c.NewRequestRaw("GET", c.workspacedBaseURL(ws), endpoint, nil, nil)
if err != nil {
return nil, err
}

resp, err := c.DoRAW(ctx, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return body, nil
}
13 changes: 13 additions & 0 deletions kong/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ func TestRoot(T *testing.T) {
assert.NotNil(root["version"])
}

func TestRootJSON(T *testing.T) {
assert := assert.New(T)

client, err := NewTestClient(nil, nil)
assert.NoError(err)
assert.NotNil(client)

root, err := client.RootJSON(defaultCtx)
assert.Nil(err)
assert.NotEmpty(root)
assert.Contains(string(root), `"version"`)
}

func TestDo(T *testing.T) {
assert := assert.New(T)

Expand Down
115 changes: 115 additions & 0 deletions kong/listeners.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package kong

import (
"context"
"encoding/json"
"fmt"

"github.com/mitchellh/mapstructure"
)

// -----------------------------------------------------------------------------
// Kong Listeners - Public Types
// -----------------------------------------------------------------------------

// ProxyListener is a configured listener on the Kong Gateway for L7 routing.
type ProxyListener struct {
SSL bool `json:"ssl" mapstructure:"ssl"`
Listener string `json:"listener" mapstructure:"listener"`
Port int `json:"port" mapstructure:"port"`
Bind bool `json:"bind" mapstructure:"bind"`
IP string `json:"ip" mapstructure:"ip"`
HTTP2 bool `json:"http2" mapstructure:"http2"`
ProxyProtocol bool `json:"proxy_protocol" mapstructure:"proxy_protocol"`
Deferred bool `json:"deferred" mapstructure:"deferred"`
ReusePort bool `json:"reuseport" mapstructure:"reuseport"`
Backlog bool `json:"backlog=%d+" mapstructure:"backlog=%d+"`
}

// StreamListener is a configured listener on the Kong Gateway for L4 routing.
type StreamListener struct {
UDP bool `json:"udp" mapstructure:"udp"`
SSL bool `json:"ssl" mapstructure:"ssl"`
ProxyProtocol bool `json:"proxy_protocol" mapstructure:"proxy_protocol"`
IP string `json:"ip" mapstructure:"ip"`
Listener string `json:"listener" mapstructure:"listener"`
Port int `json:"port" mapstructure:"port"`
Bind bool `json:"bind" mapstructure:"bind"`
ReusePort bool `json:"reuseport" mapstructure:"reuseport"`
Backlog bool `json:"backlog=%d+" mapstructure:"backlog=%d+"`
}

// -----------------------------------------------------------------------------
// Kong Listeners - Client Methods
// -----------------------------------------------------------------------------

// Listeners returns the proxy_listeners and stream_listeners that are currently configured in the
// Kong root as convenient native types rather than JSON or unstructured.
func (c *Client) Listeners(ctx context.Context) ([]ProxyListener, []StreamListener, error) {
rootJSON, err := c.RootJSON(ctx)
if err != nil {
return nil, nil, fmt.Errorf("couldn't get root JSON when trying to determine listeners: %w", err)
}

root := dataPlaneConfigWrapper{}
if err := json.Unmarshal(rootJSON, &root); err != nil {
return nil, nil, fmt.Errorf("couldn't decode root JSON when trying to determine listeners: %w", err)
}

return root.Config.ProxyListeners, root.Config.StreamListeners, nil
}

// -----------------------------------------------------------------------------
// Kong Listeners - Private Wrapper Types
// -----------------------------------------------------------------------------

type dataPlaneConfigWrapper struct {
Config dataPlaneConfig `json:"configuration"`
}

type dataPlaneConfig struct {
ProxyListeners []ProxyListener `json:"proxy_listeners"`
StreamListeners []StreamListener `json:"stream_listeners"`
}

// UnmarshalJSON implements custom JSON unmarshaling for this type which must
// be done because the Kong Admin API will return empty objects when a list
// is empty which will confuse the default unmarshaler.
func (d *dataPlaneConfig) UnmarshalJSON(data []byte) error {
wrapper := make(map[string]interface{})
if err := json.Unmarshal(data, &wrapper); err != nil {
return err
}

proxyListenersRaw, ok := wrapper["proxy_listeners"]
if ok {
listeners, ok := proxyListenersRaw.([]interface{})
if ok {
d.ProxyListeners = make([]ProxyListener, 0, len(listeners))
for _, listener := range listeners {
proxyListener := ProxyListener{}
if err := mapstructure.Decode(listener, &proxyListener); err != nil {
return err
}
d.ProxyListeners = append(d.ProxyListeners, proxyListener)
}
}
}

streamListenersRaw, ok := wrapper["stream_listeners"]
if ok {
listeners, ok := streamListenersRaw.([]interface{})
if ok {
d.StreamListeners = make([]StreamListener, 0, len(listeners))
for _, listener := range listeners {
streamListener := StreamListener{}
if err := mapstructure.Decode(listener, &streamListener); err != nil {
return err
}
d.StreamListeners = append(d.StreamListeners, streamListener)
}
}
}

return nil
}
26 changes: 26 additions & 0 deletions kong/listeners_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kong

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestListeners(t *testing.T) {
client, err := NewTestClient(nil, nil)
assert.NoError(t, err)

t.Log("pulling the listener configurations from the kong root")
proxyListeners, _, err := client.Listeners(defaultCtx)
assert.NoError(t, err)
assert.NotEmpty(t, proxyListeners)

t.Log("verifying that the standard http listener was found")
foundHTTPListener := false
for _, listener := range proxyListeners {
if listener.SSL == false {
foundHTTPListener = true
}
}
assert.True(t, foundHTTPListener)
}

0 comments on commit ad2aa69

Please sign in to comment.