Skip to content

Commit 9f59ef9

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 5c236d0 commit 9f59ef9

11 files changed

Lines changed: 404 additions & 116 deletions

File tree

api/v2/apisixconsumer_types.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ type ApisixConsumerSpec struct {
2929
IngressClassName string `json:"ingressClassName,omitempty" yaml:"ingressClassName,omitempty"`
3030

3131
// AuthParameter defines the authentication credentials and configuration for this consumer.
32-
AuthParameter ApisixConsumerAuthParameter `json:"authParameter" yaml:"authParameter"`
32+
// +kubebuilder:validation:Optional
33+
AuthParameter *ApisixConsumerAuthParameter `json:"authParameter,omitempty" yaml:"authParameter,omitempty"`
34+
35+
// Plugins lists additional consumer-scoped plugins to attach to this consumer.
36+
// These plugins are applied alongside any authentication plugin derived from AuthParameter.
37+
// An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence.
38+
Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
3339
}
3440

3541
// 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: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,32 @@ spec:
319319
IngressClassName is the name of an IngressClass cluster resource.
320320
The controller uses this field to decide whether the resource should be managed.
321321
type: string
322-
required:
323-
- authParameter
322+
plugins:
323+
description: |-
324+
Plugins lists additional consumer-scoped plugins to attach to this consumer.
325+
These plugins are applied alongside any authentication plugin derived from AuthParameter.
326+
An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence.
327+
items:
328+
description: ApisixRoutePlugin represents an APISIX plugin.
329+
properties:
330+
config:
331+
description: Plugin configuration.
332+
x-kubernetes-preserve-unknown-fields: true
333+
enable:
334+
default: true
335+
description: Whether this plugin is in use, default is true.
336+
type: boolean
337+
name:
338+
description: The plugin name.
339+
type: string
340+
secretRef:
341+
description: Plugin configuration secretRef.
342+
type: string
343+
required:
344+
- enable
345+
- name
346+
type: object
347+
type: array
324348
type: object
325349
status:
326350
description: ApisixStatus is the status report for Apisix ingress Resources

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,7 @@ ApisixConsumerSpec defines the desired state of ApisixConsumer.
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. |
877877
| `authParameter` _[ApisixConsumerAuthParameter](#apisixconsumerauthparameter)_ | AuthParameter defines the authentication credentials and configuration for this consumer. |
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. An enabled plugin with the same name as the auth plugin derived from AuthParameter takes precedence. |
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: 64 additions & 36 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)
@@ -107,7 +119,9 @@ func (t *Translator) translateConsumerKeyAuthPlugin(tctx *provider.TranslateCont
107119
if cfg.Value != nil {
108120
return &adctypes.KeyAuthConsumerConfig{Key: cfg.Value.Key}, nil
109121
}
110-
122+
if cfg.SecretRef == nil {
123+
return nil, fmt.Errorf("key-auth: either value or secretRef must be specified")
124+
}
111125
sec := tctx.Secrets[k8stypes.NamespacedName{
112126
Namespace: consumerNamespace,
113127
Name: cfg.SecretRef.Name,
@@ -129,7 +143,9 @@ func (t *Translator) translateConsumerBasicAuthPlugin(tctx *provider.TranslateCo
129143
Password: cfg.Value.Password,
130144
}, nil
131145
}
132-
146+
if cfg.SecretRef == nil {
147+
return nil, fmt.Errorf("basic-auth: either value or secretRef must be specified")
148+
}
133149
sec := tctx.Secrets[k8stypes.NamespacedName{
134150
Namespace: consumerNamespace,
135151
Name: cfg.SecretRef.Name,
@@ -159,6 +175,9 @@ func (t *Translator) translateConsumerWolfRBACPlugin(tctx *provider.TranslateCon
159175
HeaderPrefix: cfg.Value.HeaderPrefix,
160176
}, nil
161177
}
178+
if cfg.SecretRef == nil {
179+
return nil, fmt.Errorf("wolf-rbac: either value or secretRef must be specified")
180+
}
162181
sec := tctx.Secrets[k8stypes.NamespacedName{
163182
Namespace: consumerNamespace,
164183
Name: cfg.SecretRef.Name,
@@ -194,6 +213,9 @@ func (t *Translator) translateConsumerJwtAuthPlugin(tctx *provider.TranslateCont
194213
}, nil
195214
}
196215

216+
if cfg.SecretRef == nil {
217+
return nil, fmt.Errorf("jwt-auth: either value or secretRef must be specified")
218+
}
197219
sec := tctx.Secrets[k8stypes.NamespacedName{
198220
Namespace: consumerNamespace,
199221
Name: cfg.SecretRef.Name,
@@ -251,6 +273,9 @@ func (t *Translator) translateConsumerHMACAuthPlugin(tctx *provider.TranslateCon
251273
}, nil
252274
}
253275

276+
if cfg.SecretRef == nil {
277+
return nil, fmt.Errorf("hmac-auth: either value or secretRef must be specified")
278+
}
254279
sec := tctx.Secrets[k8stypes.NamespacedName{
255280
Namespace: consumerNamespace,
256281
Name: cfg.SecretRef.Name,
@@ -357,6 +382,9 @@ func (t *Translator) translateConsumerLDAPAuthPlugin(tctx *provider.TranslateCon
357382
}, nil
358383
}
359384

385+
if cfg.SecretRef == nil {
386+
return nil, fmt.Errorf("ldap-auth: either value or secretRef must be specified")
387+
}
360388
sec := tctx.Secrets[k8stypes.NamespacedName{
361389
Namespace: consumerNamespace,
362390
Name: cfg.SecretRef.Name,

0 commit comments

Comments
 (0)