Skip to content

Commit 3edc958

Browse files
AlinsRanCopilot
andcommitted
feat: support plugins field in ApisixConsumer
Add a generic `plugins` field to ApisixConsumerSpec so that consumer-scoped plugins (e.g. limit-count, limit-req) can be attached to an ApisixConsumer resource directly, without being limited to the auth plugins exposed through authParameter. Key changes: - ApisixConsumerSpec gains a Plugins []ApisixRoutePlugin field, following the same pattern as ApisixRoute and ApisixGlobalRule. Enabled plugins are merged after the auth plugin derived from authParameter; an enabled entry with the same name takes precedence. - authParameter is now optional (omitempty). A CEL x-validation rule enforces that at least one auth method within authParameter OR at least one enabled plugin in plugins must be specified. - Translator updated to process the new Plugins slice via the existing buildPluginConfig helper. - Controller and indexer extended to load and index Secrets referenced by spec.plugins[].secretRef, consistent with ApisixRoute and ApisixGlobalRule behavior. - deepcopy updated for the new Plugins slice. - No webhook changes required; validation is handled entirely by the CRD-level CEL rule (x-kubernetes-validations). - E2e tests added for authParameter+plugins and plugins-only consumers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ff8cd42 commit 3edc958

10 files changed

Lines changed: 383 additions & 101 deletions

File tree

api/v2/apisixconsumer_types.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ import (
2323
)
2424

2525
// ApisixConsumerSpec defines the desired state of ApisixConsumer.
26+
// +kubebuilder:validation:XValidation:rule="has(self.authParameter) || (has(self.plugins) && self.plugins.exists(p, p.enable))",message="at least one of authParameter (with an auth method) or an enabled plugin in plugins must be specified"
2627
type ApisixConsumerSpec struct {
2728
// IngressClassName is the name of an IngressClass cluster resource.
2829
// The controller uses this field to decide whether the resource should be managed.
2930
IngressClassName string `json:"ingressClassName,omitempty" yaml:"ingressClassName,omitempty"`
3031

3132
// AuthParameter defines the authentication credentials and configuration for this consumer.
32-
AuthParameter ApisixConsumerAuthParameter `json:"authParameter" yaml:"authParameter"`
33+
// Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified.
34+
// +kubebuilder:validation:Optional
35+
AuthParameter *ApisixConsumerAuthParameter `json:"authParameter,omitempty" yaml:"authParameter,omitempty"`
36+
37+
// Plugins lists additional consumer-scoped plugins to attach to this consumer.
38+
// These plugins are applied alongside any authentication plugin derived from AuthParameter.
39+
// Enabled plugins with the same name as the auth plugin derived from AuthParameter take precedence.
40+
// Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified.
41+
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
3342
}
3443

3544
// ApisixConsumerStatus defines the observed state of ApisixConsumer.

api/v2/zz_generated.deepcopy.go

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/apisix.apache.org_apisixconsumers.yaml

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ spec:
4343
description: ApisixConsumerSpec defines the consumer authentication configuration.
4444
properties:
4545
authParameter:
46-
description: AuthParameter defines the authentication credentials
47-
and configuration for this consumer.
46+
description: |-
47+
AuthParameter defines the authentication credentials and configuration for this consumer.
48+
Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified.
4849
properties:
4950
basicAuth:
5051
description: BasicAuth configures the basic authentication details.
@@ -312,9 +313,39 @@ spec:
312313
IngressClassName is the name of an IngressClass cluster resource.
313314
The controller uses this field to decide whether the resource should be managed.
314315
type: string
315-
required:
316-
- authParameter
316+
plugins:
317+
description: |-
318+
Plugins lists additional consumer-scoped plugins to attach to this consumer.
319+
These plugins are applied alongside any authentication plugin derived from AuthParameter.
320+
Enabled plugins with the same name as the auth plugin derived from AuthParameter take precedence.
321+
Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified.
322+
items:
323+
description: ApisixRoutePlugin represents an APISIX plugin.
324+
properties:
325+
config:
326+
description: Plugin configuration.
327+
x-kubernetes-preserve-unknown-fields: true
328+
enable:
329+
default: true
330+
description: Whether this plugin is in use, default is true.
331+
type: boolean
332+
name:
333+
description: The plugin name.
334+
type: string
335+
secretRef:
336+
description: Plugin configuration secretRef.
337+
type: string
338+
required:
339+
- enable
340+
- name
341+
type: object
342+
type: array
317343
type: object
344+
x-kubernetes-validations:
345+
- message: at least one of authParameter (with an auth method) or an enabled
346+
plugin in plugins must be specified
347+
rule: has(self.authParameter) || (has(self.plugins) && self.plugins.exists(p,
348+
p.enable))
318349
status:
319350
description: ApisixStatus is the status report for Apisix ingress Resources
320351
properties:

