diff --git a/docs/en/latest/references/apisix_route_v2.md b/docs/en/latest/references/apisix_route_v2.md index 5ea3cd5039..717b1954c8 100644 --- a/docs/en/latest/references/apisix_route_v2.md +++ b/docs/en/latest/references/apisix_route_v2.md @@ -57,6 +57,7 @@ The table below describes each of the attributes in the spec. The fields `apiVer | 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 1edb8e5521..dfec5928d7 100644 --- a/pkg/kube/apisix/apis/config/v2/types.go +++ b/pkg/kube/apisix/apis/config/v2/types.go @@ -72,10 +72,12 @@ type ApisixRouteHTTP struct { // 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 937f02faad..c06a8a0f59 100644 --- a/pkg/providers/apisix/apisix_route.go +++ b/pkg/providers/apisix/apisix_route.go @@ -397,16 +397,20 @@ updateStatus: 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 856709e62d..9fa8b2b219 100644 --- a/pkg/providers/apisix/translation/apisix_route.go +++ b/pkg/providers/apisix/translation/apisix_route.go @@ -171,7 +171,11 @@ func (t *translator) translateHTTPRouteV2(ctx *translation.TranslateContext, ar 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 @@ func (t *translator) generateHTTPRouteV2DeleteMark(ctx *translation.TranslateCon 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 36f12983b5..64fa166fd4 100644 --- a/pkg/providers/apisix/translation/apisix_route_test.go +++ b/pkg/providers/apisix/translation/apisix_route_test.go @@ -315,6 +315,46 @@ func TestTranslateApisixRouteV2WithEmptyPluginConfigName(t *testing.T) { 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 8f3ba25d9c..4f8ae42c4a 100644 --- a/samples/deploy/crd/v1/ApisixRoute.yaml +++ b/samples/deploy/crd/v1/ApisixRoute.yaml @@ -184,6 +184,9 @@ spec: 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 0a91b83761..ea4cbe9cdf 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 @@ spec: 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") + }) +})