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 ab2624e2592d3d958cd7b7b4a32558a706f24002
Author: Ashing Zheng <[email protected]>
AuthorDate: Fri Sep 26 16:01:42 2025 +0800

    feat: add apisixtls and apisixconsumer
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/manager/webhooks.go                       |   6 +
 internal/webhook/v1/apisixconsumer_webhook.go      | 116 +++++++++++++++++
 internal/webhook/v1/apisixconsumer_webhook_test.go | 137 +++++++++++++++++++++
 internal/webhook/v1/apisixtls_webhook.go           |  98 +++++++++++++++
 internal/webhook/v1/apisixtls_webhook_test.go      | 116 +++++++++++++++++
 5 files changed, 473 insertions(+)

diff --git a/internal/manager/webhooks.go b/internal/manager/webhooks.go
index be0bd360..91242972 100644
--- a/internal/manager/webhooks.go
+++ b/internal/manager/webhooks.go
@@ -38,5 +38,11 @@ func setupWebhooks(_ context.Context, mgr manager.Manager) 
error {
        if err := webhookv1.SetupGatewayProxyWebhookWithManager(mgr); err != 
nil {
                return err
        }
+       if err := webhookv1.SetupApisixConsumerWebhookWithManager(mgr); err != 
nil {
+               return err
+       }
+       if err := webhookv1.SetupApisixTlsWebhookWithManager(mgr); err != nil {
+               return err
+       }
        return nil
 }