docs/en/latest/reference/api-reference.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -871,7 +871,8 @@ ApisixConsumerSpec defines the desired state of ApisixConsumer.
871871
| Field | Description |
872872
| --- | --- |
873873
| `ingressClassName` _string_ | IngressClassName is the name of an IngressClass cluster resource. The controller uses this field to decide whether the resource should be managed. |
874-
| `authParameter` _[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter defines the authentication credentials and configuration for this consumer. |
874+
| `authParameter` _[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter defines the authentication credentials and configuration for this consumer. Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified. |
875+
| `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. Enabled plugins with the same name as the auth plugin derived from AuthParameter take precedence. Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified. |
875876

876877

877878
_Appears in:_
@@ -1160,6 +1161,7 @@ ApisixRoutePlugin represents an APISIX plugin.
11601161

11611162

11621163
_Appears in:_
1164+
- [ApisixConsumerSpec](#apisixconsumerspec)
11631165
- [ApisixGlobalRuleSpec](#apisixglobalrulespec)
11641166
- [ApisixPluginConfigSpec](#apisixpluginconfigspec)
11651167
- [ApisixRouteHTTP](#apisixroutehttp)

internal/adc/translator/apisixconsumer.go

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,42 +55,54 @@ const (
5555
func (t *Translator) TranslateApisixConsumer(tctx *provider.TranslateContext, ac *v2.ApisixConsumer) (*TranslateResult, error) {
5656
result := &TranslateResult{}
5757
plugins := make(adctypes.Plugins)
58-
if ac.Spec.AuthParameter.KeyAuth != nil {
59-
cfg, err := t.translateConsumerKeyAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.KeyAuth)
60-
if err != nil {
61-
return nil, fmt.Errorf("invalid key auth config: %s", err)
58+
if ap := ac.Spec.AuthParameter; ap != nil {
59+
if ap.KeyAuth != nil {
60+
cfg, err := t.translateConsumerKeyAuthPlugin(tctx, ac.Namespace, ap.KeyAuth)
61+
if err != nil {
62+
return nil, fmt.Errorf("invalid key auth config: %s", err)
63+
}
64+
plugins["key-auth"] = cfg
65+
} else if ap.BasicAuth != nil {
66+
cfg, err := t.translateConsumerBasicAuthPlugin(tctx, ac.Namespace, ap.BasicAuth)
67+
if err != nil {
68+
return nil, fmt.Errorf("invalid basic auth config: %s", err)
69+
}
70+
plugins["basic-auth"] = cfg
71+
} else if ap.JwtAuth != nil {
72+
cfg, err := t.translateConsumerJwtAuthPlugin(tctx, ac.Namespace, ap.JwtAuth)
73+
if err != nil {
74+
return nil, fmt.Errorf("invalid jwt auth config: %s", err)
75+
}
76+
plugins["jwt-auth"] = cfg
77+
} else if ap.WolfRBAC != nil {
78+
cfg, err := t.translateConsumerWolfRBACPlugin(tctx, ac.Namespace, ap.WolfRBAC)
79+
if err != nil {
80+
return nil, fmt.Errorf("invalid wolf rbac config: %s", err)
81+
}
82+
plugins["wolf-rbac"] = cfg
83+
} else if ap.HMACAuth != nil {
84+
cfg, err := t.translateConsumerHMACAuthPlugin(tctx, ac.Namespace, ap.HMACAuth)
85+
if err != nil {
86+
return nil, fmt.Errorf("invalid hmac auth config: %s", err)
87+
}
88+
plugins["hmac-auth"] = cfg
89+
} else if ap.LDAPAuth != nil {
90+
cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, ac.Namespace, ap.LDAPAuth)
91+
if err != nil {
92+
return nil, fmt.Errorf("invalid ldap auth config: %s", err)
93+
}
94+
plugins["ldap-auth"] = cfg
6295
}
63-
plugins["key-auth"] = cfg
64-
} else if ac.Spec.AuthParameter.BasicAuth != nil {
65-
cfg, err := t.translateConsumerBasicAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.BasicAuth)
66-
if err != nil {
67-
return nil, fmt.Errorf("invalid basic auth config: %s", err)
68-
}
69-
plugins["basic-auth"] = cfg
70-
} else if ac.Spec.AuthParameter.JwtAuth != nil {
71-
cfg, err := t.translateConsumerJwtAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.JwtAuth)
72-
if err != nil {
73-
return nil, fmt.Errorf("invalid jwt auth config: %s", err)
74-
}
75-
plugins["jwt-auth"] = cfg
76-
} else if ac.Spec.AuthParameter.WolfRBAC != nil {
77-
cfg, err := t.translateConsumerWolfRBACPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.WolfRBAC)
78-
if err != nil {
79-
return nil, fmt.Errorf("invalid wolf rbac config: %s", err)
80-
}
81-
plugins["wolf-rbac"] = cfg
82-
} else if ac.Spec.AuthParameter.HMACAuth != nil {
83-
cfg, err := t.translateConsumerHMACAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.HMACAuth)
84-
if err != nil {
85-
return nil, fmt.Errorf("invalid hmac auth config: %s", err)
86-
}
87-
plugins["hmac-auth"] = cfg
88-
} else if ac.Spec.AuthParameter.LDAPAuth != nil {
89-
cfg, err := t.translateConsumerLDAPAuthPlugin(tctx, ac.Namespace, ac.Spec.AuthParameter.LDAPAuth)
90-
if err != nil {
91-
return nil, fmt.Errorf("invalid ldap auth config: %s", err)
96+
}
97+
98+
// Merge generic consumer-scoped plugins. Only enabled entries are merged;
99+
// an enabled plugin with the same name as an auth plugin derived from authParameter takes precedence.
100+
for _, plugin := range ac.Spec.Plugins {
101+
if !plugin.Enable {
102+
continue
92103
}
93-
plugins["ldap-auth"] = cfg
104+
config := t.buildPluginConfig(plugin, ac.Namespace, tctx.Secrets)
105+
plugins[plugin.Name] = config
94106
}
95107

96108
username := adctypes.ComposeConsumerName(ac.Namespace, ac.Name)

internal/controller/apisixconsumer_controller.go

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -184,39 +184,62 @@ func (r *ApisixConsumerReconciler) listApisixConsumerForSecret(ctx context.Conte
184184

185185
func (r *ApisixConsumerReconciler) processSpec(ctx context.Context, tctx *provider.TranslateContext, ac *apiv2.ApisixConsumer) error {
186186
var secretRef *corev1.LocalObjectReference
187-
if ac.Spec.AuthParameter.KeyAuth != nil {
188-
secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
189-
} else if ac.Spec.AuthParameter.BasicAuth != nil {
190-
secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
191-
} else if ac.Spec.AuthParameter.JwtAuth != nil {
192-
secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
193-
} else if ac.Spec.AuthParameter.WolfRBAC != nil {
194-
secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
195-
} else if ac.Spec.AuthParameter.HMACAuth != nil {
196-
secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
197-
} else if ac.Spec.AuthParameter.LDAPAuth != nil {
198-
secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
199-
}
200-
if secretRef == nil {
201-
return nil
187+
if ap := ac.Spec.AuthParameter; ap != nil {
188+
if ap.KeyAuth != nil {
189+
secretRef = ap.KeyAuth.SecretRef
190+
} else if ap.BasicAuth != nil {
191+
secretRef = ap.BasicAuth.SecretRef
192+
} else if ap.JwtAuth != nil {
193+
secretRef = ap.JwtAuth.SecretRef
194+
} else if ap.WolfRBAC != nil {
195+
secretRef = ap.WolfRBAC.SecretRef
196+
} else if ap.HMACAuth != nil {
197+
secretRef = ap.HMACAuth.SecretRef
198+
} else if ap.LDAPAuth != nil {
199+
secretRef = ap.LDAPAuth.SecretRef
200+
}
202201
}
203-
204-
namespacedName := types.NamespacedName{
205-
Name: secretRef.Name,
206-
Namespace: ac.Namespace,
202+
if secretRef != nil {
203+
namespacedName := types.NamespacedName{
204+
Name: secretRef.Name,
205+
Namespace: ac.Namespace,
206+
}
207+
secret := &corev1.Secret{}
208+
if err := r.Get(ctx, namespacedName, secret); err != nil {
209+
if k8serrors.IsNotFound(err) {
210+
r.Log.Info("secret not found", "secret", namespacedName)
211+
} else {
212+
r.Log.Error(err, "failed to get secret", "secret", namespacedName)
213+
return err
214+
}
215+
} else {
216+
tctx.Secrets[namespacedName] = secret
217+
}
207218
}
208219

209-
secret := &corev1.Secret{}
210-
if err := r.Get(ctx, namespacedName, secret); err != nil {
211-
if k8serrors.IsNotFound(err) {
212-
r.Log.Info("secret not found", "secret", namespacedName)
213-
return nil
220+
for _, plugin := range ac.Spec.Plugins {
221+
if !plugin.Enable || plugin.SecretRef == "" {
222+
continue
223+
}
224+
namespacedName := types.NamespacedName{
225+
Name: plugin.SecretRef,
226+
Namespace: ac.Namespace,
227+
}
228+
if _, loaded := tctx.Secrets[namespacedName]; loaded {
229+
continue
230+
}
231+
secret := &corev1.Secret{}
232+
if err := r.Get(ctx, namespacedName, secret); err != nil {
233+
if k8serrors.IsNotFound(err) {
234+
r.Log.Info("secret not found for plugin", "plugin", plugin.Name, "secret", namespacedName)
235+
} else {
236+
r.Log.Error(err, "failed to get secret for plugin", "plugin", plugin.Name, "secret", namespacedName)
237+
return err
238+
}
214239
} else {
215-
r.Log.Error(err, "failed to get secret", "secret", namespacedName)
216-
return err
240+
tctx.Secrets[namespacedName] = secret
217241
}
218242
}
219-
tctx.Secrets[namespacedName] = secret
220243
return nil
221244
}
222245

internal/controller/indexer/indexer.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -879,22 +879,29 @@ func ApisixPluginConfigSecretIndexFunc(obj client.Object) (keys []string) {
879879
func ApisixConsumerSecretIndexFunc(rawObj client.Object) (keys []string) {
880880
ac := rawObj.(*apiv2.ApisixConsumer)
881881
var secretRef *corev1.LocalObjectReference
882-
if ac.Spec.AuthParameter.KeyAuth != nil {
883-
secretRef = ac.Spec.AuthParameter.KeyAuth.SecretRef
884-
} else if ac.Spec.AuthParameter.BasicAuth != nil {
885-
secretRef = ac.Spec.AuthParameter.BasicAuth.SecretRef
886-
} else if ac.Spec.AuthParameter.JwtAuth != nil {
887-
secretRef = ac.Spec.AuthParameter.JwtAuth.SecretRef
888-
} else if ac.Spec.AuthParameter.WolfRBAC != nil {
889-
secretRef = ac.Spec.AuthParameter.WolfRBAC.SecretRef
890-
} else if ac.Spec.AuthParameter.HMACAuth != nil {
891-
secretRef = ac.Spec.AuthParameter.HMACAuth.SecretRef
892-
} else if ac.Spec.AuthParameter.LDAPAuth != nil {
893-
secretRef = ac.Spec.AuthParameter.LDAPAuth.SecretRef
882+
if ap := ac.Spec.AuthParameter; ap != nil {
883+
if ap.KeyAuth != nil {
884+
secretRef = ap.KeyAuth.SecretRef
885+
} else if ap.BasicAuth != nil {
886+
secretRef = ap.BasicAuth.SecretRef
887+
} else if ap.JwtAuth != nil {
888+
secretRef = ap.JwtAuth.SecretRef
889+
} else if ap.WolfRBAC != nil {
890+
secretRef = ap.WolfRBAC.SecretRef
891+
} else if ap.HMACAuth != nil {
892+
secretRef = ap.HMACAuth.SecretRef
893+
} else if ap.LDAPAuth != nil {
894+
secretRef = ap.LDAPAuth.SecretRef
895+
}
894896
}
895897
if secretRef != nil {
896898
keys = append(keys, GenIndexKey(ac.GetNamespace(), secretRef.Name))
897899
}
900+
for _, plugin := range ac.Spec.Plugins {
901+
if plugin.Enable && plugin.SecretRef != "" {
902+
keys = append(keys, GenIndexKey(ac.GetNamespace(), plugin.SecretRef))
903+
}
904+
}
898905
return
899906
}
900907

internal/webhook/v1/apisixconsumer_webhook.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,24 +107,25 @@ func (v *ApisixConsumerCustomValidator) collectWarnings(ctx context.Context, con
107107
})...)
108108
}
109109

110-
params := consumer.Spec.AuthParameter
111-
if params.BasicAuth != nil {
112-
addSecretWarning(params.BasicAuth.SecretRef)
113-
}
114-
if params.KeyAuth != nil {
115-
addSecretWarning(params.KeyAuth.SecretRef)
116-
}
117-
if params.WolfRBAC != nil {
118-
addSecretWarning(params.WolfRBAC.SecretRef)
119-
}
120-
if params.JwtAuth != nil {
121-
addSecretWarning(params.JwtAuth.SecretRef)
122-
}
123-
if params.HMACAuth != nil {
124-
addSecretWarning(params.HMACAuth.SecretRef)
125-
}
126-
if params.LDAPAuth != nil {
127-
addSecretWarning(params.LDAPAuth.SecretRef)
110+
if params := consumer.Spec.AuthParameter; params != nil {
111+
if params.BasicAuth != nil {
112+
addSecretWarning(params.BasicAuth.SecretRef)
113+
}
114+
if params.KeyAuth != nil {
115+
addSecretWarning(params.KeyAuth.SecretRef)
116+
}
117+
if params.WolfRBAC != nil {
118+
addSecretWarning(params.WolfRBAC.SecretRef)
119+
}
120+
if params.JwtAuth != nil {
121+
addSecretWarning(params.JwtAuth.SecretRef)
122+
}
123+
if params.HMACAuth != nil {
124+
addSecretWarning(params.HMACAuth.SecretRef)
125+
}
126+
if params.LDAPAuth != nil {
127+
addSecretWarning(params.LDAPAuth.SecretRef)
128+
}
128129
}
129130

130131
return warnings

0 commit comments

Comments
 (0)