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

feat: add Client.ReloadDeclarativeConfig() #252

Merged
merged 1 commit into from
Dec 15, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

- Add support to filling entity defaults using JSON schemas.
[#231](https://github.com/Kong/go-kong/pull/231)
- Add possibility to client to send declarative configs via `ReloadDeclarativeRawConfig()`
[#252](https://github.com/Kong/go-kong/pull/252)

## [v0.33.0]

Expand Down
2 changes: 2 additions & 0 deletions kong/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Client struct {
workspace string // Do not access directly. Use Workspace()/SetWorkspace().
workspaceLock sync.RWMutex // Synchronizes access to workspace.
common service
Configs AbstractConfigService
Consumers AbstractConsumerService
Developers AbstractDeveloperService
DeveloperRoles AbstractDeveloperRoleService
Expand Down Expand Up @@ -127,6 +128,7 @@ func NewClient(baseURL *string, client *http.Client) (*Client, error) {
kong.defaultRootURL = url.String()

kong.common.client = kong
kong.Configs = (*ConfigService)(&kong.common)
kong.Consumers = (*ConsumerService)(&kong.common)
kong.Developers = (*DeveloperService)(&kong.common)
kong.DeveloperRoles = (*DeveloperRoleService)(&kong.common)
Expand Down
65 changes: 65 additions & 0 deletions kong/config_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//nolint:lll
package kong

import (
"context"
"fmt"
"io"
)

// AbstractConfigService handles Config in Kong.
type AbstractConfigService interface {
// ReloadDeclarativeRawConfig sends out the specified config to configured Admin
// API endpoint using the provided reader which should contain the JSON
// serialized body that adheres to the configuration format specified at:
// https://docs.konghq.com/gateway/latest/production/deployment-topologies/db-less-and-declarative-config/#declarative-configuration-format
ReloadDeclarativeRawConfig(ctx context.Context, config io.Reader, checkHash bool) error
}

// ConfigService handles Config in Kong.
type ConfigService service

// ReloadDeclarativeRawConfig sends out the specified config to configured Admin
// API endpoint using the provided reader which should contain the JSON
// serialized body that adheres to the configuration format specified at:
// https://docs.konghq.com/gateway/latest/production/deployment-topologies/db-less-and-declarative-config/#declarative-configuration-format
func (c *ConfigService) ReloadDeclarativeRawConfig(
ctx context.Context,
config io.Reader,
checkHash bool,
) error {
type sendConfigParams struct {
CheckHash int `url:"check_hash"`
}
var checkHashI int
if checkHash {
checkHashI = 1
}
req, err := c.client.NewRequest("POST", "/config", sendConfigParams{CheckHash: checkHashI}, config)
if err != nil {
return fmt.Errorf("creating new HTTP request for /config: %w", err)
}

resp, err := c.client.DoRAW(ctx, req)
if err != nil {
return fmt.Errorf("failed posting new config to /config: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode < 200 || resp.StatusCode >= 400 {
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf(
"failed posting new config to /config: got status code %d (and failed to read the response body): %w",
resp.StatusCode, err,
)
}

return fmt.Errorf(
"failed posting new config to /config: got status code %d, body: %s",
resp.StatusCode, b,
)
}

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

import (
"bytes"
"context"
"encoding/json"
"testing"

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

func TestConfigService(t *testing.T) {
RunWhenDBMode(t, "off")

tests := []struct {
name string
config Configuration
wantErr bool
}{
{
name: "basic config works",
config: Configuration{
"_format_version": "1.1",
"services": []Configuration{
{
"host": "mockbin.com",
"port": 443,
"protocol": "https",
"routes": []Configuration{
{"paths": []string{"/"}},
},
},
},
},
wantErr: false,
},
{
name: "missing _format_version fails",
config: Configuration{
"services": []Configuration{
{
"host": "mockbin.com",
"port": 443,
"protocol": "https",
"routes": []Configuration{
{"paths": []string{"/"}},
},
},
},
},
wantErr: true,
},
{
name: "invalid config fails",
config: Configuration{
"dummy_key": []Configuration{
{
"host": "mockbin.com",
"port": 443,
"protocol": "https",
},
},
},
wantErr: true,
},
}

for _, tt := range tests {
client, err := NewTestClient(nil, nil)
require.NoError(t, err)
require.NotNil(t, client)

tt := tt
t.Run("with_schema/"+tt.name, func(t *testing.T) {
ctx := context.Background()
b, err := json.Marshal(tt.config)
require.NoError(t, err)

if err := client.Configs.ReloadDeclarativeRawConfig(ctx, bytes.NewBuffer(b), true); (err != nil) != tt.wantErr {
t.Errorf("Client.SendConfig() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
22 changes: 16 additions & 6 deletions kong/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/google/go-querystring/query"
Expand All @@ -17,17 +18,26 @@ func (c *Client) NewRequestRaw(method, baseURL string, endpoint string, qs inter
return nil, fmt.Errorf("endpoint can't be nil")
}
// body to be sent in JSON
var buf []byte
var r io.Reader
if body != nil {
var err error
buf, err = json.Marshal(body)
if err != nil {
return nil, err
switch v := body.(type) {
case string:
r = bytes.NewBufferString(v)
case []byte:
r = bytes.NewBuffer(v)
case io.Reader:
r = v
default:
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
r = bytes.NewBuffer(b)
}
}

// Create a new request
req, err := http.NewRequest(method, baseURL+endpoint, bytes.NewBuffer(buf))
req, err := http.NewRequest(method, baseURL+endpoint, r)
if err != nil {
return nil, err
}
Expand Down
92 changes: 92 additions & 0 deletions kong/request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package kong

import (
"bytes"
"io"
"testing"

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

func TestNewRequestBody(t *testing.T) {
t.Run("body can be string", func(t *testing.T) {
cl, err := NewClient(nil, nil)
require.NoError(t, err)

body := `{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`

req, err := cl.NewRequest("POST", "/", nil, body)
require.NoError(t, err)

b, err := io.ReadAll(req.Body)
require.NoError(t, err)

assert.Equal(t,
`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`,
string(b),
)
})

t.Run("body can be []byte", func(t *testing.T) {
cl, err := NewClient(nil, nil)
require.NoError(t, err)

body := []byte(`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`)

req, err := cl.NewRequest("POST", "/", nil, body)
require.NoError(t, err)

b, err := io.ReadAll(req.Body)
require.NoError(t, err)

assert.Equal(t,
`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`,
string(b),
)
})

t.Run("body can be a bytes.Buffer", func(t *testing.T) {
cl, err := NewClient(nil, nil)
require.NoError(t, err)

body := bytes.NewBufferString(`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`)

req, err := cl.NewRequest("POST", "/", nil, body)
require.NoError(t, err)

b, err := io.ReadAll(req.Body)
require.NoError(t, err)

assert.Equal(t,
`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`,
string(b),
)
})

t.Run("body can be a map", func(t *testing.T) {
cl, err := NewClient(nil, nil)
require.NoError(t, err)

body := map[string]any{
"_format_version": "1.1",
"services": []map[string]any{
{
"host": "example.com",
"name": "foo",
},
},
}

req, err := cl.NewRequest("POST", "/", nil, body)
require.NoError(t, err)

b, err := io.ReadAll(req.Body)
require.NoError(t, err)

assert.Equal(t,
`{"_format_version":"1.1","services":[{"host":"example.com","name":"foo"}]}`,
string(b),
)
})
}
39 changes: 39 additions & 0 deletions kong/test_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,42 @@ func NewTestClient(baseURL *string, client *http.Client) (*Client, error) {
}
return NewClient(baseURL, client)
}

func RunWhenDBMode(t *testing.T, dbmode string) {
client, err := NewTestClient(nil, nil)
if err != nil {
t.Error(err)
}
info, err := client.Root(defaultCtx)
if err != nil {
t.Error(err)
}

config, ok := info["configuration"]
if !ok {
t.Logf("failed to find 'configuration' config key in kong configuration")
t.Skip()
}

configuration, ok := config.(map[string]any)
if !ok {
t.Logf("'configuration' key is not a map but %T", config)
t.Skip()
}

dbConfig, ok := configuration["database"]
if !ok {
t.Logf("failed to find 'database' config key in kong confiration")
t.Skip()
}

dbMode, ok := dbConfig.(string)
if !ok {
t.Logf("'database' config key is not a string but %T", dbConfig)
t.Skip()
}

if dbMode != dbmode {
t.Skip()
}
}