You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ro...@apache.org on 2025/10/13 09:03:35 UTC

(apisix-ingress-controller) branch fix/gatewayproxy_check created (now 3210a9d1)

This is an automated email from the ASF dual-hosted git repository.

ronething pushed a change to branch fix/gatewayproxy_check
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


      at 3210a9d1 feat: add conflict detection for gateway proxy

This branch includes the following new commits:

     new 3210a9d1 feat: add conflict detection for gateway proxy

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(apisix-ingress-controller) 01/01: feat: add conflict detection for gateway proxy

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch fix/gatewayproxy_check
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git

commit 3210a9d109e4af9f96399dcfda7e816f32d33af7
Author: Ashing Zheng <ax...@gmail.com>
AuthorDate: Mon Oct 13 17:02:44 2025 +0800

    feat: add conflict detection for gateway proxy
    
    Signed-off-by: Ashing Zheng <ax...@gmail.com>
---
 internal/controller/indexer/tlsroute.go          |   3 +-
 internal/provider/init/init.go                   |   3 +-
 internal/provider/register.go                    |   3 +-
 internal/webhook/v1/gatewayproxy_webhook.go      | 139 ++++++++++++++-
 internal/webhook/v1/gatewayproxy_webhook_test.go | 217 ++++++++++++++++++++++-
 test/e2e/webhook/gatewayproxy.go                 | 159 ++++++++++++++++-
 6 files changed, 510 insertions(+), 14 deletions(-)

