feat: add plugin_config_namespace parameter to ApisixRoute (#2137)
* feat: add plugin_config_namespace parameter to ApisixRoute
Add plugin_config_namespace parameter to ApisixRoute resource to allow cross namespace discovery.
* fix indentation
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
* remove route.yaml
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
* fix e2e test
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
* update gomod gosum
* fix e2e test
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
* fix e2e test
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
* Update pkg/providers/apisix/apisix_route.go
Co-authored-by: Gallardot <gallardot@apache.org>
* create namespace
* refactor test
* refactor test
* fix e2e
* fix e2e
* update crd
* Add EOL
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
---------
Signed-off-by: Ashish Tiwari <ashishjaitiwari15112000@gmail.com>
Co-authored-by: Gallardot <gallardot@apache.org>
diff --git a/docs/en/latest/references/apisix_route_v2.md b/docs/en/latest/references/apisix_route_v2.md
index 5ea3cd5..717b195 100644
--- a/docs/en/latest/references/apisix_route_v2.md
+++ b/docs/en/latest/references/apisix_route_v2.md
@@ -57,6 +57,7 @@
| http[].match.exprs[].set | array | Set to compare the subject with. Only used when the operator is `In` or `NotIn`. Can use either this or `http[].match.exprs[].value`. |
| http[].websocket | boolean | When set to `true` enables websocket proxy. |
| http[].plugin_config_name | string | Existing Plugin Config name to use in the Route. |
+| http[].plugin_config_namespace | string | Namespace in which to look for `plugin_config_name` Route. |
| http[].backends | object | List of backend services. If there are more than one, a weight based traffic split policy would be applied. |
| http[].backends[].serviceName | string | Name of the backend service. The service and the `ApisixRoute` resource should be created in the same namespace. |
| http[].backends[].servicePort | integer or string | Port number or the name defined in the service object of the backend. |
diff --git a/pkg/kube/apisix/apis/config/v2/types.go b/pkg/kube/apisix/apis/config/v2/types.go
index 1edb8e5..dfec592 100644
--- a/pkg/kube/apisix/apis/config/v2/types.go
+++ b/pkg/kube/apisix/apis/config/v2/types.go
@@ -72,10 +72,12 @@
// Upstreams refer to ApisixUpstream CRD
Upstreams []ApisixRouteUpstreamReference `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`
- Websocket bool `json:"websocket" yaml:"websocket"`
- PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
- Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
- Authentication ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
+ Websocket bool `json:"websocket" yaml:"websocket"`
+ PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"`
+ //By default, PluginConfigNamespace will be the same as the namespace of ApisixRoute
+ PluginConfigNamespace string `json:"plugin_config_namespace,omitempty" yaml:"plugin_config_namespace,omitempty"`
+ Plugins []ApisixRoutePlugin `json:"plugins,omitempty" yaml:"plugins,omitempty"`
+ Authentication ApisixRouteAuthentication `json:"authentication,omitempty" yaml:"authentication,omitempty"`
}
// ApisixRouteHTTPBackend represents an HTTP backend (a Kubernetes Service).
diff --git a/pkg/providers/apisix/apisix_route.go b/pkg/providers/apisix/apisix_route.go
index 937f02f..c06a8a0 100644
--- a/pkg/providers/apisix/apisix_route.go
+++ b/pkg/providers/apisix/apisix_route.go
@@ -397,16 +397,20 @@
func (c *apisixRouteController) checkPluginNameIfNotEmptyV2(ctx context.Context, in *v2.ApisixRoute) error {
for _, v := range in.Spec.HTTP {
if v.PluginConfigName != "" {
- _, err := c.APISIX.Cluster(c.Config.APISIX.DefaultClusterName).PluginConfig().Get(ctx, apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName))
+ ns := in.Namespace
+ if v.PluginConfigNamespace != "" {
+ ns = v.PluginConfigNamespace
+ }
+ _, err := c.APISIX.Cluster(c.Config.APISIX.DefaultClusterName).PluginConfig().Get(ctx, apisixv1.ComposePluginConfigName(ns, v.PluginConfigName))
if err != nil {
if err == apisixcache.ErrNotFound {
log.Errorw("checkPluginNameIfNotEmptyV2 error: plugin_config not found",
- zap.String("name", apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName)),
+ zap.String("name", apisixv1.ComposePluginConfigName(ns, v.PluginConfigName)),
zap.Any("obj", in),
zap.Error(err))
} else {
log.Errorw("checkPluginNameIfNotEmptyV2 PluginConfig get failed",
- zap.String("name", apisixv1.ComposePluginConfigName(in.Namespace, v.PluginConfigName)),
+ zap.String("name", apisixv1.ComposePluginConfigName(ns, v.PluginConfigName)),
zap.Any("obj", in),
zap.Error(err))
}
diff --git a/pkg/providers/apisix/translation/apisix_route.go b/pkg/providers/apisix/translation/apisix_route.go
index 856709e..9fa8b2b 100644
--- a/pkg/providers/apisix/translation/apisix_route.go
+++ b/pkg/providers/apisix/translation/apisix_route.go
@@ -171,7 +171,11 @@
route.FilterFunc = part.Match.FilterFunc
if part.PluginConfigName != "" {
- route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ar.Namespace, part.PluginConfigName))
+ ns := ar.Namespace
+ if part.PluginConfigNamespace != "" {
+ ns = part.PluginConfigNamespace
+ }
+ route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ns, part.PluginConfigName))
}
for k, v := range ar.ObjectMeta.Labels {
@@ -465,7 +469,11 @@
route.Name = apisixv1.ComposeRouteName(ar.Namespace, ar.Name, part.Name)
route.ID = id.GenID(route.Name)
if part.PluginConfigName != "" {
- route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ar.Namespace, part.PluginConfigName))
+ ns := ar.Namespace
+ if part.PluginConfigNamespace != "" {
+ ns = part.PluginConfigNamespace
+ }
+ route.PluginConfigId = id.GenID(apisixv1.ComposePluginConfigName(ns, part.PluginConfigName))
}
ctx.AddRoute(route)
diff --git a/pkg/providers/apisix/translation/apisix_route_test.go b/pkg/providers/apisix/translation/apisix_route_test.go
index 36f1298..64fa166 100644
--- a/pkg/providers/apisix/translation/apisix_route_test.go
+++ b/pkg/providers/apisix/translation/apisix_route_test.go
@@ -315,6 +315,46 @@
assert.Equal(t, "", res.Routes[2].PluginConfigId)
}
+func TestTranslateApisixRouteV2WithPluginConfigNamespace(t *testing.T) {
+ tr, processCh := mockTranslatorV2(t)
+ <-processCh
+ <-processCh
+ pluginConfigNamespace := "test-2"
+ ar := &configv2.ApisixRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "ar",
+ Namespace: "test",
+ },
+ Spec: configv2.ApisixRouteSpec{
+ HTTP: []configv2.ApisixRouteHTTP{
+ {
+ Name: "rule1",
+ Match: configv2.ApisixRouteHTTPMatch{
+ Paths: []string{
+ "/*",
+ },
+ },
+ Backends: []configv2.ApisixRouteHTTPBackend{
+ {
+ ServiceName: "svc",
+ ServicePort: intstr.IntOrString{
+ IntVal: 80,
+ },
+ },
+ },
+ PluginConfigName: "test-PluginConfigName-1",
+ PluginConfigNamespace: pluginConfigNamespace,
+ },
+ },
+ },
+ }
+ res, err := tr.TranslateRouteV2(ar)
+ assert.NoError(t, err)
+ assert.Len(t, res.PluginConfigs, 0)
+ expectedPluginId := id.GenID(apisixv1.ComposePluginConfigName(pluginConfigNamespace, ar.Spec.HTTP[0].PluginConfigName))
+ assert.Equal(t, expectedPluginId, res.Routes[0].PluginConfigId)
+}
+
func TestGenerateApisixRouteV2DeleteMark(t *testing.T) {
tr := &translator{
&TranslatorOptions{},
diff --git a/samples/deploy/crd/v1/ApisixRoute.yaml b/samples/deploy/crd/v1/ApisixRoute.yaml
index 8f3ba25..4f8ae42 100644
--- a/samples/deploy/crd/v1/ApisixRoute.yaml
+++ b/samples/deploy/crd/v1/ApisixRoute.yaml
@@ -184,6 +184,9 @@
plugin_config_name:
type: string
minLength: 1
+ plugin_config_namespace:
+ type: string
+ minLength: 1
upstreams:
description: Upstreams refer to ApisixUpstream CRD
type: array
diff --git a/test/e2e/suite-plugins/suite-plugins-other/plugin_config.go b/test/e2e/suite-plugins/suite-plugins-other/plugin_config.go
index 0a91b83..ea4cbe9 100644
--- a/test/e2e/suite-plugins/suite-plugins-other/plugin_config.go
+++ b/test/e2e/suite-plugins/suite-plugins-other/plugin_config.go
@@ -594,3 +594,93 @@
resp.Status(http.StatusOK)
})
})
+
+var _ = ginkgo.Describe("suite-plugins-other: ApisixPluginConfig cross namespace", func() {
+ s := scaffold.NewScaffold(&scaffold.Options{
+ NamespaceSelectorLabel: map[string][]string{
+ "apisix.ingress.watch": {"test"},
+ },
+ })
+ ginkgo.It("ApisixPluginConfig cross namespace", func() {
+ testns := `
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: test
+ labels:
+ apisix.ingress.watch: test
+`
+ err := s.CreateResourceFromString(testns)
+ assert.Nil(ginkgo.GinkgoT(), err, "Creating test namespace")
+ backendSvc, backendPorts := s.DefaultHTTPBackend()
+ apc := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixPluginConfig
+metadata:
+ name: echo-and-cors-apc
+ namespace: test
+spec:
+ plugins:
+ - name: echo
+ enable: true
+ config:
+ before_body: "This is the preface"
+ after_body: "This is the epilogue"
+ headers:
+ X-Foo: v1
+ X-Foo2: v2
+ - name: cors
+ enable: true
+`)
+ assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromStringWithNamespace(apc, "test"))
+
+ err = s.EnsureNumApisixPluginConfigCreated(1)
+ assert.Nil(ginkgo.GinkgoT(), err, "Checking number of pluginConfigs")
+
+ time.Sleep(time.Second * 3)
+
+ ar := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ http:
+ - name: rule1
+ match:
+ hosts:
+ - httpbin.org
+ paths:
+ - /ip
+ backends:
+ - serviceName: %s
+ servicePort: %d
+ weight: 10
+ plugin_config_name: echo-and-cors-apc
+ plugin_config_namespace: test
+`, backendSvc, backendPorts[0])
+ assert.Nil(ginkgo.GinkgoT(), s.CreateVersionedApisixResource(ar))
+
+ err = s.EnsureNumApisixRoutesCreated(1)
+ assert.Nil(ginkgo.GinkgoT(), err, "Checking number of routes")
+
+ time.Sleep(3 * time.Second)
+ pcs, err := s.ListApisixPluginConfig()
+ assert.Nil(ginkgo.GinkgoT(), err, nil, "listing pluginConfigs")
+ assert.Len(ginkgo.GinkgoT(), pcs, 1)
+ assert.Len(ginkgo.GinkgoT(), pcs[0].Plugins, 2)
+
+ resp := s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.org").Expect()
+ resp.Status(http.StatusOK)
+ resp.Header("X-Foo").Equal("v1")
+ resp.Header("X-Foo2").Equal("v2")
+ resp.Header("Access-Control-Allow-Origin").Equal("*")
+ resp.Header("Access-Control-Allow-Methods").Equal("*")
+ resp.Header("Access-Control-Allow-Headers").Equal("*")
+ resp.Header("Access-Control-Expose-Headers").Equal("*")
+ resp.Header("Access-Control-Max-Age").Equal("5")
+ resp.Body().Contains("This is the preface")
+ resp.Body().Contains("origin")
+ resp.Body().Contains("This is the epilogue")
+ })
+})