diff --git a/internal/webhook/v1/apisixconsumer_webhook.go 
b/internal/webhook/v1/apisixconsumer_webhook.go
new file mode 100644
index 00000000..ee0ddeb2
--- /dev/null
+++ b/internal/webhook/v1/apisixconsumer_webhook.go
@@ -0,0 +1,116 @@
+// 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"
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/types"
+       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"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var apisixConsumerLog = logf.Log.WithName("apisixconsumer-resource")
+
+func SetupApisixConsumerWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&apisixv2.ApisixConsumer{}).
+               WithValidator(&ApisixConsumerCustomValidator{Client: 
mgr.GetClient()}).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixconsumer,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixconsumers,verbs=create;update,versions=v2,name=vapisixconsumer-v2.kb.io,admissionReviewVersions=v1
+
+type ApisixConsumerCustomValidator struct {
+       Client client.Client
+}
+
+var _ webhook.CustomValidator = &ApisixConsumerCustomValidator{}
+
+func (v *ApisixConsumerCustomValidator) ValidateCreate(ctx context.Context, 
obj runtime.Object) (admission.Warnings, error) {
+       consumer, ok := obj.(*apisixv2.ApisixConsumer)
+       if !ok {
+               return nil, fmt.Errorf("expected an ApisixConsumer object but 
got %T", obj)
+       }
+       apisixConsumerLog.Info("Validation for ApisixConsumer upon creation", 
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
+
+       return v.collectWarnings(ctx, consumer), nil
+}
+
+func (v *ApisixConsumerCustomValidator) ValidateUpdate(ctx context.Context, 
oldObj, newObj runtime.Object) (admission.Warnings, error) {
+       consumer, ok := newObj.(*apisixv2.ApisixConsumer)
+       if !ok {
+               return nil, fmt.Errorf("expected an ApisixConsumer object for 
the newObj but got %T", newObj)
+       }
+       apisixConsumerLog.Info("Validation for ApisixConsumer upon update", 
"name", consumer.GetName(), "namespace", consumer.GetNamespace())
+
+       return v.collectWarnings(ctx, consumer), nil
+}
+
+func (*ApisixConsumerCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *ApisixConsumerCustomValidator) collectWarnings(ctx context.Context, 
consumer *apisixv2.ApisixConsumer) admission.Warnings {
+       checker := reference.NewChecker(v.Client, apisixConsumerLog)
+       namespace := consumer.GetNamespace()
+       var warnings admission.Warnings
+
+       addSecretWarning := func(ref *corev1.LocalObjectReference) {
+               if ref == nil || ref.Name == "" {
+                       return
+               }
+
+               warnings = append(warnings, checker.Secret(ctx, 
reference.SecretRef{
+                       Object: consumer,
+                       NamespacedName: types.NamespacedName{
+                               Namespace: namespace,
+                               Name:      ref.Name,
+                       },
+               })...)
+       }
+
+       params := consumer.Spec.AuthParameter
+       if params.BasicAuth != nil {
+               addSecretWarning(params.BasicAuth.SecretRef)
+       }
+       if params.KeyAuth != nil {
+               addSecretWarning(params.KeyAuth.SecretRef)
+       }
+       if params.WolfRBAC != nil {
+               addSecretWarning(params.WolfRBAC.SecretRef)
+       }
+       if params.JwtAuth != nil {
+               addSecretWarning(params.JwtAuth.SecretRef)
+       }
+       if params.HMACAuth != nil {
+               addSecretWarning(params.HMACAuth.SecretRef)
+       }
+       if params.LDAPAuth != nil {
+               addSecretWarning(params.LDAPAuth.SecretRef)
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/apisixconsumer_webhook_test.go 
b/internal/webhook/v1/apisixconsumer_webhook_test.go
new file mode 100644
index 00000000..2e88a6a9
--- /dev/null
+++ b/internal/webhook/v1/apisixconsumer_webhook_test.go
@@ -0,0 +1,137 @@
+// 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"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+)
+
+func buildApisixConsumerValidator(t *testing.T, objects ...runtime.Object) 
*ApisixConsumerCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, apisixv2.AddToScheme(scheme))
+
+       builder := fake.NewClientBuilder().WithScheme(scheme)
+       if len(objects) > 0 {
+               builder = builder.WithRuntimeObjects(objects...)
+       }
+
+       return &ApisixConsumerCustomValidator{Client: builder.Build()}
+}
+
+func TestApisixConsumerValidator_MissingBasicAuthSecret(t *testing.T) {
+       consumer := &apisixv2.ApisixConsumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
+                               },
+                       },
+               },
+       }
+
+       validator := buildApisixConsumerValidator(t)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.NoError(t, err)
+       require.Equal(t, 1, len(warnings))
+       require.Equal(t, "Referenced Secret 'default/basic-auth' not found", 
warnings[0])
+}
+
+func TestApisixConsumerValidator_MultipleSecretWarnings(t *testing.T) {
+       consumer := &apisixv2.ApisixConsumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               BasicAuth: &apisixv2.ApisixConsumerBasicAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "basic-auth"},
+                               },
+                               JwtAuth: &apisixv2.ApisixConsumerJwtAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "jwt-auth"},
+                               },
+                               HMACAuth: &apisixv2.ApisixConsumerHMACAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "hmac-auth"},
+                               },
+                       },
+               },
+       }
+
+       basicAuthSecret := &corev1.Secret{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "basic-auth",
+                       Namespace: "default",
+               },
+       }
+
+       validator := buildApisixConsumerValidator(t, basicAuthSecret)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.NoError(t, err)
+       require.Len(t, warnings, 2)
+       require.ElementsMatch(t, []string{
+               "Referenced Secret 'default/jwt-auth' not found",
+               "Referenced Secret 'default/hmac-auth' not found",
+       }, warnings)
+}
+
+func TestApisixConsumerValidator_NoWarningsWhenSecretsExist(t *testing.T) {
+       consumer := &apisixv2.ApisixConsumer{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixConsumerSpec{
+                       AuthParameter: apisixv2.ApisixConsumerAuthParameter{
+                               KeyAuth: &apisixv2.ApisixConsumerKeyAuth{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "key-auth"},
+                               },
+                               WolfRBAC: &apisixv2.ApisixConsumerWolfRBAC{
+                                       SecretRef: 
&corev1.LocalObjectReference{Name: "wolf-rbac"},
+                               },
+                       },
+               },
+       }
+
+       secrets := []runtime.Object{
+               &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "key-auth", 
Namespace: "default"}},
+               &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wolf-rbac", 
Namespace: "default"}},
+       }
+
+       validator := buildApisixConsumerValidator(t, secrets...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), 
consumer)
+       require.NoError(t, err)
+       require.Empty(t, warnings)
+}
diff --git a/internal/webhook/v1/apisixtls_webhook.go 
b/internal/webhook/v1/apisixtls_webhook.go
new file mode 100644
index 00000000..19cbb32a
--- /dev/null
+++ b/internal/webhook/v1/apisixtls_webhook.go
@@ -0,0 +1,98 @@
+// 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"
+
+       "k8s.io/apimachinery/pkg/runtime"
+       "k8s.io/apimachinery/pkg/types"
+       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"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       
"github.com/apache/apisix-ingress-controller/internal/webhook/v1/reference"
+)
+
+var apisixTlsLog = logf.Log.WithName("apisixtls-resource")
+
+func SetupApisixTlsWebhookWithManager(mgr ctrl.Manager) error {
+       return ctrl.NewWebhookManagedBy(mgr).
+               For(&apisixv2.ApisixTls{}).
+               WithValidator(&ApisixTlsCustomValidator{Client: 
mgr.GetClient()}).
+               Complete()
+}
+
+// 
+kubebuilder:webhook:path=/validate-apisix-apache-org-v2-apisixtls,mutating=false,failurePolicy=fail,sideEffects=None,groups=apisix.apache.org,resources=apisixtlses,verbs=create;update,versions=v2,name=vapisixtls-v2.kb.io,admissionReviewVersions=v1
+
+type ApisixTlsCustomValidator struct {
+       Client client.Client
+}
+
+var _ webhook.CustomValidator = &ApisixTlsCustomValidator{}
+
+func (v *ApisixTlsCustomValidator) ValidateCreate(ctx context.Context, obj 
runtime.Object) (admission.Warnings, error) {
+       tls, ok := obj.(*apisixv2.ApisixTls)
+       if !ok {
+               return nil, fmt.Errorf("expected an ApisixTls object but got 
%T", obj)
+       }
+       apisixTlsLog.Info("Validation for ApisixTls upon creation", "name", 
tls.GetName(), "namespace", tls.GetNamespace())
+
+       return v.collectWarnings(ctx, tls), nil
+}
+
+func (v *ApisixTlsCustomValidator) ValidateUpdate(ctx context.Context, oldObj, 
newObj runtime.Object) (admission.Warnings, error) {
+       tls, ok := newObj.(*apisixv2.ApisixTls)
+       if !ok {
+               return nil, fmt.Errorf("expected an ApisixTls object for the 
newObj but got %T", newObj)
+       }
+       apisixTlsLog.Info("Validation for ApisixTls upon update", "name", 
tls.GetName(), "namespace", tls.GetNamespace())
+
+       return v.collectWarnings(ctx, tls), nil
+}
+
+func (*ApisixTlsCustomValidator) ValidateDelete(context.Context, 
runtime.Object) (admission.Warnings, error) {
+       return nil, nil
+}
+
+func (v *ApisixTlsCustomValidator) collectWarnings(ctx context.Context, tls 
*apisixv2.ApisixTls) admission.Warnings {
+       checker := reference.NewChecker(v.Client, apisixTlsLog)
+       var warnings admission.Warnings
+
+       warnings = append(warnings, checker.Secret(ctx, reference.SecretRef{
+               Object: tls,
+               NamespacedName: types.NamespacedName{
+                       Namespace: tls.Spec.Secret.Namespace,
+                       Name:      tls.Spec.Secret.Name,
+               },
+       })...)
+
+       if client := tls.Spec.Client; client != nil {
+               warnings = append(warnings, checker.Secret(ctx, 
reference.SecretRef{
+                       Object: tls,
+                       NamespacedName: types.NamespacedName{
+                               Namespace: client.CASecret.Namespace,
+                               Name:      client.CASecret.Name,
+                       },
+               })...)
+       }
+
+       return warnings
+}
diff --git a/internal/webhook/v1/apisixtls_webhook_test.go 
b/internal/webhook/v1/apisixtls_webhook_test.go
new file mode 100644
index 00000000..9337c46b
--- /dev/null
+++ b/internal/webhook/v1/apisixtls_webhook_test.go
@@ -0,0 +1,116 @@
+// 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"
+
+       apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
+)
+
+func buildApisixTlsValidator(t *testing.T, objects ...runtime.Object) 
*ApisixTlsCustomValidator {
+       t.Helper()
+
+       scheme := runtime.NewScheme()
+       require.NoError(t, clientgoscheme.AddToScheme(scheme))
+       require.NoError(t, apisixv2.AddToScheme(scheme))
+
+       builder := fake.NewClientBuilder().WithScheme(scheme)
+       if len(objects) > 0 {
+               builder = builder.WithRuntimeObjects(objects...)
+       }
+
+       return &ApisixTlsCustomValidator{Client: builder.Build()}
+}
+
+func newApisixTls() *apisixv2.ApisixTls {
+       return &apisixv2.ApisixTls{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "demo",
+                       Namespace: "default",
+               },
+               Spec: apisixv2.ApisixTlsSpec{
+                       Hosts: []apisixv2.HostType{"example.com"},
+                       Secret: apisixv2.ApisixSecret{
+                               Name:      "server-cert",
+                               Namespace: "default",
+                       },
+               },
+       }
+}
+
+func TestApisixTlsValidator_MissingServerSecret(t *testing.T) {
+       tls := newApisixTls()
+       validator := buildApisixTlsValidator(t)
+
+       warnings, err := validator.ValidateCreate(context.Background(), tls)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       require.Contains(t, warnings[0], "Referenced Secret 
'default/server-cert' not found")
+}
+
+func TestApisixTlsValidator_MissingClientSecret(t *testing.T) {
+       tls := newApisixTls()
+       tls.Spec.Client = &apisixv2.ApisixMutualTlsClientConfig{
+               CASecret: apisixv2.ApisixSecret{
+                       Name:      "mtls-ca",
+                       Namespace: "mtls",
+               },
+       }
+
+       serverSecret := &corev1.Secret{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "server-cert",
+                       Namespace: "default",
+               },
+       }
+
+       validator := buildApisixTlsValidator(t, serverSecret)
+
+       warnings, err := validator.ValidateCreate(context.Background(), tls)
+       require.NoError(t, err)
+       require.Len(t, warnings, 1)
+       require.Contains(t, warnings[0], "Referenced Secret 'mtls/mtls-ca' not 
found")
+}
+
+func TestApisixTlsValidator_NoWarningsWhenSecretsExist(t *testing.T) {
+       tls := newApisixTls()
+       tls.Spec.Client = &apisixv2.ApisixMutualTlsClientConfig{
+               CASecret: apisixv2.ApisixSecret{
+                       Name:      "mtls-ca",
+                       Namespace: "mtls",
+               },
+       }
+
+       objects := []runtime.Object{
+               &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: 
"server-cert", Namespace: "default"}},
+               &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "mtls-ca", 
Namespace: "mtls"}},
+       }
+
+       validator := buildApisixTlsValidator(t, objects...)
+
+       warnings, err := validator.ValidateCreate(context.Background(), tls)
+       require.NoError(t, err)
+       require.Empty(t, warnings)
+}

Reply via email to