diff --git a/internal/controller/indexer/tlsroute.go b/internal/controller/indexer/tlsroute.go
index 567131c4..acef5317 100644
--- a/internal/controller/indexer/tlsroute.go
+++ b/internal/controller/indexer/tlsroute.go
@@ -20,10 +20,11 @@ package indexer
 import (
 	"context"
 
-	internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+	internaltypes "github.com/apache/apisix-ingress-controller/internal/types"
 )
 
 func setupTLSRouteIndexer(mgr ctrl.Manager) error {
diff --git a/internal/provider/init/init.go b/internal/provider/init/init.go
index b6ed9e99..be21c07d 100644
--- a/internal/provider/init/init.go
+++ b/internal/provider/init/init.go
@@ -18,11 +18,12 @@
 package init
 
 import (
+	"github.com/go-logr/logr"
+
 	"github.com/apache/apisix-ingress-controller/internal/controller/status"
 	"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
 	"github.com/apache/apisix-ingress-controller/internal/provider"
 	"github.com/apache/apisix-ingress-controller/internal/provider/apisix"
-	"github.com/go-logr/logr"
 )
 
 func init() {
diff --git a/internal/provider/register.go b/internal/provider/register.go
index fddb1af5..a9feb032 100644
--- a/internal/provider/register.go
+++ b/internal/provider/register.go
@@ -21,9 +21,10 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/go-logr/logr"
+
 	"github.com/apache/apisix-ingress-controller/internal/controller/status"
 	"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
-	"github.com/go-logr/logr"
 )
 
 type RegisterHandler interface {
diff --git a/internal/webhook/v1/gatewayproxy_webhook.go b/internal/webhook/v1/gatewayproxy_webhook.go
index 75bccea3..b76a8cf7 100644
--- a/internal/webhook/v1/gatewayproxy_webhook.go
+++ b/internal/webhook/v1/gatewayproxy_webhook.go
@@ -18,6 +18,8 @@ package v1
 import (
 	"context"
 	"fmt"
+	"sort"
+	"strings"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
@@ -63,7 +65,12 @@ func (v *GatewayProxyCustomValidator) ValidateCreate(ctx context.Context, obj ru
 	}
 	gatewayProxyLog.Info("Validation for GatewayProxy upon creation", "name", gp.GetName(), "namespace", gp.GetNamespace())
 
-	return v.collectWarnings(ctx, gp), nil
+	warnings := v.collectWarnings(ctx, gp)
+	if err := v.validateGatewayProxyConflict(ctx, gp); err != nil {
+		return nil, err
+	}
+
+	return warnings, nil
 }
 
 func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
@@ -73,7 +80,12 @@ func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, oldObj
 	}
 	gatewayProxyLog.Info("Validation for GatewayProxy upon update", "name", gp.GetName(), "namespace", gp.GetNamespace())
 
-	return v.collectWarnings(ctx, gp), nil
+	warnings := v.collectWarnings(ctx, gp)
+	if err := v.validateGatewayProxyConflict(ctx, gp); err != nil {
+		return nil, err
+	}
+
+	return warnings, nil
 }
 
 func (v *GatewayProxyCustomValidator) ValidateDelete(context.Context, runtime.Object) (admission.Warnings, error) {
@@ -111,3 +123,126 @@ func (v *GatewayProxyCustomValidator) collectWarnings(ctx context.Context, gp *v
 
 	return warnings
 }
+
+func (v *GatewayProxyCustomValidator) validateGatewayProxyConflict(ctx context.Context, gp *v1alpha1.GatewayProxy) error {
+	current := buildGatewayProxyConfig(gp)
+	if !current.readyForConflict() {
+		return nil
+	}
+
+	var list v1alpha1.GatewayProxyList
+	if err := v.Client.List(ctx, &list); err != nil {
+		gatewayProxyLog.Error(err, "failed to list GatewayProxy objects for conflict detection")
+		return fmt.Errorf("failed to list existing GatewayProxy resources: %w", err)
+	}
+
+	for _, other := range list.Items {
+		if other.GetNamespace() == gp.GetNamespace() && other.GetName() == gp.GetName() {
+			// skip self
+			continue
+		}
+		otherConfig := buildGatewayProxyConfig(&other)
+		if !otherConfig.readyForConflict() {
+			continue
+		}
+		if !current.sharesAdminKeyWith(otherConfig) {
+			continue
+		}
+		if current.serviceKey != "" && current.serviceKey == otherConfig.serviceKey {
+			return fmt.Errorf("gateway proxy configuration conflict: GatewayProxy %s/%s and %s/%s both target %s while sharing %s",
+				gp.GetNamespace(), gp.GetName(),
+				other.GetNamespace(), other.GetName(),
+				current.serviceDescription,
+				current.adminKeyDetail(),
+			)
+		}
+		if len(current.endpoints) > 0 && len(otherConfig.endpoints) > 0 {
+			if overlap := current.endpointOverlap(otherConfig); len(overlap) > 0 {
+				return fmt.Errorf("gateway proxy configuration conflict: GatewayProxy %s/%s and %s/%s both target control plane endpoints [%s] while sharing %s",
+					gp.GetNamespace(), gp.GetName(),
+					other.GetNamespace(), other.GetName(),
+					strings.Join(overlap, ", "),
+					current.adminKeyDetail(),
+				)
+			}
+		}
+	}
+
+	return nil
+}
+
+type gatewayProxyConfig struct {
+	inlineAdminKey     string
+	secretKey          string
+	serviceKey         string
+	serviceDescription string
+	endpoints          map[string]struct{}
+}
+
+func buildGatewayProxyConfig(gp *v1alpha1.GatewayProxy) gatewayProxyConfig {
+	var cfg gatewayProxyConfig
+
+	if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.Type != v1alpha1.ProviderTypeControlPlane || gp.Spec.Provider.ControlPlane == nil {
+		return cfg
+	}
+
+	cp := gp.Spec.Provider.ControlPlane
+
+	if cp.Auth.AdminKey != nil {
+		if value := strings.TrimSpace(cp.Auth.AdminKey.Value); value != "" {
+			cfg.inlineAdminKey = value
+		} else if cp.Auth.AdminKey.ValueFrom != nil && cp.Auth.AdminKey.ValueFrom.SecretKeyRef != nil {
+			ref := cp.Auth.AdminKey.ValueFrom.SecretKeyRef
+			cfg.secretKey = fmt.Sprintf("%s/%s:%s", gp.GetNamespace(), ref.Name, ref.Key)
+		}
+	}
+
+	if cp.Service != nil && cp.Service.Name != "" {
+		cfg.serviceKey = fmt.Sprintf("service:%s/%s:%d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port)
+		cfg.serviceDescription = fmt.Sprintf("Service %s/%s port %d", gp.GetNamespace(), cp.Service.Name, cp.Service.Port)
+	}
+
+	if len(cp.Endpoints) > 0 {
+		cfg.endpoints = make(map[string]struct{}, len(cp.Endpoints))
+		for _, endpoint := range cp.Endpoints {
+			cfg.endpoints[endpoint] = struct{}{}
+		}
+	}
+
+	return cfg
+}
+
+func (c gatewayProxyConfig) adminKeyDetail() string {
+	if c.secretKey != "" {
+		return fmt.Sprintf("AdminKey secret %s", c.secretKey)
+	}
+	return "the same inline AdminKey value"
+}
+
+func (c gatewayProxyConfig) sharesAdminKeyWith(other gatewayProxyConfig) bool {
+	if c.inlineAdminKey != "" && other.inlineAdminKey != "" {
+		return c.inlineAdminKey == other.inlineAdminKey
+	}
+	if c.secretKey != "" && other.secretKey != "" {
+		return c.secretKey == other.secretKey
+	}
+	return false
+}
+
+func (c gatewayProxyConfig) readyForConflict() bool {
+	if c.inlineAdminKey == "" && c.secretKey == "" {
+		return false
+	}
+	return c.serviceKey != "" || len(c.endpoints) > 0
+}
+
+func (c gatewayProxyConfig) endpointOverlap(other gatewayProxyConfig) []string {
+	var overlap []string
+	for endpoint := range c.endpoints {
+		if _, ok := other.endpoints[endpoint]; ok {
+			overlap = append(overlap, endpoint)
+		}
+	}
+	sort.Strings(overlap)
+	return overlap
+}
diff --git a/internal/webhook/v1/gatewayproxy_webhook_test.go b/internal/webhook/v1/gatewayproxy_webhook_test.go
index c43253c1..2768ac7a 100644
--- a/internal/webhook/v1/gatewayproxy_webhook_test.go
+++ b/internal/webhook/v1/gatewayproxy_webhook_test.go
@@ -29,6 +29,10 @@ import (
 	v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
 )
 
+const (
+	candidateName = "candidate"
+)
+
 func buildGatewayProxyValidator(t *testing.T, objects ...runtime.Object) *GatewayProxyCustomValidator {
 	t.Helper()
 
@@ -54,7 +58,7 @@ func newGatewayProxy() *v1alpha1.GatewayProxy {
 			Provider: &v1alpha1.GatewayProxyProvider{
 				Type: v1alpha1.ProviderTypeControlPlane,
 				ControlPlane: &v1alpha1.ControlPlaneProvider{
-					Service: &v1alpha1.ProviderService{Name: "control-plane"},
+					Service: &v1alpha1.ProviderService{Name: "control-plane", Port: 9180},
 					Auth: v1alpha1.ControlPlaneAuth{
 						Type: v1alpha1.AuthTypeAdminKey,
 						AdminKey: &v1alpha1.AdminKeyAuth{
@@ -72,6 +76,41 @@ func newGatewayProxy() *v1alpha1.GatewayProxy {
 	}
 }
 
+func newGatewayProxyWithEndpoints(name string, endpoints []string) *v1alpha1.GatewayProxy {
+	gp := newGatewayProxy()
+	gp.Name = name
+	gp.Spec.Provider.ControlPlane.Service = nil
+	gp.Spec.Provider.ControlPlane.Endpoints = endpoints
+	return gp
+}
+
+func setInlineAdminKey(gp *v1alpha1.GatewayProxy, value string) {
+	if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
+		return
+	}
+	if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil {
+		gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{}
+	}
+	gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = value
+	gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = nil
+}
+
+func setSecretAdminKey(gp *v1alpha1.GatewayProxy, name, key string) {
+	if gp == nil || gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
+		return
+	}
+	if gp.Spec.Provider.ControlPlane.Auth.AdminKey == nil {
+		gp.Spec.Provider.ControlPlane.Auth.AdminKey = &v1alpha1.AdminKeyAuth{}
+	}
+	gp.Spec.Provider.ControlPlane.Auth.AdminKey.Value = ""
+	gp.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom = &v1alpha1.AdminKeyValueFrom{
+		SecretKeyRef: &v1alpha1.SecretKeySelector{
+			Name: name,
+			Key:  key,
+		},
+	}
+}
+
 func TestGatewayProxyValidator_MissingService(t *testing.T) {
 	gp := newGatewayProxy()
 	gp.Spec.Provider.ControlPlane.Auth.AdminKey = nil
@@ -150,3 +189,179 @@ func TestGatewayProxyValidator_NoWarnings(t *testing.T) {
 	require.NoError(t, err)
 	require.Empty(t, warnings)
 }
+
+func TestGatewayProxyValidator_DetectsServiceConflict(t *testing.T) {
+	existing := newGatewayProxy()
+	existing.Name = "existing"
+
+	service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "control-plane",
+			Namespace: "default",
+		},
+	}
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+
+	validator := buildGatewayProxyValidator(t, existing, service, secret)
+
+	candidate := newGatewayProxy()
+	candidate.Name = candidateName
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.Error(t, err)
+	require.Len(t, warnings, 0)
+	require.Contains(t, err.Error(), "gateway proxy configuration conflict")
+	require.Contains(t, err.Error(), "Service default/control-plane port 9180")
+	require.Contains(t, err.Error(), "AdminKey secret default/admin-key:token")
+}
+
+func TestGatewayProxyValidator_DetectsEndpointConflict(t *testing.T) {
+	existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+	validator := buildGatewayProxyValidator(t, existing, secret)
+
+	candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443", "https://127.0.0.1:9443"})
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.Error(t, err)
+	require.Len(t, warnings, 0)
+	require.Contains(t, err.Error(), "gateway proxy configuration conflict")
+	require.Contains(t, err.Error(), "endpoints [https://10.0.0.1:9443, https://127.0.0.1:9443]")
+	require.Contains(t, err.Error(), "AdminKey secret default/admin-key:token")
+}
+
+func TestGatewayProxyValidator_AllowsDistinctGatewayGroups(t *testing.T) {
+	existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443"})
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+	service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "control-plane",
+			Namespace: "default",
+		},
+	}
+	validator := buildGatewayProxyValidator(t, existing, secret, service)
+
+	candidate := newGatewayProxy()
+	candidate.Name = candidateName
+	candidate.Spec.Provider.ControlPlane.Service = &v1alpha1.ProviderService{
+		Name: "control-plane",
+		Port: 9180,
+	}
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.NoError(t, err)
+	require.Empty(t, warnings)
+}
+
+func TestGatewayProxyValidator_AllowsServiceConflictWithDifferentAdminSecret(t *testing.T) {
+	existing := newGatewayProxy()
+	existing.Name = "existing"
+
+	candidate := newGatewayProxy()
+	candidate.Name = candidateName
+	setSecretAdminKey(candidate, "admin-key-alt", "token")
+
+	service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "control-plane",
+			Namespace: "default",
+		},
+	}
+	existingSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+	altSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key-alt",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+
+	validator := buildGatewayProxyValidator(t, existing, service, existingSecret, altSecret)
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.NoError(t, err)
+	require.Empty(t, warnings)
+}
+
+func TestGatewayProxyValidator_DetectsInlineAdminKeyConflict(t *testing.T) {
+	existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
+	setInlineAdminKey(existing, "inline-cred")
+
+	candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443"})
+	setInlineAdminKey(candidate, "inline-cred")
+
+	validator := buildGatewayProxyValidator(t, existing)
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.Error(t, err)
+	require.Len(t, warnings, 0)
+	require.Contains(t, err.Error(), "gateway proxy configuration conflict")
+	require.Contains(t, err.Error(), "control plane endpoints [https://10.0.0.1:9443]")
+	require.Contains(t, err.Error(), "inline AdminKey value")
+}
+
+func TestGatewayProxyValidator_AllowsEndpointOverlapWithDifferentAdminKey(t *testing.T) {
+	existing := newGatewayProxyWithEndpoints("existing", []string{"https://127.0.0.1:9443", "https://10.0.0.1:9443"})
+
+	candidate := newGatewayProxyWithEndpoints(candidateName, []string{"https://10.0.0.1:9443", "https://192.168.0.1:9443"})
+	setSecretAdminKey(candidate, "admin-key-alt", "token")
+
+	existingSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+	altSecret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "admin-key-alt",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"token": []byte("value"),
+		},
+	}
+
+	validator := buildGatewayProxyValidator(t, existing, existingSecret, altSecret)
+
+	warnings, err := validator.ValidateCreate(context.Background(), candidate)
+	require.NoError(t, err)
+	require.Empty(t, warnings)
+}
diff --git a/test/e2e/webhook/gatewayproxy.go b/test/e2e/webhook/gatewayproxy.go
index 6b1f6189..4f2d12e4 100644
--- a/test/e2e/webhook/gatewayproxy.go
+++ b/test/e2e/webhook/gatewayproxy.go
@@ -33,11 +33,7 @@ var _ = Describe("Test GatewayProxy Webhook", Label("webhook"), func() {
 		EnableWebhook: true,
 	})
 
