diff --git a/Makefile b/Makefile index 9767d86d1..7346c2b93 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,6 @@ setup-kong-ee: .PHONY: test-integration test-integration: - go test -v -tags=integration \ + go test -v -count=1 -tags=integration \ -race \ ./tests/integration/... \ No newline at end of file diff --git a/cmd/common.go b/cmd/common.go index 866188e23..61cc62c09 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -212,6 +212,10 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, return err } + if utils.Kong340Version.LTE(parsedKongVersion) { + dumpConfig.IsConsumerGroupScopedPluginSupported = true + } + // read the current state var currentState *state.KongState if workspaceExists { diff --git a/cmd/common_konnect.go b/cmd/common_konnect.go index 2850bc56e..9f5751794 100644 --- a/cmd/common_konnect.go +++ b/cmd/common_konnect.go @@ -131,6 +131,7 @@ func resetKonnectV2(ctx context.Context) error { } if dumpConfig.KonnectRuntimeGroup == "" { dumpConfig.KonnectRuntimeGroup = defaultRuntimeGroupName + dumpConfig.IsConsumerGroupScopedPluginSupported = true } currentState, err := fetchCurrentState(ctx, client, dumpConfig) if err != nil { diff --git a/cmd/reset.go b/cmd/reset.go index 96b8f7627..d0a68780e 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -60,8 +60,16 @@ By default, this command will ask for confirmation.`, if err != nil { return fmt.Errorf("reading Kong version: %w", err) } + parsedKongVersion, err := utils.ParseKongVersion(kongVersion) + if err != nil { + return fmt.Errorf("parsing Kong version: %w", err) + } _ = sendAnalytics("reset", kongVersion, mode) + if utils.Kong340Version.LTE(parsedKongVersion) { + dumpConfig.IsConsumerGroupScopedPluginSupported = true + } + var workspaces []string // Kong OSS or default workspace if !resetAllWorkspaces && resetWorkspace == "" { diff --git a/dump/dump.go b/dump/dump.go index 402ce0d0d..1ff4c96b2 100644 --- a/dump/dump.go +++ b/dump/dump.go @@ -30,6 +30,9 @@ type Config struct { // KonnectRuntimeGroup KonnectRuntimeGroup string + + // IsConsumerGroupScopedPluginSupported + IsConsumerGroupScopedPluginSupported bool } func deduplicate(stringSlice []string) []string { @@ -196,6 +199,7 @@ func getProxyConfiguration(ctx context.Context, group *errgroup.Group, plugins = excludeKonnectManagedPlugins(plugins) if config.SkipConsumers { plugins = excludeConsumersPlugins(plugins) + plugins = excludeConsumerGroupsPlugins(plugins) } state.Plugins = plugins return nil @@ -870,3 +874,15 @@ func excludeConsumersPlugins(plugins []*kong.Plugin) []*kong.Plugin { } return filtered } + +// excludeConsumerGroupsPlugins filter out consumer-groups plugins +func excludeConsumerGroupsPlugins(plugins []*kong.Plugin) []*kong.Plugin { + var filtered []*kong.Plugin + for _, p := range plugins { + if p.ConsumerGroup != nil && !utils.Empty(p.ConsumerGroup.ID) { + continue + } + filtered = append(filtered, p) + } + return filtered +} diff --git a/file/builder.go b/file/builder.go index 755ca5574..ac1a9ec3c 100644 --- a/file/builder.go +++ b/file/builder.go @@ -12,6 +12,8 @@ import ( "github.com/kong/go-kong/kong" ) +const ratelimitingAdvancedPluginName = "rate-limiting-advanced" + type stateBuilder struct { targetContent *Content rawState *utils.KongRawState @@ -35,6 +37,8 @@ type stateBuilder struct { checkRoutePaths bool + isConsumerGroupScopedPluginSupported bool + err error } @@ -69,6 +73,10 @@ func (b *stateBuilder) build() (*utils.KongRawState, *utils.KonnectRawState, err b.checkRoutePaths = true } + if utils.Kong340Version.LTE(b.kongVersion) || b.isKonnect { + b.isConsumerGroupScopedPluginSupported = true + } + // build b.certificates() if !b.skipCACerts { @@ -116,22 +124,50 @@ func (b *stateBuilder) consumerGroups() { ConsumerGroup: &cg.ConsumerGroup, } - for _, plugin := range cg.Plugins { - if utils.Empty(plugin.ID) { - current, err := b.currentState.ConsumerGroupPlugins.Get( - *plugin.Name, *cg.ConsumerGroup.ID, - ) - if errors.Is(err, state.ErrNotFound) { - plugin.ID = uuid() - } else if err != nil { - b.err = err - return - } else { - plugin.ID = kong.String(*current.ID) + err := b.intermediate.ConsumerGroups.Add(state.ConsumerGroup{ConsumerGroup: cg.ConsumerGroup}) + if err != nil { + b.err = err + return + } + + if b.isConsumerGroupScopedPluginSupported { + var plugins []FPlugin + for _, plugin := range cg.Plugins { + plugin.ConsumerGroup = utils.GetConsumerGroupReference(cg.ConsumerGroup) + plugins = append(plugins, FPlugin{ + Plugin: kong.Plugin{ + ID: plugin.ID, + Name: plugin.Name, + Config: plugin.Config, + ConsumerGroup: &kong.ConsumerGroup{ + ID: cg.ID, + }, + }, + }) + } + + if err := b.ingestPlugins(plugins); err != nil { + b.err = err + return + } + } else { + for _, plugin := range cg.Plugins { + if utils.Empty(plugin.ID) { + current, err := b.currentState.ConsumerGroupPlugins.Get( + *plugin.Name, *cg.ConsumerGroup.ID, + ) + if errors.Is(err, state.ErrNotFound) { + plugin.ID = uuid() + } else if err != nil { + b.err = err + return + } else { + plugin.ID = kong.String(*current.ID) + } } + b.defaulter.MustSet(plugin) + cgo.Plugins = append(cgo.Plugins, plugin) } - b.defaulter.MustSet(plugin) - cgo.Plugins = append(cgo.Plugins, plugin) } b.rawState.ConsumerGroups = append(b.rawState.ConsumerGroups, &cgo) } @@ -882,6 +918,37 @@ func (b *stateBuilder) plugins() { } p.Route = utils.GetRouteReference(r.Route) } + if p.ConsumerGroup != nil && !utils.Empty(p.ConsumerGroup.ID) { + cg, err := b.intermediate.ConsumerGroups.Get(*p.ConsumerGroup.ID) + if errors.Is(err, state.ErrNotFound) { + b.err = fmt.Errorf("consumer-group %v for plugin %v: %w", + p.ConsumerGroup.FriendlyName(), *p.Name, err) + return + } else if err != nil { + b.err = err + return + } + p.ConsumerGroup = utils.GetConsumerGroupReference(cg.ConsumerGroup) + } + + if b.isConsumerGroupScopedPluginSupported && *p.Name == ratelimitingAdvancedPluginName { + // check if deprecated consumer-groups configuration is present in the config + var consumerGroupsFound bool + if groups, ok := p.Config["consumer_groups"]; ok { + // if groups is an array of length > 0, then consumer_groups is set + if groupsArray, ok := groups.([]interface{}); ok && len(groupsArray) > 0 { + consumerGroupsFound = true + } + } + _, enforceConsumerGroupsFound := p.Config["enforce_consumer_groups"] + if consumerGroupsFound || enforceConsumerGroupsFound { + b.err = errors.New("a rate-limiting-advanced plugin with config.consumer_groups\n" + + "and/or config.enforce_consumer_groups was found. Please use Consumer Groups scoped\n" + + "Plugins when running against Kong Enterprise 3.4.0 and above.\n\n" + + "Check DOC_LINK for more information") + return + } + } plugins = append(plugins, p) } if err := b.ingestPlugins(plugins); err != nil { @@ -997,9 +1064,9 @@ func (b *stateBuilder) ingestPlugins(plugins []FPlugin) error { for _, p := range plugins { p := p if utils.Empty(p.ID) { - cID, rID, sID := pluginRelations(&p.Plugin) + cID, rID, sID, cgID := pluginRelations(&p.Plugin) plugin, err := b.currentState.Plugins.GetByProp(*p.Name, - sID, rID, cID) + sID, rID, cID, cgID) if errors.Is(err, state.ErrNotFound) { p.ID = uuid() } else if err != nil { @@ -1044,7 +1111,7 @@ func (b *stateBuilder) fillPluginConfig(plugin *FPlugin) error { return nil } -func pluginRelations(plugin *kong.Plugin) (cID, rID, sID string) { +func pluginRelations(plugin *kong.Plugin) (cID, rID, sID, cgID string) { if plugin.Consumer != nil && !utils.Empty(plugin.Consumer.ID) { cID = *plugin.Consumer.ID } @@ -1054,6 +1121,9 @@ func pluginRelations(plugin *kong.Plugin) (cID, rID, sID string) { if plugin.Service != nil && !utils.Empty(plugin.Service.ID) { sID = *plugin.Service.ID } + if plugin.ConsumerGroup != nil && !utils.Empty(plugin.ConsumerGroup.ID) { + cgID = *plugin.ConsumerGroup.ID + } return } diff --git a/file/builder_test.go b/file/builder_test.go index 0d3557786..1b79c2886 100644 --- a/file/builder_test.go +++ b/file/builder_test.go @@ -293,6 +293,9 @@ func existingPluginState() *state.KongState { Route: &kong.Route{ ID: kong.String("700bc504-b2b1-4abd-bd38-cec92779659e"), }, + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("69ed4618-a653-4b54-8bb6-dc33bd6fe048"), + }, }, }) return s @@ -751,6 +754,9 @@ func Test_stateBuilder_ingestPlugins(t *testing.T) { Route: &kong.Route{ ID: kong.String("700bc504-b2b1-4abd-bd38-cec92779659e"), }, + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("69ed4618-a653-4b54-8bb6-dc33bd6fe048"), + }, }, }, }, @@ -780,6 +786,9 @@ func Test_stateBuilder_ingestPlugins(t *testing.T) { Route: &kong.Route{ ID: kong.String("700bc504-b2b1-4abd-bd38-cec92779659e"), }, + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("69ed4618-a653-4b54-8bb6-dc33bd6fe048"), + }, Config: kong.Configuration{}, }, }, @@ -805,11 +814,12 @@ func Test_pluginRelations(t *testing.T) { plugin *kong.Plugin } tests := []struct { - name string - args args - wantCID string - wantRID string - wantSID string + name string + args args + wantCID string + wantRID string + wantSID string + wantCGID string }{ { args: args{ @@ -817,9 +827,10 @@ func Test_pluginRelations(t *testing.T) { Name: kong.String("foo"), }, }, - wantCID: "", - wantRID: "", - wantSID: "", + wantCID: "", + wantRID: "", + wantSID: "", + wantCGID: "", }, { args: args{ @@ -834,16 +845,20 @@ func Test_pluginRelations(t *testing.T) { Service: &kong.Service{ ID: kong.String("sID"), }, + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("cgID"), + }, }, }, - wantCID: "cID", - wantRID: "rID", - wantSID: "sID", + wantCID: "cID", + wantRID: "rID", + wantSID: "sID", + wantCGID: "cgID", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotCID, gotRID, gotSID := pluginRelations(tt.args.plugin) + gotCID, gotRID, gotSID, gotCGID := pluginRelations(tt.args.plugin) if gotCID != tt.wantCID { t.Errorf("pluginRelations() gotCID = %v, want %v", gotCID, tt.wantCID) } @@ -853,6 +868,9 @@ func Test_pluginRelations(t *testing.T) { if gotSID != tt.wantSID { t.Errorf("pluginRelations() gotSID = %v, want %v", gotSID, tt.wantSID) } + if gotCGID != tt.wantCGID { + t.Errorf("pluginRelations() gotCGID = %v, want %v", gotCGID, tt.wantCGID) + } }) } } diff --git a/file/codegen/main.go b/file/codegen/main.go index be480d51f..552b70d0b 100644 --- a/file/codegen/main.go +++ b/file/codegen/main.go @@ -97,6 +97,7 @@ func main() { schema.Definitions["FPlugin"].Properties["consumer"] = stringType schema.Definitions["FPlugin"].Properties["service"] = stringType schema.Definitions["FPlugin"].Properties["route"] = stringType + schema.Definitions["FPlugin"].Properties["consumer_group"] = stringType schema.Definitions["FService"].Properties["client_certificate"] = stringType diff --git a/file/kong_json_schema.json b/file/kong_json_schema.json index b287a13ff..5e68bbe5f 100644 --- a/file/kong_json_schema.json +++ b/file/kong_json_schema.json @@ -444,7 +444,6 @@ }, "groups": { "items": { - "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ConsumerGroup" }, "type": "array" @@ -600,6 +599,9 @@ "consumer": { "type": "string" }, + "consumer_group": { + "type": "string" + }, "created_at": { "type": "integer" }, diff --git a/file/types.go b/file/types.go index 7c6aa44c5..ffe364eb1 100644 --- a/file/types.go +++ b/file/types.go @@ -321,19 +321,20 @@ type FPlugin struct { // foo is a shadow type of Plugin. // It is used for custom marshalling of plugin. type foo 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"` - Config kong.Configuration `json:"config,omitempty" yaml:"config,omitempty"` - Service string `json:"service,omitempty" yaml:",omitempty"` - Consumer string `json:"consumer,omitempty" yaml:",omitempty"` - Route string `json:"route,omitempty" yaml:",omitempty"` - Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` - RunOn *string `json:"run_on,omitempty" yaml:"run_on,omitempty"` - Ordering *kong.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"` + Config kong.Configuration `json:"config,omitempty" yaml:"config,omitempty"` + Service string `json:"service,omitempty" yaml:",omitempty"` + Consumer string `json:"consumer,omitempty" yaml:",omitempty"` + ConsumerGroup string `json:"consumer_group,omitempty" yaml:",omitempty"` + Route string `json:"route,omitempty" yaml:",omitempty"` + Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + RunOn *string `json:"run_on,omitempty" yaml:"run_on,omitempty"` + Ordering *kong.PluginOrdering `json:"ordering,omitempty" yaml:"ordering,omitempty"` + Protocols []*string `json:"protocols,omitempty" yaml:"protocols,omitempty"` + Tags []*string `json:"tags,omitempty" yaml:"tags,omitempty"` ConfigSource *string `json:"_config,omitempty" yaml:"_config,omitempty"` } @@ -379,6 +380,9 @@ func copyToFoo(p FPlugin) foo { if p.Plugin.Service != nil { f.Service = *p.Plugin.Service.ID } + if p.Plugin.ConsumerGroup != nil { + f.ConsumerGroup = *p.Plugin.ConsumerGroup.ID + } return f } @@ -428,6 +432,11 @@ func copyFromFoo(f foo, p *FPlugin) { ID: kong.String(f.Service), } } + if f.ConsumerGroup != "" { + p.ConsumerGroup = &kong.ConsumerGroup{ + ID: kong.String(f.ConsumerGroup), + } + } } // MarshalYAML is a custom marshal method to handle @@ -480,6 +489,9 @@ func (p FPlugin) sortKey() string { if p.Service != nil { key += *p.Service.ID } + if p.ConsumerGroup != nil { + key += *p.ConsumerGroup.ID + } return key } if p.ID != nil { diff --git a/file/writer.go b/file/writer.go index 8d60385c3..e503266b3 100644 --- a/file/writer.go +++ b/file/writer.go @@ -424,6 +424,18 @@ func populatePlugins(kongState *state.KongState, file *Content, } p.Route.ID = &rID } + if p.ConsumerGroup != nil { + associations++ + cgID := *p.ConsumerGroup.ID + cg, err := kongState.ConsumerGroups.Get(cgID) + if err != nil { + return fmt.Errorf("unable to get consumer-group %s for plugin %s [%s]: %w", cgID, *p.Name, *p.ID, err) + } + if !utils.Empty(cg.Name) { + cgID = *cg.Name + } + p.ConsumerGroup.ID = &cgID + } if associations == 0 || associations > 1 { utils.ZeroOutID(p, p.Name, config.WithID) utils.ZeroOutTimestamps(p) @@ -712,13 +724,13 @@ func populateConsumerGroups(kongState *state.KongState, file *Content, if err != nil { return err } - plugins, err := kongState.ConsumerGroupPlugins.GetAll() + cgPlugins, err := kongState.ConsumerGroupPlugins.GetAll() if err != nil { return err } for _, cg := range consumerGroups { group := FConsumerGroupObject{ConsumerGroup: cg.ConsumerGroup} - for _, plugin := range plugins { + for _, plugin := range cgPlugins { if plugin.ID != nil && cg.ID != nil { if plugin.ConsumerGroup != nil && *plugin.ConsumerGroup.ID == *cg.ID { utils.ZeroOutID(plugin, plugin.Name, config.WithID) @@ -729,6 +741,25 @@ func populateConsumerGroups(kongState *state.KongState, file *Content, } } } + + plugins, err := kongState.Plugins.GetAllByConsumerGroupID(*cg.ID) + if err != nil { + return err + } + for _, plugin := range plugins { + if plugin.ID != nil && cg.ID != nil { + if plugin.ConsumerGroup != nil && *plugin.ConsumerGroup.ID == *cg.ID { + utils.ZeroOutID(plugin, plugin.Name, config.WithID) + utils.ZeroOutID(plugin.ConsumerGroup, plugin.ConsumerGroup.Name, config.WithID) + group.Plugins = append(group.Plugins, &kong.ConsumerGroupPlugin{ + ID: plugin.ID, + Name: plugin.Name, + Config: plugin.Config, + }) + } + } + } + utils.ZeroOutID(&group, group.Name, config.WithID) utils.ZeroOutTimestamps(&group) file.ConsumerGroups = append(file.ConsumerGroups, group) diff --git a/go.mod b/go.mod index c834e4737..0b62c5d71 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hexops/gotextdiff v1.0.3 github.com/imdario/mergo v0.3.16 github.com/kong/go-apiops v0.1.20 - github.com/kong/go-kong v0.44.0 + github.com/kong/go-kong v0.46.0 github.com/mitchellh/go-homedir v1.1.0 github.com/shirou/gopsutil/v3 v3.23.6 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index 8d5d63b83..72933834e 100644 --- a/go.sum +++ b/go.sum @@ -219,8 +219,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kong/go-apiops v0.1.20 h1:MesWbep8Jvnk1FlbaFWgv4IkSi+DNojlSVmFt8INzrc= github.com/kong/go-apiops v0.1.20/go.mod h1:3P9DBGLcU6Gp4wo8z4xohcg8PMutBAknc54pLZoQtDs= -github.com/kong/go-kong v0.44.0 h1:1x3w/TYdJjIZ6c1j9HiYP8755c923XN2O6j3kEaUkTA= -github.com/kong/go-kong v0.44.0/go.mod h1:41Sot1N/n8UHBp+gE/6nOw3vuzoHbhMSyU/zOS7VzPE= +github.com/kong/go-kong v0.46.0 h1:9I6nlX63WymU5Sg+d13iZDVwpW5vXh8/v0zarU27dzI= +github.com/kong/go-kong v0.46.0/go.mod h1:41Sot1N/n8UHBp+gE/6nOw3vuzoHbhMSyU/zOS7VzPE= github.com/kong/semver/v4 v4.0.1 h1:DIcNR8W3gfx0KabFBADPalxxsp+q/5COwIFkkhrFQ2Y= github.com/kong/semver/v4 v4.0.1/go.mod h1:LImQ0oT15pJvSns/hs2laLca2zcYoHu5EsSNY0J6/QA= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= diff --git a/state/builder.go b/state/builder.go index d7200aa12..97526dada 100644 --- a/state/builder.go +++ b/state/builder.go @@ -57,6 +57,18 @@ func ensureConsumer(kongState *KongState, consumerID string) (bool, *kong.Consum return true, utils.GetConsumerReference(c.Consumer), nil } +func ensureConsumerGroup(kongState *KongState, consumerGroupID string) (bool, *kong.ConsumerGroup, error) { + c, err := kongState.ConsumerGroups.Get(consumerGroupID) + if err != nil { + if errors.Is(err, ErrNotFound) { + return false, nil, nil + } + return false, nil, fmt.Errorf("looking up consumer-group %q: %w", consumerGroupID, err) + + } + return true, utils.GetConsumerGroupReference(c.ConsumerGroup), nil +} + func buildKong(kongState *KongState, raw *utils.KongRawState) error { for _, s := range raw.Services { err := kongState.Services.Add(Service{Service: *s}) @@ -282,6 +294,15 @@ func buildKong(kongState *KongState, raw *utils.KongRawState) error { p.Consumer = c } } + if p.ConsumerGroup != nil && !utils.Empty(p.ConsumerGroup.ID) { + ok, cg, err := ensureConsumerGroup(kongState, *p.ConsumerGroup.ID) + if err != nil { + return err + } + if ok { + p.ConsumerGroup = cg + } + } err := kongState.Plugins.Add(Plugin{Plugin: *p}) if err != nil { return fmt.Errorf("inserting plugins into state: %w", err) diff --git a/state/plugin.go b/state/plugin.go index 841ac1d6f..0b3951990 100644 --- a/state/plugin.go +++ b/state/plugin.go @@ -12,10 +12,11 @@ import ( var errPluginNameRequired = fmt.Errorf("name of plugin required") const ( - pluginTableName = "plugin" - pluginsByServiceID = "pluginsByServiceID" - pluginsByRouteID = "pluginsByRouteID" - pluginsByConsumerID = "pluginsByConsumerID" + pluginTableName = "plugin" + pluginsByServiceID = "pluginsByServiceID" + pluginsByRouteID = "pluginsByRouteID" + pluginsByConsumerID = "pluginsByConsumerID" + pluginsByConsumerGroupID = "pluginsByConsumerGroupID" ) var pluginTableSchema = &memdb.TableSchema{ @@ -68,6 +69,18 @@ var pluginTableSchema = &memdb.TableSchema{ }, AllowMissing: true, }, + pluginsByConsumerGroupID: { + Name: pluginsByConsumerGroupID, + Indexer: &indexers.SubFieldIndexer{ + Fields: []indexers.Field{ + { + Struct: "ConsumerGroup", + Sub: "ID", + }, + }, + }, + AllowMissing: true, + }, // combined foreign fields // FIXME bug: collision if svc/route/consumer has the same ID // and same type of plugin is created. Consider the case when only @@ -92,6 +105,10 @@ var pluginTableSchema = &memdb.TableSchema{ Struct: "Consumer", Sub: "ID", }, + { + Struct: "ConsumerGroup", + Sub: "ID", + }, }, }, }, @@ -133,7 +150,7 @@ func insertPlugin(txn *memdb.Txn, plugin Plugin) error { } // err out if another plugin with exact same combination is present - sID, rID, cID := "", "", "" + sID, rID, cID, cgID := "", "", "", "" if plugin.Service != nil && !utils.Empty(plugin.Service.ID) { sID = *plugin.Service.ID } @@ -143,7 +160,10 @@ func insertPlugin(txn *memdb.Txn, plugin Plugin) error { if plugin.Consumer != nil && !utils.Empty(plugin.Consumer.ID) { cID = *plugin.Consumer.ID } - _, err = getPluginBy(txn, *plugin.Name, sID, rID, cID) + if plugin.ConsumerGroup != nil && !utils.Empty(plugin.ConsumerGroup.ID) { + cgID = *plugin.ConsumerGroup.ID + } + _, err = getPluginBy(txn, *plugin.Name, sID, rID, cID, cgID) if err == nil { return fmt.Errorf("inserting plugin %v: %w", plugin.Console(), ErrAlreadyExists) } else if !errors.Is(err, ErrNotFound) { @@ -194,7 +214,7 @@ func (k *PluginsCollection) GetAllByName(name string) ([]*Plugin, error) { return k.getAllPluginsBy("name", name) } -func getPluginBy(txn *memdb.Txn, name, svcID, routeID, consumerID string) ( +func getPluginBy(txn *memdb.Txn, name, svcID, routeID, consumerID, consumerGroupID string) ( *Plugin, error, ) { if name == "" { @@ -202,7 +222,7 @@ func getPluginBy(txn *memdb.Txn, name, svcID, routeID, consumerID string) ( } res, err := txn.First(pluginTableName, "fields", - name, svcID, routeID, consumerID) + name, svcID, routeID, consumerID, consumerGroupID) if err != nil { return nil, err } @@ -217,18 +237,18 @@ func getPluginBy(txn *memdb.Txn, name, svcID, routeID, consumerID string) ( } // GetByProp returns a plugin which matches all the properties passed in -// the arguments. If serviceID, routeID and consumerID are empty strings, then -// a global plugin is searched. +// the arguments. If serviceID, routeID, consumerID and consumerGroupID +// are empty strings, then a global plugin is searched. // Otherwise, a plugin with name and the supplied foreign references is // searched. // name is required. -func (k *PluginsCollection) GetByProp(name, serviceID, - routeID string, consumerID string, +func (k *PluginsCollection) GetByProp( + name, serviceID, routeID, consumerID, consumerGroupID string, ) (*Plugin, error) { txn := k.db.Txn(false) defer txn.Abort() - return getPluginBy(txn, name, serviceID, routeID, consumerID) + return getPluginBy(txn, name, serviceID, routeID, consumerID, consumerGroupID) } func (k *PluginsCollection) getAllPluginsBy(index, identifier string) ( @@ -264,7 +284,7 @@ func (k *PluginsCollection) GetAllByServiceID(id string) ([]*Plugin, return k.getAllPluginsBy(pluginsByServiceID, id) } -// GetAllByRouteID returns all plugins referencing a service +// GetAllByRouteID returns all plugins referencing a route // by its id. func (k *PluginsCollection) GetAllByRouteID(id string) ([]*Plugin, error, @@ -280,6 +300,14 @@ func (k *PluginsCollection) GetAllByConsumerID(id string) ([]*Plugin, return k.getAllPluginsBy(pluginsByConsumerID, id) } +// GetAllByConsumerGroupID returns all plugins referencing a consumer-group +// by its id. +func (k *PluginsCollection) GetAllByConsumerGroupID(id string) ([]*Plugin, + error, +) { + return k.getAllPluginsBy(pluginsByConsumerGroupID, id) +} + // Update updates a plugin func (k *PluginsCollection) Update(plugin Plugin) error { // TODO abstract this check in the go-memdb library itself diff --git a/state/plugin_test.go b/state/plugin_test.go index 5ba22171b..f808806c8 100644 --- a/state/plugin_test.go +++ b/state/plugin_test.go @@ -271,9 +271,25 @@ func TestPluginsCollection_Update(t *testing.T) { }, }, } + plugin4 := Plugin{ + Plugin: kong.Plugin{ + ID: kong.String("id4"), + Name: kong.String("key-auth"), + Route: &kong.Route{ + ID: kong.String("route1"), + }, + Service: &kong.Service{ + ID: kong.String("svc1"), + }, + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("cg1"), + }, + }, + } k.Add(plugin1) k.Add(plugin2) k.Add(plugin3) + k.Add(plugin4) for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { @@ -362,6 +378,18 @@ func TestGetPluginByProp(t *testing.T) { }, }, }, + { + Plugin: kong.Plugin{ + ID: kong.String("5"), + Name: kong.String("key-auth"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("cg1"), + }, + Config: map[string]interface{}{ + "key5": "value5", + }, + }, + }, } assert := assert.New(t) collection := pluginsCollection() @@ -370,33 +398,38 @@ func TestGetPluginByProp(t *testing.T) { assert.Nil(collection.Add(p)) } - plugin, err := collection.GetByProp("", "", "", "") + plugin, err := collection.GetByProp("", "", "", "", "") assert.Nil(plugin) - assert.NotNil(err) + assert.Error(err) - plugin, err = collection.GetByProp("foo", "", "", "") + plugin, err = collection.GetByProp("foo", "", "", "", "") assert.Nil(plugin) assert.Equal(ErrNotFound, err) - plugin, err = collection.GetByProp("key-auth", "", "", "") - assert.Nil(err) + plugin, err = collection.GetByProp("key-auth", "", "", "", "") + assert.NoError(err) assert.NotNil(plugin) assert.Equal("value1", plugin.Config["key1"]) - plugin, err = collection.GetByProp("key-auth", "svc1", "", "") - assert.Nil(err) + plugin, err = collection.GetByProp("key-auth", "svc1", "", "", "") + assert.NoError(err) assert.NotNil(plugin) assert.Equal("value2", plugin.Config["key2"]) - plugin, err = collection.GetByProp("key-auth", "", "route1", "") - assert.Nil(err) + plugin, err = collection.GetByProp("key-auth", "", "route1", "", "") + assert.NoError(err) assert.NotNil(plugin) assert.Equal("value3", plugin.Config["key3"]) - plugin, err = collection.GetByProp("key-auth", "", "", "consumer1") - assert.Nil(err) + plugin, err = collection.GetByProp("key-auth", "", "", "consumer1", "") + assert.NoError(err) assert.NotNil(plugin) assert.Equal("value4", plugin.Config["key4"]) + + plugin, err = collection.GetByProp("key-auth", "", "", "", "cg1") + assert.NoError(err) + assert.NotNil(plugin) + assert.Equal("value5", plugin.Config["key5"]) } func TestPluginsInvalidType(t *testing.T) { diff --git a/state/types.go b/state/types.go index 5032d032a..b9f14fb3c 100644 --- a/state/types.go +++ b/state/types.go @@ -467,6 +467,9 @@ func (p1 *Plugin) Console() string { if p1.Consumer != nil { associations = append(associations, "consumer "+p1.Consumer.FriendlyName()) } + if p1.ConsumerGroup != nil { + associations = append(associations, "consumer-group "+p1.ConsumerGroup.FriendlyName()) + } if len(associations) > 0 { res += "for " } @@ -519,6 +522,7 @@ func (p1 *Plugin) EqualWithOpts(p2 *Plugin, ignoreID, p2Copy.Service = nil p2Copy.Route = nil p2Copy.Consumer = nil + p2Copy.ConsumerGroup = nil } if p1Copy.Service != nil { @@ -539,6 +543,12 @@ func (p1 *Plugin) EqualWithOpts(p2 *Plugin, ignoreID, if p2Copy.Consumer != nil { p2Copy.Consumer.Username = nil } + if p1Copy.ConsumerGroup != nil { + p1Copy.ConsumerGroup.Name = nil + } + if p2Copy.ConsumerGroup != nil { + p2Copy.ConsumerGroup.Name = nil + } return reflect.DeepEqual(p1Copy, p2Copy) } diff --git a/tests/integration/dump_test.go b/tests/integration/dump_test.go index 015da2835..75198d347 100644 --- a/tests/integration/dump_test.go +++ b/tests/integration/dump_test.go @@ -98,7 +98,7 @@ func Test_Dump_SkipConsumers(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.2.0") + runWhen(t, "enterprise", ">=3.2.0 <3.4.0") teardown := setup(t) defer teardown(t) @@ -122,7 +122,160 @@ func Test_Dump_SkipConsumers(t *testing.T) { expected, err := readFile(tc.expectedFile) assert.NoError(t, err) - assert.Equal(t, output, expected) + assert.Equal(t, expected, output) + }) + } +} + +func Test_Dump_SkipConsumers_34x(t *testing.T) { + tests := []struct { + name string + stateFile string + expectedFile string + skipConsumers bool + }{ + { + name: "dump with skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected.yaml", + skipConsumers: true, + }, + { + name: "dump with no skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected-no-skip-34.yaml", + skipConsumers: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.4.0") + teardown := setup(t) + defer teardown(t) + + assert.NoError(t, sync(tc.stateFile)) + + var ( + output string + err error + ) + if tc.skipConsumers { + output, err = dump( + "--skip-consumers", + "-o", "-", + ) + } else { + output, err = dump( + "-o", "-", + ) + } + assert.NoError(t, err) + + expected, err := readFile(tc.expectedFile) + assert.NoError(t, err) + assert.Equal(t, expected, output) + }) + } +} + +func Test_Dump_SkipConsumers_35x(t *testing.T) { + tests := []struct { + name string + stateFile string + expectedFile string + skipConsumers bool + }{ + { + name: "dump with skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected.yaml", + skipConsumers: true, + }, + { + name: "dump with no skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected-no-skip-35.yaml", + skipConsumers: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.5.0") + teardown := setup(t) + defer teardown(t) + + assert.NoError(t, sync(tc.stateFile)) + + var ( + output string + err error + ) + if tc.skipConsumers { + output, err = dump( + "--skip-consumers", + "-o", "-", + ) + } else { + output, err = dump( + "-o", "-", + ) + } + assert.NoError(t, err) + + expected, err := readFile(tc.expectedFile) + assert.NoError(t, err) + assert.Equal(t, expected, output) + }) + } +} + +func Test_Dump_SkipConsumers_Konnect(t *testing.T) { + tests := []struct { + name string + stateFile string + expectedFile string + skipConsumers bool + }{ + { + name: "dump with skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected_konnect.yaml", + skipConsumers: true, + }, + { + name: "dump with no skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected-no-skip_konnect.yaml", + skipConsumers: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKonnect(t) + teardown := setup(t) + defer teardown(t) + + assert.NoError(t, sync(tc.stateFile)) + + var ( + output string + err error + ) + if tc.skipConsumers { + output, err = dump( + "--skip-consumers", + "-o", "-", + ) + } else { + output, err = dump( + "-o", "-", + ) + } + assert.NoError(t, err) + + expected, err := readFile(tc.expectedFile) + assert.NoError(t, err) + assert.Equal(t, expected, output) }) } } diff --git a/tests/integration/sync_test.go b/tests/integration/sync_test.go index 348964f4c..0069dbadb 100644 --- a/tests/integration/sync_test.go +++ b/tests/integration/sync_test.go @@ -821,6 +821,328 @@ var ( Protocols: []*string{kong.String("http"), kong.String("https")}, }, } + + consumerGroupScopedPlugins = []*kong.Plugin{ + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("77e6691d-67c0-446a-9401-27be2b141aae"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(10)}, + "namespace": string("gold"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("5bcbd3a7-030b-4310-bd1d-2721ff85d236"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(7)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(5)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("key-auth"), + Config: kong.Configuration{ + "anonymous": nil, + "hide_credentials": false, + "key_in_body": false, + "key_in_header": true, + "key_in_query": true, + "key_names": []interface{}{"apikey"}, + "run_on_preflight": true, + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("http"), kong.String("https")}, + }, + } + + consumerGroupScopedPlugins35x = []*kong.Plugin{ + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("77e6691d-67c0-446a-9401-27be2b141aae"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(10)}, + "namespace": string("gold"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(256), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("5bcbd3a7-030b-4310-bd1d-2721ff85d236"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(7)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(256), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(5)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(256), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("key-auth"), + Config: kong.Configuration{ + "anonymous": nil, + "hide_credentials": false, + "key_in_body": false, + "key_in_header": true, + "key_in_query": true, + "key_names": []interface{}{"apikey"}, + "run_on_preflight": true, + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("http"), kong.String("https")}, + }, + } ) // test scope: @@ -2983,7 +3305,7 @@ func Test_Sync_ConsumerGroupsRLAFrom31(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", "==3.0.0") + runWhen(t, "enterprise", ">=3.0.0 <3.1.0") teardown := setup(t) defer teardown(t) @@ -3069,15 +3391,6 @@ func Test_Sync_ConsumerGroupsKonnect(t *testing.T) { ConsumerGroups: consumerGroupsWithTags, }, }, - { - name: "creates consumer groups and plugin", - kongFile: "testdata/sync/016-consumer-groups-and-plugins/kong3x.yaml", - kongFileInitial: "testdata/sync/016-consumer-groups-and-plugins/kong3x-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroupsWithRLAKonnect, - }, - }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { @@ -3177,7 +3490,8 @@ func Test_Sync_PluginInstanceName(t *testing.T) { } // test scope: -// - 3.2.0+ +// - 3.2.x +// - 3.3.x func Test_Sync_SkipConsumers(t *testing.T) { // setup stage client, err := getTestClient() @@ -3212,7 +3526,353 @@ func Test_Sync_SkipConsumers(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.2.0") + runWhen(t, "enterprise", ">=3.2.0 <3.4.0") + teardown := setup(t) + defer teardown(t) + + if tc.skipConsumers { + sync(tc.kongFile, "--skip-consumers") + } else { + sync(tc.kongFile) + } + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 3.4.x +func Test_Sync_SkipConsumers_34x(t *testing.T) { + runWhen(t, "enterprise", ">=3.4.0 <3.5.0") + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + skipConsumers bool + expectedState utils.KongRawState + }{ + { + name: "skip-consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + skipConsumers: true, + }, + { + name: "do not skip consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Consumers: consumerGroupsConsumers, + ConsumerGroups: []*kong.ConsumerGroupObject{ + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("silver"), + Tags: kong.StringSlice("tag1", "tag3"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("bar"), + }, + }, + }, + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + Tags: kong.StringSlice("tag1", "tag2"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, + }, + }, + Plugins: []*kong.Plugin{ + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("77e6691d-67c0-446a-9401-27be2b141aae"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(10)}, + "namespace": string("gold"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("5bcbd3a7-030b-4310-bd1d-2721ff85d236"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(7)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": float64(-1), + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + }, + }, + skipConsumers: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + teardown := setup(t) + defer teardown(t) + + if tc.skipConsumers { + sync(tc.kongFile, "--skip-consumers") + } else { + sync(tc.kongFile) + } + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - konnect +func Test_Sync_SkipConsumers_Konnect(t *testing.T) { + runWhenKonnect(t) + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + skipConsumers bool + expectedState utils.KongRawState + }{ + { + name: "skip-consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + skipConsumers: true, + }, + { + name: "do not skip consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Consumers: consumerGroupsConsumers, + ConsumerGroups: []*kong.ConsumerGroupObject{ + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("silver"), + Tags: kong.StringSlice("tag1", "tag3"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("bar"), + }, + }, + }, + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + Tags: kong.StringSlice("tag1", "tag2"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, + }, + }, + Plugins: []*kong.Plugin{ + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("77e6691d-67c0-446a-9401-27be2b141aae"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(10)}, + "namespace": string("gold"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": nil, + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + { + Name: kong.String("rate-limiting-advanced"), + ConsumerGroup: &kong.ConsumerGroup{ + ID: kong.String("5bcbd3a7-030b-4310-bd1d-2721ff85d236"), + }, + Config: kong.Configuration{ + "consumer_groups": nil, + "dictionary_name": string("kong_rate_limiting_counters"), + "disable_penalty": bool(false), + "enforce_consumer_groups": bool(false), + "error_code": float64(429), + "error_message": string("API rate limit exceeded"), + "header_name": nil, + "hide_client_headers": bool(false), + "identifier": string("consumer"), + "limit": []any{float64(7)}, + "namespace": string("silver"), + "path": nil, + "redis": map[string]any{ + "cluster_addresses": nil, + "connect_timeout": nil, + "database": float64(0), + "host": nil, + "keepalive_backlog": nil, + "keepalive_pool_size": float64(30), + "password": nil, + "port": nil, + "read_timeout": nil, + "send_timeout": nil, + "sentinel_addresses": nil, + "sentinel_master": nil, + "sentinel_password": nil, + "sentinel_role": nil, + "sentinel_username": nil, + "server_name": nil, + "ssl": false, + "ssl_verify": false, + "timeout": float64(2000), + "username": nil, + }, + "retry_after_jitter_max": float64(1), + "strategy": string("local"), + "sync_rate": nil, + "window_size": []any{float64(60)}, + "window_type": string("sliding"), + }, + Enabled: kong.Bool(true), + Protocols: []*string{kong.String("grpc"), kong.String("grpcs"), kong.String("http"), kong.String("https")}, + }, + }, + }, + skipConsumers: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { teardown := setup(t) defer teardown(t) @@ -3294,258 +3954,656 @@ func Test_Sync_ChangingIDsWhileKeepingNames(t *testing.T) { }, } - expectedConsumer = &kong.Consumer{ - Username: kong.String("c1"), - ID: expectedConsumerID, - } + expectedConsumer = &kong.Consumer{ + Username: kong.String("c1"), + ID: expectedConsumerID, + } + + expectedPlugins = []*kong.Plugin{ + { + Name: kong.String("rate-limiting"), + Route: &kong.Route{ + ID: expectedRouteID, + }, + }, + { + Name: kong.String("rate-limiting"), + Service: &kong.Service{ + ID: expectedServiceID, + }, + }, + { + Name: kong.String("rate-limiting"), + Consumer: &kong.Consumer{ + ID: expectedConsumerID, + }, + }, + } + ) + + testCases := []struct { + name string + beforeConfig string + }{ + { + name: "all entities have the same names, but different IDs", + beforeConfig: "testdata/sync/020-same-names-altered-ids/1-before.yaml", + }, + { + name: "service and consumer changed IDs, route did not", + beforeConfig: "testdata/sync/020-same-names-altered-ids/2-before.yaml", + }, + { + name: "route and consumer changed IDs, service did not", + beforeConfig: "testdata/sync/020-same-names-altered-ids/3-before.yaml", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + teardown := setup(t) + defer teardown(t) + + // First, create the entities with the original IDs. + err = sync(tc.beforeConfig) + require.NoError(t, err) + + // Then, sync again with the same names, but different IDs. + err = sync("testdata/sync/020-same-names-altered-ids/desired.yaml") + require.NoError(t, err) + + // Finally, check that the all entities exist and have the expected IDs. + testKongState(t, client, false, utils.KongRawState{ + Services: []*kong.Service{expectedService}, + Routes: []*kong.Route{expectedRoute}, + Consumers: []*kong.Consumer{expectedConsumer}, + Plugins: expectedPlugins, + }, ignoreFieldsIrrelevantForIDsTests) + }) + } +} + +// test scope: +// - 3.0.0+ +// - konnect +func Test_Sync_UpdateWithExplicitIDs(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + const ( + beforeConfig = "testdata/sync/021-update-with-explicit-ids/before.yaml" + afterConfig = "testdata/sync/021-update-with-explicit-ids/after.yaml" + ) + + // First, create entities with IDs assigned explicitly. + err = sync(beforeConfig) + require.NoError(t, err) + + // Then, sync again, adding tags to every entity just to trigger an update. + err = sync(afterConfig) + require.NoError(t, err) + + // Finally, verify that the update was successful. + testKongState(t, client, false, utils.KongRawState{ + Services: []*kong.Service{ + { + Name: kong.String("s1"), + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + Tags: kong.StringSlice("after"), + }, + }, + Routes: []*kong.Route{ + { + Name: kong.String("r1"), + ID: kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b"), + Tags: kong.StringSlice("after"), + Service: &kong.Service{ + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + }, + }, + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("c1"), + Tags: kong.StringSlice("after"), + }, + }, + }, ignoreFieldsIrrelevantForIDsTests) +} + +// test scope: +// - 3.0.0+ +// - konnect +func Test_Sync_UpdateWithExplicitIDsWithNoNames(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + const ( + beforeConfig = "testdata/sync/022-update-with-explicit-ids-with-no-names/before.yaml" + afterConfig = "testdata/sync/022-update-with-explicit-ids-with-no-names/after.yaml" + ) + + // First, create entities with IDs assigned explicitly. + err = sync(beforeConfig) + require.NoError(t, err) + + // Then, sync again, adding tags to every entity just to trigger an update. + err = sync(afterConfig) + require.NoError(t, err) + + // Finally, verify that the update was successful. + testKongState(t, client, false, utils.KongRawState{ + Services: []*kong.Service{ + { + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + Tags: kong.StringSlice("after"), + }, + }, + Routes: []*kong.Route{ + { + ID: kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b"), + Tags: kong.StringSlice("after"), + Service: &kong.Service{ + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + }, + }, + }, + }, ignoreFieldsIrrelevantForIDsTests) +} + +// test scope: +// - 3.0.0+ +// - konnect +func Test_Sync_CreateCertificateWithSNIs(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + err = sync("testdata/sync/023-create-and-update-certificate-with-snis/initial.yaml") + require.NoError(t, err) + + // To ignore noise, we ignore the Key and Cert fields because they are not relevant for this test. + ignoredFields := []cmp.Option{ + cmpopts.IgnoreFields( + kong.Certificate{}, + "Key", + "Cert", + ), + } + + testKongState(t, client, false, utils.KongRawState{ + Certificates: []*kong.Certificate{ + { + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + Tags: kong.StringSlice("before"), + }, + }, + SNIs: []*kong.SNI{ + { + Name: kong.String("example.com"), + Certificate: &kong.Certificate{ + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + }, + }, + }, + }, ignoredFields) + + err = sync("testdata/sync/023-create-and-update-certificate-with-snis/update.yaml") + require.NoError(t, err) + + testKongState(t, client, false, utils.KongRawState{ + Certificates: []*kong.Certificate{ + { + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + Tags: kong.StringSlice("after"), // Tag should be updated. + }, + }, + SNIs: []*kong.SNI{ + { + Name: kong.String("example.com"), + Certificate: &kong.Certificate{ + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + }, + }, + }, + }, ignoredFields) +} + +// test scope: +// - 3.0.0+ +// - konnect +func Test_Sync_ConsumersWithCustomIDAndUsername(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + err = sync("testdata/sync/024-consumers-with-custom_id-and-username/kong3x.yaml") + require.NoError(t, err) - expectedPlugins = []*kong.Plugin{ - { - Name: kong.String("rate-limiting"), - Route: &kong.Route{ - ID: expectedRouteID, - }, - }, + testKongState(t, client, false, utils.KongRawState{ + Consumers: []*kong.Consumer{ { - Name: kong.String("rate-limiting"), - Service: &kong.Service{ - ID: expectedServiceID, - }, + ID: kong.String("ce49186d-7670-445d-a218-897631b29ada"), + Username: kong.String("Foo"), + CustomID: kong.String("foo"), }, { - Name: kong.String("rate-limiting"), - Consumer: &kong.Consumer{ - ID: expectedConsumerID, - }, + ID: kong.String("7820f383-7b77-4fcc-af7f-14ff3e256693"), + Username: kong.String("foo"), + CustomID: kong.String("bar"), }, - } - ) + }, + }, nil) +} - testCases := []struct { - name string - beforeConfig string +// This test has 2 goals: +// - make sure consumer groups scoped plugins can be configured correctly in Kong +// - the actual consumer groups functionality works once set +// +// This is achieved via configuring: +// - 3 consumers: +// - 1 belonging to Gold Consumer Group +// - 1 belonging to Silver Consumer Group +// - 1 not belonging to any Consumer Group +// +// - 3 key-auths, one for each consumer +// - 1 global key-auth plugin +// - 2 consumer group +// - 1 global RLA plugin +// - 2 RLA plugins, scoped to the related consumer groups +// - 1 service pointing to mockbin.org +// - 1 route proxying the above service +// +// Once the configuration is verified to be matching in Kong, +// we then check whether the specific RLA configuration is correctly applied: consumers +// not belonging to the consumer group should be limited to 5 requests +// every 30s, while consumers belonging to the 'gold' and 'silver' consumer groups +// should be allowed to run respectively 10 and 7 requests in the same timeframe. +// In order to make sure this is the case, we run requests in a loop +// for all consumers and then check at what point they start to receive 429. +func Test_Sync_ConsumerGroupsScopedPlugins(t *testing.T) { + const ( + maxGoldRequestsNumber = 10 + maxSilverRequestsNumber = 7 + maxRegularRequestsNumber = 5 + ) + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "all entities have the same names, but different IDs", - beforeConfig: "testdata/sync/020-same-names-altered-ids/1-before.yaml", - }, - { - name: "service and consumer changed IDs, route did not", - beforeConfig: "testdata/sync/020-same-names-altered-ids/2-before.yaml", - }, - { - name: "route and consumer changed IDs, service did not", - beforeConfig: "testdata/sync/020-same-names-altered-ids/3-before.yaml", + name: "creates consumer groups scoped plugins", + kongFile: "testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml", + expectedState: utils.KongRawState{ + Consumers: consumerGroupsConsumers, + ConsumerGroups: []*kong.ConsumerGroupObject{ + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("silver"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("bar"), + }, + }, + }, + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, + }, + }, + Plugins: consumerGroupScopedPlugins, + Services: svc1_207, + Routes: route1_20x, + KeyAuths: []*kong.KeyAuth{ + { + Consumer: &kong.Consumer{ + ID: kong.String("87095815-5395-454e-8c18-a11c9bc0ef04"), + }, + Key: kong.String("i-am-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("5a5b9369-baeb-4faa-a902-c40ccdc2928e"), + }, + Key: kong.String("i-am-not-so-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("e894ea9e-ad08-4acf-a960-5a23aa7701c7"), + }, + Key: kong.String("i-am-just-average"), + }, + }, + }, }, } - - for _, tc := range testCases { + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.4.0 <3.5.0") teardown := setup(t) defer teardown(t) - // First, create the entities with the original IDs. - err = sync(tc.beforeConfig) - require.NoError(t, err) + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) - // Then, sync again with the same names, but different IDs. - err = sync("testdata/sync/020-same-names-altered-ids/desired.yaml") - require.NoError(t, err) + // Kong proxy may need a bit to be ready. + time.Sleep(time.Second * 10) - // Finally, check that the all entities exist and have the expected IDs. - testKongState(t, client, false, utils.KongRawState{ - Services: []*kong.Service{expectedService}, - Routes: []*kong.Route{expectedRoute}, - Consumers: []*kong.Consumer{expectedConsumer}, - Plugins: expectedPlugins, - }, ignoreFieldsIrrelevantForIDsTests) + // build simple http client + client := &http.Client{} + + // test 'foo' consumer (part of 'gold' group) + req, err := http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-special") + n := 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxGoldRequestsNumber, n) + + // test 'bar' consumer (part of 'silver' group) + req, err = http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-not-so-special") + n = 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxSilverRequestsNumber, n) + + // test 'baz' consumer (not part of any group) + req, err = http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-just-average") + n = 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxRegularRequestsNumber, n) }) } } -// test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_UpdateWithExplicitIDs(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - +func Test_Sync_ConsumerGroupsScopedPlugins_After350(t *testing.T) { + const ( + maxGoldRequestsNumber = 10 + maxSilverRequestsNumber = 7 + maxRegularRequestsNumber = 5 + ) client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - - const ( - beforeConfig = "testdata/sync/021-update-with-explicit-ids/before.yaml" - afterConfig = "testdata/sync/021-update-with-explicit-ids/after.yaml" - ) - - // First, create entities with IDs assigned explicitly. - err = sync(beforeConfig) - require.NoError(t, err) - - // Then, sync again, adding tags to every entity just to trigger an update. - err = sync(afterConfig) - require.NoError(t, err) - - // Finally, verify that the update was successful. - testKongState(t, client, false, utils.KongRawState{ - Services: []*kong.Service{ - { - Name: kong.String("s1"), - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - Tags: kong.StringSlice("after"), - }, - }, - Routes: []*kong.Route{ - { - Name: kong.String("r1"), - ID: kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b"), - Tags: kong.StringSlice("after"), - Service: &kong.Service{ - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates consumer groups scoped plugins", + kongFile: "testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml", + expectedState: utils.KongRawState{ + Consumers: consumerGroupsConsumers, + ConsumerGroups: []*kong.ConsumerGroupObject{ + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("silver"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("bar"), + }, + }, + }, + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, + }, + }, + Plugins: consumerGroupScopedPlugins35x, + Services: svc1_207, + Routes: route1_20x, + KeyAuths: []*kong.KeyAuth{ + { + Consumer: &kong.Consumer{ + ID: kong.String("87095815-5395-454e-8c18-a11c9bc0ef04"), + }, + Key: kong.String("i-am-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("5a5b9369-baeb-4faa-a902-c40ccdc2928e"), + }, + Key: kong.String("i-am-not-so-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("e894ea9e-ad08-4acf-a960-5a23aa7701c7"), + }, + Key: kong.String("i-am-just-average"), + }, }, }, }, - Consumers: []*kong.Consumer{ - { - Username: kong.String("c1"), - Tags: kong.StringSlice("after"), - }, - }, - }, ignoreFieldsIrrelevantForIDsTests) -} + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.5.0") + teardown := setup(t) + defer teardown(t) -// test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_UpdateWithExplicitIDsWithNoNames(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) - } + // Kong proxy may need a bit to be ready. + time.Sleep(time.Second * 10) - const ( - beforeConfig = "testdata/sync/022-update-with-explicit-ids-with-no-names/before.yaml" - afterConfig = "testdata/sync/022-update-with-explicit-ids-with-no-names/after.yaml" - ) + // build simple http client + client := &http.Client{} - // First, create entities with IDs assigned explicitly. - err = sync(beforeConfig) - require.NoError(t, err) + // test 'foo' consumer (part of 'gold' group) + req, err := http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-special") + n := 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxGoldRequestsNumber, n) - // Then, sync again, adding tags to every entity just to trigger an update. - err = sync(afterConfig) - require.NoError(t, err) + // test 'bar' consumer (part of 'silver' group) + req, err = http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-not-so-special") + n = 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxSilverRequestsNumber, n) - // Finally, verify that the update was successful. - testKongState(t, client, false, utils.KongRawState{ - Services: []*kong.Service{ - { - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - Tags: kong.StringSlice("after"), - }, - }, - Routes: []*kong.Route{ - { - ID: kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b"), - Tags: kong.StringSlice("after"), - Service: &kong.Service{ - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - }, - }, - }, - }, ignoreFieldsIrrelevantForIDsTests) + // test 'baz' consumer (not part of any group) + req, err = http.NewRequest("GET", "http://localhost:8000/r1", nil) + assert.NoError(t, err) + req.Header.Add("apikey", "i-am-just-average") + n = 0 + for n < 11 { + resp, err := client.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + break + } + n++ + } + assert.Equal(t, maxRegularRequestsNumber, n) + }) + } } // test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_CreateCertificateWithSNIs(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) +// - > 3.4.0 +func Test_Sync_ConsumerGroupsScopedPlugins_Post340(t *testing.T) { + tests := []struct { + name string + kongFile string + expectedError error + }{ + { + name: "attempt to createe consumer groups scoped plugins with older Kong version", + kongFile: "testdata/sync/017-consumer-groups-rla-application/kong3x.yaml", + expectedError: errors.New( + "building state: a rate-limiting-advanced plugin with config.consumer_groups\n" + + "and/or config.enforce_consumer_groups was found. Please use Consumer Groups scoped\n" + + "Plugins when running against Kong Enterprise 3.4.0 and above.\n\n" + + "Check DOC_LINK for more information"), + }, } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.4.0") + teardown := setup(t) + defer teardown(t) - err = sync("testdata/sync/023-create-and-update-certificate-with-snis/initial.yaml") - require.NoError(t, err) - - // To ignore noise, we ignore the Key and Cert fields because they are not relevant for this test. - ignoredFields := []cmp.Option{ - cmpopts.IgnoreFields( - kong.Certificate{}, - "Key", - "Cert", - ), + err := sync(tc.kongFile) + assert.EqualError(t, err, tc.expectedError.Error()) + }) } - - testKongState(t, client, false, utils.KongRawState{ - Certificates: []*kong.Certificate{ - { - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - Tags: kong.StringSlice("before"), - }, - }, - SNIs: []*kong.SNI{ - { - Name: kong.String("example.com"), - Certificate: &kong.Certificate{ - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - }, - }, - }, - }, ignoredFields) - - err = sync("testdata/sync/023-create-and-update-certificate-with-snis/update.yaml") - require.NoError(t, err) - - testKongState(t, client, false, utils.KongRawState{ - Certificates: []*kong.Certificate{ - { - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - Tags: kong.StringSlice("after"), // Tag should be updated. - }, - }, - SNIs: []*kong.SNI{ - { - Name: kong.String("example.com"), - Certificate: &kong.Certificate{ - ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), - }, - }, - }, - }, ignoredFields) } -// test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_ConsumersWithCustomIDAndUsername(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - +func Test_Sync_ConsumerGroupsScopedPluginsKonnect(t *testing.T) { client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - - err = sync("testdata/sync/024-consumers-with-custom_id-and-username/kong3x.yaml") - require.NoError(t, err) - - testKongState(t, client, false, utils.KongRawState{ - Consumers: []*kong.Consumer{ - { - ID: kong.String("ce49186d-7670-445d-a218-897631b29ada"), - Username: kong.String("Foo"), - CustomID: kong.String("foo"), - }, - { - ID: kong.String("7820f383-7b77-4fcc-af7f-14ff3e256693"), - Username: kong.String("foo"), - CustomID: kong.String("bar"), + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates consumer groups scoped plugins", + kongFile: "testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml", + expectedState: utils.KongRawState{ + Consumers: consumerGroupsConsumers, + ConsumerGroups: []*kong.ConsumerGroupObject{ + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("silver"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("bar"), + }, + }, + }, + { + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, + }, + }, + Plugins: consumerGroupScopedPlugins, + Services: svc1_207, + Routes: route1_20x, + KeyAuths: []*kong.KeyAuth{ + { + Consumer: &kong.Consumer{ + ID: kong.String("87095815-5395-454e-8c18-a11c9bc0ef04"), + }, + Key: kong.String("i-am-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("5a5b9369-baeb-4faa-a902-c40ccdc2928e"), + }, + Key: kong.String("i-am-not-so-special"), + }, + { + Consumer: &kong.Consumer{ + ID: kong.String("e894ea9e-ad08-4acf-a960-5a23aa7701c7"), + }, + Key: kong.String("i-am-just-average"), + }, + }, }, }, - }, nil) + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKonnect(t) + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } } diff --git a/tests/integration/test_utils.go b/tests/integration/test_utils.go index 228db1e28..2011c7c35 100644 --- a/tests/integration/test_utils.go +++ b/tests/integration/test_utils.go @@ -126,8 +126,8 @@ func sortSlices(x, y interface{}) bool { if xEntity.Consumer != nil { xName += *xEntity.Consumer.ID } - if xEntity.Consumer != nil { - xName += *xEntity.Consumer.ID + if xEntity.ConsumerGroup != nil { + xName += *xEntity.ConsumerGroup.ID } if yEntity.Route != nil { yName += *yEntity.Route.ID @@ -138,6 +138,9 @@ func sortSlices(x, y interface{}) bool { if yEntity.Consumer != nil { yName += *yEntity.Consumer.ID } + if yEntity.ConsumerGroup != nil { + yName += *yEntity.ConsumerGroup.ID + } } return xName < yName } diff --git a/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-34.yaml b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-34.yaml new file mode 100644 index 000000000..ee591dea4 --- /dev/null +++ b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-34.yaml @@ -0,0 +1,60 @@ +_format_version: "3.0" +consumer_groups: +- name: basic + plugins: + - config: + consumer_groups: null + dictionary_name: kong_rate_limiting_counters + disable_penalty: false + enforce_consumer_groups: false + error_code: 429 + error_message: API rate limit exceeded + header_name: null + hide_client_headers: false + identifier: consumer + limit: + - 30000 + namespace: basic + path: null + redis: + cluster_addresses: null + connect_timeout: null + database: 0 + host: null + keepalive_backlog: null + keepalive_pool_size: 30 + password: null + port: null + read_timeout: null + send_timeout: null + sentinel_addresses: null + sentinel_master: null + sentinel_password: null + sentinel_role: null + sentinel_username: null + server_name: null + ssl: false + ssl_verify: false + timeout: 2000 + username: null + retry_after_jitter_max: 0 + strategy: local + sync_rate: -1 + window_size: + - 2628000 + window_type: sliding + name: rate-limiting-advanced +consumers: +- groups: + - name: basic + username: foo +services: +- connect_timeout: 60000 + enabled: true + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 diff --git a/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-35.yaml b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-35.yaml new file mode 100644 index 000000000..86cd73d93 --- /dev/null +++ b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip-35.yaml @@ -0,0 +1,60 @@ +_format_version: "3.0" +consumer_groups: +- name: basic + plugins: + - config: + consumer_groups: null + dictionary_name: kong_rate_limiting_counters + disable_penalty: false + enforce_consumer_groups: false + error_code: 429 + error_message: API rate limit exceeded + header_name: null + hide_client_headers: false + identifier: consumer + limit: + - 30000 + namespace: basic + path: null + redis: + cluster_addresses: null + connect_timeout: null + database: 0 + host: null + keepalive_backlog: null + keepalive_pool_size: 256 + password: null + port: null + read_timeout: null + send_timeout: null + sentinel_addresses: null + sentinel_master: null + sentinel_password: null + sentinel_role: null + sentinel_username: null + server_name: null + ssl: false + ssl_verify: false + timeout: 2000 + username: null + retry_after_jitter_max: 0 + strategy: local + sync_rate: -1 + window_size: + - 2628000 + window_type: sliding + name: rate-limiting-advanced +consumers: +- groups: + - name: basic + username: foo +services: +- connect_timeout: 60000 + enabled: true + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 diff --git a/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip_konnect.yaml b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip_konnect.yaml new file mode 100644 index 000000000..b055aaeb1 --- /dev/null +++ b/tests/integration/testdata/dump/002-skip-consumers/expected-no-skip_konnect.yaml @@ -0,0 +1,62 @@ +_format_version: "3.0" +_konnect: + runtime_group_name: default +consumer_groups: +- name: basic + plugins: + - config: + consumer_groups: null + dictionary_name: kong_rate_limiting_counters + disable_penalty: false + enforce_consumer_groups: false + error_code: 429 + error_message: API rate limit exceeded + header_name: null + hide_client_headers: false + identifier: consumer + limit: + - 30000 + namespace: basic + path: null + redis: + cluster_addresses: null + connect_timeout: null + database: 0 + host: null + keepalive_backlog: null + keepalive_pool_size: 30 + password: null + port: null + read_timeout: null + send_timeout: null + sentinel_addresses: null + sentinel_master: null + sentinel_password: null + sentinel_role: null + sentinel_username: null + server_name: null + ssl: false + ssl_verify: false + timeout: 2000 + username: null + retry_after_jitter_max: 0 + strategy: local + sync_rate: null + window_size: + - 2628000 + window_type: sliding + name: rate-limiting-advanced +consumers: +- groups: + - name: basic + username: foo +services: +- connect_timeout: 60000 + enabled: true + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 diff --git a/tests/integration/testdata/dump/002-skip-consumers/expected_konnect.yaml b/tests/integration/testdata/dump/002-skip-consumers/expected_konnect.yaml new file mode 100644 index 000000000..d27edcddd --- /dev/null +++ b/tests/integration/testdata/dump/002-skip-consumers/expected_konnect.yaml @@ -0,0 +1,13 @@ +_format_version: "3.0" +_konnect: + runtime_group_name: default +services: +- connect_timeout: 60000 + enabled: true + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 diff --git a/tests/integration/testdata/dump/002-skip-consumers/kong34.yaml b/tests/integration/testdata/dump/002-skip-consumers/kong34.yaml new file mode 100644 index 000000000..5a04f7573 --- /dev/null +++ b/tests/integration/testdata/dump/002-skip-consumers/kong34.yaml @@ -0,0 +1,19 @@ +_format_version: "3.0" +consumer_groups: +- name: basic + plugins: + - config: + limit: + - 30000 + window_size: + - 2628000 + window_type: sliding + namespace: basic + name: rate-limiting-advanced +consumers: + - username: foo + groups: + - name: basic +services: +- name: svc1 + host: mockbin.org \ No newline at end of file diff --git a/tests/integration/testdata/sync/019-skip-consumers/kong34.yaml b/tests/integration/testdata/sync/019-skip-consumers/kong34.yaml new file mode 100644 index 000000000..433ef4290 --- /dev/null +++ b/tests/integration/testdata/sync/019-skip-consumers/kong34.yaml @@ -0,0 +1,49 @@ +_format_version: "3.0" +consumer_groups: +- id: 77e6691d-67c0-446a-9401-27be2b141aae + name: gold + tags: + - tag1 + - tag2 + plugins: + - name: rate-limiting-advanced + config: + namespace: gold + limit: + - 10 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding +- id: 5bcbd3a7-030b-4310-bd1d-2721ff85d236 + name: silver + tags: + - tag1 + - tag3 + plugins: + - name: rate-limiting-advanced + config: + namespace: silver + limit: + - 7 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding +consumers: +- groups: + - name: silver + username: bar +- username: baz +- groups: + - name: gold + username: foo +services: +- connect_timeout: 60000 + id: 58076db2-28b6-423b-ba39-a797193017f7 + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 diff --git a/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml b/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml new file mode 100644 index 000000000..ca22940db --- /dev/null +++ b/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml @@ -0,0 +1,79 @@ +_format_version: "3.0" +services: +- connect_timeout: 60000 + id: 58076db2-28b6-423b-ba39-a797193017f7 + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + routes: + - name: r1 + id: 87b6a97e-f3f7-4c47-857a-7464cb9e202b + https_redirect_status_code: 301 + paths: + - /r1 + +consumer_groups: +- id: 5bcbd3a7-030b-4310-bd1d-2721ff85d236 + name: silver + consumers: + - username: bar + - username: baz + plugins: + - name: rate-limiting-advanced + config: + namespace: silver + limit: + - 7 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 +- id: 77e6691d-67c0-446a-9401-27be2b141aae + name: gold + consumers: + - username: foo + plugins: + - name: rate-limiting-advanced + config: + namespace: gold + limit: + - 10 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 +consumers: +- username: foo + keyauth_credentials: + - key: i-am-special + groups: + - name: gold +- username: bar + keyauth_credentials: + - key: i-am-not-so-special + groups: + - name: silver +- username: baz + keyauth_credentials: + - key: i-am-just-average +plugins: +- name: key-auth + enabled: true + protocols: + - http + - https +- name: rate-limiting-advanced + config: + namespace: silver + limit: + - 5 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 diff --git a/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/konnect.yaml b/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/konnect.yaml new file mode 100644 index 000000000..ca22940db --- /dev/null +++ b/tests/integration/testdata/sync/025-consumer-groups-scoped-plugins/konnect.yaml @@ -0,0 +1,79 @@ +_format_version: "3.0" +services: +- connect_timeout: 60000 + id: 58076db2-28b6-423b-ba39-a797193017f7 + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + routes: + - name: r1 + id: 87b6a97e-f3f7-4c47-857a-7464cb9e202b + https_redirect_status_code: 301 + paths: + - /r1 + +consumer_groups: +- id: 5bcbd3a7-030b-4310-bd1d-2721ff85d236 + name: silver + consumers: + - username: bar + - username: baz + plugins: + - name: rate-limiting-advanced + config: + namespace: silver + limit: + - 7 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 +- id: 77e6691d-67c0-446a-9401-27be2b141aae + name: gold + consumers: + - username: foo + plugins: + - name: rate-limiting-advanced + config: + namespace: gold + limit: + - 10 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 +consumers: +- username: foo + keyauth_credentials: + - key: i-am-special + groups: + - name: gold +- username: bar + keyauth_credentials: + - key: i-am-not-so-special + groups: + - name: silver +- username: baz + keyauth_credentials: + - key: i-am-just-average +plugins: +- name: key-auth + enabled: true + protocols: + - http + - https +- name: rate-limiting-advanced + config: + namespace: silver + limit: + - 5 + retry_after_jitter_max: 1 + window_size: + - 60 + window_type: sliding + sync_rate: -1 diff --git a/types/plugin.go b/types/plugin.go index 44339c0f2..fc3a59525 100644 --- a/types/plugin.go +++ b/types/plugin.go @@ -26,6 +26,9 @@ func stripPluginReferencesName(plugin *state.Plugin) { if plugin.Plugin.Consumer != nil && plugin.Plugin.Consumer.Username != nil { plugin.Plugin.Consumer.Username = nil } + if plugin.Plugin.ConsumerGroup != nil && plugin.Plugin.ConsumerGroup.Name != nil { + plugin.Plugin.ConsumerGroup.Name = nil + } } func pluginFromStruct(arg crud.Event) *state.Plugin { @@ -111,9 +114,10 @@ func (d *pluginDiffer) Deletes(handler func(crud.Event) error) error { func (d *pluginDiffer) deletePlugin(plugin *state.Plugin) (*crud.Event, error) { plugin = &state.Plugin{Plugin: *plugin.DeepCopy()} name := *plugin.Name - serviceID, routeID, consumerID := foreignNames(plugin) - _, err := d.targetState.Plugins.GetByProp(name, serviceID, routeID, - consumerID) + serviceID, routeID, consumerID, consumerGroupID := foreignNames(plugin) + _, err := d.targetState.Plugins.GetByProp( + name, serviceID, routeID, consumerID, consumerGroupID, + ) if errors.Is(err, state.ErrNotFound) { return &crud.Event{ Op: crud.Delete, @@ -151,9 +155,10 @@ func (d *pluginDiffer) CreateAndUpdates(handler func(crud.Event) error) error { func (d *pluginDiffer) createUpdatePlugin(plugin *state.Plugin) (*crud.Event, error) { plugin = &state.Plugin{Plugin: *plugin.DeepCopy()} name := *plugin.Name - serviceID, routeID, consumerID := foreignNames(plugin) - currentPlugin, err := d.currentState.Plugins.GetByProp(name, - serviceID, routeID, consumerID) + serviceID, routeID, consumerID, consumerGroupID := foreignNames(plugin) + currentPlugin, err := d.currentState.Plugins.GetByProp( + name, serviceID, routeID, consumerID, consumerGroupID, + ) if errors.Is(err, state.ErrNotFound) { // plugin not present, create it @@ -181,7 +186,7 @@ func (d *pluginDiffer) createUpdatePlugin(plugin *state.Plugin) (*crud.Event, er return nil, nil } -func foreignNames(p *state.Plugin) (serviceID, routeID, consumerID string) { +func foreignNames(p *state.Plugin) (serviceID, routeID, consumerID, consumerGroupID string) { if p == nil { return } @@ -194,5 +199,8 @@ func foreignNames(p *state.Plugin) (serviceID, routeID, consumerID string) { if p.Consumer != nil && p.Consumer.ID != nil { consumerID = *p.Consumer.ID } + if p.ConsumerGroup != nil && p.ConsumerGroup.ID != nil { + consumerGroupID = *p.ConsumerGroup.ID + } return } diff --git a/utils/utils.go b/utils/utils.go index e00f37c6b..3f42d7ddf 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -21,6 +21,7 @@ var ( Kong140Version = semver.MustParse("1.4.0") Kong300Version = semver.MustParse("3.0.0") + Kong340Version = semver.MustParse("3.4.0") ) var UpgradeMessage = "Please upgrade your configuration to account for 3.0\n" + @@ -148,6 +149,16 @@ func GetConsumerReference(c kong.Consumer) *kong.Consumer { return consumer } +// GetConsumerGroupReference returns a name+ID only copy of the input consumer-group, +// for use in references from other objects +func GetConsumerGroupReference(c kong.ConsumerGroup) *kong.ConsumerGroup { + consumerGroup := &kong.ConsumerGroup{ID: kong.String(*c.ID)} + if c.Name != nil { + consumerGroup.Name = kong.String(*c.Name) + } + return consumerGroup +} + // GetServiceReference returns a name+ID only copy of the input service, // for use in references from other objects func GetServiceReference(s kong.Service) *kong.Service {