You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by al...@apache.org on 2025/11/03 00:58:25 UTC

(apisix-ingress-controller) branch master updated: feat: support cross-namespace access service for ingress annotations (#2634)

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 9baf472c feat: support cross-namespace access service for ingress annotations (#2634)
9baf472c is described below

commit 9baf472c58d8932e7a2ceff4c40d9933e0d0c566
Author: AlinsRan <al...@apache.org>
AuthorDate: Mon Nov 3 08:58:19 2025 +0800

    feat: support cross-namespace access service for ingress annotations (#2634)
---
 internal/adc/translator/annotations.go             |  3 ++
 .../servicenamespace/servicenamespace.go           | 30 +++++++++++
 internal/adc/translator/annotations_test.go        |  9 ++++
 internal/adc/translator/ingress.go                 | 10 ++--
 internal/controller/indexer/indexer.go             |  6 ++-
 internal/controller/ingress_controller.go          |  7 ++-
 internal/webhook/v1/ingress_webhook.go             |  1 -
 test/e2e/framework/k8s.go                          | 10 ++++
 test/e2e/ingress/annotations.go                    | 63 ++++++++++++++++++++++
 9 files changed, 133 insertions(+), 6 deletions(-)

diff --git a/internal/adc/translator/annotations.go b/internal/adc/translator/annotations.go
index 9f2efd0f..9015a088 100644
--- a/internal/adc/translator/annotations.go
+++ b/internal/adc/translator/annotations.go
@@ -25,6 +25,7 @@ import (
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/pluginconfig"
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/servicenamespace"
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/websocket"
 )
@@ -34,6 +35,7 @@ type IngressConfig struct {
 	Upstream         upstream.Upstream
 	Plugins          adctypes.Plugins
 	EnableWebsocket  bool
+	ServiceNamespace string
 	PluginConfigName string
 }
 
@@ -42,6 +44,7 @@ var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
 	"plugins":          plugins.NewParser(),
 	"EnableWebsocket":  websocket.NewParser(),
 	"PluginConfigName": pluginconfig.NewParser(),
+	"ServiceNamespace": servicenamespace.NewParser(),
 }
 
 func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig {
diff --git a/internal/adc/translator/annotations/servicenamespace/servicenamespace.go b/internal/adc/translator/annotations/servicenamespace/servicenamespace.go
new file mode 100644
index 00000000..d673654d
--- /dev/null
+++ b/internal/adc/translator/annotations/servicenamespace/servicenamespace.go
@@ -0,0 +1,30 @@
+// 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 servicenamespace
+
+import (
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+type servicenamespace struct{}
+
+func NewParser() annotations.IngressAnnotationsParser {
+	return &servicenamespace{}
+}
+
+func (w *servicenamespace) Parse(e annotations.Extractor) (any, error) {
+	return e.GetStringAnnotation(annotations.AnnotationsSvcNamespace), nil
+}
diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go
index 9b9c2dd4..46d4550a 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -279,6 +279,15 @@ func TestTranslateIngressAnnotations(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "service namespace",
+			anno: map[string]string{
+				annotations.AnnotationsSvcNamespace: "custom-namespace",
+			},
+			expected: &IngressConfig{
+				ServiceNamespace: "custom-namespace",
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/internal/adc/translator/ingress.go b/internal/adc/translator/ingress.go
index 5b6935f5..efb12cf7 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -181,7 +181,11 @@ func (t *Translator) resolveIngressUpstream(
 	backendService *networkingv1.IngressServiceBackend,
 	upstream *adctypes.Upstream,
 ) string {
-	backendRef := convertBackendRef(obj.Namespace, backendService.Name, internaltypes.KindService)
+	ns := obj.Namespace
+	if config != nil && config.ServiceNamespace != "" {
+		ns = config.ServiceNamespace
+	}
+	backendRef := convertBackendRef(ns, backendService.Name, internaltypes.KindService)
 	t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream)
 	if config != nil {
 		upConfig := config.Upstream
@@ -209,7 +213,7 @@ func (t *Translator) resolveIngressUpstream(
 	}
 
 	getService := tctx.Services[types.NamespacedName{
-		Namespace: obj.Namespace,
+		Namespace: ns,
 		Name:      backendService.Name,
 	}]
 	if getService == nil {
@@ -238,7 +242,7 @@ func (t *Translator) resolveIngressUpstream(
 	}
 
 	endpointSlices := tctx.EndpointSlices[types.NamespacedName{
-		Namespace: obj.Namespace,
+		Namespace: ns,
 		Name:      backendService.Name,
 	}]
 	if len(endpointSlices) > 0 {
diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go
index f6305791..223147ab 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -472,6 +472,10 @@ func IngressServiceIndexFunc(rawObj client.Object) []string {
 	ingress := rawObj.(*networkingv1.Ingress)
 	var services []string
 
+	ns := ingress.Namespace
+	if svcNs := ingress.Annotations[annotations.AnnotationsSvcNamespace]; svcNs != "" {
+		ns = svcNs
+	}
 	for _, rule := range ingress.Spec.Rules {
 		if rule.HTTP == nil {
 			continue
@@ -481,7 +485,7 @@ func IngressServiceIndexFunc(rawObj client.Object) []string {
 			if path.Backend.Service == nil {
 				continue
 			}
-			key := GenIndexKey(ingress.Namespace, path.Backend.Service.Name)
+			key := GenIndexKey(ns, path.Backend.Service.Name)
 			services = append(services, key)
 		}
 	}
diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go
index 7d5bfbf7..f8447ee6 100644
--- a/internal/controller/ingress_controller.go
+++ b/internal/controller/ingress_controller.go
@@ -410,6 +410,7 @@ func (r *IngressReconciler) listIngressForBackendTrafficPolicy(ctx context.Conte
 		r.Log.Error(fmt.Errorf("unexpected object type"), "failed to convert object to BackendTrafficPolicy")
 		return nil
 	}
+
 	var namespacedNameMap = make(map[types.NamespacedName]struct{})
 	ingresses := []networkingv1.Ingress{}
 	for _, ref := range v.Spec.TargetRefs {
@@ -532,7 +533,11 @@ func (r *IngressReconciler) processBackends(tctx *provider.TranslateContext, ing
 				continue
 			}
 			service := path.Backend.Service
-			if err := r.processBackendService(tctx, ingress.Namespace, service); err != nil {
+			ns := ingress.Namespace
+			if svcNs := ingress.Annotations[annotations.AnnotationsSvcNamespace]; svcNs != "" {
+				ns = ingress.Annotations[annotations.AnnotationsSvcNamespace]
+			}
+			if err := r.processBackendService(tctx, ns, service); err != nil {
 				terr = err
 			}
 		}
diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go
index 2099621c..2c85c02c 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -48,7 +48,6 @@ var unsupportedAnnotations = []string{
 	"k8s.apisix.apache.org/allowlist-source-range",
 	"k8s.apisix.apache.org/blocklist-source-range",
 	"k8s.apisix.apache.org/auth-type",
-	"k8s.apisix.apache.org/svc-namespace",
 }
 
 // checkUnsupportedAnnotations checks if the Ingress contains any unsupported annotations
diff --git a/test/e2e/framework/k8s.go b/test/e2e/framework/k8s.go
index 3fb71598..a943a1b0 100644
--- a/test/e2e/framework/k8s.go
+++ b/test/e2e/framework/k8s.go
@@ -175,6 +175,16 @@ func (f *Framework) CreateNamespaceWithTestService(name string) {
 	}
 }
 
+func (f *Framework) CreateNamespace(name string) {
+	_, err := f.clientset.CoreV1().
+		Namespaces().
+		Create(f.Context, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}, metav1.CreateOptions{})
+	if err != nil && !errors.IsAlreadyExists(err) {
+		f.GomegaT.Expect(err).ShouldNot(HaveOccurred(), "create namespace")
+		return
+	}
+}
+
 func (f *Framework) DeleteNamespace(name string) {
 	err := f.clientset.CoreV1().Namespaces().Delete(f.Context, name, metav1.DeleteOptions{})
 	if err == nil || errors.IsNotFound(err) {
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 98a0bcaf..61551015 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -952,4 +952,67 @@ spec:
 			Expect(rewriteConfig["body_base64"]).To(BeTrue(), "checking body_base64")
 		})
 	})
+
+	Context("Service Namespace", func() {
+		var (
+			ns  string
+			svc = `
+apiVersion: v1
+kind: Service
+metadata:
+  name: httpbin-external-domain
+spec:
+  type: ExternalName
+  externalName: httpbin-service-e2e-test.%s.svc
+`
+			ingressSvcNamespace = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: retries
+  annotations:
+    k8s.apisix.apache.org/svc-namespace: %s
+spec:
+  ingressClassName: %s
+  rules:
+  - host: httpbin.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-external-domain
+            port:
+              number: 80
+`
+		)
+		BeforeEach(func() {
+			ns = s.Namespace() + "-v2"
+			s.CreateNamespace(ns)
+			err := s.CreateResourceFromStringWithNamespace(fmt.Sprintf(svc, s.Namespace()), ns)
+			Expect(err).NotTo(HaveOccurred(), "creating Service in custom namespace")
+
+			By("create GatewayProxy")
+			Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")
+
+			By("create IngressClass")
+			err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+			Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+			time.Sleep(5 * time.Second)
+		})
+		AfterEach(func() {
+			s.DeleteNamespace(ns)
+		})
+		It("svc-namespace", func() {
+			Expect(s.CreateResourceFromString(fmt.Sprintf(ingressSvcNamespace, ns, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "httpbin.example",
+				Check:  scaffold.WithExpectedStatus(http.StatusOK),
+			})
+		})
+	})
 })