From c2c6ae09e1f1131de64901cce509ff43f7efde98 Mon Sep 17 00:00:00 2001 From: Sarasa Kisaragi Date: Tue, 12 Sep 2023 22:19:30 +0800 Subject: [PATCH] feat: support resource consumer group (#27) --- cmd/dump.go | 18 ++- cmd/validate.go | 4 + internal/pkg/db/memdb.go | 25 +++- internal/pkg/differ/differ.go | 108 ++++++++++++--- internal/pkg/validator/validator.go | 8 ++ pkg/api/apisix/apisix.go | 5 + pkg/api/apisix/cluster.go | 19 ++- pkg/api/apisix/consumer_group.go | 26 ++++ pkg/api/apisix/types/types.go | 29 ++-- pkg/common/file.go | 32 +++-- pkg/data/events.go | 8 ++ test/cli/cli_test.go | 1 + test/cli/scaffold/scaffold.go | 61 +++++++-- test/cli/suites-consumer-group/diff.go | 22 +++ test/cli/suites-consumer-group/dump.go | 46 +++++++ test/cli/suites-consumer-group/sdk.go | 125 ++++++++++++++++++ test/cli/suites-consumer-group/sync.go | 120 +++++++++++++++++ .../suites-consumer-group/testdata/test.yaml | 18 +++ test/cli/suites-consumer-group/validate.go | 19 +++ test/cli/suites-consumer/sync.go | 2 +- test/cli/suites-global-rule/sync.go | 2 +- test/cli/suites-plugin-config/sdk.go | 1 - test/cli/suites-plugin-config/sync.go | 2 +- 23 files changed, 632 insertions(+), 69 deletions(-) create mode 100644 pkg/api/apisix/consumer_group.go create mode 100644 test/cli/suites-consumer-group/diff.go create mode 100644 test/cli/suites-consumer-group/dump.go create mode 100644 test/cli/suites-consumer-group/sdk.go create mode 100644 test/cli/suites-consumer-group/sync.go create mode 100644 test/cli/suites-consumer-group/testdata/test.yaml create mode 100644 test/cli/suites-consumer-group/validate.go diff --git a/cmd/dump.go b/cmd/dump.go index c09e31c..b94bc54 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -86,13 +86,19 @@ func dumpConfiguration(cmd *cobra.Command) error { return err } + consumerGroups, err := cluster.ConsumerGroup().List(context.Background()) + if err != nil { + return err + } + conf := &types.Configuration{ - Routes: routes, - Services: svcs, - Consumers: consumers, - SSLs: ssls, - GlobalRules: globalRules, - PluginConfigs: pluginConfigs, + Routes: routes, + Services: svcs, + Consumers: consumers, + SSLs: ssls, + GlobalRules: globalRules, + PluginConfigs: pluginConfigs, + ConsumerGroups: consumerGroups, } if save { diff --git a/cmd/validate.go b/cmd/validate.go index 0bac8c3..3df8948 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -67,6 +67,10 @@ func newValidateCmd() *cobra.Command { msg += fmt.Sprintf(", plugin_configs: %v", len(d.PluginConfigs)) changed = true } + if len(d.ConsumerGroups) > 0 { + msg += fmt.Sprintf(", consumer_groups: %v", len(d.ConsumerGroups)) + changed = true + } if !changed { msg += "nothing changed" } diff --git a/internal/pkg/db/memdb.go b/internal/pkg/db/memdb.go index 6a65767..e9b3dd3 100644 --- a/internal/pkg/db/memdb.go +++ b/internal/pkg/db/memdb.go @@ -69,11 +69,15 @@ var schema = &memdb.DBSchema{ Unique: true, Indexer: &memdb.StringFieldIndex{Field: "ID"}, }, - "name": { - Name: "name", - Unique: true, - Indexer: &memdb.StringFieldIndex{Field: "Name"}, - AllowMissing: true, + }, + }, + "consumer_groups": { + Name: "consumer_groups", + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{Field: "ID"}, }, }, }, @@ -140,6 +144,13 @@ func NewMemDB(config *types.Configuration) (*DB, error) { } } + for _, consumerGroup := range config.ConsumerGroups { + err = txn.Insert("consumer_groups", consumerGroup) + if err != nil { + return nil, err + } + } + txn.Commit() return &DB{memDB: db}, nil @@ -181,3 +192,7 @@ func (db *DB) GetGlobalRuleByID(id string) (*types.GlobalRule, error) { func (db *DB) GetPluginConfigByID(id string) (*types.PluginConfig, error) { return getByID[types.PluginConfig](db, "plugin_configs", id) } + +func (db *DB) GetConsumerGroupByID(id string) (*types.ConsumerGroup, error) { + return getByID[types.ConsumerGroup](db, "consumer_groups", id) +} diff --git a/internal/pkg/differ/differ.go b/internal/pkg/differ/differ.go index 45bf166..e901d31 100644 --- a/internal/pkg/differ/differ.go +++ b/internal/pkg/differ/differ.go @@ -21,26 +21,36 @@ func _key(typ data.ResourceType, option int) string { return fmt.Sprintf("%s:%d", typ, option) } -// order is the events order to ensure the data dependency +// order is the events order to ensure the data dependency. Higher takes priority +// route requires: service, plugin config, consumer (soft require) +// consumer requires: consumer group +// The dependent resources should be created/updated first but deleted later var order = map[string]int{ - _key(data.ConsumerResourceType, data.DeleteOption): _order(), - _key(data.ServiceResourceType, data.DeleteOption): _order(), - _key(data.PluginConfigResourceType, data.DeleteOption): _order(), - _key(data.RouteResourceType, data.DeleteOption): _order(), - _key(data.RouteResourceType, data.UpdateOption): _order(), - _key(data.ServiceResourceType, data.UpdateOption): _order(), - _key(data.PluginConfigResourceType, data.UpdateOption): _order(), - _key(data.RouteResourceType, data.CreateOption): _order(), - _key(data.ServiceResourceType, data.CreateOption): _order(), - _key(data.PluginConfigResourceType, data.CreateOption): _order(), - _key(data.ConsumerResourceType, data.CreateOption): _order(), - _key(data.ConsumerResourceType, data.UpdateOption): _order(), - _key(data.SSLResourceType, data.DeleteOption): _order(), - _key(data.SSLResourceType, data.CreateOption): _order(), - _key(data.SSLResourceType, data.UpdateOption): _order(), - _key(data.GlobalRuleResourceType, data.DeleteOption): _order(), - _key(data.GlobalRuleResourceType, data.CreateOption): _order(), - _key(data.GlobalRuleResourceType, data.UpdateOption): _order(), + _key(data.ServiceResourceType, data.DeleteOption): _order(), + _key(data.PluginConfigResourceType, data.DeleteOption): _order(), + _key(data.ConsumerGroupResourceType, data.DeleteOption): _order(), + _key(data.ConsumerResourceType, data.DeleteOption): _order(), + _key(data.RouteResourceType, data.DeleteOption): _order(), + + _key(data.RouteResourceType, data.UpdateOption): _order(), + _key(data.ServiceResourceType, data.UpdateOption): _order(), + _key(data.PluginConfigResourceType, data.UpdateOption): _order(), + _key(data.ConsumerResourceType, data.UpdateOption): _order(), + _key(data.ConsumerGroupResourceType, data.UpdateOption): _order(), + + _key(data.RouteResourceType, data.CreateOption): _order(), + _key(data.ServiceResourceType, data.CreateOption): _order(), + _key(data.PluginConfigResourceType, data.CreateOption): _order(), + _key(data.ConsumerResourceType, data.CreateOption): _order(), + _key(data.ConsumerGroupResourceType, data.CreateOption): _order(), + + // no dependency + _key(data.SSLResourceType, data.DeleteOption): _order(), + _key(data.SSLResourceType, data.CreateOption): _order(), + _key(data.SSLResourceType, data.UpdateOption): _order(), + _key(data.GlobalRuleResourceType, data.DeleteOption): _order(), + _key(data.GlobalRuleResourceType, data.CreateOption): _order(), + _key(data.GlobalRuleResourceType, data.UpdateOption): _order(), } // Differ is the object of comparing two configurations. @@ -66,6 +76,7 @@ func NewDiffer(local, remote *types.Configuration) (*Differ, error) { }, nil } +// sortEvents sorts events descending, higher priority events will be executed first func sortEvents(events []*data.Event) { sort.Slice(events, func(i, j int) bool { return order[_key(events[i].ResourceType, events[i].Option)] > order[_key(events[j].ResourceType, events[j].Option)] @@ -107,12 +118,18 @@ func (d *Differ) Diff() ([]*data.Event, error) { return nil, err } + consumerGroupEvents, err := d.diffConsumerGroup() + if err != nil { + return nil, err + } + events = append(events, serviceEvents...) events = append(events, routeEvents...) events = append(events, consumerEvents...) events = append(events, sslEvents...) events = append(events, globalRuleEvents...) events = append(events, pluginConfigEvents...) + events = append(events, consumerGroupEvents...) sortEvents(events) @@ -437,3 +454,56 @@ func (d *Differ) diffPluginConfig() ([]*data.Event, error) { return events, nil } + +// diffConsumerGroup compares the global_rules between local and remote. +func (d *Differ) diffConsumerGroup() ([]*data.Event, error) { + var events []*data.Event + var mark = make(map[string]bool) + + for _, remoteConsumerGroup := range d.remoteConfig.ConsumerGroups { + localConsumerGroup, err := d.localDB.GetConsumerGroupByID(remoteConsumerGroup.ID) + if err != nil { + // we can't find in local config, should delete it + if err == db.NotFound { + e := data.Event{ + ResourceType: data.ConsumerGroupResourceType, + Option: data.DeleteOption, + OldValue: remoteConsumerGroup, + } + events = append(events, &e) + continue + } + + return nil, err + } + + mark[localConsumerGroup.ID] = true + // skip when equals + if equal := reflect.DeepEqual(localConsumerGroup, remoteConsumerGroup); equal { + continue + } + + // otherwise update + events = append(events, &data.Event{ + ResourceType: data.ConsumerGroupResourceType, + Option: data.UpdateOption, + OldValue: remoteConsumerGroup, + Value: localConsumerGroup, + }) + } + + // only in local, create + for _, consumerGroup := range d.localConfig.ConsumerGroups { + if mark[consumerGroup.ID] { + continue + } + + events = append(events, &data.Event{ + ResourceType: data.ConsumerGroupResourceType, + Option: data.CreateOption, + Value: consumerGroup, + }) + } + + return events, nil +} diff --git a/internal/pkg/validator/validator.go b/internal/pkg/validator/validator.go index 45bd7ee..477e900 100644 --- a/internal/pkg/validator/validator.go +++ b/internal/pkg/validator/validator.go @@ -92,5 +92,13 @@ func (v *Validator) Validate() []error { } } + for _, consumerGroup := range v.localConfig.ConsumerGroups { + consumerGroup := consumerGroup + err := v.cluster.ConsumerGroup().Validate(context.Background(), consumerGroup) + if err != nil { + allErr = append(allErr, err) + } + } + return allErr } diff --git a/pkg/api/apisix/apisix.go b/pkg/api/apisix/apisix.go index a96d327..b6d41b8 100644 --- a/pkg/api/apisix/apisix.go +++ b/pkg/api/apisix/apisix.go @@ -13,6 +13,7 @@ type Cluster interface { SSL() SSL GlobalRule() GlobalRule PluginConfig() PluginConfig + ConsumerGroup() ConsumerGroup } type ResourceClient[T any] interface { @@ -47,3 +48,7 @@ type GlobalRule interface { type PluginConfig interface { ResourceClient[types.PluginConfig] } + +type ConsumerGroup interface { + ResourceClient[types.ConsumerGroup] +} diff --git a/pkg/api/apisix/cluster.go b/pkg/api/apisix/cluster.go index 3384fd6..01aa23a 100644 --- a/pkg/api/apisix/cluster.go +++ b/pkg/api/apisix/cluster.go @@ -10,12 +10,13 @@ type cluster struct { cli *Client - route Route - service Service - consumer Consumer - ssl SSL - globalRule GlobalRule - pluginConfig PluginConfig + route Route + service Service + consumer Consumer + ssl SSL + globalRule GlobalRule + pluginConfig PluginConfig + consumerGroup ConsumerGroup } func NewCluster(ctx context.Context, url, adminKey string) Cluster { @@ -33,6 +34,7 @@ func NewCluster(ctx context.Context, url, adminKey string) Cluster { c.ssl = newSSL(cli) c.globalRule = newGlobalRule(cli) c.pluginConfig = newPluginConfig(cli) + c.consumerGroup = newConsumerGroup(cli) return c } @@ -66,3 +68,8 @@ func (c *cluster) GlobalRule() GlobalRule { func (c *cluster) PluginConfig() PluginConfig { return c.pluginConfig } + +// ConsumerGroup implements Cluster.ConsumerGroup method. +func (c *cluster) ConsumerGroup() ConsumerGroup { + return c.consumerGroup +} diff --git a/pkg/api/apisix/consumer_group.go b/pkg/api/apisix/consumer_group.go new file mode 100644 index 0000000..5128877 --- /dev/null +++ b/pkg/api/apisix/consumer_group.go @@ -0,0 +1,26 @@ +package apisix + +import ( + "context" + + "github.com/api7/adc/pkg/api/apisix/types" +) + +type consumerGroupClient struct { + *resourceClient[types.ConsumerGroup] +} + +func newConsumerGroup(c *Client) ConsumerGroup { + cli := newResourceClient[types.ConsumerGroup](c, "consumer_groups") + return &consumerGroupClient{ + resourceClient: cli, + } +} + +func (u *consumerGroupClient) Create(ctx context.Context, obj *types.ConsumerGroup) (*types.ConsumerGroup, error) { + return u.resourceClient.Create(ctx, obj.ID, obj) +} + +func (u *consumerGroupClient) Update(ctx context.Context, obj *types.ConsumerGroup) (*types.ConsumerGroup, error) { + return u.resourceClient.Update(ctx, obj.ID, obj) +} diff --git a/pkg/api/apisix/types/types.go b/pkg/api/apisix/types/types.go index 4bebbd8..fc88cb0 100644 --- a/pkg/api/apisix/types/types.go +++ b/pkg/api/apisix/types/types.go @@ -10,14 +10,15 @@ import ( // Configuration is the configuration of services type Configuration struct { - Name string `yaml:"name" json:"name"` - Version string `yaml:"version" json:"version"` - Services []*Service `yaml:"services,omitempty" json:"services,omitempty"` - Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` - Consumers []*Consumer `yaml:"consumers,omitempty" json:"consumers,omitempty"` - SSLs []*SSL `yaml:"ssls,omitempty" json:"ssls,omitempty"` - GlobalRules []*GlobalRule `yaml:"global_rules,omitempty" json:"global_rules,omitempty"` - PluginConfigs []*PluginConfig `yaml:"plugin_configs,omitempty" json:"plugin_configs,omitempty"` + Name string `yaml:"name" json:"name"` + Version string `yaml:"version" json:"version"` + Services []*Service `yaml:"services,omitempty" json:"services,omitempty"` + Routes []*Route `yaml:"routes,omitempty" json:"routes,omitempty"` + Consumers []*Consumer `yaml:"consumers,omitempty" json:"consumers,omitempty"` + SSLs []*SSL `yaml:"ssls,omitempty" json:"ssls,omitempty"` + GlobalRules []*GlobalRule `yaml:"global_rules,omitempty" json:"global_rules,omitempty"` + PluginConfigs []*PluginConfig `yaml:"plugin_configs,omitempty" json:"plugin_configs,omitempty"` + ConsumerGroups []*ConsumerGroup `yaml:"consumer_groups,omitempty" json:"consumer_groups,omitempty"` } // Labels is the APISIX resource labels @@ -303,6 +304,7 @@ type Consumer struct { Labels Labels `json:"labels,omitempty" yaml:"labels,omitempty"` Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` + GroupID string `json:"group_id,omitempty" yaml:"group_id,omitempty"` } // SSL represents the ssl object in APISIX. @@ -327,7 +329,16 @@ type GlobalRule struct { // +k8s:deepcopy-gen=true type PluginConfig struct { ID string `json:"id,omitempty" yaml:"id,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` + Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` + Labels Labels `json:"labels,omitempty" yaml:"labels,omitempty"` + + Plugins Plugins `json:"plugins" yaml:"plugins"` +} + +// ConsumerGroup apisix consumer group object +// +k8s:deepcopy-gen=true +type ConsumerGroup struct { + ID string `json:"id,omitempty" yaml:"id,omitempty"` Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` Labels Labels `json:"labels,omitempty" yaml:"labels,omitempty"` diff --git a/pkg/common/file.go b/pkg/common/file.go index 905a1a5..fb2dd64 100644 --- a/pkg/common/file.go +++ b/pkg/common/file.go @@ -28,12 +28,6 @@ func NormalizeConfiguration(content *types.Configuration) { service.Upstream.ID = service.Upstream.Name } } - - for _, pluginConfig := range content.PluginConfigs { - if pluginConfig.ID == "" { - pluginConfig.ID = pluginConfig.Name - } - } } func GetContentFromFile(filename string) (*types.Configuration, error) { @@ -86,11 +80,29 @@ func GetContentFromRemote(cluster apisix.Cluster) (*types.Configuration, error) return nil, err } + globalRules, err := cluster.GlobalRule().List(context.Background()) + if err != nil { + return nil, err + } + + pluginConfigs, err := cluster.PluginConfig().List(context.Background()) + if err != nil { + return nil, err + } + + consumerGroups, err := cluster.ConsumerGroup().List(context.Background()) + if err != nil { + return nil, err + } + return &types.Configuration{ - Routes: routes, - Services: svcs, - Consumers: consumers, - SSLs: ssls, + Routes: routes, + Services: svcs, + Consumers: consumers, + SSLs: ssls, + GlobalRules: globalRules, + PluginConfigs: pluginConfigs, + ConsumerGroups: consumerGroups, }, nil } diff --git a/pkg/data/events.go b/pkg/data/events.go index 9ee412c..322f301 100644 --- a/pkg/data/events.go +++ b/pkg/data/events.go @@ -30,6 +30,8 @@ var ( GlobalRuleResourceType ResourceType = "global_rule" // PluginConfigResourceType is the resource type of plugin config PluginConfigResourceType ResourceType = "plugin_config" + // ConsumerGroupResourceType is the resource type of consumer group + ConsumerGroupResourceType ResourceType = "consumer_group" ) const ( @@ -119,6 +121,10 @@ func applyPluginConfig(cluster apisix.Cluster, event *Event) error { return apply[types.PluginConfig](cluster.PluginConfig(), event) } +func applyConsumerGroup(cluster apisix.Cluster, event *Event) error { + return apply[types.ConsumerGroup](cluster.ConsumerGroup(), event) +} + func (e *Event) Apply(cluster apisix.Cluster) error { switch e.ResourceType { case ServiceResourceType: @@ -133,6 +139,8 @@ func (e *Event) Apply(cluster apisix.Cluster) error { return applyGlobalRule(cluster, e) case PluginConfigResourceType: return applyPluginConfig(cluster, e) + case ConsumerGroupResourceType: + return applyConsumerGroup(cluster, e) } return nil diff --git a/test/cli/cli_test.go b/test/cli/cli_test.go index 005961c..bba058d 100644 --- a/test/cli/cli_test.go +++ b/test/cli/cli_test.go @@ -8,6 +8,7 @@ import ( _ "github.com/api7/adc/test/cli/suites" _ "github.com/api7/adc/test/cli/suites-consumer" + _ "github.com/api7/adc/test/cli/suites-consumer-group" _ "github.com/api7/adc/test/cli/suites-global-rule" _ "github.com/api7/adc/test/cli/suites-plugin-config" ) diff --git a/test/cli/scaffold/scaffold.go b/test/cli/scaffold/scaffold.go index 112b616..b9afdc7 100644 --- a/test/cli/scaffold/scaffold.go +++ b/test/cli/scaffold/scaffold.go @@ -17,22 +17,24 @@ import ( type Scaffold struct { cluster apisix.Cluster - routes map[string]struct{} - services map[string]struct{} - consumers map[string]struct{} - globalRules map[string]struct{} - pluginConfigs map[string]struct{} + routes map[string]struct{} + services map[string]struct{} + consumers map[string]struct{} + globalRules map[string]struct{} + pluginConfigs map[string]struct{} + consumerGroups map[string]struct{} } func NewScaffold() *Scaffold { s := &Scaffold{ cluster: apisix.NewCluster(context.Background(), "http://127.0.0.1:9180", "edd1c9f034335f136f87ad84b625c8f1"), - routes: map[string]struct{}{}, - services: map[string]struct{}{}, - consumers: map[string]struct{}{}, - globalRules: map[string]struct{}{}, - pluginConfigs: map[string]struct{}{}, + routes: map[string]struct{}{}, + services: map[string]struct{}{}, + consumers: map[string]struct{}{}, + globalRules: map[string]struct{}{}, + pluginConfigs: map[string]struct{}{}, + consumerGroups: map[string]struct{}{}, } ginkgo.BeforeEach(func() { @@ -56,6 +58,9 @@ func NewScaffold() *Scaffold { for pluginConfig, _ := range s.pluginConfigs { s.DeletePluginConfig(pluginConfig) } + for consumerGroup, _ := range s.consumerGroups { + s.DeleteConsumerGroup(consumerGroup) + } }) return s @@ -91,6 +96,12 @@ func (s *Scaffold) AddPluginConfigsFinalizer(pluginConfigs ...string) { } } +func (s *Scaffold) AddConsumerGroupsFinalizer(consumerGroups ...string) { + for _, consumerGroup := range consumerGroups { + s.consumerGroups[consumerGroup] = struct{}{} + } +} + func (s *Scaffold) Configure(host, key string) error { input := host + "\n" + key + "\n" _, err := s.ExecWithInput(input, "configure") @@ -143,6 +154,10 @@ func (s *Scaffold) Sync(path string) (string, error) { s.AddPluginConfigsFinalizer(pluginConfig.ID) } + for _, consumerGroup := range conf.ConsumerGroups { + s.AddConsumerGroupsFinalizer(consumerGroup.ID) + } + return s.Exec("sync", "-f", path) } @@ -287,3 +302,29 @@ func (s *Scaffold) DeletePluginConfig(id string) error { return s.cluster.PluginConfig().Delete(context.Background(), id) } + +func (s *Scaffold) GetConsumerGroup(id string) (*types.ConsumerGroup, error) { + return s.cluster.ConsumerGroup().Get(context.Background(), id) +} + +func (s *Scaffold) ListConsumerGroup() ([]*types.ConsumerGroup, error) { + return s.cluster.ConsumerGroup().List(context.Background()) +} + +func (s *Scaffold) CreateConsumerGroup(consumerGroup *types.ConsumerGroup) (*types.ConsumerGroup, error) { + s.consumerGroups[consumerGroup.ID] = struct{}{} + + return s.cluster.ConsumerGroup().Create(context.Background(), consumerGroup) +} + +func (s *Scaffold) UpdateConsumerGroup(consumerGroup *types.ConsumerGroup) (*types.ConsumerGroup, error) { + s.consumerGroups[consumerGroup.ID] = struct{}{} + + return s.cluster.ConsumerGroup().Update(context.Background(), consumerGroup) +} + +func (s *Scaffold) DeleteConsumerGroup(id string) error { + delete(s.consumerGroups, id) + + return s.cluster.ConsumerGroup().Delete(context.Background(), id) +} diff --git a/test/cli/suites-consumer-group/diff.go b/test/cli/suites-consumer-group/diff.go new file mode 100644 index 0000000..38b091d --- /dev/null +++ b/test/cli/suites-consumer-group/diff.go @@ -0,0 +1,22 @@ +package consumer_group + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/api7/adc/test/cli/scaffold" +) + +var _ = ginkgo.Describe("`adc diff` consumer group tests", func() { + ginkgo.Context("Basic functions", func() { + s := scaffold.NewScaffold() + ginkgo.It("should return the diff result", func() { + out, err := s.Diff("suites-consumer-group/testdata/test.yaml") + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(out).To(gomega.Equal(`creating consumer_group: "company_a" +creating consumer: "jack" +Summary: created 2, updated 0, deleted 0 +`)) + }) + }) +}) diff --git a/test/cli/suites-consumer-group/dump.go b/test/cli/suites-consumer-group/dump.go new file mode 100644 index 0000000..b2790f8 --- /dev/null +++ b/test/cli/suites-consumer-group/dump.go @@ -0,0 +1,46 @@ +package consumer_group + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/api7/adc/test/cli/scaffold" +) + +var _ = ginkgo.Describe("`adc dump` consumer group tests", func() { + ginkgo.Context("Test the dump command", func() { + s := scaffold.NewScaffold() + ginkgo.It("should dump consumer group resources", func() { + _, err := s.Sync("suites-consumer-group/testdata/test.yaml") + gomega.Expect(err).To(gomega.BeNil(), "check sync command") + + out, err := s.Dump() + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(out).To(gomega.Equal(`consumer_groups: +- id: company_a + plugins: + limit-count: + allow_degradation: false + count: 200 + group: $consumer_group_id + key: remote_addr + key_type: var + policy: local + rejected_code: 503 + show_limit_quota_header: true + time_window: 60 +consumers: +- plugins: + key-auth: + key: auth-one + username: jack +name: "" +version: "" +`)) + + err = s.DeleteConsumer("jack") + err = s.DeleteConsumerGroup("company_a") + gomega.Expect(err).To(gomega.BeNil(), "check consumer group delete") + }) + }) +}) diff --git a/test/cli/suites-consumer-group/sdk.go b/test/cli/suites-consumer-group/sdk.go new file mode 100644 index 0000000..66aeaf5 --- /dev/null +++ b/test/cli/suites-consumer-group/sdk.go @@ -0,0 +1,125 @@ +package consumer_group + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/api7/adc/pkg/api/apisix" + "github.com/api7/adc/pkg/api/apisix/types" + "github.com/api7/adc/test/cli/scaffold" +) + +var _ = ginkgo.Describe("adc APISIX consumerGroup SDK tests", func() { + ginkgo.Context("Basic functions", func() { + s := scaffold.NewScaffold() + ginkgo.It("ConsumerGroup resource", func() { + var ( + err error + consumerGroup *types.ConsumerGroup + ) + + // utils + assertConsumerGroupEqual := func(expect, toBe *types.ConsumerGroup, plugins ...string) { + gomega.Expect(expect.ID).To(gomega.Equal(toBe.ID)) + gomega.Expect(expect.Desc).To(gomega.Equal(toBe.Desc)) + for _, plugin := range plugins { + gomega.Expect(expect.Plugins[plugin]).NotTo(gomega.BeNil()) + } + } + + // create consumerGroup 1 + baseConsumerGroup1 := &types.ConsumerGroup{ + ID: "consumerGroup1", + Plugins: map[string]interface{}{ + "limit-count": map[string]interface{}{ + "time_window": 60, + "policy": "local", + "count": 100, + "key": "remote_addr", + "rejected_code": 503, + }, + }, + } + _, err = s.CreateConsumerGroup(baseConsumerGroup1) + + // get consumerGroup 1 + consumerGroup, err = s.GetConsumerGroup("consumerGroup1") + gomega.Expect(err).To(gomega.BeNil()) + assertConsumerGroupEqual(consumerGroup, baseConsumerGroup1, "limit-count") + + // create consumerGroup 2 + baseConsumerGroup2 := &types.ConsumerGroup{ + ID: "consumerGroup2", + Plugins: map[string]interface{}{ + "limit-count": map[string]interface{}{ + "time_window": 60, + "policy": "local", + "count": 200, + "key": "remote_addr", + "rejected_code": 503, + }, + }, + } + consumerGroup, err = s.CreateConsumerGroup(baseConsumerGroup2) + gomega.Expect(err).To(gomega.BeNil()) + assertConsumerGroupEqual(consumerGroup, baseConsumerGroup2, "limit-count") + + // test list + consumerGroups, err := s.ListConsumerGroup() + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(len(consumerGroups)).To(gomega.Equal(2)) + var consumerGroup1, consumerGroup2 *types.ConsumerGroup + for _, r := range consumerGroups { + if r.ID == "consumerGroup1" { + consumerGroup1 = r + } else if r.ID == "consumerGroup2" { + consumerGroup2 = r + } + } + gomega.Expect(consumerGroup1).NotTo(gomega.BeNil()) + gomega.Expect(consumerGroup2).NotTo(gomega.BeNil()) + + assertConsumerGroupEqual(consumerGroup1, baseConsumerGroup1, "limit-count") + assertConsumerGroupEqual(consumerGroup2, baseConsumerGroup2, "limit-count") + + // update & get consumerGroup 1 + baseConsumerGroup1 = &types.ConsumerGroup{ + ID: "consumerGroup1", + Plugins: map[string]interface{}{ + "key-auth": map[string]interface{}{ + "key": "auth-one", + }, + }, + } + _, err = s.UpdateConsumerGroup(baseConsumerGroup1) + gomega.Expect(err).To(gomega.BeNil()) + + consumerGroup, err = s.GetConsumerGroup("consumerGroup1") + gomega.Expect(err).To(gomega.BeNil()) + assertConsumerGroupEqual(consumerGroup, baseConsumerGroup1, "key-auth") + + // delete consumerGroup 2 + err = s.DeleteConsumerGroup("consumerGroup2") + gomega.Expect(err).To(gomega.BeNil()) + + _, err = s.GetConsumerGroup("consumerGroup2") + gomega.Expect(err).To(gomega.Equal(apisix.ErrNotFound)) + + // delete consumerGroup 1 + err = s.DeleteConsumerGroup("consumerGroup1") + gomega.Expect(err).To(gomega.BeNil()) + + _, err = s.GetConsumerGroup("consumerGroup1") + gomega.Expect(err).To(gomega.Equal(apisix.ErrNotFound)) + + // final list + consumerGroups, err = s.ListConsumerGroup() + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(len(consumerGroups)).To(gomega.Equal(0)) + + // delete service + err = s.DeleteService("svc1") + gomega.Expect(err).To(gomega.BeNil()) + }) + }) +}) diff --git a/test/cli/suites-consumer-group/sync.go b/test/cli/suites-consumer-group/sync.go new file mode 100644 index 0000000..e2ec59e --- /dev/null +++ b/test/cli/suites-consumer-group/sync.go @@ -0,0 +1,120 @@ +package consumer_group + +import ( + "net/http" + "time" + + "github.com/gavv/httpexpect/v2" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/api7/adc/pkg/api/apisix/types" + "github.com/api7/adc/test/cli/scaffold" +) + +var _ = ginkgo.Describe("`adc sync` tests", func() { + var ( + id = "consumerGroup1" + user = "jack" + authKey = "auth-one" + upstream = "httpbin.org" + + consumerGroup = &types.ConsumerGroup{ + ID: id, + Plugins: map[string]interface{}{ + "limit-count": map[string]interface{}{ + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr", + }, + }, + } + + consumer = &types.Consumer{ + Username: user, + Plugins: map[string]interface{}{ + "key-auth": map[string]interface{}{ + "key": authKey, + }, + }, + GroupID: id, + } + + service = &types.Service{ + ID: "svc", + Name: "svc", + Hosts: []string{ + "foo.com", + }, + Upstream: types.Upstream{ + ID: "httpbin", + Name: "httpbin", + Nodes: []types.UpstreamNode{ + { + Host: upstream, + Port: 80, + Weight: 1, + }, + }, + }, + } + + route = &types.Route{ + ID: "route", + Name: "route", + Uri: "/get", + Methods: []string{ + "GET", + }, + ServiceID: "svc", + Plugins: map[string]interface{}{ + "key-auth": struct { + }{}, + }, + } + ) + + ginkgo.Context("Basic functions", func() { + s := scaffold.NewScaffold() + ginkgo.It("should sync data to APISIX", func() { + expect := httpexpect.Default(ginkgo.GinkgoT(), "http://127.0.0.1:9080") + + _, err := s.UpdateConsumerGroup(consumerGroup) + gomega.Expect(err).To(gomega.BeNil(), "check consumerGroup update") + _, err = s.UpdateConsumer(consumer) + gomega.Expect(err).To(gomega.BeNil(), "check consumer update") + _, err = s.UpdateService(service) + gomega.Expect(err).To(gomega.BeNil(), "check service update") + _, err = s.UpdateRoute(route) + gomega.Expect(err).To(gomega.BeNil(), "check route update") + + time.Sleep(time.Second * 1) + + resp := expect.GET("/get").WithHeader("apikey", authKey). + WithHost("foo.com").Expect() + resp.Status(http.StatusOK) + resp.Header("X-Ratelimit-Remaining").IsEqual("1") + + resp = expect.GET("/get").WithHeader("apikey", authKey). + WithHost("foo.com").Expect().Status(http.StatusOK) + resp.Status(http.StatusOK) + resp.Header("X-Ratelimit-Remaining").IsEqual("0") + + resp = expect.GET("/get").WithHeader("apikey", authKey). + WithHost("foo.com").Expect() + resp.Status(http.StatusServiceUnavailable) + resp.Header("X-Ratelimit-Remaining").IsEqual("0") + + err = s.DeleteRoute("route") + gomega.Expect(err).To(gomega.BeNil(), "check route delete") + err = s.DeleteService("svc") + gomega.Expect(err).To(gomega.BeNil(), "check service delete") + err = s.DeleteConsumer(user) + gomega.Expect(err).To(gomega.BeNil(), "check consumer delete") + err = s.DeleteConsumerGroup(id) + gomega.Expect(err).To(gomega.BeNil(), "check consumerGroup delete") + expect.GET("/get").WithHost("foo.com").Expect().Status(http.StatusNotFound) + }) + }) +}) diff --git a/test/cli/suites-consumer-group/testdata/test.yaml b/test/cli/suites-consumer-group/testdata/test.yaml new file mode 100644 index 0000000..74adaef --- /dev/null +++ b/test/cli/suites-consumer-group/testdata/test.yaml @@ -0,0 +1,18 @@ +consumer_groups: + - id: company_a + plugins: + limit-count: + allow_degradation: false + count: 200 + group: $consumer_group_id + key: remote_addr + key_type: var + policy: local + rejected_code: 503 + show_limit_quota_header: true + time_window: 60 +consumers: + - plugins: + key-auth: + key: auth-one + username: jack diff --git a/test/cli/suites-consumer-group/validate.go b/test/cli/suites-consumer-group/validate.go new file mode 100644 index 0000000..263c2a2 --- /dev/null +++ b/test/cli/suites-consumer-group/validate.go @@ -0,0 +1,19 @@ +package consumer_group + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/api7/adc/test/cli/scaffold" +) + +var _ = ginkgo.Describe("`adc validate` consumer group tests", func() { + ginkgo.Context("Basic functions", func() { + s := scaffold.NewScaffold() + ginkgo.It("should validate consumer group schema", func() { + validateOutput, err := s.Validate("suites-consumer-group/testdata/test.yaml") + gomega.Expect(err).To(gomega.BeNil()) + gomega.Expect(validateOutput).To(gomega.Equal("Get file content success: config name: , version: , consumers: 1, consumer_groups: 1.\nValidate file content success\n")) + }) + }) +}) diff --git a/test/cli/suites-consumer/sync.go b/test/cli/suites-consumer/sync.go index e3e8ead..b90dbaa 100644 --- a/test/cli/suites-consumer/sync.go +++ b/test/cli/suites-consumer/sync.go @@ -100,7 +100,7 @@ var _ = ginkgo.Describe("`adc sync` tests", func() { err = s.DeleteService("svc") gomega.Expect(err).To(gomega.BeNil(), "check service delete") err = s.DeleteConsumer(user) - gomega.Expect(err).To(gomega.BeNil(), "check service delete") + gomega.Expect(err).To(gomega.BeNil(), "check consumer delete") expect.GET("/get").WithHost("foo.com").Expect().Status(http.StatusNotFound) }) }) diff --git a/test/cli/suites-global-rule/sync.go b/test/cli/suites-global-rule/sync.go index 30918c3..7c1627a 100644 --- a/test/cli/suites-global-rule/sync.go +++ b/test/cli/suites-global-rule/sync.go @@ -93,7 +93,7 @@ var _ = ginkgo.Describe("`adc sync` tests", func() { err = s.DeleteService("svc") gomega.Expect(err).To(gomega.BeNil(), "check service delete") err = s.DeleteGlobalRule(id) - gomega.Expect(err).To(gomega.BeNil(), "check service delete") + gomega.Expect(err).To(gomega.BeNil(), "check globalRule delete") expect.GET("/get").WithHost("foo.com").Expect().Status(http.StatusNotFound) }) }) diff --git a/test/cli/suites-plugin-config/sdk.go b/test/cli/suites-plugin-config/sdk.go index 3bec773..ef83085 100644 --- a/test/cli/suites-plugin-config/sdk.go +++ b/test/cli/suites-plugin-config/sdk.go @@ -21,7 +21,6 @@ var _ = ginkgo.Describe("adc APISIX pluginConfig SDK tests", func() { // utils assertPluginConfigEqual := func(expect, toBe *types.PluginConfig, plugins ...string) { gomega.Expect(expect.ID).To(gomega.Equal(toBe.ID)) - gomega.Expect(expect.Name).To(gomega.Equal(toBe.Name)) gomega.Expect(expect.Desc).To(gomega.Equal(toBe.Desc)) for _, plugin := range plugins { gomega.Expect(expect.Plugins[plugin]).NotTo(gomega.BeNil()) diff --git a/test/cli/suites-plugin-config/sync.go b/test/cli/suites-plugin-config/sync.go index 3278b00..ec7d301 100644 --- a/test/cli/suites-plugin-config/sync.go +++ b/test/cli/suites-plugin-config/sync.go @@ -94,7 +94,7 @@ var _ = ginkgo.Describe("`adc sync` tests", func() { err = s.DeleteService("svc") gomega.Expect(err).To(gomega.BeNil(), "check service delete") err = s.DeletePluginConfig(id) - gomega.Expect(err).To(gomega.BeNil(), "check service delete") + gomega.Expect(err).To(gomega.BeNil(), "check pluginConfig delete") expect.GET("/get").WithHost("foo.com").Expect().Status(http.StatusNotFound) }) })