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

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

commit 63bed834a314fcb592648231051be9e5ec85162b
Author: Ashing Zheng <[email protected]>
AuthorDate: Fri Oct 10 17:09:18 2025 +0800

    feat: support gateway proxy webhook
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/webhook/v1/gatewayproxy_webhook.go      | 127 +++++++++++++-
 internal/webhook/v1/gatewayproxy_webhook_test.go | 213 ++++++++++++++++++++++-
 2 files changed, 337 insertions(+), 3 deletions(-)

diff --git a/internal/webhook/v1/gatewayproxy_webhook.go 
b/internal/webhook/v1/gatewayproxy_webhook.go
index 75bccea3..764e655b 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.validateGatewayGroupConflict(ctx, gp); err != nil {
+               return warnings, 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.validateGatewayGroupConflict(ctx, gp); err != nil {
+               return warnings, err
+       }
+
+       return warnings, nil
 }
 
 func (v *GatewayProxyCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
@@ -111,3 +123,114 @@ func (v *GatewayProxyCustomValidator) collectWarnings(ctx 
context.Context, gp *v
 
        return warnings
 }
+
+func (v *GatewayProxyCustomValidator) validateGatewayGroupConflict(ctx 
context.Context, gp *v1alpha1.GatewayProxy) error {
+       current := buildGatewayGroupConfig(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 := buildGatewayGroupConfig(&other)
+               if !otherConfig.readyForConflict() {
+                       continue
+               }
+               if current.adminKeyKey != otherConfig.adminKeyKey {
+                       continue
+               }
+               if current.serviceKey != "" && current.serviceKey == 
otherConfig.serviceKey {
+                       return fmt.Errorf("gateway group conflict: GatewayProxy 
%s/%s and %s/%s both target %s while sharing %s",
+                               gp.GetNamespace(), gp.GetName(),
+                               other.GetNamespace(), other.GetName(),
+                               current.serviceDescription,
+                               current.adminKeyDescription,
+                       )
+               }
+               if len(current.endpoints) > 0 && len(otherConfig.endpoints) > 0 
{
+                       if overlap := current.endpointOverlap(otherConfig); 
len(overlap) > 0 {
+                               return fmt.Errorf("gateway group 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.adminKeyDescription,
+                               )
+                       }
+               }
+       }
+
+       return nil
+}
+
+type gatewayGroupConfig struct {
+       adminKeyKey         string
+       adminKeyDescription string
+       serviceKey          string
+       serviceDescription  string
+       endpoints           map[string]struct{}
+       sortedEndpoints     []string
+}
+
+func buildGatewayGroupConfig(gp *v1alpha1.GatewayProxy) gatewayGroupConfig {
+       var cfg gatewayGroupConfig
+
+       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.adminKeyKey = "value:" + value
+                       cfg.adminKeyDescription = "the same inline AdminKey 
value"
+               } else if cp.Auth.AdminKey.ValueFrom != nil && 
cp.Auth.AdminKey.ValueFrom.SecretKeyRef != nil {
+                       ref := cp.Auth.AdminKey.ValueFrom.SecretKeyRef
+                       cfg.adminKeyKey = fmt.Sprintf("secret:%s/%s:%s", 
gp.GetNamespace(), ref.Name, ref.Key)
+                       cfg.adminKeyDescription = fmt.Sprintf("AdminKey secret 
%s/%s key %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))
+               cfg.sortedEndpoints = append([]string(nil), cp.Endpoints...)
+               for _, endpoint := range cfg.sortedEndpoints {
+                       cfg.endpoints[endpoint] = struct{}{}
+               }
+               sort.Strings(cfg.sortedEndpoints)
+       }
+
+       return cfg
+}
+
+func (c gatewayGroupConfig) readyForConflict() bool {
+       if c.adminKeyKey == "" {
+               return false
+       }
+       return c.serviceKey != "" || len(c.endpoints) > 0
+}
+
+func (c gatewayGroupConfig) endpointOverlap(other gatewayGroupConfig) []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..82ab018b 100644
--- a/internal/webhook/v1/gatewayproxy_webhook_test.go
+++ b/internal/webhook/v1/gatewayproxy_webhook_test.go
@@ -54,7 +54,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 +72,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 +185,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 = "candidate"
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
candidate)
+       require.Error(t, err)
+       require.Len(t, warnings, 0)
+       require.Contains(t, err.Error(), "gateway group conflict")
+       require.Contains(t, err.Error(), "Service default/control-plane port 
9180")
+       require.Contains(t, err.Error(), "AdminKey secret default/admin-key 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("candidate", 
[]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 group 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 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 = "candidate"
+       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 = "candidate"
+       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("candidate", 
[]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 group 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("candidate", 
[]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)
+}

Reply via email to