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: support scoping plugins to consumer-groups #352

Merged
merged 3 commits into from
Jul 7, 2023
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@

> Release date: TBD

- Added support for scoping plugins to consumer-groups.
[#352](https://github.com/Kong/go-kong/pull/352)

## [v0.45.0]

> Release date: 2023/07/03
Expand Down
27 changes: 14 additions & 13 deletions kong/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ package kong
// Read https://docs.konghq.com/gateway/latest/admin-api/#plugin-object
// +k8s:deepcopy-gen=true
type Plugin struct {
CreatedAt *int `json:"created_at,omitempty" yaml:"created_at,omitempty"`
ID *string `json:"id,omitempty" yaml:"id,omitempty"`
Name *string `json:"name,omitempty" yaml:"name,omitempty"`
InstanceName *string `json:"instance_name,omitempty" yaml:"instance_name,omitempty"`
Route *Route `json:"route,omitempty" yaml:"route,omitempty"`
Service *Service `json:"service,omitempty" yaml:"service,omitempty"`
Consumer *Consumer `json:"consumer,omitempty" yaml:"consumer,omitempty"`
Config Configuration `json:"config,omitempty" yaml:"config,omitempty"`
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
RunOn *string `json:"run_on,omitempty" yaml:"run_on,omitempty"`
Ordering *PluginOrdering `json:"ordering,omitempty" yaml:"ordering,omitempty"`
Protocols []*string `json:"protocols,omitempty" yaml:"protocols,omitempty"`
Tags []*string `json:"tags,omitempty" yaml:"tags,omitempty"`
CreatedAt *int `json:"created_at,omitempty" yaml:"created_at,omitempty"`
ID *string `json:"id,omitempty" yaml:"id,omitempty"`
Name *string `json:"name,omitempty" yaml:"name,omitempty"`
InstanceName *string `json:"instance_name,omitempty" yaml:"instance_name,omitempty"`
Route *Route `json:"route,omitempty" yaml:"route,omitempty"`
Service *Service `json:"service,omitempty" yaml:"service,omitempty"`
Consumer *Consumer `json:"consumer,omitempty" yaml:"consumer,omitempty"`
ConsumerGroup *ConsumerGroup `json:"consumer_group,omitempty" yaml:"consumer_group,omitempty"`
Config Configuration `json:"config,omitempty" yaml:"config,omitempty"`
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
RunOn *string `json:"run_on,omitempty" yaml:"run_on,omitempty"`
Ordering *PluginOrdering `json:"ordering,omitempty" yaml:"ordering,omitempty"`
Protocols []*string `json:"protocols,omitempty" yaml:"protocols,omitempty"`
Tags []*string `json:"tags,omitempty" yaml:"tags,omitempty"`
}

// PluginOrdering contains before or after instructions for plugin execution order
Expand Down
59 changes: 59 additions & 0 deletions kong/plugin_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type AbstractPluginService interface {
CreateForService(ctx context.Context, serviceIDorName *string, plugin *Plugin) (*Plugin, error)
// CreateForRoute creates a Plugin in Kong.
CreateForRoute(ctx context.Context, routeIDorName *string, plugin *Plugin) (*Plugin, error)
// CreateForConsumerGroup creates a Plugin in Kong.
CreateForConsumerGroup(ctx context.Context, cgIDorName *string, plugin *Plugin) (*Plugin, error)
// Get fetches a Plugin in Kong.
Get(ctx context.Context, usernameOrID *string) (*Plugin, error)
// Update updates a Plugin in Kong
Expand All @@ -24,6 +26,8 @@ type AbstractPluginService interface {
UpdateForService(ctx context.Context, serviceIDorName *string, plugin *Plugin) (*Plugin, error)
// UpdateForRoute updates a Plugin in Kong for a service
UpdateForRoute(ctx context.Context, routeIDorName *string, plugin *Plugin) (*Plugin, error)
// UpdateForConsumerGrou updates a Plugin in Kong for a consumer-group
UpdateForConsumerGroup(ctx context.Context, cgIDorName *string, plugin *Plugin) (*Plugin, error)
// Delete deletes a Plugin in Kong
Delete(ctx context.Context, usernameOrID *string) error
// DeleteForService deletes a Plugin in Kong
Expand All @@ -40,6 +44,8 @@ type AbstractPluginService interface {
ListAllForService(ctx context.Context, serviceIDorName *string) ([]*Plugin, error)
// ListAllForRoute fetches all Plugins in Kong enabled for a service.
ListAllForRoute(ctx context.Context, routeID *string) ([]*Plugin, error)
// ListAllForConsumerGroups fetches all Plugins in Kong enabled for a consumer group.
ListAllForConsumerGroups(ctx context.Context, cgID *string) ([]*Plugin, error)
// Validate validates a Plugin against its schema
Validate(ctx context.Context, plugin *Plugin) (bool, string, error)
// GetSchema retrieves the config schema of a plugin.
Expand Down Expand Up @@ -153,6 +159,31 @@ func (s *PluginService) CreateForRoute(ctx context.Context,
return s.sendRequest(ctx, plugin, fmt.Sprintf("/routes/%v"+queryPath, *routeIDorName), method)
}

// CreateForConsumerGroup creates a Plugin in Kong at ConsumerGroup level.
// If an ID is specified, it will be used to
// create a plugin in Kong, otherwise an ID
// is auto-generated.
func (s *PluginService) CreateForConsumerGroup(ctx context.Context,
cgIDorName *string, plugin *Plugin,
) (*Plugin, error) {
if plugin == nil {
return nil, fmt.Errorf("plugin cannot be nil")
}

queryPath := "/plugins"
method := "POST"

if plugin.ID != nil {
pmalek marked this conversation as resolved.
Show resolved Hide resolved
queryPath = queryPath + "/" + *plugin.ID
method = "PUT"
}
if isEmptyString(cgIDorName) {
pmalek marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("cgIDorName cannot be nil")
}

return s.sendRequest(ctx, plugin, fmt.Sprintf("/consumer_groups/%v"+queryPath, *cgIDorName), method)
}

// Get fetches a Plugin in Kong.
func (s *PluginService) Get(ctx context.Context,
usernameOrID *string,
Expand Down Expand Up @@ -217,6 +248,24 @@ func (s *PluginService) UpdateForRoute(ctx context.Context,
return s.sendRequest(ctx, plugin, endpoint, "PATCH")
}

// UpdateForConsumerGroup updates a Plugin in Kong at Consumer Group level.
func (s *PluginService) UpdateForConsumerGroup(ctx context.Context,
cgIDorName *string, plugin *Plugin,
) (*Plugin, error) {
if plugin == nil {
return nil, fmt.Errorf("plugin cannot be nil")
}
if isEmptyString(plugin.ID) {
return nil, fmt.Errorf("ID cannot be nil for Update operation")
}
if isEmptyString(cgIDorName) {
return nil, fmt.Errorf("cgIDorName cannot be nil")
}

endpoint := fmt.Sprintf("/consumer_groups/%v/plugins/%v", *cgIDorName, *plugin.ID)
return s.sendRequest(ctx, plugin, endpoint, "PATCH")
}

// Delete deletes a Plugin in Kong
func (s *PluginService) Delete(ctx context.Context,
pluginID *string,
Expand Down Expand Up @@ -394,6 +443,16 @@ func (s *PluginService) ListAllForRoute(ctx context.Context,
return s.listAllByPath(ctx, "/routes/"+*routeID+"/plugins")
}

// ListAllForConsumerGroups fetches all Plugins in Kong enabled for a consumer group.
func (s *PluginService) ListAllForConsumerGroups(ctx context.Context,
cgID *string,
pmalek marked this conversation as resolved.
Show resolved Hide resolved
) ([]*Plugin, error) {
if isEmptyString(cgID) {
return nil, fmt.Errorf("cgID cannot be nil")
}
return s.listAllByPath(ctx, "/consumer_groups/"+*cgID+"/plugins")
}

func (s *PluginService) sendRequest(ctx context.Context, plugin *Plugin, endpoint, method string) (*Plugin, error) {
var req *http.Request
var err error
Expand Down
123 changes: 123 additions & 0 deletions kong/plugin_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,129 @@ func TestFillPluginDefaultsArbitraryMap(T *testing.T) {
}
}

func TestPluginsWithConsumerGroup(T *testing.T) {
RunWhenDBMode(T, "postgres")
RunWhenEnterprise(T, ">=3.4.0", RequiredFeatures{})

assert := assert.New(T)
require := require.New(T)

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

// create consumer group
cg := &ConsumerGroup{
Name: String("foo"),
}
createdCG, err := client.ConsumerGroups.Create(defaultCtx, cg)
require.NoError(err)
assert.NotNil(createdCG)

plugin := &Plugin{
Name: String("rate-limiting-advanced"),
Config: Configuration{
"limit": []interface{}{5},
"window_size": []interface{}{30},
},
ConsumerGroup: &ConsumerGroup{
ID: createdCG.ID,
},
}

createdPlugin, err := client.Plugins.Create(defaultCtx, plugin)
assert.NoError(err)
require.NotNil(createdPlugin)
require.Nil(createdPlugin.InstanceName)

plugin, err = client.Plugins.Get(defaultCtx, createdPlugin.ID)
assert.NoError(err)
assert.NotNil(plugin)
assert.Equal(plugin.ConsumerGroup.ID, createdCG.ID)
assert.Equal("sliding", plugin.Config["window_type"])

createdPlugin.Config["window_type"] = "fixed"
updatedPlugin, err := client.Plugins.UpdateForConsumerGroup(defaultCtx, createdCG.Name, createdPlugin)
assert.NoError(err)
assert.NotNil(createdPlugin)
assert.Equal("fixed", updatedPlugin.Config["window_type"])

assert.NoError(client.ConsumerGroups.Delete(defaultCtx, createdCG.ID))
// assert the plugin was cascade deleted
plugin, err = client.Plugins.Get(defaultCtx, createdPlugin.ID)
assert.Nil(plugin)
assert.True(IsNotFoundErr(err))

// create another consumer group
cg = &ConsumerGroup{
Name: String("bar"),
}
createdCG, err = client.ConsumerGroups.Create(defaultCtx, cg)
require.NoError(err)
assert.NotNil(createdCG)

id := uuid.NewString()
pluginForCG := &Plugin{
Name: String("request-transformer"),
ID: String(id),
}

createdPlugin, err = client.Plugins.CreateForConsumerGroup(defaultCtx, createdCG.Name, pluginForCG)
assert.NoError(err)
assert.NotNil(createdPlugin)
assert.Equal(createdPlugin.ConsumerGroup.ID, createdCG.ID)

assert.NoError(client.ConsumerGroups.Delete(defaultCtx, createdCG.ID))
// assert the plugin was cascade deleted
plugin, err = client.Plugins.Get(defaultCtx, createdPlugin.ID)
assert.Nil(plugin)
assert.True(IsNotFoundErr(err))

// create another consumer group
cg = &ConsumerGroup{
Name: String("baz"),
}
createdCG, err = client.ConsumerGroups.Create(defaultCtx, cg)
require.NoError(err)
assert.NotNil(createdCG)

plugins := []*Plugin{
{
Name: String("request-transformer"),
ConsumerGroup: createdCG,
},
{
Name: String("rate-limiting-advanced"),
Config: Configuration{
"limit": []interface{}{5},
"window_size": []interface{}{30},
},
ConsumerGroup: createdCG,
},
}

// create fixturs
for i := 0; i < len(plugins); i++ {
plugin, err := client.Plugins.Create(defaultCtx, plugins[i])
assert.NoError(err)
assert.NotNil(plugin)
plugins[i] = plugin
}

pluginsFromKong, err := client.Plugins.ListAllForConsumerGroups(defaultCtx, createdCG.ID)
assert.NoError(err)
assert.NotNil(pluginsFromKong)
assert.Len(pluginsFromKong, 2)

assert.NoError(client.ConsumerGroups.Delete(defaultCtx, createdCG.ID))
// assert the plugins were cascade deleted
for _, plugin := range plugins {
res, err := client.Plugins.Get(defaultCtx, plugin.ID)
assert.Nil(res)
assert.True(IsNotFoundErr(err))
}
}

func comparePlugins(T *testing.T, expected, actual []*Plugin) bool {
var expectedNames, actualNames []string
for _, plugin := range expected {
Expand Down
5 changes: 5 additions & 0 deletions kong/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.