diff --git a/CHANGELOG.md b/CHANGELOG.md index c736fc5b59..4f464fdc7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,7 @@ Adding a new version? You'll need three changes: ### Added - **WIP** Introduce `KongConsumerGroup` CRD (supported by Kong Enterprise only) - [#4325](https://github.com/Kong/kubernetes-ingress-controller/pull/4325), [#4387](https://github.com/Kong/kubernetes-ingress-controller/pull/4387) + [#4325](https://github.com/Kong/kubernetes-ingress-controller/pull/4325), [#4387](https://github.com/Kong/kubernetes-ingress-controller/pull/4387), [#4419](https://github.com/Kong/kubernetes-ingress-controller/pull/4419) - The ResponseHeaderModifier Gateway API filter is now supported and translated to the proper set of Kong plugins. [#4350](https://github.com/Kong/kubernetes-ingress-controller/pull/4350) diff --git a/config/crd/bases/configuration.konghq.com_kongconsumers.yaml b/config/crd/bases/configuration.konghq.com_kongconsumers.yaml index 9cfd911b6f..dc833a6428 100644 --- a/config/crd/bases/configuration.konghq.com_kongconsumers.yaml +++ b/config/crd/bases/configuration.konghq.com_kongconsumers.yaml @@ -40,6 +40,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless-enterprise.yaml b/deploy/single/all-in-one-dbless-enterprise.yaml index 9f67d4368e..6b2b139583 100644 --- a/deploy/single/all-in-one-dbless-enterprise.yaml +++ b/deploy/single/all-in-one-dbless-enterprise.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless-k4k8s-enterprise.yaml b/deploy/single/all-in-one-dbless-k4k8s-enterprise.yaml index 8108325d7b..43c5986974 100644 --- a/deploy/single/all-in-one-dbless-k4k8s-enterprise.yaml +++ b/deploy/single/all-in-one-dbless-k4k8s-enterprise.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless-konnect-enterprise.yaml b/deploy/single/all-in-one-dbless-konnect-enterprise.yaml index 33b121193d..b2e120b0c8 100644 --- a/deploy/single/all-in-one-dbless-konnect-enterprise.yaml +++ b/deploy/single/all-in-one-dbless-konnect-enterprise.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless-konnect.yaml b/deploy/single/all-in-one-dbless-konnect.yaml index 3f9dd87ed7..daa06a2e13 100644 --- a/deploy/single/all-in-one-dbless-konnect.yaml +++ b/deploy/single/all-in-one-dbless-konnect.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless-legacy.yaml b/deploy/single/all-in-one-dbless-legacy.yaml index cba55ecdd8..244cea2e6b 100644 --- a/deploy/single/all-in-one-dbless-legacy.yaml +++ b/deploy/single/all-in-one-dbless-legacy.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-dbless.yaml b/deploy/single/all-in-one-dbless.yaml index 2198e9df7e..d646e02517 100644 --- a/deploy/single/all-in-one-dbless.yaml +++ b/deploy/single/all-in-one-dbless.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-postgres-enterprise.yaml b/deploy/single/all-in-one-postgres-enterprise.yaml index 4167f6ed0f..3b9c71d6ff 100644 --- a/deploy/single/all-in-one-postgres-enterprise.yaml +++ b/deploy/single/all-in-one-postgres-enterprise.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/deploy/single/all-in-one-postgres.yaml b/deploy/single/all-in-one-postgres.yaml index 5c0e409060..2e62bad5e2 100644 --- a/deploy/single/all-in-one-postgres.yaml +++ b/deploy/single/all-in-one-postgres.yaml @@ -484,6 +484,12 @@ spec: of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string + consumerGroups: + description: ConsumerGroups are references to consumer groups (that consumer + wants to be part of) provisioned in Kong. + items: + type: string + type: array credentials: description: Credentials are references to secrets containing a credential to be provisioned in Kong. diff --git a/docs/api-reference.md b/docs/api-reference.md index 9e97e47afb..6f97851997 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -57,6 +57,7 @@ KongConsumer is the Schema for the kongconsumers API. | `username` _string_ | Username is a Kong cluster-unique username of the consumer. | | `custom_id` _string_ | CustomID is a Kong cluster-unique existing ID for the consumer - useful for mapping Kong with users in your existing database. | | `credentials` _string array_ | Credentials are references to secrets containing a credential to be provisioned in Kong. | +| `consumerGroups` _string array_ | ConsumerGroups are references to consumer groups (that consumer wants to be part of) provisioned in Kong. | diff --git a/internal/dataplane/deckgen/generate.go b/internal/dataplane/deckgen/generate.go index e469ccc6f4..e829490b83 100644 --- a/internal/dataplane/deckgen/generate.go +++ b/internal/dataplane/deckgen/generate.go @@ -164,6 +164,11 @@ func ToDeckContent( continue } + for _, cg := range c.ConsumerGroups { + cg := cg + consumer.Groups = append(consumer.Groups, &cg) + } + for _, p := range c.Plugins { consumer.Plugins = append(consumer.Plugins, &file.FPlugin{Plugin: p}) } diff --git a/internal/dataplane/kongstate/consumer.go b/internal/dataplane/kongstate/consumer.go index 6e58a2bec6..0baa382e97 100644 --- a/internal/dataplane/kongstate/consumer.go +++ b/internal/dataplane/kongstate/consumer.go @@ -13,7 +13,9 @@ import ( // Consumer holds a Kong consumer and its plugins and credentials. type Consumer struct { kong.Consumer - Plugins []kong.Plugin + Plugins []kong.Plugin + ConsumerGroups []kong.ConsumerGroup + KeyAuths []*KeyAuth HMACAuths []*HMACAuth JWTAuths []*JWTAuth diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go index ee56d6a775..768d34a257 100644 --- a/internal/dataplane/kongstate/kongstate.go +++ b/internal/dataplane/kongstate/kongstate.go @@ -73,6 +73,18 @@ func (ks *KongState) FillConsumersAndCredentials( c.K8sKongConsumer = *consumer c.Tags = util.GenerateTagsForObject(consumer) + // Get consumer groups + for _, cgName := range consumer.ConsumerGroups { + cg, err := s.GetKongConsumerGroup(consumer.Namespace, cgName) + if err != nil { + failuresCollector.PushResourceFailure(fmt.Sprintf("nonexistent consumer group: %q", err), consumer) + continue + } + c.ConsumerGroups = append(c.ConsumerGroups, kong.ConsumerGroup{ + Name: &cg.Name, + }) + } + for _, cred := range consumer.Credentials { pushCredentialResourceFailures := func(message string) { failuresCollector.PushResourceFailure(fmt.Sprintf("credential %q failure: %s", cred, message), consumer) diff --git a/internal/dataplane/parser/testdata/golden/consumer-group-example/default_golden.yaml b/internal/dataplane/parser/testdata/golden/consumer-group-example/default_golden.yaml new file mode 100644 index 0000000000..0983c85865 --- /dev/null +++ b/internal/dataplane/parser/testdata/golden/consumer-group-example/default_golden.yaml @@ -0,0 +1,32 @@ +_format_version: "3.0" +consumer_groups: +- name: consumer-group-2 + tags: + - k8s-name:consumer-group-2 + - k8s-kind:KongConsumerGroup + - k8s-group:configuration.konghq.com + - k8s-version:v1beta1 +- name: consumer-group-1 + tags: + - k8s-name:consumer-group-1 + - k8s-kind:KongConsumerGroup + - k8s-group:configuration.konghq.com + - k8s-version:v1beta1 +consumers: +- groups: + - name: consumer-group-1 + - name: consumer-group-2 + tags: + - k8s-name:consumer-2 + - k8s-kind:KongConsumer + - k8s-group:configuration.konghq.com + - k8s-version:v1 + username: consumer-2 +- groups: + - name: consumer-group-1 + tags: + - k8s-name:consumer-1 + - k8s-kind:KongConsumer + - k8s-group:configuration.konghq.com + - k8s-version:v1 + username: consumer-1 diff --git a/internal/dataplane/parser/testdata/golden/consumer-group-example/in.yaml b/internal/dataplane/parser/testdata/golden/consumer-group-example/in.yaml new file mode 100644 index 0000000000..0fafaa3133 --- /dev/null +++ b/internal/dataplane/parser/testdata/golden/consumer-group-example/in.yaml @@ -0,0 +1,34 @@ +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: consumer-group-2 + annotations: + kubernetes.io/ingress.class: kong +--- +apiVersion: configuration.konghq.com/v1beta1 +kind: KongConsumerGroup +metadata: + name: consumer-group-1 + annotations: + kubernetes.io/ingress.class: kong +--- +apiVersion: configuration.konghq.com/v1 +kind: KongConsumer +metadata: + name: consumer-1 + annotations: + kubernetes.io/ingress.class: kong +username: consumer-1 +consumerGroups: + - consumer-group-1 +--- +apiVersion: configuration.konghq.com/v1 +kind: KongConsumer +metadata: + name: consumer-2 + annotations: + kubernetes.io/ingress.class: kong +username: consumer-2 +consumerGroups: + - consumer-group-1 + - consumer-group-2 diff --git a/pkg/apis/configuration/v1/kongconsumer_types.go b/pkg/apis/configuration/v1/kongconsumer_types.go index 411804f034..eb36e3f58a 100644 --- a/pkg/apis/configuration/v1/kongconsumer_types.go +++ b/pkg/apis/configuration/v1/kongconsumer_types.go @@ -46,6 +46,9 @@ type KongConsumer struct { // Credentials are references to secrets containing a credential to be // provisioned in Kong. Credentials []string `json:"credentials,omitempty"` + // ConsumerGroups are references to consumer groups (that consumer wants to be part of) + // provisioned in Kong. + ConsumerGroups []string `json:"consumerGroups,omitempty"` // Status represents the current status of the KongConsumer resource. Status KongConsumerStatus `json:"status,omitempty"` diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index fb0bb1d996..2b9a6224a5 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -149,6 +149,11 @@ func (in *KongConsumer) DeepCopyInto(out *KongConsumer) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ConsumerGroups != nil { + in, out := &in.ConsumerGroups, &out.ConsumerGroups + *out = make([]string, len(*in)) + copy(*out, *in) + } in.Status.DeepCopyInto(&out.Status) } diff --git a/test/integration/consumer_group_test.go b/test/integration/consumer_group_test.go index 2c3076dc37..00c28a6672 100644 --- a/test/integration/consumer_group_test.go +++ b/test/integration/consumer_group_test.go @@ -14,6 +14,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" "github.com/kong/kubernetes-ingress-controller/v2/internal/versions" + kongv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1beta1" "github.com/kong/kubernetes-ingress-controller/v2/pkg/clientset" "github.com/kong/kubernetes-ingress-controller/v2/test/consts" @@ -27,63 +28,96 @@ func TestConsumerGroup(t *testing.T) { RunWhenKongVersion(t, fmt.Sprintf(">=%s", versions.ConsumerGroupsVersionCutoff)) RunWhenKongEnterprise(t) + // Get rid of skip when Gateway 3.4 will be released. + // Issue https://konghq.atlassian.net/browse/FTI-5264 will be resolved. + if testenv.DBMode() == testenv.DBModeOff { + t.Skip("due to a bug in Kong Gateway for DB-less mode and consumer groups, this test has to be skipped") + } + ctx := context.Background() ns, cleaner := helpers.Setup(ctx, t, env) c, err := clientset.NewForConfig(env.Cluster().Config()) require.NoError(t, err) - const consumerGroupName = "test-consumer-group" - t.Logf("configuring consumer group: %q", consumerGroupName) - cg, err := c.ConfigurationV1beta1().KongConsumerGroups(ns.Name).Create( + consumerGroupNames := []string{ + "test-consumer-group-1", + "test-consumer-group-2", + } + for _, cgName := range consumerGroupNames { + t.Logf("configuring consumer group: %q", cgName) + cg, err := c.ConfigurationV1beta1().KongConsumerGroups(ns.Name).Create( + ctx, + &kongv1beta1.KongConsumerGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: cgName, + Annotations: map[string]string{ + annotations.IngressClassKey: consts.IngressClass, + }, + }, + }, + metav1.CreateOptions{}, + ) + require.NoError(t, err) + cleaner.Add(cg) + } + + const consumerName = "test-consumer" + t.Logf("configuring consumer: %q", consumerName) + consumer, err := c.ConfigurationV1().KongConsumers(ns.Name).Create( ctx, - &kongv1beta1.KongConsumerGroup{ + &kongv1.KongConsumer{ ObjectMeta: metav1.ObjectMeta{ - Name: consumerGroupName, - Namespace: ns.Name, + Name: consumerName, Annotations: map[string]string{ annotations.IngressClassKey: consts.IngressClass, }, }, + Username: consumerName, + ConsumerGroups: consumerGroupNames, }, metav1.CreateOptions{}, ) require.NoError(t, err) - cleaner.Add(cg) + cleaner.Add(consumer) - t.Logf("validating that consumer group: %q was successfully configured", consumerGroupName) - require.Eventually(t, func() bool { - cgPath := fmt.Sprintf("/consumer_groups/%s", consumerGroupName) - var headers map[string]string - if testenv.DBMode() != testenv.DBModeOff { - cgPath = fmt.Sprintf("/%s/consumer_groups/%s", consts.KongTestWorkspace, consumerGroupName) - headers = map[string]string{ - "Kong-Admin-Token": consts.KongTestPassword, + for _, cgName := range consumerGroupNames { + t.Logf("validating that consumer %q was successfully added to previously configured consumer group: %q", consumerName, cgName) + require.Eventually(t, func() bool { + cgPath := fmt.Sprintf("/consumer_groups/%s/consumers/%s", cgName, consumerName) + var headers map[string]string + if testenv.DBMode() != testenv.DBModeOff { + cgPath = fmt.Sprintf( + "/%s/consumer_groups/%s/consumers/%s", consts.KongTestWorkspace, cgName, consumerName, + ) + headers = map[string]string{ + "Kong-Admin-Token": consts.KongTestPassword, + } + } + req := helpers.MustHTTPRequest(t, http.MethodGet, proxyAdminURL, cgPath, headers) + resp, err := helpers.DefaultHTTPClient().Do(req) + if err != nil { + t.Logf("WARNING: error while waiting for %s: %v", req.URL, err) + return false + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Logf("WARNING: error while reading response from %s: %v", req.URL, err) + } + switch resp.StatusCode { + case http.StatusOK: + return true + case http.StatusForbidden: + t.Logf( + "WARNING: it seems Kong Gateway Enterprise hasn't got a valid license passed - from: %s received: %s with body: %s", + req.URL, resp.Status, body, + ) + return false + default: + t.Logf("WARNING: from: %s received unexpected: %s with body: %s", req.URL, resp.Status, body) + return false } - } - req := helpers.MustHTTPRequest(t, http.MethodGet, proxyAdminURL, cgPath, headers) - resp, err := helpers.DefaultHTTPClient().Do(req) - if err != nil { - t.Logf("WARNING: error while waiting for %s: %v", resp.Request.URL, err) - return false - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - t.Logf("WARNING: error while reading response from %s: %v", resp.Request.URL, err) - } - switch resp.StatusCode { - case http.StatusOK: - return true - case http.StatusForbidden: - t.Logf( - "WARNING: it seems Kong Gateway Enterprise hasn't got a valid license passed - from: %s received: %s with body: %s", - resp.Request.URL, resp.Status, body, - ) - return false - default: - t.Logf("WARNING: from: %s received unexpected: %s with body: %s", resp.Request.URL, resp.Status, body) - return false - } - }, ingressWait, waitTick) + }, ingressWait, waitTick) + } }