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

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

commit a726717ca61639d45f14d72bb21ad27e64212a21
Author: Ashing Zheng <[email protected]>
AuthorDate: Thu Sep 25 17:45:12 2025 +0800

    feat: support gateway proxy webhook
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/manager/webhooks.go                     |   3 +
 internal/webhook/v1/gatewayproxy_webhook.go      | 133 ++++++++++++++++++++
 internal/webhook/v1/gatewayproxy_webhook_test.go | 152 +++++++++++++++++++++++
 3 files changed, 288 insertions(+)

diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go
index 07b6f381..be0bd360 100644
--- a/internal/manager/webhooks.go
+++ b/internal/manager/webhooks.go
@@ -35,5 +35,8 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) 
error {
        if err := webhookv1.SetupGatewayWebhookWithManager(mgr); err != nil {
                return err
        }
+       if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != 
nil {
+               return err
+       }
        return nil
 }
diff --git a/internal/webhook/v1/gatewayproxy_webhook.go 
b/internal/webhook/v1/gatewayproxy_webhook.go
new file mode 100644
index 00000000..0abd5e34
--- /dev/null
+++ b/internal/webhook/v1/gatewayproxy_webhook.go
@@ -0,0 +1,133 @@
+// 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 v1
+
+import (
+       "context"
+       "fmt"
+
+       corev1 "k8s.io/api/core/v1"
+       k8serrors "k8s.io/apimachinery/pkg/api/errors"
+       "k8s.io/apimachinery/pkg/runtime"
+       ctrl "sigs.k8s.io/controller-runtime"
+       "sigs.k8s.io/controller-runtime/pkg/client"
+       logf "sigs.k8s.io/controller-runtime/pkg/log"
+       "sigs.k8s.io/controller-runtime/pkg/webhook"
+       "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+)
+
+var gatewayProxyLog = logf.Log.WithName("gatewayproxy-resource")
+
+func SetupGatewayProxyWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&v1alpha1.GatewayProxy{}).
+               WithValidator(&GatewayProxyCustomValidator{Client: 
mgr.GetClient()}).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v1alpha1-gatewayproxy,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=gatewayproxies,verbs=create;update,versions=v1alpha1,name=vgatewayproxy-v1alpha1.kb.io,admissionReviewVersions=v1
+
+type GatewayProxyCustomValidator struct {
+       Client client.Client
+}
+
+var _ webhook.CustomValidator = &GatewayProxyCustomValidator{}
+
+func (v *GatewayProxyCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       gp, ok := obj.(*v1alpha1.GatewayProxy)
+       if !ok {
+               return nil, fmt.Errorf("expected a GatewayProxy object but got 
%T", obj)
+       }
+       gatewayProxyLog.Info("Validation for GatewayProxy upon creation", 
"name", gp.GetName(), "namespace", gp.GetNamespace())
+
+       return v.collectWarnings(ctx, gp), nil
+}
+
+func (v *GatewayProxyCustomValidator) ValidateUpdate(ctx context.Context, 
oldObj, newObj runtime.Object) (admission.Warnings, error) {
+       gp, ok := newObj.(*v1alpha1.GatewayProxy)
+       if !ok {
+               return nil, fmt.Errorf("expected a GatewayProxy object for the 
newObj but got %T", newObj)
+       }
+       gatewayProxyLog.Info("Validation for GatewayProxy upon update", "name", 
gp.GetName(), "namespace", gp.GetNamespace())
+
+       return v.collectWarnings(ctx, gp), nil
+}
+
+func (v *GatewayProxyCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *GatewayProxyCustomValidator) collectWarnings(ctx context.Context, gp 
*v1alpha1.GatewayProxy) admission.Warnings {
+       var warnings admission.Warnings
+
+       warnings = append(warnings, v.warnIfProviderServiceMissing(ctx, gp)...)
+       warnings = append(warnings, v.warnIfAdminKeySecretMissing(ctx, gp)...)
+
+       return warnings
+}
+
+func (v *GatewayProxyCustomValidator) warnIfProviderServiceMissing(ctx 
context.Context, gp *v1alpha1.GatewayProxy) admission.Warnings {
+       if gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil || 
gp.Spec.Provider.ControlPlane.Service == nil {
+               return nil
+       }
+
+       svcRef := gp.Spec.Provider.ControlPlane.Service
+       key := client.ObjectKey{Namespace: gp.GetNamespace(), Name: svcRef.Name}
+       var svc corev1.Service
+       if err := v.Client.Get(ctx, key, &svc); err != nil {
+               if k8serrors.IsNotFound(err) {
+                       msg := fmt.Sprintf("Referenced Service '%s/%s' not 
found at spec.provider.controlPlane.service", key.Namespace, key.Name)
+                       gatewayProxyLog.Info("GatewayProxy references missing 
Service", "gatewayproxy", gp.GetName(), "namespace", key.Namespace, "service", 
key.Name)
+                       return admission.Warnings{msg}
+               }
+               gatewayProxyLog.Error(err, "failed to resolve Service for 
GatewayProxy", "gatewayproxy", gp.GetName(), "namespace", key.Namespace, 
"service", key.Name)
+       }
+       return nil
+}
+
+func (v *GatewayProxyCustomValidator) warnIfAdminKeySecretMissing(ctx 
context.Context, gp *v1alpha1.GatewayProxy) admission.Warnings {
+       if gp.Spec.Provider == nil || gp.Spec.Provider.ControlPlane == nil {
+               return nil
+       }
+
+       auth := gp.Spec.Provider.ControlPlane.Auth
+       if auth.Type != v1alpha1.AuthTypeAdminKey || auth.AdminKey == nil || 
auth.AdminKey.ValueFrom == nil || auth.AdminKey.ValueFrom.SecretKeyRef == nil {
+               return nil
+       }
+
+       ref := auth.AdminKey.ValueFrom.SecretKeyRef
+       key := client.ObjectKey{Namespace: gp.GetNamespace(), Name: ref.Name}
+       var secret corev1.Secret
+       if err := v.Client.Get(ctx, key, &secret); err != nil {
+               if k8serrors.IsNotFound(err) {
+                       msg := fmt.Sprintf("Referenced Secret '%s/%s' not found 
at spec.provider.controlPlane.auth.adminKey.valueFrom.secretKeyRef", 
key.Namespace, key.Name)
+                       gatewayProxyLog.Info("GatewayProxy references missing 
Secret", "gatewayproxy", gp.GetName(), "namespace", key.Namespace, "secret", 
key.Name)
+                       return admission.Warnings{msg}
+               }
+               gatewayProxyLog.Error(err, "failed to resolve Secret for 
GatewayProxy", "gatewayproxy", gp.GetName(), "namespace", key.Namespace, 
"secret", key.Name)
+               return nil
+       }
+
+       if _, ok := secret.Data[ref.Key]; !ok {
+               msg := fmt.Sprintf("Secret key '%s' not found in Secret '%s/%s' 
at spec.provider.controlPlane.auth.adminKey.valueFrom.secretKeyRef", ref.Key, 
key.Namespace, key.Name)
+               gatewayProxyLog.Info("GatewayProxy references Secret without 
required key", "gatewayproxy", gp.GetName(), "namespace", key.Namespace, 
"secret", key.Name, "key", ref.Key)
+               return admission.Warnings{msg}
+       }
+
+       return nil
+}
diff --git a/internal/webhook/v1/gatewayproxy_webhook_test.go 
b/internal/webhook/v1/gatewayproxy_webhook_test.go
new file mode 100644
index 00000000..03d310b7
--- /dev/null
+++ b/internal/webhook/v1/gatewayproxy_webhook_test.go
@@ -0,0 +1,152 @@
+// 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 v1
+
+import (
+       "context"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+       corev1 "k8s.io/api/core/v1"
+       metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/runtime"
+       clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+       "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+       v1alpha1 "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+)
+
+func buildGatewayProxyValidator(t *testing.T, objects ...runtime.Object) 
*GatewayProxyCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, v1alpha1.AddToScheme(scheme))
+
+       builder := fake.NewClientBuilder().WithScheme(scheme)
+       if len(objects) > 0 {
+               builder = builder.WithRuntimeObjects(objects...)
+       }
+
+       return &GatewayProxyCustomValidator{Client: builder.Build()}
+}
+
+func newGatewayProxy() *v1alpha1.GatewayProxy {
+       return &v1alpha1.GatewayProxy{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: v1alpha1.GatewayProxySpec{
+                       Provider: &v1alpha1.GatewayProxyProvider{
+                               Type: v1alpha1.ProviderTypeControlPlane,
+                               ControlPlane: &v1alpha1.ControlPlaneProvider{
+                                       Service: 
&v1alpha1.ProviderService{Name: "control-plane"},
+                                       Auth: v1alpha1.ControlPlaneAuth{
+                                               Type: v1alpha1.AuthTypeAdminKey,
+                                               AdminKey: 
&v1alpha1.AdminKeyAuth{
+                                                       ValueFrom: 
&v1alpha1.AdminKeyValueFrom{
+                                                               SecretKeyRef: 
&v1alpha1.SecretKeySelector{
+                                                                       Name: 
"admin-key",
+                                                                       Key:  
"token",
+                                                               },
+                                                       },
+                                               },
+                                       },
+                               },
+                       },
+               },
+       }
+}
+
+func TestGatewayProxyValidator_MissingService(t *testing.T) {
+       gp := newGatewayProxy()
+       gp.Spec.Provider.ControlPlane.Auth.AdminKey = nil
+       validator := buildGatewayProxyValidator(t)
+
+       warnings, err := validator.ValidateCreate(context.Background(), gp)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       require.Contains(t, warnings[0], "Service 'default/control-plane' not 
found")
+}
+
+func TestGatewayProxyValidator_MissingAdminSecret(t *testing.T) {
+       gp := newGatewayProxy()
+       service := &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "control-plane",
+                       Namespace: "default",
+               },
+       }
+       validator := buildGatewayProxyValidator(t, service)
+
+       warnings, err := validator.ValidateCreate(context.Background(), gp)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       require.Contains(t, warnings[0], "Secret 'default/admin-key' not found")
+}
+
+func TestGatewayProxyValidator_MissingAdminSecretKey(t *testing.T) {
+       gp := newGatewayProxy()
+       secret := &corev1.Secret{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "admin-key",
+                       Namespace: "default",
+               },
+               Data: map[string][]byte{
+                       "wrong": []byte("value"),
+               },
+       }
+
+       service := &corev1.Service{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "control-plane",
+                       Namespace: "default",
+               },
+       }
+
+       validator := buildGatewayProxyValidator(t, secret, service)
+
+       warnings, err := validator.ValidateCreate(context.Background(), gp)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       require.Contains(t, warnings[0], "Secret key 'token' not found")
+}
+
+func TestGatewayProxyValidator_NoWarnings(t *testing.T) {
+       gp := newGatewayProxy()
+       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, secret, service)
+
+       warnings, err := validator.ValidateCreate(context.Background(), gp)
+       require.NoError(t, err)
+       require.Empty(t, warnings)
+}

Reply via email to