diff --git a/CHANGELOG.md b/CHANGELOG.md index 7594d42879..f9d0812eb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,9 +81,12 @@ Adding a new version? You'll need three changes: ### Changed -- `KongPlugin` and `KongClusterPlugin` now enforce only one of `config` and `configFrom` - to be set using the CRD validation expressions - [#5119](https://github.com/Kong/kubernetes-ingress-controller/pull/5119) +- CRD Validation Expressions + - `KongPlugin` and `KongClusterPlugin` now enforce only one of `config` and `configFrom` + to be set. + [#5119](https://github.com/Kong/kubernetes-ingress-controller/pull/5119) + - `KongConsumer` now enforces that at least one of `username` or `custom_id` is provided. + [#5137](https://github.com/Kong/kubernetes-ingress-controller/pull/5137) ## [3.0.0] @@ -92,13 +95,13 @@ Adding a new version? You'll need three changes: ### Highlights - 🚀 Support for [Gateway API](https://kubernetes.io/docs/concepts/services-networking/gateways/) is now GA! - - You only need to install Gateway API CRDs to use GA features of Gateway API with KIC. + - You only need to install Gateway API CRDs to use GA features of Gateway API with KIC. - Check the [Ingress to Gateway migration guide] to learn how to start using Gateway API already. - 🏎️ Performance boosting expression router is now the default for DB-less mode. - 📈 Gateway Discovery feature is enabled by default both in DB-less and DB mode, allowing for scaling your gateways independently of the controller. -- 📖 Brand-new docs: [The KIC docs] have been totally revamped to be Gateway API first, and every single guide - is as easy as copying and pasting your way down the page. +- 📖 Brand-new docs: [The KIC docs] have been totally revamped to be Gateway API first, and every single guide + is as easy as copying and pasting your way down the page. [Ingress to Gateway migration guide]: https://docs.konghq.com/kubernetes-ingress-controller/latest/guides/migrate/ingress-to-gateway/ [The KIC docs]: https://docs.konghq.com/kubernetes-ingress-controller/latest/ diff --git a/config/crd/bases/configuration.konghq.com_kongconsumers.yaml b/config/crd/bases/configuration.konghq.com_kongconsumers.yaml index 4e407f191b..02ac427eb2 100644 --- a/config/crd/bases/configuration.konghq.com_kongconsumers.yaml +++ b/config/crd/bases/configuration.konghq.com_kongconsumers.yaml @@ -152,6 +152,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/examples/kong-consumer-key-auth.yaml b/examples/kong-consumer-key-auth.yaml index 7b6b583287..83db70b91e 100644 --- a/examples/kong-consumer-key-auth.yaml +++ b/examples/kong-consumer-key-auth.yaml @@ -1,13 +1,3 @@ -apiVersion: configuration.konghq.com/v1 -kind: KongConsumer -metadata: - name: consumer1 - annotations: - kubernetes.io/ingress.class: kong -username: consumer1 -credentials: -- consumer1-auth ---- apiVersion: v1 kind: Secret metadata: @@ -18,6 +8,16 @@ type: Opaque stringData: key: password --- +apiVersion: configuration.konghq.com/v1 +kind: KongConsumer +metadata: + name: consumer1 + annotations: + kubernetes.io/ingress.class: kong +# username: consumer1 +credentials: +- consumer1-auth +--- apiVersion: apps/v1 kind: Deployment metadata: diff --git a/internal/admission/errors.go b/internal/admission/errors.go index 66730d52ef..27de8384c3 100644 --- a/internal/admission/errors.go +++ b/internal/admission/errors.go @@ -9,7 +9,6 @@ const ( ErrTextConsumerGroupUnsupported = "consumer group support requires Kong Enterprise" ErrTextConsumerGroupUnlicensed = "consumer group support requires a valid Kong Enterprise license" ErrTextConsumerGroupUnexpected = "unexpected error during checking support for consumer group" - ErrTextConsumerUsernameEmpty = "username cannot be empty" ErrTextFailedToRetrieveSecret = "could not retrieve secrets from the kubernetes API" //nolint:revive,gosec ErrTextPluginConfigInvalid = "could not parse plugin configuration" ErrTextPluginConfigValidationFailed = "unable to validate plugin schema" diff --git a/internal/admission/validator.go b/internal/admission/validator.go index 4350362c2d..326678b2c4 100644 --- a/internal/admission/validator.go +++ b/internal/admission/validator.go @@ -105,11 +105,6 @@ func (validator KongHTTPValidator) ValidateConsumer( return true, "", nil } - // a consumer without a username is not valid - if consumer.Username == "" { - return false, ErrTextConsumerUsernameEmpty, nil - } - errText, err := validator.ensureConsumerDoesNotExistInGateway(ctx, consumer.Username) if err != nil || errText != "" { return false, errText, err diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go index 2e96b96db7..e582c95d42 100644 --- a/internal/dataplane/kongstate/kongstate.go +++ b/internal/dataplane/kongstate/kongstate.go @@ -72,6 +72,8 @@ func (ks *KongState) FillConsumersAndCredentials( // build consumer index for _, consumer := range s.ListKongConsumers() { var c Consumer + // This is now enforce that the CRD level but we're keeping this just for those + // rare cases where the CRD Validation Expressions are disabled. if consumer.Username == "" && consumer.CustomID == "" { failuresCollector.PushResourceFailure("no username or custom_id specified", consumer) continue diff --git a/pkg/apis/configuration/v1/kongconsumer_types.go b/pkg/apis/configuration/v1/kongconsumer_types.go index eb36e3f58a..007b317ca8 100644 --- a/pkg/apis/configuration/v1/kongconsumer_types.go +++ b/pkg/apis/configuration/v1/kongconsumer_types.go @@ -30,6 +30,7 @@ import ( // +kubebuilder:printcolumn:name="Username",type=string,JSONPath=`.username`,description="Username of a Kong Consumer" // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="Age" // +kubebuilder:printcolumn:name="Programmed",type=string,JSONPath=`.status.conditions[?(@.type=="Programmed")].status` +// +kubebuilder:validation:XValidation:rule="has(self.username) || has(self.custom_id)", message="Need to provide either username or custom_id" // KongConsumer is the Schema for the kongconsumers API. type KongConsumer struct { diff --git a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml index 2ae4f1256f..2e3129c044 100644 --- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml index 5f53e024f8..6b7d911b70 100644 --- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml index c2dee12e0f..58092edcf7 100644 --- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml +++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml index 3739e40e40..04478faafa 100644 --- a/test/e2e/manifests/all-in-one-dbless.yaml +++ b/test/e2e/manifests/all-in-one-dbless.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml index 531a57e62d..afe7e9e9cc 100644 --- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml index dcd68cb0e1..a0a202e760 100644 --- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml +++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml index 15f4c3ffbb..b510dcce5e 100644 --- a/test/e2e/manifests/all-in-one-postgres.yaml +++ b/test/e2e/manifests/all-in-one-postgres.yaml @@ -601,6 +601,9 @@ spec: description: Username is a Kong cluster-unique username of the consumer. type: string type: object + x-kubernetes-validations: + - message: Need to provide either username or custom_id + rule: has(self.username) || has(self.custom_id) served: true storage: true subresources: diff --git a/test/envtest/kongstate_consumer_failures_test.go b/test/envtest/kongstate_consumer_failures_test.go index 38608d2c74..1bbf132cb0 100644 --- a/test/envtest/kongstate_consumer_failures_test.go +++ b/test/envtest/kongstate_consumer_failures_test.go @@ -58,6 +58,9 @@ func TestKongStateFillConsumersAndCredentialsFailure(t *testing.T) { }, }, } + for _, secret := range secrets { + require.NoError(t, client.Create(ctx, secret)) + } kongConsumers := []*kongv1.KongConsumer{ { @@ -82,9 +85,16 @@ func TestKongStateFillConsumersAndCredentialsFailure(t *testing.T) { "empty-cred", }, }, + } + for _, kongConsumer := range kongConsumers { + require.NoError(t, client.Create(ctx, kongConsumer)) + } + + // These KongConsumers should fail admission via the CRD Validation Expressions. + brokenKongConsumers := []*kongv1.KongConsumer{ { ObjectMeta: metav1.ObjectMeta{ - Name: "consumer-no-username", + Name: "consumer-no-username-and-no-custom-id", Namespace: ns.Name, Annotations: map[string]string{annotations.IngressClassKey: annotations.DefaultIngressClass}, }, @@ -93,18 +103,13 @@ func TestKongStateFillConsumersAndCredentialsFailure(t *testing.T) { }, }, } - - for _, secret := range secrets { - require.NoError(t, client.Create(ctx, secret)) - } - for _, kongConsumer := range kongConsumers { - require.NoError(t, client.Create(ctx, kongConsumer)) + for _, brokenKongConsumer := range brokenKongConsumers { + require.Error(t, client.Create(ctx, brokenKongConsumer)) } // KongConsumer name -> event message kongConsumerTranslationFailureMessages := map[string]string{ - "consumer-empty-cred": `credential "empty-cred" failure: failed to provision credential: key-auth is invalid: no key`, - "consumer-no-username": `no username or custom_id specified`, + "consumer-empty-cred": `credential "empty-cred" failure: failed to provision credential: key-auth is invalid: no key`, } RunManager(ctx, t, cfg, AdminAPIOptFns(), WithProxySyncSeconds(0.5)) diff --git a/test/envtest/programmed_condition_envtest_test.go b/test/envtest/programmed_condition_envtest_test.go index 368ac87454..72bc327688 100644 --- a/test/envtest/programmed_condition_envtest_test.go +++ b/test/envtest/programmed_condition_envtest_test.go @@ -14,6 +14,7 @@ import ( kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" "github.com/kong/kubernetes-ingress-controller/v3/test" + "github.com/kong/kubernetes-ingress-controller/v3/test/helpers" "github.com/kong/kubernetes-ingress-controller/v3/test/helpers/conditions" ) @@ -28,10 +29,12 @@ func TestKongCRDs_ProgrammedCondition(t *testing.T) { ctrlClient := NewControllerClient(t, scheme, envcfg) ns := CreateNamespace(ctx, t, ctrlClient) + healthProbePort := helpers.GetFreePort(t) RunManager(ctx, t, envcfg, AdminAPIOptFns(), WithUpdateStatus(), + WithHealthProbePort(healthProbePort), WithPublishService(ns.Name), WithPublishStatusAddress("http://localhost:8080"), )