-	It("should warn on missing service or secret references", func() {
-		missingService := "missing-control-plane"
-		missingSecret := "missing-admin-secret"
-		gpName := "webhook-gateway-proxy"
-		gpWithSecrets := `
+	gatewayProxyTemplate := `
 apiVersion: apisix.apache.org/v1alpha1
 kind: GatewayProxy
 metadata:
@@ -58,7 +54,12 @@ spec:
               key: token
 `
 
-		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+	It("should warn on missing service or secret references", func() {
+		missingService := "missing-control-plane"
+		missingSecret := "missing-admin-secret"
+		gpName := "webhook-gateway-proxy"
+
+		output, err := s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret))
 		Expect(err).ShouldNot(HaveOccurred())
 		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
 		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Referenced Secret '%s/%s' not found", s.Namespace(), missingSecret)))
@@ -98,7 +99,7 @@ stringData:
 		err = s.DeleteResource("GatewayProxy", gpName)
 		Expect(err).ShouldNot(HaveOccurred())
 
-		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret))
 		Expect(err).ShouldNot(HaveOccurred())
 		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
 		Expect(output).To(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret)))
@@ -121,9 +122,151 @@ stringData:
 		err = s.DeleteResource("GatewayProxy", gpName)
 		Expect(err).ShouldNot(HaveOccurred())
 
-		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gpWithSecrets, gpName, missingService, missingSecret))
+		output, err = s.CreateResourceFromStringAndGetOutput(fmt.Sprintf(gatewayProxyTemplate, gpName, missingService, missingSecret))
 		Expect(err).ShouldNot(HaveOccurred())
 		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Referenced Service '%s/%s' not found", s.Namespace(), missingService)))
 		Expect(output).NotTo(ContainSubstring(fmt.Sprintf("Warning: Secret key 'token' not found in Secret '%s/%s'", s.Namespace(), missingSecret)))
 	})
+
+	Context("GatewayProxy configuration conflicts", func() {
+		It("should reject GatewayProxy that reuses the same Service and AdminKey Secret as an existing one on create and update", func() {
+			serviceTemplate := `
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: dummy-control-plane
+  ports:
+  - name: admin
+    port: 9180
+    targetPort: 9180
+`
+			secretTemplate := `
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+type: Opaque
+stringData:
+  %s: %s
+`
+			serviceName := "gatewayproxy-shared-service"
+			secretName := "gatewayproxy-shared-secret"
+			initialProxy := "gatewayproxy-shared-primary"
+			conflictingProxy := "gatewayproxy-shared-conflict"
+
+			Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, serviceName))).ShouldNot(HaveOccurred(), "creating shared Service")
+			Expect(s.CreateResourceFromString(fmt.Sprintf(secretTemplate, secretName, "token", "value"))).ShouldNot(HaveOccurred(), "creating shared Secret")
+
+			err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, initialProxy, serviceName, secretName))
+			Expect(err).ShouldNot(HaveOccurred(), "creating initial GatewayProxy")
+
+			time.Sleep(2 * time.Second)
+
+			err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, conflictingProxy, serviceName, secretName))
+			Expect(err).Should(HaveOccurred(), "expecting conflict for duplicated GatewayProxy")
+			Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict"))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), conflictingProxy)))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), initialProxy)))
+			Expect(err.Error()).To(ContainSubstring("Service"))
+			Expect(err.Error()).To(ContainSubstring("AdminKey secret"))
+
+			Expect(s.DeleteResource("GatewayProxy", initialProxy)).ShouldNot(HaveOccurred())
+			Expect(s.DeleteResource("Service", serviceName)).ShouldNot(HaveOccurred())
+			Expect(s.DeleteResource("Secret", secretName)).ShouldNot(HaveOccurred())
+		})
+
+		It("should reject GatewayProxy that overlaps endpoints when sharing inline AdminKey value", func() {
+			gatewayProxyTemplate := `
+apiVersion: apisix.apache.org/v1alpha1
+kind: GatewayProxy
+metadata:
+  name: %s
+spec:
+  provider:
+    type: ControlPlane
+    controlPlane:
+      endpoints:
+      - %s
+      - %s
+      auth:
+        type: AdminKey
+        adminKey:
+          value: "%s"
+`
+
+			existingProxy := "gatewayproxy-inline-primary"
+			conflictingProxy := "gatewayproxy-inline-conflict"
+			endpointA := "https://127.0.0.1:9443"
+			endpointB := "https://10.0.0.1:9443"
+			endpointC := "https://192.168.0.1:9443"
+			inlineKey := "inline-credential"
+
+			err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, existingProxy, endpointA, endpointB, inlineKey))
+			Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy with inline AdminKey")
+
+			time.Sleep(2 * time.Second)
+
+			err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, conflictingProxy, endpointB, endpointC, inlineKey))
+			Expect(err).Should(HaveOccurred(), "expecting conflict for overlapping endpoints with shared AdminKey")
+			Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict"))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), conflictingProxy)))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), existingProxy)))
+			Expect(err.Error()).To(ContainSubstring("control plane endpoints"))
+			Expect(err.Error()).To(ContainSubstring("inline AdminKey value"))
+		})
+
+		It("should reject GatewayProxy update that creates conflict with another GatewayProxy", func() {
+			serviceTemplate := `
+apiVersion: v1
+kind: Service
+metadata:
+  name: %s
+spec:
+  selector:
+    app: dummy-control-plane
+  ports:
+  - name: admin
+    port: 9180
+    targetPort: 9180
+`
+			secretTemplate := `
+apiVersion: v1
+kind: Secret
+metadata:
+  name: %s
+type: Opaque
+stringData:
+  %s: %s
+`
+			sharedServiceName := "gatewayproxy-update-shared-service"
+			sharedSecretName := "gatewayproxy-update-shared-secret"
+			uniqueServiceName := "gatewayproxy-update-unique-service"
+			proxyA := "gatewayproxy-update-a"
+			proxyB := "gatewayproxy-update-b"
+
+			Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, sharedServiceName))).ShouldNot(HaveOccurred(), "creating shared Service")
+			Expect(s.CreateResourceFromString(fmt.Sprintf(serviceTemplate, uniqueServiceName))).ShouldNot(HaveOccurred(), "creating unique Service")
+			Expect(s.CreateResourceFromString(fmt.Sprintf(secretTemplate, sharedSecretName, "token", "value"))).ShouldNot(HaveOccurred(), "creating shared Secret")
+
+			err := s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyA, sharedServiceName, sharedSecretName))
+			Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy A with shared Service and Secret")
+
+			time.Sleep(2 * time.Second)
+
+			err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyB, uniqueServiceName, sharedSecretName))
+			Expect(err).ShouldNot(HaveOccurred(), "creating GatewayProxy B with unique Service but same Secret")
+
+			time.Sleep(2 * time.Second)
+
+			By("updating GatewayProxy B to use the same Service as GatewayProxy A, causing conflict")
+			err = s.CreateResourceFromString(fmt.Sprintf(gatewayProxyTemplate, proxyB, sharedServiceName, sharedSecretName))
+			Expect(err).Should(HaveOccurred(), "expecting conflict when updating to same Service")
+			Expect(err.Error()).To(ContainSubstring("gateway proxy configuration conflict"))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), proxyA)))
+			Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("%s/%s", s.Namespace(), proxyB)))
+		})
+	})
 })