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 8610842f feat: support secretRef in v1alpha1 plugin configuration
(#2855)
8610842f is described below
commit 8610842f6f122ccdbd711338596a8d79bcc029fc
Author: AlinsRan <[email protected]>
AuthorDate: Wed Sep 9 19:44:29 2026 +0800
feat: support secretRef in v1alpha1 plugin configuration (#2855)
---
api/v1alpha1/pluginconfig_types.go | 8 +
api/v1alpha1/zz_generated.deepcopy.go | 6 +
config/crd/bases/apisix.apache.org_consumers.yaml | 19 +++
.../bases/apisix.apache.org_l4routepolicies.yaml | 19 +++
.../crd/bases/apisix.apache.org_pluginconfigs.yaml | 19 +++
docs/en/latest/reference/api-reference.md | 1 +
docs/en/latest/reference/example.md | 79 +++++++++
internal/adc/translator/consumer.go | 17 +-
internal/adc/translator/grpcroute.go | 11 +-
internal/adc/translator/httproute.go | 54 ++++---
internal/adc/translator/l4routepolicy_test.go | 10 +-
internal/adc/translator/plugin.go | 57 +++++++
internal/adc/translator/plugin_test.go | 177 +++++++++++++++++++++
internal/adc/translator/policies.go | 22 +--
internal/adc/translator/tcproute.go | 2 +-
internal/adc/translator/tlsroute.go | 2 +-
internal/controller/consumer_controller.go | 2 +-
internal/controller/grpcroute_controller.go | 36 ++++-
internal/controller/httproute_controller.go | 36 ++++-
internal/controller/indexer/indexer.go | 36 +++++
internal/controller/policies.go | 20 ++-
internal/controller/tcproute_controller.go | 23 ++-
internal/controller/tlsroute_controller.go | 23 ++-
internal/controller/udproute_controller.go | 23 ++-
internal/controller/utils.go | 39 +++++
test/e2e/gatewayapi/httproute.go | 101 ++++++++++++
test/e2e/gatewayapi/tcproute.go | 40 +++++
27 files changed, 812 insertions(+), 70 deletions(-)
diff --git a/api/v1alpha1/pluginconfig_types.go
b/api/v1alpha1/pluginconfig_types.go
index 600cb9a4..a341b887 100644
--- a/api/v1alpha1/pluginconfig_types.go
+++ b/api/v1alpha1/pluginconfig_types.go
@@ -18,6 +18,7 @@
package v1alpha1
import (
+ corev1 "k8s.io/api/core/v1"
apiextensionsv1
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -57,6 +58,13 @@ type Plugin struct {
Name string `json:"name" yaml:"name"`
// Config is plugin configuration details.
Config apiextensionsv1.JSON `json:"config,omitempty"
yaml:"config,omitempty"`
+ // SecretRef references a Secret in the same namespace holding
sensitive parts of
+ // the plugin configuration, so they do not have to be written in
`config`.
+ // Each Secret key is a dot separated path into the configuration, so
the key
+ // `session.secret` sets the `secret` field of the `session` object.
Values are
+ // merged as strings and take precedence over the same path in `config`.
+ // +optional
+ SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"
yaml:"secretRef,omitempty"`
}
func init() {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go
b/api/v1alpha1/zz_generated.deepcopy.go
index 4c112170..8b9c5438 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -20,6 +20,7 @@
package v1alpha1
import (
+ corev1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
@@ -908,6 +909,11 @@ func (in *PassiveHealthCheckUnhealthy) DeepCopy()
*PassiveHealthCheckUnhealthy {
func (in *Plugin) DeepCopyInto(out *Plugin) {
*out = *in
in.Config.DeepCopyInto(&out.Config)
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(corev1.LocalObjectReference)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver,
creating a new Plugin.
diff --git a/config/crd/bases/apisix.apache.org_consumers.yaml
b/config/crd/bases/apisix.apache.org_consumers.yaml
index 187bc113..c1851e84 100644
--- a/config/crd/bases/apisix.apache.org_consumers.yaml
+++ b/config/crd/bases/apisix.apache.org_consumers.yaml
@@ -111,6 +111,25 @@ spec:
name:
description: Name is the name of the plugin.
type: string
+ secretRef:
+ description: |-
+ SecretRef references a Secret in the same namespace
holding sensitive parts of
+ the plugin configuration, so they do not have to be
written in `config`.
+ Each Secret key is a dot separated path into the
configuration, so the key
+ `session.secret` sets the `secret` field of the
`session` object. Values are
+ merged as strings and take precedence over the same
path in `config`.
+ properties:
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to
backwards compatibility is
+ allowed to be empty. Instances of this type with
an empty value here are
+ almost certainly wrong.
+ More info:
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
required:
- name
type: object
diff --git a/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
b/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
index 752086e0..b9bda392 100644
--- a/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
+++ b/config/crd/bases/apisix.apache.org_l4routepolicies.yaml
@@ -54,6 +54,25 @@ spec:
name:
description: Name is the name of the plugin.
type: string
+ secretRef:
+ description: |-
+ SecretRef references a Secret in the same namespace
holding sensitive parts of
+ the plugin configuration, so they do not have to be
written in `config`.
+ Each Secret key is a dot separated path into the
configuration, so the key
+ `session.secret` sets the `secret` field of the
`session` object. Values are
+ merged as strings and take precedence over the same
path in `config`.
+ properties:
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to
backwards compatibility is
+ allowed to be empty. Instances of this type with
an empty value here are
+ almost certainly wrong.
+ More info:
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
required:
- name
type: object
diff --git a/config/crd/bases/apisix.apache.org_pluginconfigs.yaml
b/config/crd/bases/apisix.apache.org_pluginconfigs.yaml
index 0891f5e8..f0bee28e 100644
--- a/config/crd/bases/apisix.apache.org_pluginconfigs.yaml
+++ b/config/crd/bases/apisix.apache.org_pluginconfigs.yaml
@@ -52,6 +52,25 @@ spec:
name:
description: Name is the name of the plugin.
type: string
+ secretRef:
+ description: |-
+ SecretRef references a Secret in the same namespace
holding sensitive parts of
+ the plugin configuration, so they do not have to be
written in `config`.
+ Each Secret key is a dot separated path into the
configuration, so the key
+ `session.secret` sets the `secret` field of the
`session` object. Values are
+ merged as strings and take precedence over the same
path in `config`.
+ properties:
+ name:
+ default: ""
+ description: |-
+ Name of the referent.
+ This field is effectively required, but due to
backwards compatibility is
+ allowed to be empty. Instances of this type with
an empty value here are
+ almost certainly wrong.
+ More info:
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
required:
- name
type: object
diff --git a/docs/en/latest/reference/api-reference.md
b/docs/en/latest/reference/api-reference.md
index 09b6fd83..84cf404b 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -567,6 +567,7 @@ _Appears in:_
| --- | --- |
| `name` _string_ | Name is the name of the plugin. |
| `config`
_[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#json-v1-apiextensions-k8s-io)_
| Config is plugin configuration details. |
+| `secretRef`
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#localobjectreference-v1-core)_
| SecretRef references a Secret in the same namespace holding sensitive parts
of the plugin configuration, so they do not have to be written in `config`.
Each Secret key is a dot separated path into the configuration, so the key
`session.secret` sets the `secret` field of the `session` object. Values are
merged as strings and take precedence over the s [...]
_Appears in:_
diff --git a/docs/en/latest/reference/example.md
b/docs/en/latest/reference/example.md
index 1656521d..18be93c0 100644
--- a/docs/en/latest/reference/example.md
+++ b/docs/en/latest/reference/example.md
@@ -1118,6 +1118,85 @@ spec:
</Tabs>
+## Configure Plugin Secrets
+
+To keep sensitive plugin configuration in a Secret instead of in the resource,
reference the
+Secret from the plugin. Each Secret key is a dot separated path into the
plugin configuration,
+so `session.secret` sets the `secret` field of the `session` object. Values
are merged as
+strings and take precedence over the same path in `config`, so keep numeric
and boolean fields
+in `config`. The Secret must be in the same namespace as the resource that
references it.
+
+<Tabs
+groupId="k8s-api"
+defaultValue="gateway"
+values={[
+{label: 'Gateway API', value: 'gateway'},
+{label: 'APISIX CRD', value: 'apisix-crd'}
+]}>
+
+<TabItem value="gateway">
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ namespace: ingress-apisix
+ name: oidc-credentials
+stringData:
+ client_id: "my-client"
+ client_secret: "s3cr3t"
+ session.secret: "8f2a6c1d9e4b7a30"
+---
+apiVersion: apisix.apache.org/v1alpha1
+kind: PluginConfig
+metadata:
+ namespace: ingress-apisix
+ name: oidc
+spec:
+ plugins:
+ - name: openid-connect
+ secretRef:
+ name: oidc-credentials
+ config:
+ discovery: https://idp.example.com/.well-known/openid-configuration
+ scope: openid profile
+```
+
+</TabItem>
+
+<TabItem value="apisix-crd">
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ namespace: ingress-apisix
+ name: oidc-credentials
+stringData:
+ client_id: "my-client"
+ client_secret: "s3cr3t"
+ session.secret: "8f2a6c1d9e4b7a30"
+---
+apiVersion: apisix.apache.org/v2
+kind: ApisixPluginConfig
+metadata:
+ namespace: ingress-apisix
+ name: oidc
+spec:
+ ingressClassName: apisix
+ plugins:
+ - name: openid-connect
+ enable: true
+ secretRef: oidc-credentials
+ config:
+ discovery: https://idp.example.com/.well-known/openid-configuration
+ scope: openid profile
+```
+
+</TabItem>
+
+</Tabs>
+
## Configure Gateway Access Information
These configurations allow Ingress Controller users to access the gateway.
diff --git a/internal/adc/translator/consumer.go
b/internal/adc/translator/consumer.go
index 0d73b483..d7e638be 100644
--- a/internal/adc/translator/consumer.go
+++ b/internal/adc/translator/consumer.go
@@ -77,17 +77,14 @@ func (t *Translator) TranslateConsumerV1alpha1(tctx
*provider.TranslateContext,
consumer.Labels = label.GenLabelWithObjectLabels(consumerV)
plugins := adctypes.Plugins{}
for _, plugin := range consumerV.Spec.Plugins {
- pluginName := plugin.Name
- pluginConfig := make(map[string]any)
- if len(plugin.Config.Raw) > 0 {
- if err := json.Unmarshal(plugin.Config.Raw,
&pluginConfig); err != nil {
- t.Log.Error(err, "failed to unmarshal plugin
config",
- "consumer",
consumerV.Namespace+"/"+consumerV.Name,
- "plugin", plugin.Name)
- continue
- }
+ pluginConfig, err := renderPluginConfig(plugin,
consumerV.Namespace, tctx.Secrets)
+ if err != nil {
+ t.Log.Error(err, "failed to render plugin config",
+ "consumer",
consumerV.Namespace+"/"+consumerV.Name,
+ "plugin", plugin.Name)
+ continue
}
- plugins[pluginName] = pluginConfig
+ plugins[plugin.Name] = pluginConfig
}
consumer.Plugins = plugins
result.Consumers = append(result.Consumers, consumer)
diff --git a/internal/adc/translator/grpcroute.go
b/internal/adc/translator/grpcroute.go
index 53cd591a..89f31355 100644
--- a/internal/adc/translator/grpcroute.go
+++ b/internal/adc/translator/grpcroute.go
@@ -39,7 +39,7 @@ func (t *Translator) fillPluginsFromGRPCRouteFilters(
namespace string,
filters []gatewayv1.GRPCRouteFilter,
tctx *provider.TranslateContext,
-) {
+) error {
for _, filter := range filters {
switch filter.Type {
case gatewayv1.GRPCRouteFilterRequestHeaderModifier:
@@ -49,9 +49,12 @@ func (t *Translator) fillPluginsFromGRPCRouteFilters(
case gatewayv1.GRPCRouteFilterResponseHeaderModifier:
t.fillPluginFromHTTPResponseHeaderFilter(plugins,
filter.ResponseHeaderModifier)
case gatewayv1.GRPCRouteFilterExtensionRef:
- t.fillPluginFromExtensionRef(plugins, namespace,
filter.ExtensionRef, tctx)
+ if err := t.fillPluginFromExtensionRef(plugins,
namespace, filter.ExtensionRef, tctx); err != nil {
+ return err
+ }
}
}
+ return nil
}
func calculateGRPCRoutePriority(match *gatewayv1.GRPCRouteMatch, ruleIndex
int, hosts []string) uint64 {
@@ -287,7 +290,9 @@ func (t *Translator) TranslateGRPCRoute(tctx
*provider.TranslateContext, grpcRou
}
}
- t.fillPluginsFromGRPCRouteFilters(service.Plugins,
grpcRoute.GetNamespace(), rule.Filters, tctx)
+ if err := t.fillPluginsFromGRPCRouteFilters(service.Plugins,
grpcRoute.GetNamespace(), rule.Filters, tctx); err != nil {
+ return nil, err
+ }
matches := rule.Matches
if len(matches) == 0 {
diff --git a/internal/adc/translator/httproute.go
b/internal/adc/translator/httproute.go
index 82f48256..d933a958 100644
--- a/internal/adc/translator/httproute.go
+++ b/internal/adc/translator/httproute.go
@@ -46,7 +46,7 @@ func (t *Translator) fillPluginsFromHTTPRouteFilters(
filters []gatewayv1.HTTPRouteFilter,
matches []gatewayv1.HTTPRouteMatch,
tctx *provider.TranslateContext,
-) {
+) error {
for _, filter := range filters {
switch filter.Type {
case gatewayv1.HTTPRouteFilterRequestHeaderModifier:
@@ -60,38 +60,42 @@ func (t *Translator) fillPluginsFromHTTPRouteFilters(
case gatewayv1.HTTPRouteFilterResponseHeaderModifier:
t.fillPluginFromHTTPResponseHeaderFilter(plugins,
filter.ResponseHeaderModifier)
case gatewayv1.HTTPRouteFilterExtensionRef:
- t.fillPluginFromExtensionRef(plugins, namespace,
filter.ExtensionRef, tctx)
+ if err := t.fillPluginFromExtensionRef(plugins,
namespace, filter.ExtensionRef, tctx); err != nil {
+ return err
+ }
case gatewayv1.HTTPRouteFilterCORS:
t.fillPluginFromHTTPCORSFilter(plugins, filter.CORS)
}
}
+ return nil
}
-func (t *Translator) fillPluginFromExtensionRef(plugins adctypes.Plugins,
namespace string, extensionRef *gatewayv1.LocalObjectReference, tctx
*provider.TranslateContext) {
+func (t *Translator) fillPluginFromExtensionRef(plugins adctypes.Plugins,
namespace string, extensionRef *gatewayv1.LocalObjectReference, tctx
*provider.TranslateContext) error {
if extensionRef == nil {
- return
+ return nil
}
- if extensionRef.Kind == internaltypes.KindPluginConfig {
- pluginconfig := tctx.PluginConfigs[types.NamespacedName{
- Namespace: namespace,
- Name: string(extensionRef.Name),
- }]
- if pluginconfig == nil {
- return
- }
- for _, plugin := range pluginconfig.Spec.Plugins {
- pluginName := plugin.Name
- pluginconfig := make(map[string]any)
- if len(plugin.Config.Raw) > 0 {
- if err := json.Unmarshal(plugin.Config.Raw,
&pluginconfig); err != nil {
- t.Log.Error(err, "plugin config
unmarshal failed", "plugin", plugin.Name)
- continue
- }
- }
- plugins[pluginName] = pluginconfig
+ if extensionRef.Kind != internaltypes.KindPluginConfig {
+ return nil
+ }
+ pluginconfig := tctx.PluginConfigs[types.NamespacedName{
+ Namespace: namespace,
+ Name: string(extensionRef.Name),
+ }]
+ if pluginconfig == nil {
+ return nil
+ }
+ names := make([]string, 0, len(pluginconfig.Spec.Plugins))
+ for _, plugin := range pluginconfig.Spec.Plugins {
+ config, err := renderPluginConfig(plugin, namespace,
tctx.Secrets)
+ if err != nil {
+ return err
}
- t.Log.V(1).Info("fill plugin from extension ref", "plugins",
plugins)
+ plugins[plugin.Name] = config
+ names = append(names, plugin.Name)
}
+ // The rendered configuration may hold Secret data, so log the plugin
names only.
+ t.Log.V(1).Info("fill plugin from extension ref", "pluginConfig",
string(extensionRef.Name), "plugins", names)
+ return nil
}
func (t *Translator) fillPluginFromURLRewriteFilter(plugins adctypes.Plugins,
urlRewrite *gatewayv1.HTTPURLRewriteFilter, matches []gatewayv1.HTTPRouteMatch)
{
@@ -706,7 +710,9 @@ func (t *Translator) TranslateHTTPRoute(tctx
*provider.TranslateContext, httpRou
enableWebsocket, _ := t.translateBackendsToUpstreams(tctx,
rule, httpRoute, service)
- t.fillPluginsFromHTTPRouteFilters(service.Plugins,
httpRoute.GetNamespace(), rule.Filters, rule.Matches, tctx)
+ if err := t.fillPluginsFromHTTPRouteFilters(service.Plugins,
httpRoute.GetNamespace(), rule.Filters, rule.Matches, tctx); err != nil {
+ return nil, err
+ }
matches := rule.Matches
if len(matches) == 0 {
diff --git a/internal/adc/translator/l4routepolicy_test.go
b/internal/adc/translator/l4routepolicy_test.go
index f172e534..289dab98 100644
--- a/internal/adc/translator/l4routepolicy_test.go
+++ b/internal/adc/translator/l4routepolicy_test.go
@@ -74,7 +74,7 @@ func TestAttachL4RoutePolicyPlugins_AttachesMatchingPolicy(t
*testing.T) {
}
plugins := adctypes.Plugins{}
- tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins)
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins, nil)
assert.Len(t, plugins, 2)
assert.Contains(t, plugins, "limit-conn")
@@ -97,7 +97,7 @@ func TestAttachL4RoutePolicyPlugins_NoMatchOnKind(t
*testing.T) {
plugins := adctypes.Plugins{}
// Looking for TCPRoute, but policy targets UDPRoute — should not match.
- tr.AttachL4RoutePolicyPlugins(policies, "default", "my-udp-route",
"TCPRoute", plugins)
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-udp-route",
"TCPRoute", plugins, nil)
assert.Empty(t, plugins)
}
@@ -115,7 +115,7 @@ func TestAttachL4RoutePolicyPlugins_NoMatchOnNamespace(t
*testing.T) {
plugins := adctypes.Plugins{}
// Route is in "default" namespace, policy is in "other-ns" — should
not match.
- tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins)
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins, nil)
assert.Empty(t, plugins)
}
@@ -130,7 +130,7 @@ func TestAttachL4RoutePolicyPlugins_EmptyPlugins(t
*testing.T) {
}
plugins := adctypes.Plugins{}
- tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins)
+ tr.AttachL4RoutePolicyPlugins(policies, "default", "my-tcp-route",
"TCPRoute", plugins, nil)
assert.Empty(t, plugins)
}
@@ -138,6 +138,6 @@ func TestAttachL4RoutePolicyPlugins_EmptyPlugins(t
*testing.T) {
func TestAttachL4RoutePolicyPlugins_EmptyPolicies(t *testing.T) {
tr := NewTranslator(logr.Discard(), "")
plugins := adctypes.Plugins{}
- tr.AttachL4RoutePolicyPlugins(nil, "default", "my-tcp-route",
"TCPRoute", plugins)
+ tr.AttachL4RoutePolicyPlugins(nil, "default", "my-tcp-route",
"TCPRoute", plugins, nil)
assert.Empty(t, plugins)
}
diff --git a/internal/adc/translator/plugin.go
b/internal/adc/translator/plugin.go
new file mode 100644
index 00000000..d16180fc
--- /dev/null
+++ b/internal/adc/translator/plugin.go
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "encoding/json"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/types"
+
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
+)
+
+// renderPluginConfig renders the configuration of an
apisix.apache.org/v1alpha1 Plugin.
+// The data of the referenced Secret is merged over spec.config, with each
Secret key
+// read as a dot separated path so that `session.secret` nests under `session`.
+func renderPluginConfig(plugin v1alpha1.Plugin, namespace string, secrets
map[types.NamespacedName]*corev1.Secret) (map[string]any, error) {
+ config := make(map[string]any)
+ if len(plugin.Config.Raw) > 0 {
+ if err := json.Unmarshal(plugin.Config.Raw, &config); err !=
nil {
+ return nil, fmt.Errorf("failed to unmarshal config of
plugin %s: %w", plugin.Name, err)
+ }
+ }
+ // A literal `config: null` unmarshals to a nil map, which serializes
back to
+ // null and is rejected by most APISIX plugins; normalize it to an
empty object.
+ if config == nil {
+ config = make(map[string]any)
+ }
+ if plugin.SecretRef == nil || plugin.SecretRef.Name == "" {
+ return config, nil
+ }
+ secret, ok := secrets[types.NamespacedName{Namespace: namespace, Name:
plugin.SecretRef.Name}]
+ if !ok || secret == nil {
+ return nil, fmt.Errorf("secret %s/%s referenced by plugin %s
not found", namespace, plugin.SecretRef.Name, plugin.Name)
+ }
+ for key, value := range secret.Data {
+ pkgutils.InsertKeyInMap(key, string(value), config)
+ }
+ return config, nil
+}
diff --git a/internal/adc/translator/plugin_test.go
b/internal/adc/translator/plugin_test.go
new file mode 100644
index 00000000..6881ea42
--- /dev/null
+++ b/internal/adc/translator/plugin_test.go
@@ -0,0 +1,177 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package translator
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "github.com/go-logr/logr/funcr"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ apiextensionsv1
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ "github.com/apache/apisix-ingress-controller/internal/provider"
+ internaltypes
"github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+func TestRenderPluginConfig_NoSecretRef(t *testing.T) {
+ config, err := renderPluginConfig(v1alpha1.Plugin{
+ Name: "response-rewrite",
+ Config: apiextensionsv1.JSON{Raw: []byte(`{"body":"hello"}`)},
+ }, "default", nil)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{"body": "hello"}, config)
+}
+
+func TestRenderPluginConfig_NullConfigBecomesEmptyObject(t *testing.T) {
+ config, err := renderPluginConfig(v1alpha1.Plugin{
+ Name: "prometheus",
+ Config: apiextensionsv1.JSON{Raw: []byte(`null`)},
+ }, "default", nil)
+ require.NoError(t, err)
+ assert.Equal(t, map[string]any{}, config)
+}
+
+func TestRenderPluginConfig_SecretKeysAreNestedAndWin(t *testing.T) {
+ secrets := map[types.NamespacedName]*corev1.Secret{
+ {Namespace: "default", Name: "oidc-credentials"}: {
+ Data: map[string][]byte{
+ "client_id": []byte("my-client"),
+ "client_secret": []byte("s3cr3t"),
+ "session.secret": []byte("8f2a"),
+ },
+ },
+ }
+ config, err := renderPluginConfig(v1alpha1.Plugin{
+ Name: "openid-connect",
+ Config: apiextensionsv1.JSON{Raw:
[]byte(`{"client_id":"placeholder","scope":"openid profile"}`)},
+ SecretRef: &corev1.LocalObjectReference{Name:
"oidc-credentials"},
+ }, "default", secrets)
+ require.NoError(t, err)
+ assert.Equal(t, "my-client", config["client_id"])
+ assert.Equal(t, "s3cr3t", config["client_secret"])
+ assert.Equal(t, "openid profile", config["scope"])
+ assert.Equal(t, map[string]any{"secret": "8f2a"}, config["session"])
+}
+
+func TestRenderPluginConfig_MissingSecretIsAnError(t *testing.T) {
+ config, err := renderPluginConfig(v1alpha1.Plugin{
+ Name: "openid-connect",
+ SecretRef: &corev1.LocalObjectReference{Name:
"oidc-credentials"},
+ }, "default", nil)
+ assert.ErrorContains(t, err, "default/oidc-credentials")
+ assert.Nil(t, config)
+}
+
+func TestRenderPluginConfig_MalformedConfigIsAnError(t *testing.T) {
+ config, err := renderPluginConfig(v1alpha1.Plugin{
+ Name: "ip-restriction",
+ Config: apiextensionsv1.JSON{Raw: []byte(`["10.0.0.0/8"]`)},
+ }, "default", nil)
+ assert.ErrorContains(t, err, "ip-restriction")
+ assert.Nil(t, config)
+}
+
+func TestFillPluginFromExtensionRef_ResolvesSecretRef(t *testing.T) {
+ translator := NewTranslator(logr.Discard(), "")
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+ tctx.PluginConfigs[types.NamespacedName{Namespace: "default", Name:
"oidc"}] = &v1alpha1.PluginConfig{
+ ObjectMeta: metav1.ObjectMeta{Name: "oidc", Namespace:
"default"},
+ Spec: v1alpha1.PluginConfigSpec{
+ Plugins: []v1alpha1.Plugin{{
+ Name: "openid-connect",
+ Config: apiextensionsv1.JSON{Raw:
[]byte(`{"scope":"openid profile"}`)},
+ SecretRef: &corev1.LocalObjectReference{Name:
"oidc-credentials"},
+ }},
+ },
+ }
+ tctx.Secrets[types.NamespacedName{Namespace: "default", Name:
"oidc-credentials"}] = &corev1.Secret{
+ Data: map[string][]byte{"client_secret": []byte("s3cr3t")},
+ }
+
+ plugins := adctypes.Plugins{}
+ require.NoError(t, translator.fillPluginFromExtensionRef(plugins,
"default", &gatewayv1.LocalObjectReference{
+ Kind: internaltypes.KindPluginConfig,
+ Name: "oidc",
+ }, tctx))
+
+ assert.Equal(t, map[string]any{
+ "scope": "openid profile",
+ "client_secret": "s3cr3t",
+ }, plugins["openid-connect"])
+}
+
+func TestFillPluginFromExtensionRef_MissingSecretFailsTranslation(t
*testing.T) {
+ translator := NewTranslator(logr.Discard(), "")
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+ tctx.PluginConfigs[types.NamespacedName{Namespace: "default", Name:
"oidc"}] = &v1alpha1.PluginConfig{
+ ObjectMeta: metav1.ObjectMeta{Name: "oidc", Namespace:
"default"},
+ Spec: v1alpha1.PluginConfigSpec{
+ Plugins: []v1alpha1.Plugin{{
+ Name: "openid-connect",
+ SecretRef: &corev1.LocalObjectReference{Name:
"oidc-credentials"},
+ }},
+ },
+ }
+
+ // The route must not be programmed without the plugin its filter asks
for.
+ err := translator.fillPluginFromExtensionRef(adctypes.Plugins{},
"default", &gatewayv1.LocalObjectReference{
+ Kind: internaltypes.KindPluginConfig,
+ Name: "oidc",
+ }, tctx)
+ assert.ErrorContains(t, err, "default/oidc-credentials")
+}
+
+func TestFillPluginFromExtensionRef_DoesNotLogSecretValues(t *testing.T) {
+ var logged strings.Builder
+ logger := funcr.New(func(prefix, args string) {
+ logged.WriteString(args)
+ }, funcr.Options{Verbosity: 10})
+
+ translator := NewTranslator(logger, "")
+ tctx := provider.NewDefaultTranslateContext(context.Background())
+ tctx.PluginConfigs[types.NamespacedName{Namespace: "default", Name:
"oidc"}] = &v1alpha1.PluginConfig{
+ ObjectMeta: metav1.ObjectMeta{Name: "oidc", Namespace:
"default"},
+ Spec: v1alpha1.PluginConfigSpec{
+ Plugins: []v1alpha1.Plugin{{
+ Name: "openid-connect",
+ SecretRef: &corev1.LocalObjectReference{Name:
"oidc-credentials"},
+ }},
+ },
+ }
+ tctx.Secrets[types.NamespacedName{Namespace: "default", Name:
"oidc-credentials"}] = &corev1.Secret{
+ Data: map[string][]byte{"client_secret": []byte("s3cr3t")},
+ }
+
+ require.NoError(t,
translator.fillPluginFromExtensionRef(adctypes.Plugins{}, "default",
&gatewayv1.LocalObjectReference{
+ Kind: internaltypes.KindPluginConfig,
+ Name: "oidc",
+ }, tctx))
+
+ assert.Contains(t, logged.String(), "openid-connect")
+ assert.NotContains(t, logged.String(), "s3cr3t")
+}
diff --git a/internal/adc/translator/policies.go
b/internal/adc/translator/policies.go
index 003c6cc4..aad0ff3f 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -18,8 +18,6 @@
package translator
import (
- "encoding/json"
-
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
@@ -230,6 +228,7 @@ func (t *Translator) AttachL4RoutePolicyPlugins(
policies map[types.NamespacedName]*v1alpha1.L4RoutePolicy,
routeNamespace, routeName, routeKind string,
plugins adctypes.Plugins,
+ secrets map[types.NamespacedName]*corev1.Secret,
) {
if len(policies) == 0 {
return
@@ -253,25 +252,18 @@ func (t *Translator) AttachL4RoutePolicyPlugins(
if ref.SectionName != nil && *ref.SectionName != "" {
continue
}
- t.mergeL4PolicyPlugins(policy, plugins)
+ t.mergeL4PolicyPlugins(policy, plugins, secrets)
return
}
}
}
-func (t *Translator) mergeL4PolicyPlugins(policy *v1alpha1.L4RoutePolicy,
plugins adctypes.Plugins) {
+func (t *Translator) mergeL4PolicyPlugins(policy *v1alpha1.L4RoutePolicy,
plugins adctypes.Plugins, secrets map[types.NamespacedName]*corev1.Secret) {
for _, plugin := range policy.Spec.Plugins {
- cfg := make(map[string]any)
- if len(plugin.Config.Raw) > 0 {
- if err := json.Unmarshal(plugin.Config.Raw, &cfg); err
!= nil {
- t.Log.Error(err, "failed to unmarshal
L4RoutePolicy plugin config", "plugin", plugin.Name, "policy", policy.Name)
- continue
- }
- }
- // A literal `config: null` unmarshals to a nil map, which
serializes back to
- // null and is rejected by most APISIX plugins; normalize it to
an empty object.
- if cfg == nil {
- cfg = map[string]any{}
+ cfg, err := renderPluginConfig(plugin, policy.Namespace,
secrets)
+ if err != nil {
+ t.Log.Error(err, "failed to render L4RoutePolicy plugin
config", "plugin", plugin.Name, "policy", policy.Name)
+ continue
}
plugins[plugin.Name] = cfg
}
diff --git a/internal/adc/translator/tcproute.go
b/internal/adc/translator/tcproute.go
index 605b609e..7de4a67e 100644
--- a/internal/adc/translator/tcproute.go
+++ b/internal/adc/translator/tcproute.go
@@ -98,7 +98,7 @@ func (t *Translator) buildL4StreamRoutes(tctx
*provider.TranslateContext, namesp
// Attach L4RoutePolicy plugins at the stream_route level: the
APISIX stream proxy
// applies plugins from the stream_route, not from the service.
streamRoute.Plugins = make(adctypes.Plugins)
- t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, namespace,
name, routeKind, streamRoute.Plugins)
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies, namespace,
name, routeKind, streamRoute.Plugins, tctx.Secrets)
streamRoutes = append(streamRoutes, streamRoute)
}
return streamRoutes
diff --git a/internal/adc/translator/tlsroute.go
b/internal/adc/translator/tlsroute.go
index 413f4790..8d1fd0a6 100644
--- a/internal/adc/translator/tlsroute.go
+++ b/internal/adc/translator/tlsroute.go
@@ -154,7 +154,7 @@ func (t *Translator) TranslateTLSRoute(tctx
*provider.TranslateContext, tlsRoute
// applies plugins from the stream_route, not from the
service. With multiple SNIs
// each stream_route carries its own copy of the
plugins.
streamRoute.Plugins = make(adctypes.Plugins)
- t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies,
tlsRoute.Namespace, tlsRoute.Name, "TLSRoute", streamRoute.Plugins)
+ t.AttachL4RoutePolicyPlugins(tctx.L4RoutePolicies,
tlsRoute.Namespace, tlsRoute.Name, "TLSRoute", streamRoute.Plugins,
tctx.Secrets)
service.StreamRoutes = append(service.StreamRoutes,
streamRoute)
}
diff --git a/internal/controller/consumer_controller.go
b/internal/controller/consumer_controller.go
index ece20ec1..f3fb80a9 100644
--- a/internal/controller/consumer_controller.go
+++ b/internal/controller/consumer_controller.go
@@ -290,7 +290,7 @@ func (r *ConsumerReconciler) processSpec(ctx
context.Context, tctx *provider.Tra
}] = &secret
}
- return nil
+ return loadPluginSecrets(ctx, r.Client, tctx, consumer.GetNamespace(),
consumer.Spec.Plugins)
}
func (r *ConsumerReconciler) updateStatus(consumer *v1alpha1.Consumer, err
error) {
diff --git a/internal/controller/grpcroute_controller.go
b/internal/controller/grpcroute_controller.go
index bbc45265..df50f135 100644
--- a/internal/controller/grpcroute_controller.go
+++ b/internal/controller/grpcroute_controller.go
@@ -45,6 +45,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/types"
"github.com/apache/apisix-ingress-controller/internal/utils"
+ pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
)
// GRPCRouteReconciler reconciles a GatewayClass object.
@@ -68,13 +69,23 @@ func (r *GRPCRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
bdr := ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1.GRPCRoute{}).
- WithEventFilter(predicate.GenerationChangedPredicate{}).
+ // A Secret carries no generation, so
GenerationChangedPredicate would drop its
+ // updates and a plugin would keep the Secret data read at the
last spec change.
+ WithEventFilter(
+ predicate.Or(
+ predicate.GenerationChangedPredicate{},
+
predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
+ ),
+ ).
Watches(&discoveryv1.EndpointSlice{},
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesByServiceRef),
).
Watches(&v1alpha1.PluginConfig{},
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesByExtensionRef),
).
+ Watches(&corev1.Secret{},
+
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForSecret),
+ ).
Watches(&gatewayv1.Gateway{},
handler.EnqueueRequestsFromMapFunc(r.listGRPCRoutesForGateway),
builder.WithPredicates(
@@ -505,6 +516,10 @@ func (r *GRPCRouteReconciler) processGRPCRoute(tctx
*provider.TranslateContext,
Namespace: grpcroute.GetNamespace(),
Name:
string(filter.ExtensionRef.Name),
}] = pluginconfig
+ if err := loadPluginSecrets(tctx, r.Client,
tctx, grpcroute.GetNamespace(), pluginconfig.Spec.Plugins); err != nil {
+ terror = err
+ continue
+ }
}
}
for _, backend := range rule.BackendRefs {
@@ -602,3 +617,22 @@ func (r *GRPCRouteReconciler)
listGRPCRoutesForReferenceGrant(ctx context.Contex
}
return requests
}
+
+// listGRPCRoutesForSecret maps a Secret to the GRPCRoutes that reference,
through a PluginConfig
+// extension filter, a plugin configured with that Secret.
+func (r *GRPCRouteReconciler) listGRPCRoutesForSecret(ctx context.Context, obj
client.Object) []reconcile.Request {
+ secret, ok := obj.(*corev1.Secret)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to Secret")
+ return nil
+ }
+ var requests []reconcile.Request
+ for _, pcRef := range ListRequests(ctx, r.Client, r.Log,
&v1alpha1.PluginConfigList{}, client.MatchingFields{
+ indexer.SecretIndexRef:
indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()),
+ }) {
+ requests = append(requests, ListRequests(ctx, r.Client, r.Log,
&gatewayv1.GRPCRouteList{}, client.MatchingFields{
+ indexer.ExtensionRef:
indexer.GenIndexKey(pcRef.Namespace, pcRef.Name),
+ })...)
+ }
+ return pkgutils.DedupComparable(requests)
+}
diff --git a/internal/controller/httproute_controller.go
b/internal/controller/httproute_controller.go
index 26b94683..285899b8 100644
--- a/internal/controller/httproute_controller.go
+++ b/internal/controller/httproute_controller.go
@@ -48,6 +48,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/provider"
"github.com/apache/apisix-ingress-controller/internal/types"
"github.com/apache/apisix-ingress-controller/internal/utils"
+ pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
)
// HTTPRouteReconciler reconciles a GatewayClass object.
@@ -71,13 +72,23 @@ func (r *HTTPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
bdr := ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1.HTTPRoute{}).
- WithEventFilter(predicate.GenerationChangedPredicate{}).
+ // A Secret carries no generation, so
GenerationChangedPredicate would drop its
+ // updates and a plugin would keep the Secret data read at the
last spec change.
+ WithEventFilter(
+ predicate.Or(
+ predicate.GenerationChangedPredicate{},
+
predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
+ ),
+ ).
Watches(&discoveryv1.EndpointSlice{},
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByServiceRef),
).
Watches(&v1alpha1.PluginConfig{},
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesByExtensionRef),
).
+ Watches(&corev1.Secret{},
+
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForSecret),
+ ).
Watches(&gatewayv1.Gateway{},
handler.EnqueueRequestsFromMapFunc(r.listHTTPRoutesForGateway),
builder.WithPredicates(
@@ -590,6 +601,10 @@ func (r *HTTPRouteReconciler) processHTTPRoute(tctx
*provider.TranslateContext,
Namespace: httpRoute.GetNamespace(),
Name:
string(filter.ExtensionRef.Name),
}] = pluginconfig
+ if err := loadPluginSecrets(tctx, r.Client,
tctx, httpRoute.GetNamespace(), pluginconfig.Spec.Plugins); err != nil {
+ terror = err
+ continue
+ }
}
}
for _, backend := range rule.BackendRefs {
@@ -719,3 +734,22 @@ func (r *HTTPRouteReconciler)
listHTTPRoutesForReferenceGrant(ctx context.Contex
}
return requests
}
+
+// listHTTPRoutesForSecret maps a Secret to the HTTPRoutes that reference,
through a PluginConfig
+// extension filter, a plugin configured with that Secret.
+func (r *HTTPRouteReconciler) listHTTPRoutesForSecret(ctx context.Context, obj
client.Object) []reconcile.Request {
+ secret, ok := obj.(*corev1.Secret)
+ if !ok {
+ r.Log.Error(fmt.Errorf("unexpected object type"), "failed to
convert object to Secret")
+ return nil
+ }
+ var requests []reconcile.Request
+ for _, pcRef := range ListRequests(ctx, r.Client, r.Log,
&v1alpha1.PluginConfigList{}, client.MatchingFields{
+ indexer.SecretIndexRef:
indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()),
+ }) {
+ requests = append(requests, ListRequests(ctx, r.Client, r.Log,
&gatewayv1.HTTPRouteList{}, client.MatchingFields{
+ indexer.ExtensionRef:
indexer.GenIndexKey(pcRef.Namespace, pcRef.Name),
+ })...)
+ }
+ return pkgutils.DedupComparable(requests)
+}
diff --git a/internal/controller/indexer/indexer.go
b/internal/controller/indexer/indexer.go
index 7c5eaaf7..716e44e2 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -63,6 +63,7 @@ func SetupAPIv1alpha1Indexer(mgr ctrl.Manager) error {
&v1alpha1.Consumer{}: setupConsumerIndexer,
&v1alpha1.GatewayProxy{}: setupGatewayProxyIndexer,
&v1alpha1.L4RoutePolicy{}: setupL4RoutePolicyIndexer,
+ &v1alpha1.PluginConfig{}: setupPluginConfigIndexer,
} {
installed, err := utils.HasAPIResource(mgr, resource)
if err != nil {
@@ -258,6 +259,7 @@ func ConsumerSecretIndexFunc(rawObj client.Object) []string
{
key := GenIndexKey(ns, credential.SecretRef.Name)
secretKeys = append(secretKeys, key)
}
+ secretKeys = append(secretKeys,
PluginSecretIndexKeys(consumer.GetNamespace(), consumer.Spec.Plugins)...)
return secretKeys
}
@@ -518,9 +520,43 @@ func setupL4RoutePolicyIndexer(mgr ctrl.Manager) error {
); err != nil {
return err
}
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &v1alpha1.L4RoutePolicy{},
+ SecretIndexRef,
+ func(obj client.Object) []string {
+ return PluginSecretIndexKeys(obj.GetNamespace(),
obj.(*v1alpha1.L4RoutePolicy).Spec.Plugins)
+ },
+ ); err != nil {
+ return err
+ }
return nil
}
+func setupPluginConfigIndexer(mgr ctrl.Manager) error {
+ return mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &v1alpha1.PluginConfig{},
+ SecretIndexRef,
+ func(obj client.Object) []string {
+ return PluginSecretIndexKeys(obj.GetNamespace(),
obj.(*v1alpha1.PluginConfig).Spec.Plugins)
+ },
+ )
+}
+
+// PluginSecretIndexKeys returns the index keys of the Secrets referenced by
+// apisix.apache.org/v1alpha1 plugins. Such Secrets are always in the
namespace of the
+// object that declares the plugins.
+func PluginSecretIndexKeys(namespace string, plugins []v1alpha1.Plugin) (keys
[]string) {
+ for _, plugin := range plugins {
+ if plugin.SecretRef == nil || plugin.SecretRef.Name == "" {
+ continue
+ }
+ keys = append(keys, GenIndexKey(namespace,
plugin.SecretRef.Name))
+ }
+ return
+}
+
func IngressClassIndexFunc(rawObj client.Object) []string {
ingressClass := rawObj.(*networkingv1.IngressClass)
if ingressClass.Spec.Controller == "" {
diff --git a/internal/controller/policies.go b/internal/controller/policies.go
index d2152a5d..1cfb5c7f 100644
--- a/internal/controller/policies.go
+++ b/internal/controller/policies.go
@@ -300,12 +300,28 @@ func ProcessL4RoutePolicy(
})
winner := list.Items[0].DeepCopy()
- tctx.L4RoutePolicies[types.NamespacedName{Namespace: winner.Namespace,
Name: winner.Name}] = winner
+ // A policy whose Secrets cannot be read is not attached at all, so a
route is never
+ // programmed with a subset of the plugins the policy asks for.
+ secretErr := loadPluginSecrets(tctx, c, tctx, winner.Namespace,
winner.Spec.Plugins)
+ if secretErr != nil {
+ log.Error(secretErr, "failed to load Secrets referenced by
L4RoutePolicy plugins", "policy", types.NamespacedName{Namespace:
winner.Namespace, Name: winner.Name})
+ } else {
+ tctx.L4RoutePolicies[types.NamespacedName{Namespace:
winner.Namespace, Name: winner.Name}] = winner
+ }
for i := range list.Items {
policy := list.Items[i]
var condition metav1.Condition
- if i == 0 {
+ if i == 0 && secretErr != nil {
+ condition = metav1.Condition{
+ Type:
string(gatewayv1.PolicyConditionAccepted),
+ Status: metav1.ConditionFalse,
+ ObservedGeneration: policy.GetGeneration(),
+ LastTransitionTime: metav1.Now(),
+ Reason:
string(gatewayv1.PolicyReasonInvalid),
+ Message: secretErr.Error(),
+ }
+ } else if i == 0 {
condition = metav1.Condition{
Type:
string(gatewayv1.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
diff --git a/internal/controller/tcproute_controller.go
b/internal/controller/tcproute_controller.go
index 4cf16ebe..4df650a3 100644
--- a/internal/controller/tcproute_controller.go
+++ b/internal/controller/tcproute_controller.go
@@ -67,7 +67,14 @@ func (r *TCPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
bdr := ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1.TCPRoute{}).
- WithEventFilter(predicate.GenerationChangedPredicate{}).
+ // A Secret carries no generation, so
GenerationChangedPredicate would drop its
+ // updates and a plugin would keep the Secret data read at the
last spec change.
+ WithEventFilter(
+ predicate.Or(
+ predicate.GenerationChangedPredicate{},
+
predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
+ ),
+ ).
Watches(&discoveryv1.EndpointSlice{},
handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesByServiceRef),
).
@@ -107,7 +114,10 @@ func (r *TCPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
if r.supportsL4RoutePolicy {
bdr.Watches(&v1alpha1.L4RoutePolicy{},
handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForL4RoutePolicy),
- )
+ ).
+ Watches(&corev1.Secret{},
+
handler.EnqueueRequestsFromMapFunc(r.listTCPRoutesForSecret),
+ )
}
if GetEnableReferenceGrant() {
@@ -573,3 +583,12 @@ func (r *TCPRouteReconciler)
listTCPRoutesForL4RoutePolicy(ctx context.Context,
}
return requests
}
+
+// listTCPRoutesForSecret maps a Secret to the TCPRoutes whose L4RoutePolicy
plugins reference it.
+func (r *TCPRouteReconciler) listTCPRoutesForSecret(ctx context.Context, obj
client.Object) []reconcile.Request {
+ var requests []reconcile.Request
+ for _, policy := range listL4RoutePoliciesForSecret(ctx, r.Client,
r.Log, obj) {
+ requests = append(requests,
r.listTCPRoutesForL4RoutePolicy(ctx, &policy)...)
+ }
+ return pkgutils.DedupComparable(requests)
+}
diff --git a/internal/controller/tlsroute_controller.go
b/internal/controller/tlsroute_controller.go
index c9df0e62..3b23f000 100644
--- a/internal/controller/tlsroute_controller.go
+++ b/internal/controller/tlsroute_controller.go
@@ -67,7 +67,14 @@ func (r *TLSRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
bdr := ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1.TLSRoute{}).
- WithEventFilter(predicate.GenerationChangedPredicate{}).
+ // A Secret carries no generation, so
GenerationChangedPredicate would drop its
+ // updates and a plugin would keep the Secret data read at the
last spec change.
+ WithEventFilter(
+ predicate.Or(
+ predicate.GenerationChangedPredicate{},
+
predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
+ ),
+ ).
Watches(&discoveryv1.EndpointSlice{},
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesByServiceRef),
).
@@ -107,7 +114,10 @@ func (r *TLSRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
if r.supportsL4RoutePolicy {
bdr.Watches(&v1alpha1.L4RoutePolicy{},
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForL4RoutePolicy),
- )
+ ).
+ Watches(&corev1.Secret{},
+
handler.EnqueueRequestsFromMapFunc(r.listTLSRoutesForSecret),
+ )
}
if GetEnableReferenceGrant() {
@@ -565,3 +575,12 @@ func (r *TLSRouteReconciler)
listTLSRoutesForL4RoutePolicy(ctx context.Context,
}
return requests
}
+
+// listTLSRoutesForSecret maps a Secret to the TLSRoutes whose L4RoutePolicy
plugins reference it.
+func (r *TLSRouteReconciler) listTLSRoutesForSecret(ctx context.Context, obj
client.Object) []reconcile.Request {
+ var requests []reconcile.Request
+ for _, policy := range listL4RoutePoliciesForSecret(ctx, r.Client,
r.Log, obj) {
+ requests = append(requests,
r.listTLSRoutesForL4RoutePolicy(ctx, &policy)...)
+ }
+ return pkgutils.DedupComparable(requests)
+}
diff --git a/internal/controller/udproute_controller.go
b/internal/controller/udproute_controller.go
index 2c8a910b..31a3ab2a 100644
--- a/internal/controller/udproute_controller.go
+++ b/internal/controller/udproute_controller.go
@@ -67,7 +67,14 @@ func (r *UDPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
bdr := ctrl.NewControllerManagedBy(mgr).
For(&gatewayv1.UDPRoute{}).
- WithEventFilter(predicate.GenerationChangedPredicate{}).
+ // A Secret carries no generation, so
GenerationChangedPredicate would drop its
+ // updates and a plugin would keep the Secret data read at the
last spec change.
+ WithEventFilter(
+ predicate.Or(
+ predicate.GenerationChangedPredicate{},
+
predicate.NewPredicateFuncs(TypePredicate[*corev1.Secret]()),
+ ),
+ ).
Watches(&discoveryv1.EndpointSlice{},
handler.EnqueueRequestsFromMapFunc(r.listUDPRoutesByServiceRef),
).
@@ -107,7 +114,10 @@ func (r *UDPRouteReconciler) SetupWithManager(mgr
ctrl.Manager) error {
if r.supportsL4RoutePolicy {
bdr.Watches(&v1alpha1.L4RoutePolicy{},
handler.EnqueueRequestsFromMapFunc(r.listUDPRoutesForL4RoutePolicy),
- )
+ ).
+ Watches(&corev1.Secret{},
+
handler.EnqueueRequestsFromMapFunc(r.listUDPRoutesForSecret),
+ )
}
if GetEnableReferenceGrant() {
@@ -573,3 +583,12 @@ func (r *UDPRouteReconciler)
listUDPRoutesForL4RoutePolicy(ctx context.Context,
}
return requests
}
+
+// listUDPRoutesForSecret maps a Secret to the UDPRoutes whose L4RoutePolicy
plugins reference it.
+func (r *UDPRouteReconciler) listUDPRoutesForSecret(ctx context.Context, obj
client.Object) []reconcile.Request {
+ var requests []reconcile.Request
+ for _, policy := range listL4RoutePoliciesForSecret(ctx, r.Client,
r.Log, obj) {
+ requests = append(requests,
r.listUDPRoutesForL4RoutePolicy(ctx, &policy)...)
+ }
+ return pkgutils.DedupComparable(requests)
+}
diff --git a/internal/controller/utils.go b/internal/controller/utils.go
index cf5167b4..deceecdd 100644
--- a/internal/controller/utils.go
+++ b/internal/controller/utils.go
@@ -2041,3 +2041,42 @@ func serviceLoadBalancerAddresses(svc *corev1.Service)
[]string {
}
return addrs
}
+
+// loadPluginSecrets loads the Secrets referenced by
apisix.apache.org/v1alpha1 plugins
+// into the translate context. A plugin may only reference a Secret in the
namespace of
+// the object that declares it.
+func loadPluginSecrets(ctx context.Context, c client.Client, tctx
*provider.TranslateContext, namespace string, plugins []v1alpha1.Plugin) error {
+ for _, plugin := range plugins {
+ if plugin.SecretRef == nil || plugin.SecretRef.Name == "" {
+ continue
+ }
+ secretNN := k8stypes.NamespacedName{Namespace: namespace, Name:
plugin.SecretRef.Name}
+ if _, ok := tctx.Secrets[secretNN]; ok {
+ continue
+ }
+ secret := new(corev1.Secret)
+ if err := c.Get(ctx, secretNN, secret); err != nil {
+ return fmt.Errorf("failed to get Secret %s referenced
by plugin %s: %w", secretNN, plugin.Name, err)
+ }
+ tctx.Secrets[secretNN] = secret
+ }
+ return nil
+}
+
+// listL4RoutePoliciesForSecret returns the L4RoutePolicies whose plugins
reference the
+// given Secret.
+func listL4RoutePoliciesForSecret(ctx context.Context, c client.Client, log
logr.Logger, obj client.Object) []v1alpha1.L4RoutePolicy {
+ secret, ok := obj.(*corev1.Secret)
+ if !ok {
+ log.Error(errors.New("unexpected object type"), "failed to
convert object to Secret")
+ return nil
+ }
+ var list v1alpha1.L4RoutePolicyList
+ if err := c.List(ctx, &list, client.MatchingFields{
+ indexer.SecretIndexRef:
indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()),
+ }); err != nil {
+ log.Error(err, "failed to list L4RoutePolicy by secret
reference", "secret", utils.NamespacedName(secret))
+ return nil
+ }
+ return list.Items
+}
diff --git a/test/e2e/gatewayapi/httproute.go b/test/e2e/gatewayapi/httproute.go
index 17783e06..48cc9e47 100644
--- a/test/e2e/gatewayapi/httproute.go
+++ b/test/e2e/gatewayapi/httproute.go
@@ -1873,6 +1873,65 @@ spec:
config:
body: "Updated"
`
+ var echoSecret = `
+apiVersion: v1
+kind: Secret
+metadata:
+ name: echo-secret
+stringData:
+ body: "Hello from Secret"
+ headers.X-Origin: "secret"
+`
+ var echoSecretUpdated = `
+apiVersion: v1
+kind: Secret
+metadata:
+ name: echo-secret
+stringData:
+ body: "Updated from Secret"
+ headers.X-Origin: "secret"
+`
+ var echoPluginWithSecretRef = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: PluginConfig
+metadata:
+ name: example-plugin-config-secret
+spec:
+ plugins:
+ - name: echo
+ secretRef:
+ name: echo-secret
+ config:
+ headers:
+ X-Config: "config"
+`
+ var extensionRefEchoPluginWithSecretRef = `
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ name: httpbin-secret
+ namespace: %s
+spec:
+ parentRefs:
+ - name: %s
+ hostnames:
+ - httpbin.example
+ rules:
+ - matches:
+ - path:
+ type: Exact
+ value: /get
+ filters:
+ - type: ExtensionRef
+ extensionRef:
+ group: apisix.apache.org
+ kind: PluginConfig
+ name: example-plugin-config-secret
+ backendRefs:
+ - name: httpbin-service-e2e-test
+ port: 80
+`
+
var extensionRefEchoPlugin = `
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
@@ -2239,6 +2298,48 @@ spec:
})
})
+ It("HTTPRoute ExtensionRef with plugin secretRef", func() {
+ By("create Secret and PluginConfig")
+
Expect(s.CreateResourceFromStringWithNamespace(echoSecret, s.Namespace())).
+ NotTo(HaveOccurred(), "creating Secret")
+
Expect(s.CreateResourceFromStringWithNamespace(echoPluginWithSecretRef,
s.Namespace())).
+ NotTo(HaveOccurred(), "creating PluginConfig")
+ s.ResourceApplied("HTTPRoute", "httpbin-secret",
fmt.Sprintf(extensionRefEchoPluginWithSecretRef, s.Namespace(), s.Namespace()),
1)
+
+ By("the Secret provides the plugin config, spec.config
is kept")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedBodyContains("Hello from Secret"),
+ Timeout: time.Second * 30,
+ Interval: time.Second * 2,
+ })
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedHeaders(map[string]string{
+ "X-Config": "config",
+ "X-Origin": "secret",
+ }),
+ Timeout: time.Second * 30,
+ Interval: time.Second * 2,
+ })
+
+ By("updating the Secret updates the plugin config")
+
Expect(s.CreateResourceFromStringWithNamespace(echoSecretUpdated,
s.Namespace())).
+ NotTo(HaveOccurred(), "updating Secret")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin.example",
+ Check:
scaffold.WithExpectedBodyContains("Updated from Secret"),
+ Timeout: time.Second * 30,
+ Interval: time.Second * 2,
+ })
+ })
+
It("HTTPRoute CORS Filter", func() {
By("create test service and deployment")
Expect(s.CreateResourceFromStringWithNamespace(corsTestService, s.Namespace())).
diff --git a/test/e2e/gatewayapi/tcproute.go b/test/e2e/gatewayapi/tcproute.go
index cda0b40c..cf5ae447 100644
--- a/test/e2e/gatewayapi/tcproute.go
+++ b/test/e2e/gatewayapi/tcproute.go
@@ -217,6 +217,25 @@ spec:
- "0.0.0.0/0"
`
+ var l4RoutePolicyMissingSecret = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: L4RoutePolicy
+metadata:
+ name: tcp-block-all
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcp-l4policy
+ plugins:
+ - name: ip-restriction
+ secretRef:
+ name: no-such-secret
+ config:
+ blacklist:
+ - "0.0.0.0/0"
+`
+
BeforeEach(func() {
Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(),
"creating GatewayProxy")
Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(),
"creating GatewayClass")
@@ -251,5 +270,26 @@ spec:
By("verifying TCP traffic recovers after L4RoutePolicy
deletion")
s.HTTPOverTCPConnectAssert(true, time.Minute*3)
})
+
+ It("L4RoutePolicy with a missing plugin Secret is rejected",
func() {
+ By("creating TCPRoute")
+ s.ResourceApplied("TCPRoute", "tcp-l4policy",
fmt.Sprintf(tcpRoute, s.Namespace()), 1)
+ s.HTTPOverTCPConnectAssert(true, time.Minute*3)
+
+ By("applying an L4RoutePolicy whose plugin references a
Secret that does not exist")
+ s.ApplyL4RoutePolicy(
+ types.NamespacedName{Name: s.Namespace()},
+ types.NamespacedName{Namespace: s.Namespace(),
Name: "tcp-block-all"},
+ l4RoutePolicyMissingSecret,
+ metav1.Condition{
+ Type:
string(gatewayv1.PolicyConditionAccepted),
+ Status: metav1.ConditionFalse,
+ Reason:
string(gatewayv1.PolicyReasonInvalid),
+ },
+ )
+
+ By("verifying the policy plugins are not attached")
+ s.HTTPOverTCPConnectAssert(true, time.Minute*3)
+ })
})
})