Skip to content

Commit

Permalink
fix: correct tags filtering on consumer-group consumers (#88)
Browse files Browse the repository at this point in the history
Due to the nature of the Consumer Group API, it's not possible
to natively filter its Consumers using tags:

```
$ http :8001/consumer_groups/foo_group_1/consumers\?tags="managed-by:deck"
HTTP/1.1 400 Bad Request

{
    "code": 11,
    "message": "invalid option (tags: cannot be used with 'consumer_group_consumers')",
    "name": "invalid options",
    "options": {
        "tags": "cannot be used with 'consumer_group_consumers'"
    }
}
```

So, in order to fetch ConsumerGroup:Consumer mappings, we need to rely
on the unfiltered Consumer Group API:

```
$ http :8001/consumer_groups/foo_group_1
HTTP/1.1 200 OK

{
    "consumer_group": {
        "created_at": 1715183554,
        "id": "e0e495f2-9acd-41f7-b4da-33659033af13",
        "name": "foo_group_1",
        "tags": [
            "managed-by-deck"
        ],
        "updated_at": 1715183554
    },
    "consumers": [
        {
            "created_at": 1715183563,
            "id": "a704b13e-1a9b-409b-9611-fd2bfcea63bb",
            "tags": [
                "managed-by:api"
            ],
            "type": 0,
            "updated_at": 1715183563,
            "username": "foo",
            "username_lower": "foo"
        }
    ]
}
```

This is a problem in cases when Consumer Groups are managed by decK
while their mappings with Consumers are managed directly via the API.
In such case, since the current library implementation doesn't allow
to filter mappings by tags, decK would wipe out the API managed
mappings when running against the specific tags it handle:

```
$ cat kong.yaml
_format_version: "3.0"
_info:
  select_tags:
  - managed-by:deck
consumer_groups:
- id: c0f6c818-470c-4df7-8515-c8e904765fcc
  name: foo_group_1
  tags:
  - managed-by:deck
```

```
$ deck gateway sync kong.yaml
deleting consumer-group-consumer foo
Summary:
  Created: 0
  Updated: 0
  Deleted: 1
```

This commit makes sure the correct filtering is "manually" done,
avoiding existing Consumers to be wiped out during syncs when using
Tags:

```
$ deck gateway sync kong.yaml
Summary:
  Created: 0
  Updated: 0
  Deleted: 0
```

```
$ http :8001/consumer_groups/foo_group_1
HTTP/1.1 200 OK

{
    "consumer_group": {
        "created_at": 1716450737,
        "id": "c0f6c818-470c-4df7-8515-c8e904765fcc",
        "name": "foo_group_1",
        "tags": [
            "managed-by:deck"
        ],
        "updated_at": 1716451431
    },
    "consumers": [
        {
            "created_at": 1716450861,
            "id": "bafd4f5d-3f4e-4c0f-8d6c-6901b0b8f483",
            "tags": [
                "managed-by:api"
            ],
            "type": 0,
            "updated_at": 1716450861,
            "username": "foo"
        }
    ]
}
```

---------

Co-authored-by: Patryk Małek <[email protected]>
  • Loading branch information
GGabriele and pmalek authored May 23, 2024
1 parent 92e000d commit ff4cfa3
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 1 deletion.
10 changes: 9 additions & 1 deletion pkg/dump/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,9 +647,17 @@ func GetAllConsumerGroups(ctx context.Context,
}
group := &kong.ConsumerGroupObject{
ConsumerGroup: r.ConsumerGroup,
Consumers: r.Consumers,
Plugins: r.Plugins,
}
consumers := []*kong.Consumer{}
for _, c := range r.Consumers {
// if tags are set and if the consumer is not tagged, skip it
if len(tags) > 0 && !utils.HasTags(c, tags) {
continue
}
consumers = append(consumers, c)
}
group.Consumers = consumers
consumerGroupObjects = append(consumerGroupObjects, group)
}
if nextopt == nil {
Expand Down
30 changes: 30 additions & 0 deletions pkg/utils/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package utils
import (
"fmt"
"reflect"

"github.com/kong/go-kong/kong"
)

// MustMergeTags is same as MergeTags but panics if there is an error.
Expand Down Expand Up @@ -82,3 +84,31 @@ func RemoveTags(obj interface{}, tags []string) error {
structTags.Set(res)
return nil
}

// HasTags checks if the given object has any of the specified tags.
// The function returns true if at least one of the provided tags is present in the object's tags.
func HasTags[T *kong.Consumer](obj T, tags []string) bool {
if len(tags) == 0 {
return true
}

m := make(map[string]struct{})
for _, tag := range tags {
m[tag] = struct{}{}
}

switch obj := any(obj).(type) {
case *kong.Consumer:
for _, tag := range obj.Tags {
if tag == nil {
continue
}
if _, ok := m[*tag]; ok {
return true
}
}
default:
return false
}
return false
}
31 changes: 31 additions & 0 deletions pkg/utils/tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"testing"

"github.com/kong/go-kong/kong"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -90,3 +91,33 @@ func TestRemoveTags(t *testing.T) {
RemoveTags(&f, nil)
assert.True(equalArray([]*string{&a, &b}, f.Tags))
}

func TestHasTags(t *testing.T) {
assert := assert.New(t)

assert.False(HasTags(&kong.Consumer{}, []string{"tag1"}))

consumer := &kong.Consumer{
Tags: []*string{
kong.String("tag1"),
kong.String("tag2"),
},
}
assert.True(HasTags(consumer, []string{"tag1"}))
assert.True(HasTags(consumer, []string{"tag1", "tag2"}))
assert.True(HasTags(consumer, []string{"tag1", "tag2", "tag3"}))
assert.False(HasTags(consumer, []string{"tag3"}))
}

func BenchmarkHasTags(b *testing.B) {
consumer := &kong.Consumer{
Tags: []*string{
kong.String("tag1"),
kong.String("tag2"),
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
HasTags(consumer, []string{"tag1", "tag2", "tag3"})
}
}

0 comments on commit ff4cfa3

Please sign in to comment.