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/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..db8107e0c 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.isConsumerGroupScopedPluginSupported = true + } + // build b.certificates() if !b.skipCACerts { @@ -92,6 +100,46 @@ func (b *stateBuilder) build() (*utils.KongRawState, *utils.KonnectRawState, err return b.rawState, b.konnectRawState, nil } +func (b *stateBuilder) ingestConsumerGroupScopedPlugins(cg FConsumerGroupObject) error { + 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, + }, + }, + }) + } + return b.ingestPlugins(plugins) +} + +func (b *stateBuilder) addConsumerGroupPlugins( + cg FConsumerGroupObject, cgo *kong.ConsumerGroupObject, +) error { + 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 { + return err + } else { + plugin.ID = kong.String(*current.ID) + } + } + b.defaulter.MustSet(plugin) + cgo.Plugins = append(cgo.Plugins, plugin) + } + return nil +} + func (b *stateBuilder) consumerGroups() { if b.err != nil { return @@ -116,22 +164,29 @@ 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 + } + + // Plugins and Consumer Groups can be handled in two ways: + // 1. directly in the ConsumerGroup object + // 2. by scoping the plugin to the ConsumerGroup (Kong >= 3.4.0) + // + // The first method is deprecated and will be removed in the future, but + // we still need to support it for now. The isConsumerGroupScopedPluginSupported + // flag is used to determine which method to use based on the Kong version. + if b.isConsumerGroupScopedPluginSupported { + if err := b.ingestConsumerGroupScopedPlugins(cg); err != nil { + b.err = err + return + } + } else { + if err := b.addConsumerGroupPlugins(cg, &cgo); err != nil { + b.err = err + return } - b.defaulter.MustSet(plugin) - cgo.Plugins = append(cgo.Plugins, plugin) } b.rawState.ConsumerGroups = append(b.rawState.ConsumerGroups, &cgo) } @@ -882,6 +937,23 @@ 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 err := b.validatePlugin(p); err != nil { + b.err = err + return + } plugins = append(plugins, p) } if err := b.ingestPlugins(plugins); err != nil { @@ -890,6 +962,29 @@ func (b *stateBuilder) plugins() { } } +func (b *stateBuilder) validatePlugin(p FPlugin) error { + if (b.isConsumerGroupScopedPluginSupported && !b.isKonnect) && *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 + } + } + var enforceConsumerGroupsFound bool + if enforceConsumerGroups, ok := p.Config["enforce_consumer_groups"]; ok { + if enforceConsumerGroupsBool, ok := enforceConsumerGroups.(bool); ok && enforceConsumerGroupsBool { + enforceConsumerGroupsFound = true + } + } + if consumerGroupsFound || enforceConsumerGroupsFound { + return utils.ErrorConsumerGroupUpgrade + } + } + return nil +} + // strip_path schema default value is 'true', but it cannot be set when // protocols include 'grpc' and/or 'grpcs'. When users explicitly set // strip_path to 'true' with grpc/s protocols, deck returns a schema violation error. @@ -997,9 +1092,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 +1139,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 +1149,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..09460d919 100644 --- a/tests/integration/dump_test.go +++ b/tests/integration/dump_test.go @@ -82,23 +82,54 @@ func Test_Dump_SkipConsumers(t *testing.T) { stateFile string expectedFile string skipConsumers bool + runWhen func(t *testing.T) }{ { - name: "dump with skip-consumers", + name: "3.2 & 3.3 dump with skip-consumers", stateFile: "testdata/dump/002-skip-consumers/kong.yaml", expectedFile: "testdata/dump/002-skip-consumers/expected.yaml", skipConsumers: true, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.2.0 <3.4.0") }, }, { - name: "dump with no skip-consumers", + name: "3.2 & 3.3 dump with no skip-consumers", stateFile: "testdata/dump/002-skip-consumers/kong.yaml", expectedFile: "testdata/dump/002-skip-consumers/expected-no-skip.yaml", skipConsumers: false, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.2.0 <3.4.0") }, + }, + { + name: "3.4 dump with skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected.yaml", + skipConsumers: true, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.4.0 <3.5.0") }, + }, + { + name: "3.4 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, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.4.0 <3.5.0") }, + }, + { + name: "3.5 dump with skip-consumers", + stateFile: "testdata/dump/002-skip-consumers/kong34.yaml", + expectedFile: "testdata/dump/002-skip-consumers/expected.yaml", + skipConsumers: true, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.5.0") }, + }, + { + name: "3.5 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, + runWhen: func(t *testing.T) { runWhen(t, "enterprise", ">=3.5.0") }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.2.0") + tc.runWhen(t) teardown := setup(t) defer teardown(t) @@ -122,7 +153,60 @@ 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_Konnect(t *testing.T) { + t.Skip("remove skip once Konnect support is enabled.") + + 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..91d91e7ee 100644 --- a/tests/integration/sync_test.go +++ b/tests/integration/sync_test.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "net/http" "testing" "time" @@ -821,20 +822,478 @@ 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: -// - 1.4.3 -func Test_Sync_ServicesRoutes_Till_1_4_3(t *testing.T) { +// - 1.4.3 +func Test_Sync_ServicesRoutes_Till_1_4_3(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + // ignore entities fields based on Kong version + ignoreFields := []cmp.Option{ + cmpopts.IgnoreFields(kong.Route{}, "Service"), + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + Routes: route1_143, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", "<=1.4.3") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, ignoreFields) + }) + } +} + +// test scope: +// - 1.5.1 +// - 1.5.0.11+enterprise +func Test_Sync_ServicesRoutes_Till_1_5_1(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + Routes: route1_151, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", ">1.4.3 <=1.5.1") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 2.0.5 +// - 2.1.4 +func Test_Sync_ServicesRoutes_From_2_0_5_To_2_1_4(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - // ignore entities fields based on Kong version - ignoreFields := []cmp.Option{ - cmpopts.IgnoreFields(kong.Route{}, "Service"), + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + Routes: route1_205_214, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", ">=2.0.5 <=2.1.4") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 2.2.2 +// - 2.3.3 +// - 2.4.1 +// - 2.5.1 +// - 2.6.0 +// - 2.2.1.3+enterprise +// - 2.3.3.4+enterprise +// - 2.4.1.3+enterprise +// - 2.5.1.2+enterprise +func Test_Sync_ServicesRoutes_From_2_2_1_to_2_6_0(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) } tests := []struct { @@ -850,30 +1309,193 @@ func Test_Sync_ServicesRoutes_Till_1_4_3(t *testing.T) { }, }, { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1, + Routes: route1_20x, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", ">2.2.1 <=2.6.0") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 2.7.0 +// - 2.6.0.2+enterprise +// - 2.7.0.0+enterprise +// - 2.8.0.0+enterprise +func Test_Sync_ServicesRoutes_From_2_6_9_Till_2_8_0(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Routes: route1_20x, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", ">2.6.9 <3.0.0") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 3.x +func Test_Sync_ServicesRoutes_From_3x(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Routes: route1_20x, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - konnect +func Test_Sync_ServicesRoutes_Konnect(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + }, + { + name: "create services and routes", + kongFile: "testdata/sync/002-create-services-and-routes/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Routes: route1_20x, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "konnect", "") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 1.4.3 +func Test_Sync_BasicAuth_Plugin_1_4_3(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState + }{ + { + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_143, + Plugins: plugin_143_151, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "<=1.4.3") + runWhen(t, "kong", "==1.4.3") teardown := setup(t) defer teardown(t) sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, ignoreFields) + testKongState(t, client, false, tc.expectedState, nil) }) } } // test scope: -// - 1.5.1 // - 1.5.0.11+enterprise -func Test_Sync_ServicesRoutes_Till_1_5_1(t *testing.T) { +func Test_Sync_BasicAuth_Plugin_Earlier_Than_1_5_1(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -881,29 +1503,22 @@ func Test_Sync_ServicesRoutes_Till_1_5_1(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong.yaml", - expectedState: utils.KongRawState{ - Services: svc1, - }, - }, - { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_151, + Plugins: plugin, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">1.4.3 <=1.5.1") + runWhen(t, "kong", "<1.5.1 !1.4.3") teardown := setup(t) defer teardown(t) @@ -914,9 +1529,8 @@ func Test_Sync_ServicesRoutes_Till_1_5_1(t *testing.T) { } // test scope: -// - 2.0.5 -// - 2.1.4 -func Test_Sync_ServicesRoutes_From_2_0_5_To_2_1_4(t *testing.T) { +// - 1.5.1 +func Test_Sync_BasicAuth_Plugin_1_5_1(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -924,29 +1538,22 @@ func Test_Sync_ServicesRoutes_From_2_0_5_To_2_1_4(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong.yaml", - expectedState: utils.KongRawState{ - Services: svc1, - }, - }, - { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_205_214, + Plugins: plugin_143_151, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.0.5 <=2.1.4") + runWhen(t, "kong", "==1.5.1") teardown := setup(t) defer teardown(t) @@ -957,16 +1564,23 @@ func Test_Sync_ServicesRoutes_From_2_0_5_To_2_1_4(t *testing.T) { } // test scope: +// - 2.0.5 +// - 2.1.4 // - 2.2.2 // - 2.3.3 // - 2.4.1 // - 2.5.1 // - 2.6.0 +// - 2.7.0 +// - 2.1.4.6+enterprise // - 2.2.1.3+enterprise // - 2.3.3.4+enterprise // - 2.4.1.3+enterprise // - 2.5.1.2+enterprise -func Test_Sync_ServicesRoutes_From_2_2_1_to_2_6_0(t *testing.T) { +// - 2.6.0.2+enterprise +// - 2.7.0.0+enterprise +// - 2.8.0.0+enterprise +func Test_Sync_BasicAuth_Plugin_From_2_0_5_Till_2_8_0(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -974,29 +1588,22 @@ func Test_Sync_ServicesRoutes_From_2_2_1_to_2_6_0(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong.yaml", - expectedState: utils.KongRawState{ - Services: svc1, - }, - }, - { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_20x, + Plugins: plugin, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">2.2.1 <=2.6.0") + runWhen(t, "kong", ">=2.0.5 <3.0.0") teardown := setup(t) defer teardown(t) @@ -1007,11 +1614,8 @@ func Test_Sync_ServicesRoutes_From_2_2_1_to_2_6_0(t *testing.T) { } // test scope: -// - 2.7.0 -// - 2.6.0.2+enterprise -// - 2.7.0.0+enterprise -// - 2.8.0.0+enterprise -func Test_Sync_ServicesRoutes_From_2_6_9_Till_2_8_0(t *testing.T) { +// - 3.x +func Test_Sync_BasicAuth_Plugin_From_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1019,29 +1623,57 @@ func Test_Sync_ServicesRoutes_From_2_6_9_Till_2_8_0(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong.yaml", + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong3x.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, + Plugins: plugin, }, }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - konnect +func Test_Sync_BasicAuth_Plugin_Konnect(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState + }{ { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong.yaml", + name: "create a plugin", + kongFile: "testdata/sync/003-create-a-plugin/kong3x.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, + Plugins: plugin, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">2.6.9 <3.0.0") + runWhen(t, "konnect", "") teardown := setup(t) defer teardown(t) @@ -1052,50 +1684,66 @@ func Test_Sync_ServicesRoutes_From_2_6_9_Till_2_8_0(t *testing.T) { } // test scope: -// - 3.x -func Test_Sync_ServicesRoutes_From_3x(t *testing.T) { +// - 1.4.3 +// - 1.5.1 +// - 1.5.0.11+enterprise +func Test_Sync_Upstream_Target_Till_1_5_2(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } + // ignore entities fields based on Kong version + ignoreFields := []cmp.Option{ + cmpopts.IgnoreFields(kong.Healthcheck{}, "Threshold"), + } + tests := []struct { name string kongFile string expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong3x.yaml", - expectedState: utils.KongRawState{ - Services: svc1_207, - }, - }, - { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong3x.yaml", + name: "creates an upstream and target", + kongFile: "testdata/sync/004-create-upstream-and-target/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, + Upstreams: upstream_pre31, + Targets: target, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") + runWhen(t, "kong", "<=1.5.2") teardown := setup(t) defer teardown(t) sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + testKongState(t, client, false, tc.expectedState, ignoreFields) }) } } // test scope: -// - konnect -func Test_Sync_ServicesRoutes_Konnect(t *testing.T) { +// - 2.0.5 +// - 2.1.4 +// - 2.2.2 +// - 2.3.3 +// - 2.4.1 +// - 2.5.1 +// - 2.6.0 +// - 2.7.0 +// - 2.1.4.6+enterprise +// - 2.2.1.3+enterprise +// - 2.3.3.4+enterprise +// - 2.4.1.3+enterprise +// - 2.5.1.2+enterprise +// - 2.6.0.2+enterprise +// - 2.7.0.0+enterprise +// - 2.8.0.0+enterprise +func Test_Sync_Upstream_Target_From_2x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1108,24 +1756,18 @@ func Test_Sync_ServicesRoutes_Konnect(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong3x.yaml", - expectedState: utils.KongRawState{ - Services: svc1_207, - }, - }, - { - name: "create services and routes", - kongFile: "testdata/sync/002-create-services-and-routes/kong3x.yaml", + name: "creates an upstream and target", + kongFile: "testdata/sync/004-create-upstream-and-target/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, + Upstreams: upstream_pre31, + Targets: target, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "konnect", "") + runWhen(t, "kong", ">=2.1.0 <3.0.0") teardown := setup(t) defer teardown(t) @@ -1136,8 +1778,8 @@ func Test_Sync_ServicesRoutes_Konnect(t *testing.T) { } // test scope: -// - 1.4.3 -func Test_Sync_BasicAuth_Plugin_1_4_3(t *testing.T) { +// - 3.0 +func Test_Sync_Upstream_Target_From_30(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1145,22 +1787,23 @@ func Test_Sync_BasicAuth_Plugin_1_4_3(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", + name: "creates an upstream and target", + kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", expectedState: utils.KongRawState{ - Plugins: plugin_143_151, + Upstreams: upstream_pre31, + Targets: target, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "==1.4.3") + runWhen(t, "kong", ">=3.0.0 <3.1.0") teardown := setup(t) defer teardown(t) @@ -1171,8 +1814,8 @@ func Test_Sync_BasicAuth_Plugin_1_4_3(t *testing.T) { } // test scope: -// - 1.5.0.11+enterprise -func Test_Sync_BasicAuth_Plugin_Earlier_Than_1_5_1(t *testing.T) { +// - 3.x +func Test_Sync_Upstream_Target_From_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1180,22 +1823,23 @@ func Test_Sync_BasicAuth_Plugin_Earlier_Than_1_5_1(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", + name: "creates an upstream and target", + kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", expectedState: utils.KongRawState{ - Plugins: plugin, + Upstreams: upstream, + Targets: target, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "<1.5.1 !1.4.3") + runWhenKongOrKonnect(t, ">=3.1.0") teardown := setup(t) defer teardown(t) @@ -1206,8 +1850,8 @@ func Test_Sync_BasicAuth_Plugin_Earlier_Than_1_5_1(t *testing.T) { } // test scope: -// - 1.5.1 -func Test_Sync_BasicAuth_Plugin_1_5_1(t *testing.T) { +// - konnect +func Test_Sync_Upstream_Target_Konnect(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1215,22 +1859,23 @@ func Test_Sync_BasicAuth_Plugin_1_5_1(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", + name: "creates an upstream and target", + kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", expectedState: utils.KongRawState{ - Plugins: plugin_143_151, + Upstreams: upstream, + Targets: target, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "==1.5.1") + runWhen(t, "konnect", "") teardown := setup(t) defer teardown(t) @@ -1241,23 +1886,16 @@ func Test_Sync_BasicAuth_Plugin_1_5_1(t *testing.T) { } // test scope: -// - 2.0.5 -// - 2.1.4 -// - 2.2.2 -// - 2.3.3 // - 2.4.1 // - 2.5.1 // - 2.6.0 // - 2.7.0 -// - 2.1.4.6+enterprise -// - 2.2.1.3+enterprise -// - 2.3.3.4+enterprise // - 2.4.1.3+enterprise // - 2.5.1.2+enterprise // - 2.6.0.2+enterprise // - 2.7.0.0+enterprise // - 2.8.0.0+enterprise -func Test_Sync_BasicAuth_Plugin_From_2_0_5_Till_2_8_0(t *testing.T) { +func Test_Sync_Upstreams_Target_ZeroWeight_2x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1265,22 +1903,23 @@ func Test_Sync_BasicAuth_Plugin_From_2_0_5_Till_2_8_0(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong.yaml", + name: "creates an upstream and target with weight equals to zero", + kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong.yaml", expectedState: utils.KongRawState{ - Plugins: plugin, + Upstreams: upstream_pre31, + Targets: targetZeroWeight, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.0.5 <3.0.0") + runWhen(t, "kong", ">=2.4.1 <3.0.0") teardown := setup(t) defer teardown(t) @@ -1291,8 +1930,8 @@ func Test_Sync_BasicAuth_Plugin_From_2_0_5_Till_2_8_0(t *testing.T) { } // test scope: -// - 3.x -func Test_Sync_BasicAuth_Plugin_From_3x(t *testing.T) { +// - 3.0 +func Test_Sync_Upstreams_Target_ZeroWeight_30(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1300,22 +1939,23 @@ func Test_Sync_BasicAuth_Plugin_From_3x(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong3x.yaml", + name: "creates an upstream and target with weight equals to zero", + kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", expectedState: utils.KongRawState{ - Plugins: plugin, + Upstreams: upstream_pre31, + Targets: targetZeroWeight, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") + runWhen(t, "kong", ">=3.0.0 <3.1.0") teardown := setup(t) defer teardown(t) @@ -1326,8 +1966,8 @@ func Test_Sync_BasicAuth_Plugin_From_3x(t *testing.T) { } // test scope: -// - konnect -func Test_Sync_BasicAuth_Plugin_Konnect(t *testing.T) { +// - 3.x +func Test_Sync_Upstreams_Target_ZeroWeight_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1335,22 +1975,23 @@ func Test_Sync_BasicAuth_Plugin_Konnect(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + expectedState utils.KongRawState }{ { - name: "create a plugin", - kongFile: "testdata/sync/003-create-a-plugin/kong3x.yaml", + name: "creates an upstream and target with weight equals to zero", + kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", expectedState: utils.KongRawState{ - Plugins: plugin, + Upstreams: upstream, + Targets: targetZeroWeight, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "konnect", "") + runWhenKongOrKonnect(t, ">=3.1.0") teardown := setup(t) defer teardown(t) @@ -1361,66 +2002,42 @@ func Test_Sync_BasicAuth_Plugin_Konnect(t *testing.T) { } // test scope: -// - 1.4.3 -// - 1.5.1 -// - 1.5.0.11+enterprise -func Test_Sync_Upstream_Target_Till_1_5_2(t *testing.T) { +// - konnect +func Test_Sync_Upstreams_Target_ZeroWeight_Konnect(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - // ignore entities fields based on Kong version - ignoreFields := []cmp.Option{ - cmpopts.IgnoreFields(kong.Healthcheck{}, "Threshold"), - } - tests := []struct { name string kongFile string expectedState utils.KongRawState }{ { - name: "creates an upstream and target", - kongFile: "testdata/sync/004-create-upstream-and-target/kong.yaml", + name: "creates an upstream and target with weight equals to zero", + kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream_pre31, - Targets: target, + Upstreams: upstream, + Targets: targetZeroWeight, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "<=1.5.2") + runWhen(t, "konnect", "") teardown := setup(t) defer teardown(t) sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, ignoreFields) + testKongState(t, client, false, tc.expectedState, nil) }) } } -// test scope: -// - 2.0.5 -// - 2.1.4 -// - 2.2.2 -// - 2.3.3 -// - 2.4.1 -// - 2.5.1 -// - 2.6.0 -// - 2.7.0 -// - 2.1.4.6+enterprise -// - 2.2.1.3+enterprise -// - 2.3.3.4+enterprise -// - 2.4.1.3+enterprise -// - 2.5.1.2+enterprise -// - 2.6.0.2+enterprise -// - 2.7.0.0+enterprise -// - 2.8.0.0+enterprise -func Test_Sync_Upstream_Target_From_2x(t *testing.T) { +func Test_Sync_RateLimitingPlugin(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1433,18 +2050,24 @@ func Test_Sync_Upstream_Target_From_2x(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target", - kongFile: "testdata/sync/004-create-upstream-and-target/kong.yaml", + name: "fill defaults", + kongFile: "testdata/sync/006-fill-defaults-rate-limiting/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream_pre31, - Targets: target, + Plugins: rateLimitingPlugin, + }, + }, + { + name: "fill defaults with dedup", + kongFile: "testdata/sync/007-fill-defaults-rate-limiting-dedup/kong.yaml", + expectedState: utils.KongRawState{ + Plugins: rateLimitingPlugin, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.1.0 <3.0.0") + runWhen(t, "kong", "==2.7.0") teardown := setup(t) defer teardown(t) @@ -1455,44 +2078,53 @@ func Test_Sync_Upstream_Target_From_2x(t *testing.T) { } // test scope: -// - 3.0 -func Test_Sync_Upstream_Target_From_30(t *testing.T) { +// - 1.5.0.11+enterprise +func Test_Sync_FillDefaults_Earlier_Than_1_5_1(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } + // ignore entities fields based on Kong version + ignoreFields := []cmp.Option{ + cmpopts.IgnoreFields(kong.Route{}, "Service"), + cmpopts.IgnoreFields(kong.Healthcheck{}, "Threshold"), + } + tests := []struct { name string kongFile string expectedState utils.KongRawState }{ { - name: "creates an upstream and target", - kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", + name: "creates a service", + kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream_pre31, + Services: svc1, + Routes: route1_151, + Plugins: plugin, Targets: target, + Upstreams: upstream_pre31, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=3.0.0 <3.1.0") + runWhen(t, "kong", "<1.5.1 !1.4.3") teardown := setup(t) defer teardown(t) sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + testKongState(t, client, false, tc.expectedState, ignoreFields) }) } } // test scope: -// - 3.x -func Test_Sync_Upstream_Target_From_3x(t *testing.T) { +// - 2.0.5 +// - 2.1.4 +func Test_Sync_FillDefaults_From_2_0_5_To_2_1_4(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1505,18 +2137,20 @@ func Test_Sync_Upstream_Target_From_3x(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target", - kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", + name: "create services and routes", + kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream, + Services: svc1, + Routes: route1_205_214, + Upstreams: upstream_pre31, Targets: target, + Plugins: plugin, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.1.0") + runWhen(t, "kong", ">=2.0.5 <=2.1.4") teardown := setup(t) defer teardown(t) @@ -1527,8 +2161,16 @@ func Test_Sync_Upstream_Target_From_3x(t *testing.T) { } // test scope: -// - konnect -func Test_Sync_Upstream_Target_Konnect(t *testing.T) { +// - 2.2.2 +// - 2.3.3 +// - 2.4.1 +// - 2.5.1 +// - 2.6.0 +// - 2.2.1.3+enterprise +// - 2.3.3.4+enterprise +// - 2.4.1.3+enterprise +// - 2.5.1.2+enterprise +func Test_Sync_FillDefaults_From_2_2_1_to_2_6_0(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1541,18 +2183,20 @@ func Test_Sync_Upstream_Target_Konnect(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target", - kongFile: "testdata/sync/004-create-upstream-and-target/kong3x.yaml", + name: "create services and routes", + kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream, + Services: svc1, + Routes: route1_20x, + Upstreams: upstream_pre31, Targets: target, + Plugins: plugin, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "konnect", "") + runWhen(t, "kong", ">2.2.1 <=2.6.0") teardown := setup(t) defer teardown(t) @@ -1563,16 +2207,47 @@ func Test_Sync_Upstream_Target_Konnect(t *testing.T) { } // test scope: -// - 2.4.1 -// - 2.5.1 -// - 2.6.0 // - 2.7.0 -// - 2.4.1.3+enterprise -// - 2.5.1.2+enterprise // - 2.6.0.2+enterprise // - 2.7.0.0+enterprise // - 2.8.0.0+enterprise -func Test_Sync_Upstreams_Target_ZeroWeight_2x(t *testing.T) { +func Test_Sync_FillDefaults_From_2_6_9(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + + tests := []struct { + name string + kongFile string + expectedState utils.KongRawState + }{ + { + name: "creates entities with minimum configuration", + kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Routes: route1_20x, + Plugins: plugin, + Targets: target, + Upstreams: upstream_pre31, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "kong", ">2.6.9 <3.0.0") + teardown := setup(t) + defer teardown(t) + + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +func Test_Sync_SkipCACert_2x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1585,30 +2260,31 @@ func Test_Sync_Upstreams_Target_ZeroWeight_2x(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target with weight equals to zero", - kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong.yaml", + name: "syncing with --skip-ca-certificates should ignore CA certs", + kongFile: "testdata/sync/009-skip-ca-cert/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream_pre31, - Targets: targetZeroWeight, + Services: svc1_207, + CACertificates: []*kong.CACertificate{}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.4.1 <3.0.0") + // ca_certificates first appeared in 1.3, but we limit to 2.7+ + // here because the schema changed and the entities aren't the same + // across all versions, even though the skip functionality works the same. + runWhen(t, "kong", ">=2.7.0 <3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) + sync(tc.kongFile, "--skip-ca-certificates") testKongState(t, client, false, tc.expectedState, nil) }) } } -// test scope: -// - 3.0 -func Test_Sync_Upstreams_Target_ZeroWeight_30(t *testing.T) { +func Test_Sync_SkipCACert_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1621,30 +2297,31 @@ func Test_Sync_Upstreams_Target_ZeroWeight_30(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target with weight equals to zero", - kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", + name: "syncing with --skip-ca-certificates should ignore CA certs", + kongFile: "testdata/sync/009-skip-ca-cert/kong3x.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream_pre31, - Targets: targetZeroWeight, + Services: svc1_207, + CACertificates: []*kong.CACertificate{}, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=3.0.0 <3.1.0") + // ca_certificates first appeared in 1.3, but we limit to 2.7+ + // here because the schema changed and the entities aren't the same + // across all versions, even though the skip functionality works the same. + runWhenKongOrKonnect(t, ">=3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) + sync(tc.kongFile, "--skip-ca-certificates") testKongState(t, client, false, tc.expectedState, nil) }) } } -// test scope: -// - 3.x -func Test_Sync_Upstreams_Target_ZeroWeight_3x(t *testing.T) { +func Test_Sync_RBAC_2x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1657,30 +2334,100 @@ func Test_Sync_Upstreams_Target_ZeroWeight_3x(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target with weight equals to zero", - kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", + name: "rbac", + kongFile: "testdata/sync/xxx-rbac-endpoint-permissions/kong.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream, - Targets: targetZeroWeight, + RBACRoles: []*kong.RBACRole{ + { + Name: kong.String("workspace-portal-admin"), + Comment: kong.String("Full access to Dev Portal related endpoints in the workspace"), + }, + }, + RBACEndpointPermissions: []*kong.RBACEndpointPermission{ + { + Workspace: kong.String("default"), + Endpoint: kong.String("/developers"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/developers/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/files"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/files/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/kong"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/workspaces/default"), + Actions: []*string{kong.String("read"), kong.String("update")}, + Negative: kong.Bool(false), + }, + }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.1.0") + runWhen(t, "enterprise", ">=2.7.0 <3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) + sync(tc.kongFile, "--rbac-resources-only") testKongState(t, client, false, tc.expectedState, nil) }) } } -// test scope: -// - konnect -func Test_Sync_Upstreams_Target_ZeroWeight_Konnect(t *testing.T) { +func Test_Sync_RBAC_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1693,28 +2440,100 @@ func Test_Sync_Upstreams_Target_ZeroWeight_Konnect(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates an upstream and target with weight equals to zero", - kongFile: "testdata/sync/005-create-upstream-and-target-weight/kong3x.yaml", + name: "rbac", + kongFile: "testdata/sync/xxx-rbac-endpoint-permissions/kong3x.yaml", expectedState: utils.KongRawState{ - Upstreams: upstream, - Targets: targetZeroWeight, + RBACRoles: []*kong.RBACRole{ + { + Name: kong.String("workspace-portal-admin"), + Comment: kong.String("Full access to Dev Portal related endpoints in the workspace"), + }, + }, + RBACEndpointPermissions: []*kong.RBACEndpointPermission{ + { + Workspace: kong.String("default"), + Endpoint: kong.String("/developers"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/developers/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/files"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/files/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/kong"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(false), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/rbac/*/*/*/*/*"), + Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, + Negative: kong.Bool(true), + }, + { + Workspace: kong.String("default"), + Endpoint: kong.String("/workspaces/default"), + Actions: []*string{kong.String("read"), kong.String("update")}, + Negative: kong.Bool(false), + }, + }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "konnect", "") + runWhen(t, "enterprise", ">=3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) + sync(tc.kongFile, "--rbac-resources-only") testKongState(t, client, false, tc.expectedState, nil) }) } } -func Test_Sync_RateLimitingPlugin(t *testing.T) { +func Test_Sync_Create_Route_With_Service_Name_Reference_2x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1727,24 +2546,18 @@ func Test_Sync_RateLimitingPlugin(t *testing.T) { expectedState utils.KongRawState }{ { - name: "fill defaults", - kongFile: "testdata/sync/006-fill-defaults-rate-limiting/kong.yaml", - expectedState: utils.KongRawState{ - Plugins: rateLimitingPlugin, - }, - }, - { - name: "fill defaults with dedup", - kongFile: "testdata/sync/007-fill-defaults-rate-limiting-dedup/kong.yaml", + name: "create a route with a service name reference", + kongFile: "testdata/sync/010-create-route-with-service-name-reference/kong.yaml", expectedState: utils.KongRawState{ - Plugins: rateLimitingPlugin, + Services: svc1_207, + Routes: route1_20x, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "==2.7.0") + runWhen(t, "kong", ">=2.7.0 <3.0.0") teardown := setup(t) defer teardown(t) @@ -1754,54 +2567,44 @@ func Test_Sync_RateLimitingPlugin(t *testing.T) { } } -// test scope: -// - 1.5.0.11+enterprise -func Test_Sync_FillDefaults_Earlier_Than_1_5_1(t *testing.T) { +func Test_Sync_Create_Route_With_Service_Name_Reference_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - // ignore entities fields based on Kong version - ignoreFields := []cmp.Option{ - cmpopts.IgnoreFields(kong.Route{}, "Service"), - cmpopts.IgnoreFields(kong.Healthcheck{}, "Threshold"), - } - tests := []struct { name string kongFile string expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", + name: "create a route with a service name reference", + kongFile: "testdata/sync/010-create-route-with-service-name-reference/kong3x.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_151, - Plugins: plugin, - Targets: target, - Upstreams: upstream_pre31, + Services: svc1_207, + Routes: route1_20x, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", "<1.5.1 !1.4.3") + runWhen(t, "kong", ">=2.7.0 <3.0.0") teardown := setup(t) defer teardown(t) sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, ignoreFields) + testKongState(t, client, false, tc.expectedState, nil) }) } } // test scope: -// - 2.0.5 -// - 2.1.4 -func Test_Sync_FillDefaults_From_2_0_5_To_2_1_4(t *testing.T) { +// - 1.x.x +// - 2.x.x +func Test_Sync_PluginsOnEntitiesTill_3_0_0(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1814,20 +2617,20 @@ func Test_Sync_FillDefaults_From_2_0_5_To_2_1_4(t *testing.T) { expectedState utils.KongRawState }{ { - name: "create services and routes", - kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", + name: "create plugins on services, routes and consumers", + kongFile: "testdata/sync/xxx-plugins-on-entities/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, - Routes: route1_205_214, - Upstreams: upstream_pre31, - Targets: target, - Plugins: plugin, + Services: svc1_207, + Routes: route1_20x, + Plugins: plugin_on_entities, + Consumers: consumer, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.0.5 <=2.1.4") + runWhen(t, "kong", ">=2.8.0 <3.0.0") teardown := setup(t) defer teardown(t) @@ -1838,16 +2641,8 @@ func Test_Sync_FillDefaults_From_2_0_5_To_2_1_4(t *testing.T) { } // test scope: -// - 2.2.2 -// - 2.3.3 -// - 2.4.1 -// - 2.5.1 -// - 2.6.0 -// - 2.2.1.3+enterprise -// - 2.3.3.4+enterprise -// - 2.4.1.3+enterprise -// - 2.5.1.2+enterprise -func Test_Sync_FillDefaults_From_2_2_1_to_2_6_0(t *testing.T) { +// - 3.0.0+ +func Test_Sync_PluginsOnEntitiesFrom_3_0_0(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1860,20 +2655,20 @@ func Test_Sync_FillDefaults_From_2_2_1_to_2_6_0(t *testing.T) { expectedState utils.KongRawState }{ { - name: "create services and routes", - kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", + name: "create plugins on services, routes and consumers", + kongFile: "testdata/sync/xxx-plugins-on-entities/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1, + Services: svc1_207, Routes: route1_20x, - Upstreams: upstream_pre31, - Targets: target, - Plugins: plugin, + Plugins: plugin_on_entities3x, + Consumers: consumer, }, }, } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">2.2.1 <=2.6.0") + runWhenKongOrKonnect(t, ">=3.0.0") teardown := setup(t) defer teardown(t) @@ -1884,11 +2679,8 @@ func Test_Sync_FillDefaults_From_2_2_1_to_2_6_0(t *testing.T) { } // test scope: -// - 2.7.0 -// - 2.6.0.2+enterprise -// - 2.7.0.0+enterprise -// - 2.8.0.0+enterprise -func Test_Sync_FillDefaults_From_2_6_9(t *testing.T) { +// - 3.0.0+ +func Test_Sync_PluginOrdering(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1896,25 +2688,46 @@ func Test_Sync_FillDefaults_From_2_6_9(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "creates entities with minimum configuration", - kongFile: "testdata/sync/008-create-simple-entities/kong.yaml", + name: "create a plugin with ordering", + kongFile: "testdata/sync/011-plugin-ordering/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, - Plugins: plugin, - Targets: target, - Upstreams: upstream_pre31, + Plugins: []*kong.Plugin{ + { + Name: kong.String("request-termination"), + Protocols: []*string{ + kong.String("grpc"), + kong.String("grpcs"), + kong.String("http"), + kong.String("https"), + }, + Enabled: kong.Bool(true), + Config: kong.Configuration{ + "status_code": float64(200), + "echo": false, + "content_type": nil, + "body": nil, + "message": nil, + "trigger": nil, + }, + Ordering: &kong.PluginOrdering{ + Before: kong.PluginOrderingPhase{ + "access": []string{"basic-auth"}, + }, + }, + }, + }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">2.6.9 <3.0.0") + runWhen(t, "enterprise", ">=3.0.0") teardown := setup(t) defer teardown(t) @@ -1924,44 +2737,111 @@ func Test_Sync_FillDefaults_From_2_6_9(t *testing.T) { } } -func Test_Sync_SkipCACert_2x(t *testing.T) { - // setup stage - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) - } - +// test scope: +// - 3.x +func Test_Sync_Unsupported_Formats(t *testing.T) { tests := []struct { name string kongFile string - expectedState utils.KongRawState + expectedError error }{ { - name: "syncing with --skip-ca-certificates should ignore CA certs", - kongFile: "testdata/sync/009-skip-ca-cert/kong.yaml", - expectedState: utils.KongRawState{ - Services: svc1_207, - CACertificates: []*kong.CACertificate{}, - }, + name: "creates a service", + kongFile: "testdata/sync/001-create-a-service/kong.yaml", + expectedError: errors.New( + "cannot apply '1.1' config format version to Kong version 3.0 or above.\n" + + utils.UpgradeMessage), }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // ca_certificates first appeared in 1.3, but we limit to 2.7+ - // here because the schema changed and the entities aren't the same - // across all versions, even though the skip functionality works the same. - runWhen(t, "kong", ">=2.7.0 <3.0.0") + runWhen(t, "kong", ">=3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile, "--skip-ca-certificates") - testKongState(t, client, false, tc.expectedState, nil) + err := sync(tc.kongFile) + assert.Equal(t, err, tc.expectedError) }) } } -func Test_Sync_SkipCACert_3x(t *testing.T) { +var ( + goodCACertPEM = []byte(`-----BEGIN CERTIFICATE----- +MIIE6DCCAtACCQCjgi452nKnUDANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy +MTAwNDE4NTEyOFoXDTMyMTAwMTE4NTEyOFowNjELMAkGA1UEBhMCVVMxEzARBgNV +BAgMCkNhbGlmb3JuaWExEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBALUwleXMo+CxQFvgtmJbWHO4k3YBJwzWqcr2xWn+ +vgeoLiKFDQC11F/nnWNKkPZyilLeJda5c9YEVaA9IW6/PZhxQ430RM53EJHoiIPB +B9j7BHGzsvWYHEkjXvGQWeD3mR4TAkoCVTfPAjBji/SL+WvLpgPW5hKRVuedD8ja +cTvkNfk6u2TwPYGgekh9+wS9zcEQs4OwsEiQxmi3Z8if1m1uD09tjqAHb0klPEzM +64tPvlzJrIcH3Z5iF+B9qr91PCQJVYOCjGWlUgPULaqIoTVtY+AnaNnNcol0LM/i +oq7uD0JbeyIFDFMDJVqZwDf/zowzLLlP8Hkok4M8JTefXvB0puQoxmGwOAhwlA0G +KF5etrmhg+dOb+f3nWdgbyjPEytyOeMOOA/4Lb8dHRlf9JnEc4DJqwRVPM9BMeUu +9ZlrSWvURRk8nUZfkjTstLqO2aeubfOvb+tDKUq5Ue2B+AFs0ETLy3bds8TU9syV +5Kl+tIwek2TXzc7afvmeCDoRunAx5nVhmW8dpGhknOmJM0GxOi5s2tiu8/3T9XdH +WcH/GMrocZrkhvzkZccSLYoo1jcDn9LwxHVr/BZ43NymjVa6T3QRTta4Kg5wWpfS +yXi4gIW7VJM12CmNfSDEXqhF03+fjFzoWH+YfBK/9GgUMNjnXWIL9PgFFOBomwEL +tv5zAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAKH8eUGgH/OSS3mHB3Gqv1m2Ea04 +Cs03KNEt1weelcHIBWVnPp+jGcSIIfMBnDFAwgxtBKhwptJ9ZKXIzjh7YFxbOT01 +NU+KQ6tD+NFDf+SAUC4AWV9Cam63JIaCVNDoo5UjVMlssnng7NefM1q2+ucoP+gs ++bvUCTJcp3FZsq8aUI9Rka575HqRhl/8kyhcwICCgT5UHQJvCQYrInJ0Faem6dr0 +tHw+PZ1bo6qB7uxBjK9kyu7dK/vEKliUGM4/MXMDKIc5qXUs47wPLbjxvKsuDglK +KftgUWNYRxx9Bf9ylbjd+ayo3+1Lb9cbvdZnh0UHN6677NvXlWNheCmeysLGQHtm +5H6iIhZ75r6QuC7m6hBSJYtLU3fsQECrmaS/+xBGoSSZjacciO7b7qjQdWOfQREn +7vc5eu0N+CJkp8t3SsyQP6v2Su3ILeTt2EWrmmE4K7SYlJe1HrUVj0AWUwzLa6+Z ++Dx16p3M0RBdFMGNNhLqvG3WRfE5c5md34Aq/C5ePjN7pQGmJhI6weowuX9wCrnh +nJJJRfqyJvqgnVBZ6IawNcOyIofITZHlYVKuaDB1odzWCDNEvFftgJvH0MnO7OY9 +Pb9hILPoCy+91jQAVh6Z/ghIcZKHV+N6zV3uS3t5vCejhCNK8mUPSOwAeDf3Bq5r +wQPXd0DdsYGmXVIh +-----END CERTIFICATE-----`) + + badCACertPEM = []byte(`-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUYGc07pbHSjOBPreXh7OcNT2+sD4wDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRUwEwYDVQQKDAxZb2xvNDIs +IEluYy4xJjAkBgNVBAMMHVlvbG80MiBzZWxmLXNpZ25lZCB0ZXN0aW5nIENBMB4X +DTIyMDMyOTE5NDczM1oXDTMyMDMyNjE5NDczM1owWTELMAkGA1UEBhMCVVMxCzAJ +BgNVBAgMAkNBMRUwEwYDVQQKDAxZb2xvNDIsIEluYy4xJjAkBgNVBAMMHVlvbG80 +MiBzZWxmLXNpZ25lZCB0ZXN0aW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAvnhTgdJALnuLKDA0ZUZRVMqcaaC+qvfJkiEFGYwX2ZJiFtzU65F/ +sB2L0ToFqY4tmMVlOmiSZFnRLDZecmQDbbNwc3wtNikmxIOzx4qR4kbRP8DDdyIf +gaNmGCuaXTM5+FYy2iNBn6CeibIjqdErQlAbFLwQs5t3mLsjii2U4cyvfRtO+0RV +HdJ6Np5LsVziN0c5gVIesIrrbxLcOjtXDzwd/w/j5NXqL/OwD5EBH2vqd3QKKX4t +s83BLl2EsbUse47VAImavrwDhmV6S/p/NuJHqjJ6dIbXLYxNS7g26ijcrXxvNhiu +YoZTykSgdI3BXMNAm1ahP/BtJPZpU7CVdQIDAQABo1MwUTAdBgNVHQ4EFgQUe1WZ +fMfZQ9QIJIttwTmcrnl40ccwHwYDVR0jBBgwFoAUe1WZfMfZQ9QIJIttwTmcrnl4 +0ccwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAs4Z8VYbvEs93 +haTHdbbaKk0V6xAL/Q8I8GitK9E8cgf8C5rwwn+wU/Gf39dtMUlnW8uxyzRPx53u +CAAcJAWkabT+xwrlrqjO68H3MgIAwgWA5yZC+qW7ECA8xYEK6DzEHIaOpagJdKcL +IaZr/qTJlEQClvwDs4x/BpHRB5XbmJs86GqEB7XWAm+T2L8DluHAXvek+welF4Xo +fQtLlNS/vqTDqPxkSbJhFv1L7/4gdwfAz51wH/iL7AG/ubFEtoGZPK9YCJ40yTWz +8XrUoqUC+2WIZdtmo6dFFJcLfQg4ARJZjaK6lmxJun3iRMZjKJdQKm/NEKz4y9kA +u8S6yNlu2Q== +-----END CERTIFICATE-----`) +) + +// test scope: +// - 3.0.0+ +// +// This test does two things: +// 1. makes sure decK can correctly configure a Vault entity +// 2. makes sure secrets management works as expected end-to-end +// +// Specifically, for (2) we make use of: +// - a Service and a Route to verify the overall flow works end-to-end +// - a Certificate with secret references +// - an {env} Vault using 'MY_SECRET_' as env variables prefix +// +// The Kong EE instance running in the CI includes the MY_SECRET_CERT +// and MY_SECRET_KEY env variables storing cert/key signed with `caCert`. +// These variables are pulled into the {env} Vault after decK deploy +// the configuration. +// +// After the `deck sync` and the configuration verification step, +// an HTTPS client is created using the `caCert` used to sign the +// deployed certificate, and then a GET is performed to test the +// proxy functionality, which should return a 200. +func Test_Sync_Vault(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -1969,36 +2849,122 @@ func Test_Sync_SkipCACert_3x(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + initialKongFile string + expectedState utils.KongRawState }{ { - name: "syncing with --skip-ca-certificates should ignore CA certs", - kongFile: "testdata/sync/009-skip-ca-cert/kong3x.yaml", + name: "create an SSL service/route using an ENV vault", + kongFile: "testdata/sync/012-vaults/kong3x.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - CACertificates: []*kong.CACertificate{}, + Vaults: []*kong.Vault{ + { + Name: kong.String("env"), + Prefix: kong.String("my-env-vault"), + Description: kong.String("ENV vault for secrets"), + Config: kong.Configuration{ + "prefix": "MY_SECRET_", + }, + }, + }, + Services: []*kong.Service{ + { + ID: kong.String("58076db2-28b6-423b-ba39-a797193017f7"), + Name: kong.String("svc1"), + ConnectTimeout: kong.Int(60000), + Host: kong.String("mockbin.org"), + Port: kong.Int(80), + Path: kong.String("/status/200"), + Protocol: kong.String("http"), + ReadTimeout: kong.Int(60000), + Retries: kong.Int(5), + WriteTimeout: kong.Int(60000), + Tags: nil, + Enabled: kong.Bool(true), + }, + }, + Routes: route1_20x, + Certificates: []*kong.Certificate{ + { + ID: kong.String("13c562a1-191c-4464-9b18-e5222b46035b"), + Cert: kong.String("{vault://my-env-vault/cert}"), + Key: kong.String("{vault://my-env-vault/key}"), + }, + }, + SNIs: []*kong.SNI{ + { + Name: kong.String("localhost"), + Certificate: &kong.Certificate{ + ID: kong.String("13c562a1-191c-4464-9b18-e5222b46035b"), + }, + }, + }, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // ca_certificates first appeared in 1.3, but we limit to 2.7+ - // here because the schema changed and the entities aren't the same - // across all versions, even though the skip functionality works the same. - runWhenKongOrKonnect(t, ">=3.0.0") + runWhen(t, "enterprise", ">=3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile, "--skip-ca-certificates") + sync(tc.kongFile) testKongState(t, client, false, tc.expectedState, nil) + + // Kong proxy may need a bit to be ready. + time.Sleep(time.Second * 5) + + // build simple http client + client := &http.Client{} + + // use simple http client with https should result + // in a failure due missing certificate. + _, err := client.Get("https://localhost:8443/r1") + assert.NotNil(t, err) + + // use transport with wrong CA cert this should result + // in a failure due to unknown authority. + badCACertPool := x509.NewCertPool() + badCACertPool.AppendCertsFromPEM(badCACertPEM) + + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: badCACertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + }, + }, + } + + _, err = client.Get("https://localhost:8443/r1") + assert.NotNil(t, err) + + // use transport with good CA cert should pass + // if referenced secrets are resolved correctly + // using the ENV vault. + goodCACertPool := x509.NewCertPool() + goodCACertPool.AppendCertsFromPEM(goodCACertPEM) + + client = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: goodCACertPool, + ClientAuth: tls.RequireAndVerifyClientCert, + }, + }, + } + + res, err := client.Get("https://localhost:8443/r1") + assert.NoError(t, err) + assert.Equal(t, res.StatusCode, http.StatusOK) }) } } -func Test_Sync_RBAC_2x(t *testing.T) { +// test scope: +// - 2.8.x +func Test_Sync_UpdateUsernameInConsumerWithCustomID(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -2006,86 +2972,20 @@ func Test_Sync_RBAC_2x(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState }{ { - name: "rbac", - kongFile: "testdata/sync/xxx-rbac-endpoint-permissions/kong.yaml", + name: "update username on a consumer with custom_id", + kongFile: "testdata/sync/013-update-username-consumer-with-custom-id/kong.yaml", + kongFileInitial: "testdata/sync/013-update-username-consumer-with-custom-id/kong-initial.yaml", expectedState: utils.KongRawState{ - RBACRoles: []*kong.RBACRole{ - { - Name: kong.String("workspace-portal-admin"), - Comment: kong.String("Full access to Dev Portal related endpoints in the workspace"), - }, - }, - RBACEndpointPermissions: []*kong.RBACEndpointPermission{ - { - Workspace: kong.String("default"), - Endpoint: kong.String("/developers"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/developers/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/files"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/files/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/kong"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, + Consumers: []*kong.Consumer{ { - Workspace: kong.String("default"), - Endpoint: kong.String("/workspaces/default"), - Actions: []*string{kong.String("read"), kong.String("update")}, - Negative: kong.Bool(false), + Username: kong.String("test_new"), + CustomID: kong.String("custom_test"), }, }, }, @@ -2094,17 +2994,22 @@ func Test_Sync_RBAC_2x(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=2.7.0 <3.0.0") + runWhen(t, "kong", ">=2.8.0 <3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile, "--rbac-resources-only") + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state + sync(tc.kongFile) testKongState(t, client, false, tc.expectedState, nil) }) } } -func Test_Sync_RBAC_3x(t *testing.T) { +// test scope: +// - 2.8.x +func Test_Sync_UpdateConsumerWithCustomID(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -2112,86 +3017,20 @@ func Test_Sync_RBAC_3x(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState }{ { - name: "rbac", - kongFile: "testdata/sync/xxx-rbac-endpoint-permissions/kong3x.yaml", + name: "update username on a consumer with custom_id", + kongFile: "testdata/sync/014-update-consumer-with-custom-id/kong.yaml", + kongFileInitial: "testdata/sync/014-update-consumer-with-custom-id/kong-initial.yaml", expectedState: utils.KongRawState{ - RBACRoles: []*kong.RBACRole{ - { - Name: kong.String("workspace-portal-admin"), - Comment: kong.String("Full access to Dev Portal related endpoints in the workspace"), - }, - }, - RBACEndpointPermissions: []*kong.RBACEndpointPermission{ - { - Workspace: kong.String("default"), - Endpoint: kong.String("/developers"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/developers/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/files"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/files/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/kong"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(false), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, - { - Workspace: kong.String("default"), - Endpoint: kong.String("/rbac/*/*/*/*/*"), - Actions: []*string{kong.String("read"), kong.String("delete"), kong.String("create"), kong.String("update")}, - Negative: kong.Bool(true), - }, + Consumers: []*kong.Consumer{ { - Workspace: kong.String("default"), - Endpoint: kong.String("/workspaces/default"), - Actions: []*string{kong.String("read"), kong.String("update")}, - Negative: kong.Bool(false), + Username: kong.String("test"), + CustomID: kong.String("new_custom_test"), }, }, }, @@ -2200,17 +3039,22 @@ func Test_Sync_RBAC_3x(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.0.0") + runWhen(t, "kong", ">=2.8.0 <3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile, "--rbac-resources-only") + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state + sync(tc.kongFile) testKongState(t, client, false, tc.expectedState, nil) }) } } -func Test_Sync_Create_Route_With_Service_Name_Reference_2x(t *testing.T) { +// test scope: +// - 3.x +func Test_Sync_UpdateUsernameInConsumerWithCustomID_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -2218,57 +3062,118 @@ func Test_Sync_Create_Route_With_Service_Name_Reference_2x(t *testing.T) { } tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState }{ { - name: "create a route with a service name reference", - kongFile: "testdata/sync/010-create-route-with-service-name-reference/kong.yaml", + name: "update username on a consumer with custom_id", + kongFile: "testdata/sync/013-update-username-consumer-with-custom-id/kong3x.yaml", + kongFileInitial: "testdata/sync/013-update-username-consumer-with-custom-id/kong3x-initial.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, + Consumers: []*kong.Consumer{ + { + Username: kong.String("test_new"), + CustomID: kong.String("custom_test"), + }, + }, }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.7.0 <3.0.0") + runWhenKongOrKonnect(t, ">=3.0.0") teardown := setup(t) defer teardown(t) + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state sync(tc.kongFile) testKongState(t, client, false, tc.expectedState, nil) }) } } -func Test_Sync_Create_Route_With_Service_Name_Reference_3x(t *testing.T) { +// test scope: +// - 3.x +func Test_Sync_UpdateConsumerWithCustomID_3x(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } + tests := []struct { + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState + }{ + { + name: "update username on a consumer with custom_id", + kongFile: "testdata/sync/014-update-consumer-with-custom-id/kong3x.yaml", + kongFileInitial: "testdata/sync/014-update-consumer-with-custom-id/kong3x-initial.yaml", + expectedState: utils.KongRawState{ + Consumers: []*kong.Consumer{ + { + Username: kong.String("test"), + CustomID: kong.String("new_custom_test"), + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} + +// test scope: +// - 2.7+ +func Test_Sync_ConsumerGroupsTill30(t *testing.T) { + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } tests := []struct { name string kongFile string expectedState utils.KongRawState }{ { - name: "create a route with a service name reference", - kongFile: "testdata/sync/010-create-route-with-service-name-reference/kong3x.yaml", + name: "creates consumer groups", + kongFile: "testdata/sync/015-consumer-groups/kong.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, + Consumers: consumerGroupsConsumers, + ConsumerGroups: consumerGroups, + }, + }, + { + name: "creates consumer groups and plugin", + kongFile: "testdata/sync/016-consumer-groups-and-plugins/kong.yaml", + expectedState: utils.KongRawState{ + Consumers: consumerGroupsConsumers, + ConsumerGroups: consumerGroupsWithRLA, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.7.0 <3.0.0") + runWhen(t, "enterprise", ">=2.7.0 <3.0.0") teardown := setup(t) defer teardown(t) @@ -2279,85 +3184,234 @@ func Test_Sync_Create_Route_With_Service_Name_Reference_3x(t *testing.T) { } // test scope: -// - 1.x.x -// - 2.x.x -func Test_Sync_PluginsOnEntitiesTill_3_0_0(t *testing.T) { - // setup stage +// - 3.1 +func Test_Sync_ConsumerGroups_31(t *testing.T) { client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } + tests := []struct { + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState + }{ + { + name: "creates consumer groups", + kongFile: "testdata/sync/015-consumer-groups/kong3x.yaml", + kongFileInitial: "testdata/sync/015-consumer-groups/kong3x-initial.yaml", + expectedState: utils.KongRawState{ + Consumers: consumerGroupsConsumers, + 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: consumerGroupsWithTagsAndRLA, + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", "==3.1.0") + teardown := setup(t) + defer teardown(t) + + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state + sync(tc.kongFile) + + testKongState(t, client, false, tc.expectedState, nil) + }) + } +} +// This test has 2 goals: +// - make sure consumer groups and their related properties +// 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 +// - 1 global RLA plugin +// - 2 consumer group +// - 2 RLA override, 1 for each consumer group +// - 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 override 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_ConsumerGroupsRLAFrom31(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: "create plugins on services, routes and consumers", - kongFile: "testdata/sync/xxx-plugins-on-entities/kong.yaml", + name: "creates consumer groups application", + kongFile: "testdata/sync/017-consumer-groups-rla-application/kong3x.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, - Plugins: plugin_on_entities, - Consumers: consumer, + Consumers: consumerGroupsConsumers, + ConsumerGroups: consumerGroupsWithRLAApp, + Plugins: consumerGroupAppPlugins, + 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 tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.8.0 <3.0.0") + runWhen(t, "enterprise", ">=3.0.0 <3.1.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + + // Kong proxy may need a bit to be ready. + time.Sleep(time.Second * 10) + + // 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+ -func Test_Sync_PluginsOnEntitiesFrom_3_0_0(t *testing.T) { - // setup stage +// - konnect +func Test_Sync_ConsumerGroupsKonnect(t *testing.T) { client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - expectedState utils.KongRawState + name string + kongFile string + kongFileInitial string + expectedState utils.KongRawState }{ { - name: "create plugins on services, routes and consumers", - kongFile: "testdata/sync/xxx-plugins-on-entities/kong.yaml", + name: "creates consumer groups", + kongFile: "testdata/sync/015-consumer-groups/kong3x.yaml", + kongFileInitial: "testdata/sync/015-consumer-groups/kong3x-initial.yaml", expectedState: utils.KongRawState{ - Services: svc1_207, - Routes: route1_20x, - Plugins: plugin_on_entities3x, - Consumers: consumer, + Consumers: consumerGroupsConsumers, + ConsumerGroups: consumerGroupsWithTags, }, }, } - for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") + runWhen(t, "konnect", "") teardown := setup(t) defer teardown(t) + // set up initial state + sync(tc.kongFileInitial) + // update with desired final state sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + + testKongState(t, client, true, tc.expectedState, nil) }) } } // test scope: -// - 3.0.0+ -func Test_Sync_PluginOrdering(t *testing.T) { +// - 3.2.0+ +func Test_Sync_PluginInstanceName(t *testing.T) { // setup stage client, err := getTestClient() if err != nil { @@ -2371,12 +3425,13 @@ func Test_Sync_PluginOrdering(t *testing.T) { expectedState utils.KongRawState }{ { - name: "create a plugin with ordering", - kongFile: "testdata/sync/011-plugin-ordering/kong.yaml", + name: "create a plugin with instance_name", + kongFile: "testdata/sync/018-plugin-instance_name/kong-with-instance_name.yaml", expectedState: utils.KongRawState{ Plugins: []*kong.Plugin{ { - Name: kong.String("request-termination"), + Name: kong.String("request-termination"), + InstanceName: kong.String("my-plugin"), Protocols: []*string{ kong.String("grpc"), kong.String("grpcs"), @@ -2392,10 +3447,31 @@ func Test_Sync_PluginOrdering(t *testing.T) { "message": nil, "trigger": nil, }, - Ordering: &kong.PluginOrdering{ - Before: kong.PluginOrderingPhase{ - "access": []string{"basic-auth"}, - }, + }, + }, + }, + }, + { + name: "create a plugin without instance_name", + kongFile: "testdata/sync/018-plugin-instance_name/kong-without-instance_name.yaml", + expectedState: utils.KongRawState{ + Plugins: []*kong.Plugin{ + { + Name: kong.String("request-termination"), + Protocols: []*string{ + kong.String("grpc"), + kong.String("grpcs"), + kong.String("http"), + kong.String("https"), + }, + Enabled: kong.Bool(true), + Config: kong.Configuration{ + "status_code": float64(200), + "echo": false, + "content_type": nil, + "body": nil, + "message": nil, + "trigger": nil, }, }, }, @@ -2404,7 +3480,7 @@ func Test_Sync_PluginOrdering(t *testing.T) { } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.0.0") + runWhenKongOrKonnect(t, ">=3.2.0") teardown := setup(t) defer teardown(t) @@ -2415,110 +3491,60 @@ func Test_Sync_PluginOrdering(t *testing.T) { } // test scope: -// - 3.x -func Test_Sync_Unsupported_Formats(t *testing.T) { +// - 3.2.x +// - 3.3.x +func Test_Sync_SkipConsumers(t *testing.T) { + // setup stage + client, err := getTestClient() + if err != nil { + t.Errorf(err.Error()) + } + tests := []struct { name string kongFile string - expectedError error + skipConsumers bool + expectedState utils.KongRawState }{ { - name: "creates a service", - kongFile: "testdata/sync/001-create-a-service/kong.yaml", - expectedError: errors.New( - "cannot apply '1.1' config format version to Kong version 3.0 or above.\n" + - utils.UpgradeMessage), + name: "skip-consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + }, + skipConsumers: true, + }, + { + name: "do not skip consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong3x.yaml", + expectedState: utils.KongRawState{ + Services: svc1_207, + Consumers: consumerGroupsConsumers, + ConsumerGroups: consumerGroupsWithTagsAndRLA, + }, + skipConsumers: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=3.0.0") + runWhen(t, "enterprise", ">=3.2.0 <3.4.0") teardown := setup(t) defer teardown(t) - err := sync(tc.kongFile) - assert.Equal(t, err, tc.expectedError) + if tc.skipConsumers { + sync(tc.kongFile, "--skip-consumers") + } else { + sync(tc.kongFile) + } + testKongState(t, client, false, tc.expectedState, nil) }) } } -var ( - goodCACertPEM = []byte(`-----BEGIN CERTIFICATE----- -MIIE6DCCAtACCQCjgi452nKnUDANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJV -UzETMBEGA1UECAwKQ2FsaWZvcm5pYTESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy -MTAwNDE4NTEyOFoXDTMyMTAwMTE4NTEyOFowNjELMAkGA1UEBhMCVVMxEzARBgNV -BAgMCkNhbGlmb3JuaWExEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcN -AQEBBQADggIPADCCAgoCggIBALUwleXMo+CxQFvgtmJbWHO4k3YBJwzWqcr2xWn+ -vgeoLiKFDQC11F/nnWNKkPZyilLeJda5c9YEVaA9IW6/PZhxQ430RM53EJHoiIPB -B9j7BHGzsvWYHEkjXvGQWeD3mR4TAkoCVTfPAjBji/SL+WvLpgPW5hKRVuedD8ja -cTvkNfk6u2TwPYGgekh9+wS9zcEQs4OwsEiQxmi3Z8if1m1uD09tjqAHb0klPEzM -64tPvlzJrIcH3Z5iF+B9qr91PCQJVYOCjGWlUgPULaqIoTVtY+AnaNnNcol0LM/i -oq7uD0JbeyIFDFMDJVqZwDf/zowzLLlP8Hkok4M8JTefXvB0puQoxmGwOAhwlA0G -KF5etrmhg+dOb+f3nWdgbyjPEytyOeMOOA/4Lb8dHRlf9JnEc4DJqwRVPM9BMeUu -9ZlrSWvURRk8nUZfkjTstLqO2aeubfOvb+tDKUq5Ue2B+AFs0ETLy3bds8TU9syV -5Kl+tIwek2TXzc7afvmeCDoRunAx5nVhmW8dpGhknOmJM0GxOi5s2tiu8/3T9XdH -WcH/GMrocZrkhvzkZccSLYoo1jcDn9LwxHVr/BZ43NymjVa6T3QRTta4Kg5wWpfS -yXi4gIW7VJM12CmNfSDEXqhF03+fjFzoWH+YfBK/9GgUMNjnXWIL9PgFFOBomwEL -tv5zAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAKH8eUGgH/OSS3mHB3Gqv1m2Ea04 -Cs03KNEt1weelcHIBWVnPp+jGcSIIfMBnDFAwgxtBKhwptJ9ZKXIzjh7YFxbOT01 -NU+KQ6tD+NFDf+SAUC4AWV9Cam63JIaCVNDoo5UjVMlssnng7NefM1q2+ucoP+gs -+bvUCTJcp3FZsq8aUI9Rka575HqRhl/8kyhcwICCgT5UHQJvCQYrInJ0Faem6dr0 -tHw+PZ1bo6qB7uxBjK9kyu7dK/vEKliUGM4/MXMDKIc5qXUs47wPLbjxvKsuDglK -KftgUWNYRxx9Bf9ylbjd+ayo3+1Lb9cbvdZnh0UHN6677NvXlWNheCmeysLGQHtm -5H6iIhZ75r6QuC7m6hBSJYtLU3fsQECrmaS/+xBGoSSZjacciO7b7qjQdWOfQREn -7vc5eu0N+CJkp8t3SsyQP6v2Su3ILeTt2EWrmmE4K7SYlJe1HrUVj0AWUwzLa6+Z -+Dx16p3M0RBdFMGNNhLqvG3WRfE5c5md34Aq/C5ePjN7pQGmJhI6weowuX9wCrnh -nJJJRfqyJvqgnVBZ6IawNcOyIofITZHlYVKuaDB1odzWCDNEvFftgJvH0MnO7OY9 -Pb9hILPoCy+91jQAVh6Z/ghIcZKHV+N6zV3uS3t5vCejhCNK8mUPSOwAeDf3Bq5r -wQPXd0DdsYGmXVIh ------END CERTIFICATE-----`) - - badCACertPEM = []byte(`-----BEGIN CERTIFICATE----- -MIIDkzCCAnugAwIBAgIUYGc07pbHSjOBPreXh7OcNT2+sD4wDQYJKoZIhvcNAQEL -BQAwWTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRUwEwYDVQQKDAxZb2xvNDIs -IEluYy4xJjAkBgNVBAMMHVlvbG80MiBzZWxmLXNpZ25lZCB0ZXN0aW5nIENBMB4X -DTIyMDMyOTE5NDczM1oXDTMyMDMyNjE5NDczM1owWTELMAkGA1UEBhMCVVMxCzAJ -BgNVBAgMAkNBMRUwEwYDVQQKDAxZb2xvNDIsIEluYy4xJjAkBgNVBAMMHVlvbG80 -MiBzZWxmLXNpZ25lZCB0ZXN0aW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAvnhTgdJALnuLKDA0ZUZRVMqcaaC+qvfJkiEFGYwX2ZJiFtzU65F/ -sB2L0ToFqY4tmMVlOmiSZFnRLDZecmQDbbNwc3wtNikmxIOzx4qR4kbRP8DDdyIf -gaNmGCuaXTM5+FYy2iNBn6CeibIjqdErQlAbFLwQs5t3mLsjii2U4cyvfRtO+0RV -HdJ6Np5LsVziN0c5gVIesIrrbxLcOjtXDzwd/w/j5NXqL/OwD5EBH2vqd3QKKX4t -s83BLl2EsbUse47VAImavrwDhmV6S/p/NuJHqjJ6dIbXLYxNS7g26ijcrXxvNhiu -YoZTykSgdI3BXMNAm1ahP/BtJPZpU7CVdQIDAQABo1MwUTAdBgNVHQ4EFgQUe1WZ -fMfZQ9QIJIttwTmcrnl40ccwHwYDVR0jBBgwFoAUe1WZfMfZQ9QIJIttwTmcrnl4 -0ccwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAs4Z8VYbvEs93 -haTHdbbaKk0V6xAL/Q8I8GitK9E8cgf8C5rwwn+wU/Gf39dtMUlnW8uxyzRPx53u -CAAcJAWkabT+xwrlrqjO68H3MgIAwgWA5yZC+qW7ECA8xYEK6DzEHIaOpagJdKcL -IaZr/qTJlEQClvwDs4x/BpHRB5XbmJs86GqEB7XWAm+T2L8DluHAXvek+welF4Xo -fQtLlNS/vqTDqPxkSbJhFv1L7/4gdwfAz51wH/iL7AG/ubFEtoGZPK9YCJ40yTWz -8XrUoqUC+2WIZdtmo6dFFJcLfQg4ARJZjaK6lmxJun3iRMZjKJdQKm/NEKz4y9kA -u8S6yNlu2Q== ------END CERTIFICATE-----`) -) - // test scope: -// - 3.0.0+ -// -// This test does two things: -// 1. makes sure decK can correctly configure a Vault entity -// 2. makes sure secrets management works as expected end-to-end -// -// Specifically, for (2) we make use of: -// - a Service and a Route to verify the overall flow works end-to-end -// - a Certificate with secret references -// - an {env} Vault using 'MY_SECRET_' as env variables prefix -// -// The Kong EE instance running in the CI includes the MY_SECRET_CERT -// and MY_SECRET_KEY env variables storing cert/key signed with `caCert`. -// These variables are pulled into the {env} Vault after decK deploy -// the configuration. -// -// After the `deck sync` and the configuration verification step, -// an HTTPS client is created using the `caCert` used to sign the -// deployed certificate, and then a GET is performed to test the -// proxy functionality, which should return a 200. -func Test_Sync_Vault(t *testing.T) { +// - 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 { @@ -2526,122 +3552,173 @@ func Test_Sync_Vault(t *testing.T) { } tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState + name string + kongFile string + skipConsumers bool + expectedState utils.KongRawState }{ { - name: "create an SSL service/route using an ENV vault", - kongFile: "testdata/sync/012-vaults/kong3x.yaml", + name: "skip-consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", expectedState: utils.KongRawState{ - Vaults: []*kong.Vault{ - { - Name: kong.String("env"), - Prefix: kong.String("my-env-vault"), - Description: kong.String("ENV vault for secrets"), - Config: kong.Configuration{ - "prefix": "MY_SECRET_", - }, - }, - }, - Services: []*kong.Service{ - { - ID: kong.String("58076db2-28b6-423b-ba39-a797193017f7"), - Name: kong.String("svc1"), - ConnectTimeout: kong.Int(60000), - Host: kong.String("mockbin.org"), - Port: kong.Int(80), - Path: kong.String("/status/200"), - Protocol: kong.String("http"), - ReadTimeout: kong.Int(60000), - Retries: kong.Int(5), - WriteTimeout: kong.Int(60000), - Tags: nil, - Enabled: kong.Bool(true), + 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"), + }, + }, }, - }, - Routes: route1_20x, - Certificates: []*kong.Certificate{ { - ID: kong.String("13c562a1-191c-4464-9b18-e5222b46035b"), - Cert: kong.String("{vault://my-env-vault/cert}"), - Key: kong.String("{vault://my-env-vault/key}"), + ConsumerGroup: &kong.ConsumerGroup{ + Name: kong.String("gold"), + Tags: kong.StringSlice("tag1", "tag2"), + }, + Consumers: []*kong.Consumer{ + { + Username: kong.String("foo"), + }, + }, }, }, - SNIs: []*kong.SNI{ + Plugins: []*kong.Plugin{ { - Name: kong.String("localhost"), - Certificate: &kong.Certificate{ - ID: kong.String("13c562a1-191c-4464-9b18-e5222b46035b"), + 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) { - runWhen(t, "enterprise", ">=3.0.0") teardown := setup(t) defer teardown(t) - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) - - // Kong proxy may need a bit to be ready. - time.Sleep(time.Second * 5) - - // build simple http client - client := &http.Client{} - - // use simple http client with https should result - // in a failure due missing certificate. - _, err := client.Get("https://localhost:8443/r1") - assert.NotNil(t, err) - - // use transport with wrong CA cert this should result - // in a failure due to unknown authority. - badCACertPool := x509.NewCertPool() - badCACertPool.AppendCertsFromPEM(badCACertPEM) - - client = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: badCACertPool, - ClientAuth: tls.RequireAndVerifyClientCert, - }, - }, - } - - _, err = client.Get("https://localhost:8443/r1") - assert.NotNil(t, err) - - // use transport with good CA cert should pass - // if referenced secrets are resolved correctly - // using the ENV vault. - goodCACertPool := x509.NewCertPool() - goodCACertPool.AppendCertsFromPEM(goodCACertPEM) - - client = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: goodCACertPool, - ClientAuth: tls.RequireAndVerifyClientCert, - }, - }, + if tc.skipConsumers { + sync(tc.kongFile, "--skip-consumers") + } else { + sync(tc.kongFile) } - - res, err := client.Get("https://localhost:8443/r1") - assert.NoError(t, err) - assert.Equal(t, res.StatusCode, http.StatusOK) + testKongState(t, client, false, tc.expectedState, nil) }) } } // test scope: -// - 2.8.x -func Test_Sync_UpdateUsernameInConsumerWithCustomID(t *testing.T) { +// - konnect +func Test_Sync_SkipConsumers_Konnect(t *testing.T) { + t.Skip("remove skip once Konnect support is enabled.") + runWhenKonnect(t) // setup stage client, err := getTestClient() if err != nil { @@ -2649,268 +3726,502 @@ func Test_Sync_UpdateUsernameInConsumerWithCustomID(t *testing.T) { } tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState + name string + kongFile string + skipConsumers bool + expectedState utils.KongRawState }{ { - name: "update username on a consumer with custom_id", - kongFile: "testdata/sync/013-update-username-consumer-with-custom-id/kong.yaml", - kongFileInitial: "testdata/sync/013-update-username-consumer-with-custom-id/kong-initial.yaml", + name: "skip-consumers successfully", + kongFile: "testdata/sync/019-skip-consumers/kong34.yaml", expectedState: utils.KongRawState{ - Consumers: []*kong.Consumer{ + 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{ { - Username: kong.String("test_new"), - CustomID: kong.String("custom_test"), + 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) { - runWhen(t, "kong", ">=2.8.0 <3.0.0") teardown := setup(t) defer teardown(t) - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) + if tc.skipConsumers { + sync(tc.kongFile, "--skip-consumers") + } else { + sync(tc.kongFile) + } testKongState(t, client, false, tc.expectedState, nil) }) } } +// In the tests we're concerned only with the IDs and names of the entities we'll ignore other fields when comparing states. +var ignoreFieldsIrrelevantForIDsTests = []cmp.Option{ + cmpopts.IgnoreFields( + kong.Plugin{}, + "Config", + "Protocols", + "Enabled", + ), + cmpopts.IgnoreFields( + kong.Service{}, + "ConnectTimeout", + "Enabled", + "Host", + "Port", + "Protocol", + "ReadTimeout", + "WriteTimeout", + "Retries", + ), + cmpopts.IgnoreFields( + kong.Route{}, + "Paths", + "PathHandling", + "PreserveHost", + "Protocols", + "RegexPriority", + "StripPath", + "HTTPSRedirectStatusCode", + "Sources", + "Destinations", + "RequestBuffering", + "ResponseBuffering", + ), +} + // test scope: -// - 2.8.x -func Test_Sync_UpdateConsumerWithCustomID(t *testing.T) { - // setup stage +// - 3.0.0+ +// - konnect +func Test_Sync_ChangingIDsWhileKeepingNames(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState - }{ - { - name: "update username on a consumer with custom_id", - kongFile: "testdata/sync/014-update-consumer-with-custom-id/kong.yaml", - kongFileInitial: "testdata/sync/014-update-consumer-with-custom-id/kong-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: []*kong.Consumer{ - { - Username: kong.String("test"), - CustomID: kong.String("new_custom_test"), - }, + // These are the IDs that should be present in Kong after the second sync in all cases. + var ( + expectedServiceID = kong.String("98076db2-28b6-423b-ba39-a797193017f7") + expectedRouteID = kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b") + expectedConsumerID = kong.String("9a1e49a8-2536-41fa-a4e9-605bf218a4fa") + ) + + // These are the entities that should be present in Kong after the second sync in all cases. + var ( + expectedService = &kong.Service{ + Name: kong.String("s1"), + ID: expectedServiceID, + } + + expectedRoute = &kong.Route{ + Name: kong.String("r1"), + ID: expectedRouteID, + Service: &kong.Service{ + ID: expectedServiceID, + }, + } + + 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 tests { + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "kong", ">=2.8.0 <3.0.0") teardown := setup(t) defer teardown(t) - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + // 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.x -func Test_Sync_UpdateUsernameInConsumerWithCustomID_3x(t *testing.T) { - // setup stage +// - 3.0.0+ +// - konnect +func Test_Sync_UpdateWithExplicitIDs(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState - }{ - { - name: "update username on a consumer with custom_id", - kongFile: "testdata/sync/013-update-username-consumer-with-custom-id/kong3x.yaml", - kongFileInitial: "testdata/sync/013-update-username-consumer-with-custom-id/kong3x-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: []*kong.Consumer{ - { - Username: kong.String("test_new"), - CustomID: kong.String("custom_test"), - }, + 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"), }, }, }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - teardown := setup(t) - defer teardown(t) - - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) - }) - } + Consumers: []*kong.Consumer{ + { + Username: kong.String("c1"), + Tags: kong.StringSlice("after"), + }, + }, + }, ignoreFieldsIrrelevantForIDsTests) } // test scope: -// - 3.x -func Test_Sync_UpdateConsumerWithCustomID_3x(t *testing.T) { - // setup stage +// - 3.0.0+ +// - konnect +func Test_Sync_UpdateWithExplicitIDsWithNoNames(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState - }{ - { - name: "update username on a consumer with custom_id", - kongFile: "testdata/sync/014-update-consumer-with-custom-id/kong3x.yaml", - kongFileInitial: "testdata/sync/014-update-consumer-with-custom-id/kong3x-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: []*kong.Consumer{ - { - Username: kong.String("test"), - CustomID: kong.String("new_custom_test"), - }, + 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"), }, }, }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - teardown := setup(t) - defer teardown(t) - - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) - }) - } + }, ignoreFieldsIrrelevantForIDsTests) } // test scope: -// - 2.7+ -func Test_Sync_ConsumerGroupsTill30(t *testing.T) { +// - 3.0.0+ +// - konnect +func Test_Sync_CreateCertificateWithSNIs(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - expectedState utils.KongRawState - }{ - { - name: "creates consumer groups", - kongFile: "testdata/sync/015-consumer-groups/kong.yaml", - expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroups, + + 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"), }, }, - { - name: "creates consumer groups and plugin", - kongFile: "testdata/sync/016-consumer-groups-and-plugins/kong.yaml", - expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroupsWithRLA, + SNIs: []*kong.SNI{ + { + Name: kong.String("example.com"), + Certificate: &kong.Certificate{ + ID: kong.String("c75a775b-3a32-4b73-8e05-f68169c23941"), + }, }, }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=2.7.0 <3.0.0") - teardown := setup(t) - defer teardown(t) + }, ignoredFields) - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) - }) - } + 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.1 -func Test_Sync_ConsumerGroups_31(t *testing.T) { +// test scope: +// - 3.0.0+ +// - konnect +func Test_Sync_ConsumersWithCustomIDAndUsername(t *testing.T) { + runWhenKongOrKonnect(t, ">=3.0.0") + teardown := setup(t) + defer teardown(t) + client, err := getTestClient() if err != nil { t.Errorf(err.Error()) } - tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState - }{ - { - name: "creates consumer groups", - kongFile: "testdata/sync/015-consumer-groups/kong3x.yaml", - kongFileInitial: "testdata/sync/015-consumer-groups/kong3x-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroupsWithTags, + + 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"), }, - }, - { - 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: consumerGroupsWithTagsAndRLA, + { + ID: kong.String("7820f383-7b77-4fcc-af7f-14ff3e256693"), + Username: kong.String("foo"), + CustomID: kong.String("bar"), }, }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", "==3.1.0") - teardown := setup(t) - defer teardown(t) - - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) - - testKongState(t, client, false, tc.expectedState, nil) - }) - } + }, nil) } // This test has 2 goals: -// - make sure consumer groups and their related properties -// can be configured correctly in Kong +// - 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: @@ -2921,20 +4232,20 @@ func Test_Sync_ConsumerGroups_31(t *testing.T) { // // - 3 key-auths, one for each consumer // - 1 global key-auth plugin -// - 1 global RLA plugin // - 2 consumer group -// - 2 RLA override, 1 for each 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 override is correctly applied: consumers +// 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_ConsumerGroupsRLAFrom31(t *testing.T) { +func Test_Sync_ConsumerGroupsScopedPlugins(t *testing.T) { const ( maxGoldRequestsNumber = 10 maxSilverRequestsNumber = 7 @@ -2950,14 +4261,35 @@ func Test_Sync_ConsumerGroupsRLAFrom31(t *testing.T) { expectedState utils.KongRawState }{ { - name: "creates consumer groups application", - kongFile: "testdata/sync/017-consumer-groups-rla-application/kong3x.yaml", + name: "creates consumer groups scoped plugins", + kongFile: "testdata/sync/025-consumer-groups-scoped-plugins/kong3x.yaml", expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroupsWithRLAApp, - Plugins: consumerGroupAppPlugins, - Services: svc1_207, - Routes: route1_20x, + 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{ @@ -2983,7 +4315,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.4.0 <3.5.0") teardown := setup(t) defer teardown(t) @@ -2996,556 +4328,298 @@ func Test_Sync_ConsumerGroupsRLAFrom31(t *testing.T) { // 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: -// - konnect -func Test_Sync_ConsumerGroupsKonnect(t *testing.T) { - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) - } - tests := []struct { - name string - kongFile string - kongFileInitial string - expectedState utils.KongRawState - }{ - { - name: "creates consumer groups", - kongFile: "testdata/sync/015-consumer-groups/kong3x.yaml", - kongFileInitial: "testdata/sync/015-consumer-groups/kong3x-initial.yaml", - expectedState: utils.KongRawState{ - Consumers: consumerGroupsConsumers, - 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) { - runWhen(t, "konnect", "") - teardown := setup(t) - defer teardown(t) - - // set up initial state - sync(tc.kongFileInitial) - // update with desired final state - sync(tc.kongFile) - - testKongState(t, client, true, tc.expectedState, nil) - }) - } -} - -// test scope: -// - 3.2.0+ -func Test_Sync_PluginInstanceName(t *testing.T) { - // setup stage - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) - } - - tests := []struct { - name string - kongFile string - initialKongFile string - expectedState utils.KongRawState - }{ - { - name: "create a plugin with instance_name", - kongFile: "testdata/sync/018-plugin-instance_name/kong-with-instance_name.yaml", - expectedState: utils.KongRawState{ - Plugins: []*kong.Plugin{ - { - Name: kong.String("request-termination"), - InstanceName: kong.String("my-plugin"), - Protocols: []*string{ - kong.String("grpc"), - kong.String("grpcs"), - kong.String("http"), - kong.String("https"), - }, - Enabled: kong.Bool(true), - Config: kong.Configuration{ - "status_code": float64(200), - "echo": false, - "content_type": nil, - "body": nil, - "message": nil, - "trigger": nil, - }, - }, - }, - }, - }, - { - name: "create a plugin without instance_name", - kongFile: "testdata/sync/018-plugin-instance_name/kong-without-instance_name.yaml", - expectedState: utils.KongRawState{ - Plugins: []*kong.Plugin{ - { - Name: kong.String("request-termination"), - Protocols: []*string{ - kong.String("grpc"), - kong.String("grpcs"), - kong.String("http"), - kong.String("https"), - }, - Enabled: kong.Bool(true), - Config: kong.Configuration{ - "status_code": float64(200), - "echo": false, - "content_type": nil, - "body": nil, - "message": nil, - "trigger": nil, - }, - }, - }, - }, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.2.0") - teardown := setup(t) - defer teardown(t) - - sync(tc.kongFile) - testKongState(t, client, false, tc.expectedState, nil) + // 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.2.0+ -func Test_Sync_SkipConsumers(t *testing.T) { - // setup stage +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()) } - tests := []struct { name string kongFile string - skipConsumers bool expectedState utils.KongRawState }{ { - name: "skip-consumers successfully", - kongFile: "testdata/sync/019-skip-consumers/kong3x.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: 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"), + }, + }, }, - skipConsumers: true, - }, - { - name: "do not skip consumers successfully", - kongFile: "testdata/sync/019-skip-consumers/kong3x.yaml", - expectedState: utils.KongRawState{ - Services: svc1_207, - Consumers: consumerGroupsConsumers, - ConsumerGroups: consumerGroupsWithTagsAndRLA, - }, - skipConsumers: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - runWhen(t, "enterprise", ">=3.2.0") + runWhen(t, "enterprise", ">=3.5.0") teardown := setup(t) defer teardown(t) - if tc.skipConsumers { - sync(tc.kongFile, "--skip-consumers") - } else { - sync(tc.kongFile) - } + sync(tc.kongFile) testKongState(t, client, false, tc.expectedState, nil) - }) - } -} - -// In the tests we're concerned only with the IDs and names of the entities we'll ignore other fields when comparing states. -var ignoreFieldsIrrelevantForIDsTests = []cmp.Option{ - cmpopts.IgnoreFields( - kong.Plugin{}, - "Config", - "Protocols", - "Enabled", - ), - cmpopts.IgnoreFields( - kong.Service{}, - "ConnectTimeout", - "Enabled", - "Host", - "Port", - "Protocol", - "ReadTimeout", - "WriteTimeout", - "Retries", - ), - cmpopts.IgnoreFields( - kong.Route{}, - "Paths", - "PathHandling", - "PreserveHost", - "Protocols", - "RegexPriority", - "StripPath", - "HTTPSRedirectStatusCode", - "Sources", - "Destinations", - "RequestBuffering", - "ResponseBuffering", - ), -} - -// test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_ChangingIDsWhileKeepingNames(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") - - client, err := getTestClient() - if err != nil { - t.Errorf(err.Error()) - } - // These are the IDs that should be present in Kong after the second sync in all cases. - var ( - expectedServiceID = kong.String("98076db2-28b6-423b-ba39-a797193017f7") - expectedRouteID = kong.String("97b6a97e-f3f7-4c47-857a-7464cb9e202b") - expectedConsumerID = kong.String("9a1e49a8-2536-41fa-a4e9-605bf218a4fa") - ) + // Kong proxy may need a bit to be ready. + time.Sleep(time.Second * 10) - // These are the entities that should be present in Kong after the second sync in all cases. - var ( - expectedService = &kong.Service{ - Name: kong.String("s1"), - ID: expectedServiceID, - } + // build simple http client + client := &http.Client{} - expectedRoute = &kong.Route{ - Name: kong.String("r1"), - ID: expectedRouteID, - Service: &kong.Service{ - ID: expectedServiceID, - }, - } + // 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) - expectedConsumer = &kong.Consumer{ - Username: kong.String("c1"), - ID: expectedConsumerID, - } + // 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) - 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, - }, - }, - } - ) + // 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) + }) + } +} - testCases := []struct { - name string - beforeConfig string +// test scope: +// - > 3.4.0 +func Test_Sync_ConsumerGroupsScopedPlugins_Post340(t *testing.T) { + tests := []struct { + name string + kongFile string + expectedError error }{ { - 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: "attempt to create deprecated consumer groups configuration with Kong version >= 3.4.0 fails", + kongFile: "testdata/sync/017-consumer-groups-rla-application/kong3x.yaml", + expectedError: fmt.Errorf("building state: %v", utils.ErrorConsumerGroupUpgrade), }, { - name: "route and consumer changed IDs, service did not", - beforeConfig: "testdata/sync/020-same-names-altered-ids/3-before.yaml", + name: "empty deprecated consumer groups configuration fields do not fail with Kong version >= 3.4.0", + kongFile: "testdata/sync/017-consumer-groups-rla-application/kong3x-empty-application.yaml", }, } - - for _, tc := range testCases { + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + runWhen(t, "enterprise", ">=3.4.0") 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) + err := sync(tc.kongFile) + if tc.expectedError == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tc.expectedError.Error()) + } }) } } -// test scope: -// - 3.0.0+ -// - konnect -func Test_Sync_UpdateWithExplicitIDs(t *testing.T) { - runWhenKongOrKonnect(t, ">=3.0.0") +func Test_Sync_ConsumerGroupsScopedPluginsKonnect(t *testing.T) { + t.Skip("remove skip once Konnect support is enabled.") 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"), + }, + }, + }, }, - }, - }, - 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"), + 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"), + }, }, }, }, - }, 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", - ), } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runWhenKonnect(t) + teardown := setup(t) + defer teardown(t) - 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()) + sync(tc.kongFile) + testKongState(t, client, false, tc.expectedState, nil) + }) } - - 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"), - }, - }, - }, 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/017-consumer-groups-rla-application/kong3x-empty-application.yaml b/tests/integration/testdata/sync/017-consumer-groups-rla-application/kong3x-empty-application.yaml new file mode 100644 index 000000000..cd5cde8a5 --- /dev/null +++ b/tests/integration/testdata/sync/017-consumer-groups-rla-application/kong3x-empty-application.yaml @@ -0,0 +1,48 @@ +_format_version: "3.0" +plugins: +- config: + consumer_groups: null + dictionary_name: kong_rate_limiting_counters + enforce_consumer_groups: false + header_name: null + hide_client_headers: false + identifier: consumer + limit: + - 5 + namespace: dNRC6xKsRL8Koc1uVYA4Nki6DLW7XIdx + 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: + - 60 + window_type: sliding + enabled: true + name: rate-limiting-advanced + protocols: + - grpc + - grpcs + - http + - https + 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..884842cef 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "context" + "errors" "fmt" "net/url" "os" @@ -21,6 +22,14 @@ var ( Kong140Version = semver.MustParse("1.4.0") Kong300Version = semver.MustParse("3.0.0") + Kong340Version = semver.MustParse("3.4.0") +) + +var ErrorConsumerGroupUpgrade = 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", ) var UpgradeMessage = "Please upgrade your configuration to account for 3.0\n" + @@ -148,6 +157,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 {