diff --git a/CHANGELOG.md b/CHANGELOG.md index d73990ace..b5f8f66f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/kong/plugin.go b/kong/plugin.go index 38e0ca09d..646169a51 100644 --- a/kong/plugin.go +++ b/kong/plugin.go @@ -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 diff --git a/kong/plugin_service.go b/kong/plugin_service.go index 5f1a0886f..8a93401e3 100644 --- a/kong/plugin_service.go +++ b/kong/plugin_service.go @@ -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 @@ -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 @@ -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. @@ -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 { + queryPath = queryPath + "/" + *plugin.ID + method = "PUT" + } + if isEmptyString(cgIDorName) { + 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, @@ -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, @@ -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, +) ([]*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 diff --git a/kong/plugin_service_test.go b/kong/plugin_service_test.go index bd1d63dbb..7b1e3643f 100644 --- a/kong/plugin_service_test.go +++ b/kong/plugin_service_test.go @@ -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 { diff --git a/kong/zz_generated.deepcopy.go b/kong/zz_generated.deepcopy.go index 0dfe91132..da6d642d6 100644 --- a/kong/zz_generated.deepcopy.go +++ b/kong/zz_generated.deepcopy.go @@ -1555,6 +1555,11 @@ func (in *Plugin) DeepCopyInto(out *Plugin) { *out = new(Consumer) (*in).DeepCopyInto(*out) } + if in.ConsumerGroup != nil { + in, out := &in.ConsumerGroup, &out.ConsumerGroup + *out = new(ConsumerGroup) + (*in).DeepCopyInto(*out) + } out.Config = in.Config.DeepCopy() if in.Enabled != nil { in, out := &in.Enabled, &out.Enabled