Skip to content

Commit 9c51678

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 e89f0f2 commit 9c51678

11 files changed

Lines changed: 395 additions & 113 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/apisixconsumer_validation_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS256(t *testing.T) {
109109
v := loadApisixConsumerSchema(t)
110110
ac := &apisixv2.ApisixConsumer{
111111
Spec: apisixv2.ApisixConsumerSpec{
112-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
112+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
113113
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
114114
Value: &apisixv2.ApisixConsumerJwtAuthValue{
115115
Key: "my-key",
@@ -130,7 +130,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricWithWhitespaceOnlyPublicKey(t *testing
130130
v := loadApisixConsumerSchema(t)
131131
ac := &apisixv2.ApisixConsumer{
132132
Spec: apisixv2.ApisixConsumerSpec{
133-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
133+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
134134
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
135135
Value: &apisixv2.ApisixConsumerJwtAuthValue{
136136
Key: "my-key",
@@ -150,7 +150,7 @@ func TestApisixConsumer_JwtAuth_SymmetricHS512(t *testing.T) {
150150
v := loadApisixConsumerSchema(t)
151151
ac := &apisixv2.ApisixConsumer{
152152
Spec: apisixv2.ApisixConsumerSpec{
153-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
153+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
154154
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
155155
Value: &apisixv2.ApisixConsumerJwtAuthValue{
156156
Key: "my-key",
@@ -168,7 +168,7 @@ func TestApisixConsumer_JwtAuth_NoAlgorithmDefaultsToSymmetric(t *testing.T) {
168168
v := loadApisixConsumerSchema(t)
169169
ac := &apisixv2.ApisixConsumer{
170170
Spec: apisixv2.ApisixConsumerSpec{
171-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
171+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
172172
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
173173
Value: &apisixv2.ApisixConsumerJwtAuthValue{
174174
Key: "my-key",
@@ -185,7 +185,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPublicKey(t *testing.T) {
185185
v := loadApisixConsumerSchema(t)
186186
ac := &apisixv2.ApisixConsumer{
187187
Spec: apisixv2.ApisixConsumerSpec{
188-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
188+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
189189
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
190190
Value: &apisixv2.ApisixConsumerJwtAuthValue{
191191
Key: "my-key",
@@ -203,7 +203,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithPrivateKey(t *testing.T) {
203203
v := loadApisixConsumerSchema(t)
204204
ac := &apisixv2.ApisixConsumer{
205205
Spec: apisixv2.ApisixConsumerSpec{
206-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
206+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
207207
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
208208
Value: &apisixv2.ApisixConsumerJwtAuthValue{
209209
Key: "my-key",
@@ -221,7 +221,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithBothKeys(t *testing.T) {
221221
v := loadApisixConsumerSchema(t)
222222
ac := &apisixv2.ApisixConsumer{
223223
Spec: apisixv2.ApisixConsumerSpec{
224-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
224+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
225225
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
226226
Value: &apisixv2.ApisixConsumerJwtAuthValue{
227227
Key: "my-key",
@@ -240,7 +240,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricRS256WithoutAnyKey(t *testing.T) {
240240
v := loadApisixConsumerSchema(t)
241241
ac := &apisixv2.ApisixConsumer{
242242
Spec: apisixv2.ApisixConsumerSpec{
243-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
243+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
244244
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
245245
Value: &apisixv2.ApisixConsumerJwtAuthValue{
246246
Key: "my-key",
@@ -259,7 +259,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricES256WithoutAnyKey(t *testing.T) {
259259
v := loadApisixConsumerSchema(t)
260260
ac := &apisixv2.ApisixConsumer{
261261
Spec: apisixv2.ApisixConsumerSpec{
262-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
262+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
263263
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
264264
Value: &apisixv2.ApisixConsumerJwtAuthValue{
265265
Key: "my-key",
@@ -278,7 +278,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricEdDSAWithoutAnyKey(t *testing.T) {
278278
v := loadApisixConsumerSchema(t)
279279
ac := &apisixv2.ApisixConsumer{
280280
Spec: apisixv2.ApisixConsumerSpec{
281-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
281+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
282282
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
283283
Value: &apisixv2.ApisixConsumerJwtAuthValue{
284284
Key: "my-key",
@@ -297,7 +297,7 @@ func TestApisixConsumer_JwtAuth_AsymmetricWithEmptyPublicKey(t *testing.T) {
297297
v := loadApisixConsumerSchema(t)
298298
ac := &apisixv2.ApisixConsumer{
299299
Spec: apisixv2.ApisixConsumerSpec{
300-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
300+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
301301
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
302302
Value: &apisixv2.ApisixConsumerJwtAuthValue{
303303
Key: "my-key",
@@ -321,7 +321,7 @@ func TestApisixConsumer_JwtAuth_EmptyAlgorithmTreatedAsSymmetric(t *testing.T) {
321321
v := loadApisixConsumerSchema(t)
322322
ac := &apisixv2.ApisixConsumer{
323323
Spec: apisixv2.ApisixConsumerSpec{
324-
AuthParameter: apisixv2.ApisixConsumerAuthParameter{
324+
AuthParameter: &apisixv2.ApisixConsumerAuthParameter{
325325
JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
326326
Value: &apisixv2.ApisixConsumerJwtAuthValue{
327327
Key: "my-key",

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.
@@ -319,9 +320,39 @@ spec:
319320
IngressClassName is the name of an IngressClass cluster resource.
320321
The controller uses this field to decide whether the resource should be managed.
321322
type: string
322-
required:
323-
- authParameter
323+
plugins:
324+
description: |-
325+
Plugins lists additional consumer-scoped plugins to attach to this consumer.
326+
These plugins are applied alongside any authentication plugin derived from AuthParameter.
327+
Enabled plugins with the same name as the auth plugin derived from AuthParameter take precedence.
328+
Either AuthParameter (with at least one auth method) or Plugins (or both) must be specified.
329+
items:
330+
description: ApisixRoutePlugin represents an APISIX plugin.
331+
properties:
332+
config:
333+
description: Plugin configuration.
334+
x-kubernetes-preserve-unknown-fields: true
335+
enable:
336+
default: true
337+
description: Whether this plugin is in use, default is true.
338+
type: boolean
339+
name:
340+
description: The plugin name.
341+
type: string
342+
secretRef:
343+
description: Plugin configuration secretRef.
344+
type: string
345+
required:
346+
- enable
347+
- name
348+
type: object
349+
type: array
324350
type: object
351+
x-kubernetes-validations:
352+
- message: at least one of authParameter (with an auth method) or an enabled
353+
plugin in plugins must be specified
354+
rule: has(self.authParameter) || (has(self.plugins) && self.plugins.exists(p,
355+
p.enable))
325356
status:
326357
description: ApisixStatus is the status report for Apisix ingress Resources
327358
properties:

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,8 @@ ApisixConsumerSpec defines the desired state of ApisixConsumer.
874874
| Field | Description |
875875
| --- | --- |
876876
| `ingressClassName` _string_ | IngressClassName is the name of an IngressClass cluster resource. The controller uses this field to decide whether the resource should be managed. |
877-
| `authParameter` _[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter defines the authentication credentials and configuration for this consumer. |
877+
| `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. |
878+
| `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. |
878879

879880

880881
_Appears in:_
@@ -1163,6 +1164,7 @@ ApisixRoutePlugin represents an APISIX plugin.
11631164

11641165

11651166
_Appears in:_
1167+
- [ApisixConsumerSpec](#apisixconsumerspec)
11661168
- [ApisixGlobalRuleSpec](#apisixglobalrulespec)
11671169
- [ApisixPluginConfigSpec](#apisixpluginconfigspec)
11681170
- [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)

0 commit comments

Comments
 (0)