This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 336f4270 feat: support plugins field in ApisixConsumer (#2761)
336f4270 is described below
commit 336f427059cae1f9eaf62895da204a89a22562f3
Author: AlinsRan <[email protected]>
AuthorDate: Wed May 13 14:16:23 2026 +0800
feat: support plugins field in ApisixConsumer (#2761)
---
api/v2/apisixconsumer_types.go | 8 +-
api/v2/apisixconsumer_validation_test.go | 24 +--
api/v2/zz_generated.deepcopy.go | 13 +-
.../bases/apisix.apache.org_apisixconsumers.yaml | 28 +++-
docs/en/latest/reference/api-reference.md | 2 +
internal/adc/translator/apisixconsumer.go | 100 +++++++----
internal/controller/apisixconsumer_controller.go | 77 ++++++---
internal/controller/indexer/indexer.go | 35 ++--
internal/webhook/v1/apisixconsumer_webhook.go | 37 ++--
internal/webhook/v1/apisixconsumer_webhook_test.go | 10 +-
test/e2e/crds/v2/consumer.go | 186 +++++++++++++++++++++
11 files changed, 404 insertions(+), 116 deletions(-)
diff --git a/api/v2/apisixconsumer_types.go b/api/v2/apisixconsumer_types.go
index 7a05f9ca..03d1cfce 100644
--- a/api/v2/apisixconsumer_types.go
+++ b/api/v2/apisixconsumer_types.go
@@ -29,7 +29,13 @@ type ApisixConsumerSpec struct {
IngressClassName string `json:"ingressClassName,omitempty"
yaml:"ingressClassName,omitempty"`
// AuthParameter defines the authentication credentials and
configuration for this consumer.
- AuthParameter ApisixConsumerAuthParameter `json:"authParameter"
yaml:"authParameter"`
+ // +kubebuilder:validation:Optional
+ AuthParameter *ApisixConsumerAuthParameter
`json:"authParameter,omitempty" yaml:"authParameter,omitempty"`
+
+ // Plugins lists additional consumer-scoped plugins to attach to this
consumer.
+ // These plugins are applied alongside any authentication plugin
derived from AuthParameter.
+ // An enabled plugin with the same name as the auth plugin derived from
AuthParameter takes precedence.
+ Plugins []ApisixRoutePlugin `json:"plugins,omitempty"
yaml:"plugins,omitempty"`
}
// ApisixConsumerStatus defines the observed state of ApisixConsumer.
diff --git a/api/v2/apisixconsumer_validation_test.go
b/api/v2/apisixconsumer_validation_test.go
index 88fdd1d6..5c421315 100644
--- a/api/v2/apisixconsumer_validation_test.go
+++ b/api/v2/apisixconsumer_validation_test.go
@@ -109,7 +109,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS256(t
*testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -130,7 +130,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t *testing
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -150,7 +150,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS512(t
*testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -168,7 +168,7 @@ func
TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -185,7 +185,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -203,7 +203,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -221,7 +221,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -240,7 +240,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -259,7 +259,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -278,7 +278,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -297,7 +297,7 @@ func
TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
@@ -321,7 +321,7 @@ func
TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T) {
v := loadApisixConsumerSchema(t)
ac := &apisixv2.ApisixConsumer{
Spec: apisixv2.ApisixConsumerSpec{
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
Value:
&apisixv2.ApisixConsumerJwtAuthValue{
Key: "my-key",
diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go
index 73be4b23..8e659cd9 100644
--- a/api/v2/zz_generated.deepcopy.go
+++ b/api/v2/zz_generated.deepcopy.go
@@ -406,7 +406,18 @@ func (in *ApisixConsumerList) DeepCopyObject()
runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver,
writing into out. in must be non-nil.
func (in *ApisixConsumerSpec) DeepCopyInto(out *ApisixConsumerSpec) {
*out = *in
- in.AuthParameter.DeepCopyInto(&out.AuthParameter)
+ if in.AuthParameter != nil {
+ in, out := &in.AuthParameter, &out.AuthParameter
+ *out = new(ApisixConsumerAuthParameter)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Plugins != nil {
+ in, out := &in.Plugins, &out.Plugins
+ *out = make([]ApisixRoutePlugin, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver,
creating a new ApisixConsumerSpec.
diff --git a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
index db0ec861..4b004137 100644
--- a/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixconsumers.yaml
@@ -319,8 +319,32 @@ spec:
IngressClassName is the name of an IngressClass cluster
resource.
The controller uses this field to decide whether the
resource should be managed.
type: string
- required:
- - authParameter
+ plugins:
+ description: |-
+ Plugins lists additional consumer-scoped plugins to attach
to this consumer.
+ These plugins are applied alongside any authentication
plugin derived from AuthParameter.
+ An enabled plugin with the same name as the auth plugin
derived from AuthParameter takes precedence.
+ items:
+ description: ApisixRoutePlugin represents an APISIX plugin.
+ properties:
+ config:
+ description: Plugin configuration.
+ x-kubernetes-preserve-unknown-fields: true
+ enable:
+ default: true
+ description: Whether this plugin is in use, default is
true.
+ type: boolean
+ name:
+ description: The plugin name.
+ type: string
+ secretRef:
+ description: Plugin configuration secretRef.
+ type: string
+ required:
+ - enable
+ - name
+ type: object
+ type: array
type: object
status:
description: ApisixStatus is the status report for Apisix ingress
Resources
diff --git a/docs/en/latest/reference/api-reference.md
b/docs/en/latest/reference/api-reference.md
index 4ee8a633..ce700a73 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -875,6 +875,7 @@ ApisixConsumerSpec defines the desired state of
ApisixConsumer.
| --- | --- |
| `ingressClassName` _string_ | IngressClassName is the name of an
IngressClass cluster resource. The controller uses this field to decide whether
the resource should be managed. |
| `authParameter`
_[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter
defines the authentication credentials and configuration for this consumer. |
+| `plugins` _[ApisixRoutePlugin](#apisixrouteplugin) array_ | Plugins lists
additional consumer-scoped plugins to attach to this consumer. These plugins
are applied alongside any authentication plugin derived from AuthParameter. An
enabled plugin with the same name as the auth plugin derived from AuthParameter
takes precedence. |
_Appears in:_
@@ -1163,6 +1164,7 @@ ApisixRoutePlugin represents an APISIX plugin.
_Appears in:_
+- [ApisixConsumerSpec](#apisixconsumerspec)
- [ApisixGlobalRuleSpec](#apisixglobalrulespec)
- [ApisixPluginConfigSpec](#apisixpluginconfigspec)
- [ApisixRouteHTTP](#apisixroutehttp)
diff --git a/internal/adc/translator/apisixconsumer.go
b/internal/adc/translator/apisixconsumer.go
index 823d65bb..3cae6e08 100644
--- a/internal/adc/translator/apisixconsumer.go
+++ b/internal/adc/translator/apisixconsumer.go
@@ -55,42 +55,54 @@ const (
func (t *Translator) TranslateApisixConsumer(tctx *provider.TranslateContext,
ac *v2.ApisixConsumer) (*TranslateResult, error) {
result := &TranslateResult{}
plugins := make(adctypes.Plugins)
- if ac.Spec.AuthParameter.KeyAuth != nil {
- cfg, err := t.translateConsumerKeyAuthPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.KeyAuth)
- if err != nil {
- return nil, fmt.Errorf("invalid key auth config: %s",
err)
+ if ap := ac.Spec.AuthParameter; ap != nil {
+ if ap.KeyAuth != nil {
+ cfg, err := t.translateConsumerKeyAuthPlugin(tctx,
ac.Namespace, ap.KeyAuth)
+ if err != nil {
+ return nil, fmt.Errorf("invalid key auth
config: %s", err)
+ }
+ plugins["key-auth"] = cfg
+ } else if ap.BasicAuth != nil {
+ cfg, err := t.translateConsumerBasicAuthPlugin(tctx,
ac.Namespace, ap.BasicAuth)
+ if err != nil {
+ return nil, fmt.Errorf("invalid basic auth
config: %s", err)
+ }
+ plugins["basic-auth"] = cfg
+ } else if ap.JwtAuth != nil {
+ cfg, err := t.translateConsumerJwtAuthPlugin(tctx,
ac.Namespace, ap.JwtAuth)
+ if err != nil {
+ return nil, fmt.Errorf("invalid jwt auth
config: %s", err)
+ }
+ plugins["jwt-auth"] = cfg
+ } else if ap.WolfRBAC != nil {
+ cfg, err := t.translateConsumerWolfRBACPlugin(tctx,
ac.Namespace, ap.WolfRBAC)
+ if err != nil {
+ return nil, fmt.Errorf("invalid wolf rbac
config: %s", err)
+ }
+ plugins["wolf-rbac"] = cfg
+ } else if ap.HMACAuth != nil {
+ cfg, err := t.translateConsumerHMACAuthPlugin(tctx,
ac.Namespace, ap.HMACAuth)
+ if err != nil {
+ return nil, fmt.Errorf("invalid hmac auth
config: %s", err)
+ }
+ plugins["hmac-auth"] = cfg
+ } else if ap.LDAPAuth != nil {
+ cfg, err := t.translateConsumerLDAPAuthPlugin(tctx,
ac.Namespace, ap.LDAPAuth)
+ if err != nil {
+ return nil, fmt.Errorf("invalid ldap auth
config: %s", err)
+ }
+ plugins["ldap-auth"] = cfg
}
- plugins["key-auth"] = cfg
- } else if ac.Spec.AuthParameter.BasicAuth != nil {
- cfg, err := t.translateConsumerBasicAuthPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.BasicAuth)
- if err != nil {
- return nil, fmt.Errorf("invalid basic auth config: %s",
err)
- }
- plugins["basic-auth"] = cfg
- } else if ac.Spec.AuthParameter.JwtAuth != nil {
- cfg, err := t.translateConsumerJwtAuthPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.JwtAuth)
- if err != nil {
- return nil, fmt.Errorf("invalid jwt auth config: %s",
err)
- }
- plugins["jwt-auth"] = cfg
- } else if ac.Spec.AuthParameter.WolfRBAC != nil {
- cfg, err := t.translateConsumerWolfRBACPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.WolfRBAC)
- if err != nil {
- return nil, fmt.Errorf("invalid wolf rbac config: %s",
err)
- }
- plugins["wolf-rbac"] = cfg
- } else if ac.Spec.AuthParameter.HMACAuth != nil {
- cfg, err := t.translateConsumerHMACAuthPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.HMACAuth)
- if err != nil {
- return nil, fmt.Errorf("invalid hmac auth config: %s",
err)
- }
- plugins["hmac-auth"] = cfg
- } else if ac.Spec.AuthParameter.LDAPAuth != nil {
- cfg, err := t.translateConsumerLDAPAuthPlugin(tctx,
ac.Namespace, ac.Spec.AuthParameter.LDAPAuth)
- if err != nil {
- return nil, fmt.Errorf("invalid ldap auth config: %s",
err)
+ }
+
+ // Merge generic consumer-scoped plugins. Only enabled entries are
merged;
+ // an enabled plugin with the same name as an auth plugin derived from
authParameter takes precedence.
+ for _, plugin := range ac.Spec.Plugins {
+ if !plugin.Enable {
+ continue
}
- plugins["ldap-auth"] = cfg
+ config := t.buildPluginConfig(plugin, ac.Namespace,
tctx.Secrets)
+ plugins[plugin.Name] = config
}
username := adctypes.ComposeConsumerName(ac.Namespace, ac.Name)
@@ -107,7 +119,9 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx
*provider.TranslateCont
if cfg.Value != nil {
return &adctypes.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil
}
-
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("key-auth: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
@@ -129,7 +143,9 @@ func (t *Translator) translateConsumerBasicAuthPlugin(tctx
*provider.TranslateCo
Password: cfg.Value.Password,
}, nil
}
-
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("basic-auth: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
@@ -159,6 +175,9 @@ func (t *Translator) translateConsumerWolfRBACPlugin(tctx
*provider.TranslateCon
HeaderPrefix: cfg.Value.HeaderPrefix,
}, nil
}
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("wolf-rbac: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
@@ -194,6 +213,9 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx
*provider.TranslateCont
}, nil
}
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("jwt-auth: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
@@ -251,6 +273,9 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx
*provider.TranslateCon
}, nil
}
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("hmac-auth: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
@@ -357,6 +382,9 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx
*provider.TranslateCon
}, nil
}
+ if cfg.SecretRef == nil {
+ return nil, fmt.Errorf("ldap-auth: either value or secretRef
must be specified")
+ }
sec := tctx.Secrets[k8stypes.NamespacedName{
Namespace: consumerNamespace,
Name: cfg.SecretRef.Name,
diff --git a/internal/controller/apisixconsumer_controller.go
b/internal/controller/apisixconsumer_controller.go
index c40345a5..da3594df 100644
--- a/internal/controller/apisixconsumer_controller.go
+++ b/internal/controller/apisixconsumer_controller.go
@@ -184,39 +184,62 @@ func (r *ApisixConsumerReconciler)
listApisixConsumerForSecret(ctx context.Conte
func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx
*provider.TranslateContext, ac *apiv2.ApisixConsumer) error {
var secretRef *corev1.LocalObjectReference
- if ac.Spec.AuthParameter.KeyAuth != nil {
- secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
- } else if ac.Spec.AuthParameter.BasicAuth != nil {
- secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
- } else if ac.Spec.AuthParameter.JwtAuth != nil {
- secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
- } else if ac.Spec.AuthParameter.WolfRBAC != nil {
- secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
- } else if ac.Spec.AuthParameter.HMACAuth != nil {
- secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
- } else if ac.Spec.AuthParameter.LDAPAuth != nil {
- secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
- }
- if secretRef == nil {
- return nil
+ if ap := ac.Spec.AuthParameter; ap != nil {
+ if ap.KeyAuth != nil {
+ secretRef = ap.KeyAuth.SecretRef
+ } else if ap.BasicAuth != nil {
+ secretRef = ap.BasicAuth.SecretRef
+ } else if ap.JwtAuth != nil {
+ secretRef = ap.JwtAuth.SecretRef
+ } else if ap.WolfRBAC != nil {
+ secretRef = ap.WolfRBAC.SecretRef
+ } else if ap.HMACAuth != nil {
+ secretRef = ap.HMACAuth.SecretRef
+ } else if ap.LDAPAuth != nil {
+ secretRef = ap.LDAPAuth.SecretRef
+ }
}
-
- namespacedName := types.NamespacedName{
- Name: secretRef.Name,
- Namespace: ac.Namespace,
+ if secretRef != nil && secretRef.Name != "" {
+ namespacedName := types.NamespacedName{
+ Name: secretRef.Name,
+ Namespace: ac.Namespace,
+ }
+ secret := &corev1.Secret{}
+ if err := r.Get(ctx, namespacedName, secret); err != nil {
+ if k8serrors.IsNotFound(err) {
+ r.Log.Info("secret not found", "secret",
namespacedName)
+ } else {
+ r.Log.Error(err, "failed to get secret",
"secret", namespacedName)
+ return err
+ }
+ } else {
+ tctx.Secrets[namespacedName] = secret
+ }
}
- secret := &corev1.Secret{}
- if err := r.Get(ctx, namespacedName, secret); err != nil {
- if k8serrors.IsNotFound(err) {
- r.Log.Info("secret not found", "secret", namespacedName)
- return nil
+ for _, plugin := range ac.Spec.Plugins {
+ if !plugin.Enable || plugin.SecretRef == "" {
+ continue
+ }
+ namespacedName := types.NamespacedName{
+ Name: plugin.SecretRef,
+ Namespace: ac.Namespace,
+ }
+ if _, loaded := tctx.Secrets[namespacedName]; loaded {
+ continue
+ }
+ secret := &corev1.Secret{}
+ if err := r.Get(ctx, namespacedName, secret); err != nil {
+ if k8serrors.IsNotFound(err) {
+ r.Log.Info("secret not found for plugin",
"plugin", plugin.Name, "secret", namespacedName)
+ } else {
+ r.Log.Error(err, "failed to get secret for
plugin", "plugin", plugin.Name, "secret", namespacedName)
+ return err
+ }
} else {
- r.Log.Error(err, "failed to get secret", "secret",
namespacedName)
- return err
+ tctx.Secrets[namespacedName] = secret
}
}
- tctx.Secrets[namespacedName] = secret
return nil
}
diff --git a/internal/controller/indexer/indexer.go
b/internal/controller/indexer/indexer.go
index 6f0d66a4..5172e487 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -879,22 +879,29 @@ func ApisixPluginConfigSecretIndexFunc(obj client.Object)
(keys []string) {
func ApisixConsumerSecretIndexFunc(rawObj client.Object) (keys []string) {
ac := rawObj.(*apiv2.ApisixConsumer)
var secretRef *corev1.LocalObjectReference
- if ac.Spec.AuthParameter.KeyAuth != nil {
- secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
- } else if ac.Spec.AuthParameter.BasicAuth != nil {
- secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
- } else if ac.Spec.AuthParameter.JwtAuth != nil {
- secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
- } else if ac.Spec.AuthParameter.WolfRBAC != nil {
- secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
- } else if ac.Spec.AuthParameter.HMACAuth != nil {
- secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
- } else if ac.Spec.AuthParameter.LDAPAuth != nil {
- secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
- }
- if secretRef != nil {
+ if ap := ac.Spec.AuthParameter; ap != nil {
+ if ap.KeyAuth != nil {
+ secretRef = ap.KeyAuth.SecretRef
+ } else if ap.BasicAuth != nil {
+ secretRef = ap.BasicAuth.SecretRef
+ } else if ap.JwtAuth != nil {
+ secretRef = ap.JwtAuth.SecretRef
+ } else if ap.WolfRBAC != nil {
+ secretRef = ap.WolfRBAC.SecretRef
+ } else if ap.HMACAuth != nil {
+ secretRef = ap.HMACAuth.SecretRef
+ } else if ap.LDAPAuth != nil {
+ secretRef = ap.LDAPAuth.SecretRef
+ }
+ }
+ if secretRef != nil && secretRef.Name != "" {
keys = append(keys, GenIndexKey(ac.GetNamespace(),
secretRef.Name))
}
+ for _, plugin := range ac.Spec.Plugins {
+ if plugin.Enable && plugin.SecretRef != "" {
+ keys = append(keys, GenIndexKey(ac.GetNamespace(),
plugin.SecretRef))
+ }
+ }
return
}
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go
b/internal/webhook/v1/apisixconsumer_webhook.go
index 796491f5..59212015 100644
--- a/internal/webhook/v1/apisixconsumer_webhook.go
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -128,24 +128,25 @@ func (v *ApisixConsumerCustomValidator)
collectWarnings(ctx context.Context, con
})...)
}
- params := consumer.Spec.AuthParameter
- if params.BasicAuth != nil {
- addSecretWarning(params.BasicAuth.SecretRef)
- }
- if params.KeyAuth != nil {
- addSecretWarning(params.KeyAuth.SecretRef)
- }
- if params.WolfRBAC != nil {
- addSecretWarning(params.WolfRBAC.SecretRef)
- }
- if params.JwtAuth != nil {
- addSecretWarning(params.JwtAuth.SecretRef)
- }
- if params.HMACAuth != nil {
- addSecretWarning(params.HMACAuth.SecretRef)
- }
- if params.LDAPAuth != nil {
- addSecretWarning(params.LDAPAuth.SecretRef)
+ if params := consumer.Spec.AuthParameter; params != nil {
+ if params.BasicAuth != nil {
+ addSecretWarning(params.BasicAuth.SecretRef)
+ }
+ if params.KeyAuth != nil {
+ addSecretWarning(params.KeyAuth.SecretRef)
+ }
+ if params.WolfRBAC != nil {
+ addSecretWarning(params.WolfRBAC.SecretRef)
+ }
+ if params.JwtAuth != nil {
+ addSecretWarning(params.JwtAuth.SecretRef)
+ }
+ if params.HMACAuth != nil {
+ addSecretWarning(params.HMACAuth.SecretRef)
+ }
+ if params.LDAPAuth != nil {
+ addSecretWarning(params.LDAPAuth.SecretRef)
+ }
}
return warnings
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go
b/internal/webhook/v1/apisixconsumer_webhook_test.go
index e1be420d..b1e0f5c7 100644
--- a/internal/webhook/v1/apisixconsumer_webhook_test.go
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -80,7 +80,7 @@ func TestApisixConsumerValidator_MissingBasicAuthSecret(t
*testing.T) {
},
Spec: apisixv2.ApisixConsumerSpec{
IngressClassName: "apisix",
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "basic-auth"},
},
@@ -104,7 +104,7 @@ func TestApisixConsumerValidator_MultipleSecretWarnings(t
*testing.T) {
},
Spec: apisixv2.ApisixConsumerSpec{
IngressClassName: "apisix",
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "basic-auth"},
},
@@ -144,7 +144,7 @@ func
TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
},
Spec: apisixv2.ApisixConsumerSpec{
IngressClassName: "apisix",
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "key-auth"},
},
@@ -181,7 +181,7 @@ func
TestApisixConsumerValidator_DeniesOnADCValidationFailure(t *testing.T) {
},
Spec: apisixv2.ApisixConsumerSpec{
IngressClassName: "apisix",
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
SecretRef:
&corev1.LocalObjectReference{Name: "key-auth"},
},
@@ -220,7 +220,7 @@ func
TestApisixConsumerValidator_UsesADCValidateEndpointForControlPlane(t *testi
},
Spec: apisixv2.ApisixConsumerSpec{
IngressClassName: managedIngressClassName,
- AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+ AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
Value:
&apisixv2.ApisixConsumerKeyAuthValue{Key: "shared-key"},
},
diff --git a/test/e2e/crds/v2/consumer.go b/test/e2e/crds/v2/consumer.go
index 589e3329..9d646b80 100644
--- a/test/e2e/crds/v2/consumer.go
+++ b/test/e2e/crds/v2/consumer.go
@@ -671,4 +671,190 @@ spec:
Eventually(request).WithArguments("/get", "jack",
"jackPassword").WithTimeout(5 *
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
})
})
+
+ Context("Test Consumer Plugins - authParameter with extra plugins",
func() {
+ // Verify that a consumer with authParameter + plugins (e.g.
limit-count) works:
+ // auth is enforced via authParameter and limit-count throttles
authenticated traffic.
+ const (
+ consumerWithPlugins = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+ name: consumer-with-plugins
+spec:
+ ingressClassName: %s
+ authParameter:
+ keyAuth:
+ value:
+ key: plugin-test-key
+ plugins:
+ - name: limit-count
+ enable: true
+ config:
+ count: 2
+ time_window: 60
+ rejected_code: 429
+ key: consumer_name
+ policy: local
+`
+ pluginRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: plugin-route
+spec:
+ ingressClassName: %s
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin
+ paths:
+ - /get
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+ authentication:
+ enable: true
+ type: keyAuth
+`
+ )
+
+ It("consumer-level limit-count plugin is enforced", func() {
+ By("apply ApisixRoute")
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace:
s.Namespace(), Name: "plugin-route"},
+ &apiv2.ApisixRoute{}, fmt.Sprintf(pluginRoute,
s.Namespace()))
+
+ By("apply ApisixConsumer with plugins")
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace:
s.Namespace(), Name: "consumer-with-plugins"},
+ &apiv2.ApisixConsumer{},
fmt.Sprintf(consumerWithPlugins, s.Namespace()))
+
+ By("unauthenticated request is rejected")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Headers: map[string]string{"apikey":
"wrong-key"},
+ Check:
scaffold.WithExpectedStatus(http.StatusUnauthorized),
+ })
+
+ By("first authenticated request succeeds")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Headers: map[string]string{"apikey":
"plugin-test-key"},
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ })
+
+ By("second authenticated request succeeds")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Headers: map[string]string{"apikey":
"plugin-test-key"},
+ Check:
scaffold.WithExpectedStatus(http.StatusOK),
+ })
+
+ By("third request is rate-limited by consumer-level
limit-count")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Headers: map[string]string{"apikey":
"plugin-test-key"},
+ Check:
scaffold.WithExpectedStatus(http.StatusTooManyRequests),
+ })
+
+ By("delete ApisixConsumer")
+ err := s.DeleteResource("ApisixConsumer",
"consumer-with-plugins")
+ Expect(err).ShouldNot(HaveOccurred(), "deleting
ApisixConsumer")
+
+ By("delete ApisixRoute")
+ err = s.DeleteResource("ApisixRoute", "plugin-route")
+ Expect(err).ShouldNot(HaveOccurred(), "deleting
ApisixRoute")
+ })
+ })
+
+ Context("Test Consumer Plugins - plugins only (no authParameter)",
func() {
+ // Verify that authParameter can be omitted entirely and auth
can be
+ // configured directly via the plugins field.
+ const (
+ consumerPluginsOnly = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixConsumer
+metadata:
+ name: consumer-plugins-only
+spec:
+ ingressClassName: %s
+ plugins:
+ - name: key-auth
+ enable: true
+ config:
+ key: plugins-only-key
+`
+ pluginsOnlyRoute = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: plugins-only-route
+spec:
+ ingressClassName: %s
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin
+ paths:
+ - /get
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+ authentication:
+ enable: true
+ type: keyAuth
+`
+ )
+
+ It("auth plugin configured via plugins field only", func() {
+ By("apply ApisixRoute")
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace:
s.Namespace(), Name: "plugins-only-route"},
+ &apiv2.ApisixRoute{},
fmt.Sprintf(pluginsOnlyRoute, s.Namespace()))
+
+ By("apply ApisixConsumer with plugins only (no
authParameter)")
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace:
s.Namespace(), Name: "consumer-plugins-only"},
+ &apiv2.ApisixConsumer{},
fmt.Sprintf(consumerPluginsOnly, s.Namespace()))
+
+ By("request with wrong key is rejected")
+ Eventually(func() int {
+ return s.NewAPISIXClient().GET("/get").
+ WithHeader("apikey", "wrong-key").
+ WithHost("httpbin").
+ Expect().Raw().StatusCode
+ }).WithTimeout(10 *
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+ By("request with correct key succeeds")
+ Eventually(func() int {
+ return s.NewAPISIXClient().GET("/get").
+ WithHeader("apikey",
"plugins-only-key").
+ WithHost("httpbin").
+ Expect().Raw().StatusCode
+ }).WithTimeout(10 *
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
+
+ By("delete ApisixConsumer")
+ err := s.DeleteResource("ApisixConsumer",
"consumer-plugins-only")
+ Expect(err).ShouldNot(HaveOccurred(), "deleting
ApisixConsumer")
+
+ By("request with correct key is rejected after consumer
deletion")
+ Eventually(func() int {
+ return s.NewAPISIXClient().GET("/get").
+ WithHeader("apikey",
"plugins-only-key").
+ WithHost("httpbin").
+ Expect().Raw().StatusCode
+ }).WithTimeout(10 *
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusUnauthorized))
+
+ By("delete ApisixRoute")
+ err = s.DeleteResource("ApisixRoute",
"plugins-only-route")
+ Expect(err).ShouldNot(HaveOccurred(), "deleting
ApisixRoute")
+ })
+ })
})