From 56eb706b2fc60696fd46f453b6aa8b0d68da00bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Thu, 15 Dec 2022 19:26:03 +0100 Subject: [PATCH] feat: add ReloadDeclarativeRawConfig() for sending declarative config (#252) --- CHANGELOG.md | 2 + kong/client.go | 2 + kong/config_service.go | 65 ++++++++++++++++++++++++++ kong/config_service_test.go | 84 +++++++++++++++++++++++++++++++++ kong/request.go | 22 ++++++--- kong/request_test.go | 92 +++++++++++++++++++++++++++++++++++++ kong/test_utils.go | 39 ++++++++++++++++ 7 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 kong/config_service.go create mode 100644 kong/config_service_test.go create mode 100644 kong/request_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ae153a9ea..ab5fa4e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] diff --git a/kong/client.go b/kong/client.go index 12da503ab..4abbd43f0 100644 --- a/kong/client.go +++ b/kong/client.go @@ -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 @@ -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) diff --git a/kong/config_service.go b/kong/config_service.go new file mode 100644 index 000000000..1a2740fb3 --- /dev/null +++ b/kong/config_service.go @@ -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 +} diff --git a/kong/config_service_test.go b/kong/config_service_test.go new file mode 100644 index 000000000..0345f0c3f --- /dev/null +++ b/kong/config_service_test.go @@ -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) + } + }) + } +} diff --git a/kong/request.go b/kong/request.go index 56e8aabb4..5c28b2400 100644 --- a/kong/request.go +++ b/kong/request.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "net/http" "github.com/google/go-querystring/query" @@ -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 } diff --git a/kong/request_test.go b/kong/request_test.go new file mode 100644 index 000000000..45603a376 --- /dev/null +++ b/kong/request_test.go @@ -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), + ) + }) +} diff --git a/kong/test_utils.go b/kong/test_utils.go index 69c6d3721..8e54cc9c7 100644 --- a/kong/test_utils.go +++ b/kong/test_utils.go @@ -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() + } +}