You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by kv...@apache.org on 2021/10/08 06:57:46 UTC

[apisix-ingress-controller] branch master updated: feat: add webhooks for consumer/tls/upstream (#667)

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

kvn 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 9dd4f40  feat: add webhooks for consumer/tls/upstream (#667)
9dd4f40 is described below

commit 9dd4f40b9fc74be6c29ba11cf9086ecbbd51f9e2
Author: Hoshea Jiang <fg...@gmail.com>
AuthorDate: Fri Oct 8 14:57:41 2021 +0800

    feat: add webhooks for consumer/tls/upstream (#667)
---
 pkg/api/router/webhook.go               | 10 ++++-
 pkg/api/validation/apisix_consumer.go   | 80 +++++++++++++++++++++++++++++++++
 pkg/api/validation/apisix_route.go      | 78 +++++++++++++++++---------------
 pkg/api/validation/apisix_route_test.go | 18 +++++---
 pkg/api/validation/apisix_tls.go        | 80 +++++++++++++++++++++++++++++++++
 pkg/api/validation/apisix_upstream.go   | 80 +++++++++++++++++++++++++++++++++
 pkg/api/validation/utils.go             | 37 +++++++++++----
 pkg/api/validation/utils_test.go        | 48 ++++++++++++++++++++
 pkg/apisix/apisix.go                    |  1 +
 pkg/apisix/nonexistentclient.go         |  4 ++
 pkg/apisix/schema.go                    |  5 +++
 pkg/apisix/schema_test.go               |  6 +++
 test/e2e/ingress/webhook.go             |  4 +-
 test/e2e/scaffold/ingress.go            | 52 +++++++++++++++++++--
 14 files changed, 447 insertions(+), 56 deletions(-)

diff --git a/pkg/api/router/webhook.go b/pkg/api/router/webhook.go
index 280866b..f6d7f72 100644
--- a/pkg/api/router/webhook.go
+++ b/pkg/api/router/webhook.go
@@ -26,5 +26,13 @@ import (
 func MountWebhooks(r *gin.Engine, co *apisix.ClusterOptions) {
 	// init the schema client, it will be used to query schema of objects.
 	_, _ = validation.GetSchemaClient(co)
-	r.POST("/validation/apisixroutes/plugin", gin.WrapH(validation.NewPluginValidatorHandler()))
+
+	// grouping validation routes
+	validationGroup := r.Group("/validation")
+	{
+		validationGroup.POST("/apisixroutes", validation.NewHandlerFunc("ApisixRoute", validation.ApisixRouteValidator))
+		validationGroup.POST("/apisixupstreams", validation.NewHandlerFunc("ApisixUpstream", validation.ApisixUpstreamValidator))
+		validationGroup.POST("/apisixconsumers", validation.NewHandlerFunc("ApisixConsumer", validation.ApisixConsumerValidator))
+		validationGroup.POST("/apisixtlses", validation.NewHandlerFunc("ApisixTls", validation.ApisixTlsValidator))
+	}
 }
diff --git a/pkg/api/validation/apisix_consumer.go b/pkg/api/validation/apisix_consumer.go
new file mode 100644
index 0000000..41952ff
--- /dev/null
+++ b/pkg/api/validation/apisix_consumer.go
@@ -0,0 +1,80 @@
+// 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 validation
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
+	kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
+	"github.com/xeipuuv/gojsonschema"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/apache/apisix-ingress-controller/pkg/apisix"
+	v1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta1"
+	"github.com/apache/apisix-ingress-controller/pkg/log"
+)
+
+// errNotApisixConsumer will be used when the validating object is not ApisixConsumer.
+var errNotApisixConsumer = errors.New("object is not ApisixConsumer")
+
+// ApisixConsumerValidator validates ApisixConsumer's spec.
+var ApisixConsumerValidator = kwhvalidating.ValidatorFunc(
+	func(ctx context.Context, review *kwhmodel.AdmissionReview, object metav1.Object) (result *kwhvalidating.ValidatorResult, err error) {
+		log.Debug("arrive ApisixConsumer validator webhook")
+
+		valid := true
+		var spec interface{}
+
+		switch ac := object.(type) {
+		case *v2beta1.ApisixRoute:
+			spec = ac.Spec
+		case *v2alpha1.ApisixRoute:
+			spec = ac.Spec
+		case *v1.ApisixRoute:
+			spec = ac.Spec
+		default:
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: errNotApisixConsumer.Error()}, errNotApisixConsumer
+		}
+
+		client, err := GetSchemaClient(&apisix.ClusterOptions{})
+		if err != nil {
+			msg := "failed to get the schema client"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+
+		cs, err := client.GetConsumerSchema(ctx)
+		if err != nil {
+			msg := "failed to get consumer's schema"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+		acSchemaLoader := gojsonschema.NewStringLoader(cs.Content)
+
+		var msgs []string
+		if _, err := validateSchema(&acSchemaLoader, spec); err != nil {
+			valid = false
+			msgs = append(msgs, err.Error())
+		}
+
+		return &kwhvalidating.ValidatorResult{Valid: valid, Message: strings.Join(msgs, "\n")}, nil
+	},
+)
diff --git a/pkg/api/validation/apisix_route.go b/pkg/api/validation/apisix_route.go
index 073cb5b..b3860b0 100644
--- a/pkg/api/validation/apisix_route.go
+++ b/pkg/api/validation/apisix_route.go
@@ -19,13 +19,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/http"
 	"strings"
 
 	"github.com/hashicorp/go-multierror"
-	kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
 	kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
 	kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
+	"github.com/xeipuuv/gojsonschema"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/apache/apisix-ingress-controller/pkg/apisix"
@@ -35,44 +34,29 @@ import (
 	"github.com/apache/apisix-ingress-controller/pkg/log"
 )
 
-// NewPluginValidatorHandler returns a new http.Handler ready to handle admission reviews using the pluginValidator.
-func NewPluginValidatorHandler() http.Handler {
-	// Create a validating webhook.
-	wh, err := kwhvalidating.NewWebhook(kwhvalidating.WebhookConfig{
-		ID:        "apisixRoute-plugin",
-		Validator: pluginValidator,
-	})
-	if err != nil {
-		log.Errorf("failed to create webhook: %s", err)
-	}
-
-	h, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh})
-	if err != nil {
-		log.Errorf("failed to create webhook handle: %s", err)
-	}
-
-	return h
-}
-
-// ErrNotApisixRoute will be used when the validating object is not ApisixRoute.
-var ErrNotApisixRoute = errors.New("object is not ApisixRoute")
+// errNotApisixRoute will be used when the validating object is not ApisixRoute.
+var errNotApisixRoute = errors.New("object is not ApisixRoute")
 
 type apisixRoutePlugin struct {
 	Name   string
 	Config interface{}
 }
 
-// pluginValidator validates plugins in ApisixRoute.
+// ApisixRouteValidator validates ApisixRoute and its plugins.
 // When the validation of one plugin fails, it will continue to validate the rest of plugins.
-var pluginValidator = kwhvalidating.ValidatorFunc(
+var ApisixRouteValidator = kwhvalidating.ValidatorFunc(
 	func(ctx context.Context, review *kwhmodel.AdmissionReview, object metav1.Object) (result *kwhvalidating.ValidatorResult, err error) {
-		log.Debug("arrive plugin validator webhook")
+		log.Debug("arrive ApisixRoute validator webhook")
 
 		valid := true
 		var plugins []apisixRoutePlugin
+		var spec interface{}
 
 		switch ar := object.(type) {
 		case *v2beta1.ApisixRoute:
+			spec = ar.Spec
+
+			// validate plugins
 			for _, h := range ar.Spec.HTTP {
 				for _, p := range h.Plugins {
 					// only check plugins that are enabled.
@@ -84,6 +68,8 @@ var pluginValidator = kwhvalidating.ValidatorFunc(
 				}
 			}
 		case *v2alpha1.ApisixRoute:
+			spec = ar.Spec
+
 			for _, h := range ar.Spec.HTTP {
 				for _, p := range h.Plugins {
 					if p.Enable {
@@ -94,6 +80,8 @@ var pluginValidator = kwhvalidating.ValidatorFunc(
 				}
 			}
 		case *v1.ApisixRoute:
+			spec = ar.Spec
+
 			for _, r := range ar.Spec.Rules {
 				for _, path := range r.Http.Paths {
 					for _, p := range path.Plugins {
@@ -106,29 +94,44 @@ var pluginValidator = kwhvalidating.ValidatorFunc(
 				}
 			}
 		default:
-			return &kwhvalidating.ValidatorResult{Valid: false, Message: ErrNotApisixRoute.Error()}, ErrNotApisixRoute
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: errNotApisixRoute.Error()}, errNotApisixRoute
 		}
 
 		client, err := GetSchemaClient(&apisix.ClusterOptions{})
 		if err != nil {
-			log.Errorf("failed to get the schema client: %s", err)
-			return &kwhvalidating.ValidatorResult{Valid: false, Message: "failed to get the schema client"}, err
+			msg := "failed to get the schema client"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+
+		rs, err := client.GetRouteSchema(ctx)
+		if err != nil {
+			msg := "failed to get route's schema"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+		arSchemaLoader := gojsonschema.NewStringLoader(rs.Content)
+
+		var msgs []string
+		if _, err := validateSchema(&arSchemaLoader, spec); err != nil {
+			valid = false
+			msgs = append(msgs, err.Error())
+			log.Warnf("failed to validate ApisixRoute: %s", err)
 		}
 
-		var msg []string
 		for _, p := range plugins {
-			if v, m, err := validatePlugin(client, p.Name, p.Config); !v {
+			if v, err := validatePlugin(client, p.Name, p.Config); !v {
 				valid = false
-				msg = append(msg, m)
+				msgs = append(msgs, err.Error())
 				log.Warnf("failed to validate plugin %s: %s", p.Name, err)
 			}
 		}
 
-		return &kwhvalidating.ValidatorResult{Valid: valid, Message: strings.Join(msg, "\n")}, nil
+		return &kwhvalidating.ValidatorResult{Valid: valid, Message: strings.Join(msgs, "\n")}, nil
 	},
 )
 
-func validatePlugin(client apisix.Schema, pluginName string, pluginConfig interface{}) (valid bool, msg string, result error) {
+func validatePlugin(client apisix.Schema, pluginName string, pluginConfig interface{}) (valid bool, result error) {
 	valid = true
 
 	pluginSchema, err := client.GetPluginSchema(context.TODO(), pluginName)
@@ -136,14 +139,15 @@ func validatePlugin(client apisix.Schema, pluginName string, pluginConfig interf
 		result = fmt.Errorf("failed to get the schema of plugin %s: %s", pluginName, err)
 		log.Error(result)
 		valid = false
-		msg = result.Error()
 		return
 	}
 
-	if _, err := validateSchema(pluginSchema.Content, pluginConfig); err != nil {
+	pluginSchemaLoader := gojsonschema.NewStringLoader(pluginSchema.Content)
+	if _, err := validateSchema(&pluginSchemaLoader, pluginConfig); err != nil {
 		valid = false
-		msg = fmt.Sprintf("%s plugin's config is invalid\n", pluginName)
+		result = multierror.Append(result, fmt.Errorf("%s plugin's config is invalid", pluginName))
 		result = multierror.Append(result, err)
+		log.Warn(result)
 	}
 
 	return
diff --git a/pkg/api/validation/apisix_route_test.go b/pkg/api/validation/apisix_route_test.go
index 90a5e0a..2a5c16f 100644
--- a/pkg/api/validation/apisix_route_test.go
+++ b/pkg/api/validation/apisix_route_test.go
@@ -41,13 +41,19 @@ func (c fakeSchemaClient) GetPluginSchema(ctx context.Context, name string) (*ap
 	return nil, fmt.Errorf("can't find the plugin schema")
 }
 
-func (c fakeSchemaClient) GetRouteSchema(context.Context) (*api.Schema, error) {
+func (c fakeSchemaClient) GetRouteSchema(_ context.Context) (*api.Schema, error) {
 	return nil, nil
 }
-func (c fakeSchemaClient) GetUpstreamSchema(context.Context) (*api.Schema, error) {
+
+func (c fakeSchemaClient) GetUpstreamSchema(_ context.Context) (*api.Schema, error) {
 	return nil, nil
 }
-func (c fakeSchemaClient) GetConsumerSchema(context.Context) (*api.Schema, error) {
+
+func (c fakeSchemaClient) GetConsumerSchema(_ context.Context) (*api.Schema, error) {
+	return nil, nil
+}
+
+func (c fakeSchemaClient) GetSslSchema(_ context.Context) (*api.Schema, error) {
 	return nil, nil
 }
 
@@ -114,17 +120,17 @@ func Test_validatePlugin(t *testing.T) {
 	fakeClient := newFakeSchemaClient()
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			gotValid, _, _ := validatePlugin(fakeClient, tt.pluginName, v2beta1.ApisixRouteHTTPPluginConfig(tt.pluginConfig))
+			gotValid, _ := validatePlugin(fakeClient, tt.pluginName, v2beta1.ApisixRouteHTTPPluginConfig(tt.pluginConfig))
 			if gotValid != tt.wantValid {
 				t.Errorf("validatePlugin() gotValid = %v, want %v", gotValid, tt.wantValid)
 			}
 
-			gotValid, _, _ = validatePlugin(fakeClient, tt.pluginName, v2alpha1.ApisixRouteHTTPPluginConfig(tt.pluginConfig))
+			gotValid, _ = validatePlugin(fakeClient, tt.pluginName, v2alpha1.ApisixRouteHTTPPluginConfig(tt.pluginConfig))
 			if gotValid != tt.wantValid {
 				t.Errorf("validatePlugin() gotValid = %v, want %v", gotValid, tt.wantValid)
 			}
 
-			gotValid, _, _ = validatePlugin(fakeClient, tt.pluginName, v1.Config(tt.pluginConfig))
+			gotValid, _ = validatePlugin(fakeClient, tt.pluginName, v1.Config(tt.pluginConfig))
 			if gotValid != tt.wantValid {
 				t.Errorf("validatePlugin() gotValid = %v, want %v", gotValid, tt.wantValid)
 			}
diff --git a/pkg/api/validation/apisix_tls.go b/pkg/api/validation/apisix_tls.go
new file mode 100644
index 0000000..15926a6
--- /dev/null
+++ b/pkg/api/validation/apisix_tls.go
@@ -0,0 +1,80 @@
+// 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 validation
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
+	kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
+	"github.com/xeipuuv/gojsonschema"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/apache/apisix-ingress-controller/pkg/apisix"
+	v1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta1"
+	"github.com/apache/apisix-ingress-controller/pkg/log"
+)
+
+// errNotApisixTls will be used when the validating object is not ApisixTls.
+var errNotApisixTls = errors.New("object is not ApisixTls")
+
+// ApisixTlsValidator validates ApisixTls's spec.
+var ApisixTlsValidator = kwhvalidating.ValidatorFunc(
+	func(ctx context.Context, review *kwhmodel.AdmissionReview, object metav1.Object) (result *kwhvalidating.ValidatorResult, err error) {
+		log.Debug("arrive ApisixTls validator webhook")
+
+		valid := true
+		var spec interface{}
+
+		switch at := object.(type) {
+		case *v2beta1.ApisixRoute:
+			spec = at.Spec
+		case *v2alpha1.ApisixRoute:
+			spec = at.Spec
+		case *v1.ApisixRoute:
+			spec = at.Spec
+		default:
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: errNotApisixTls.Error()}, errNotApisixTls
+		}
+
+		client, err := GetSchemaClient(&apisix.ClusterOptions{})
+		if err != nil {
+			msg := "failed to get the schema client"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+
+		ss, err := client.GetSslSchema(ctx)
+		if err != nil {
+			msg := "failed to get SSL's schema"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+		atSchemaLoader := gojsonschema.NewStringLoader(ss.Content)
+
+		var msgs []string
+		if _, err := validateSchema(&atSchemaLoader, spec); err != nil {
+			valid = false
+			msgs = append(msgs, err.Error())
+		}
+
+		return &kwhvalidating.ValidatorResult{Valid: valid, Message: strings.Join(msgs, "\n")}, nil
+	},
+)
diff --git a/pkg/api/validation/apisix_upstream.go b/pkg/api/validation/apisix_upstream.go
new file mode 100644
index 0000000..b3e0501
--- /dev/null
+++ b/pkg/api/validation/apisix_upstream.go
@@ -0,0 +1,80 @@
+// 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 validation
+
+import (
+	"context"
+	"errors"
+	"strings"
+
+	kwhmodel "github.com/slok/kubewebhook/v2/pkg/model"
+	kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
+	"github.com/xeipuuv/gojsonschema"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/apache/apisix-ingress-controller/pkg/apisix"
+	v1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1"
+	"github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2beta1"
+	"github.com/apache/apisix-ingress-controller/pkg/log"
+)
+
+// errNotApisixUpstream will be used when the validating object is not ApisixUpstream.
+var errNotApisixUpstream = errors.New("object is not ApisixUpstream")
+
+// ApisixUpstreamValidator validates ApisixUpstream's spec.
+var ApisixUpstreamValidator = kwhvalidating.ValidatorFunc(
+	func(ctx context.Context, review *kwhmodel.AdmissionReview, object metav1.Object) (result *kwhvalidating.ValidatorResult, err error) {
+		log.Debug("arrive ApisixUpstream validator webhook")
+
+		valid := true
+		var spec interface{}
+
+		switch au := object.(type) {
+		case *v2beta1.ApisixRoute:
+			spec = au.Spec
+		case *v2alpha1.ApisixRoute:
+			spec = au.Spec
+		case *v1.ApisixRoute:
+			spec = au.Spec
+		default:
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: errNotApisixUpstream.Error()}, errNotApisixUpstream
+		}
+
+		client, err := GetSchemaClient(&apisix.ClusterOptions{})
+		if err != nil {
+			msg := "failed to get the schema client"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+
+		us, err := client.GetUpstreamSchema(ctx)
+		if err != nil {
+			msg := "failed to get upstream's schema"
+			log.Errorf("%s: %s", msg, err)
+			return &kwhvalidating.ValidatorResult{Valid: false, Message: msg}, err
+		}
+		auSchemaLoader := gojsonschema.NewStringLoader(us.Content)
+
+		var msgs []string
+		if _, err := validateSchema(&auSchemaLoader, spec); err != nil {
+			valid = false
+			msgs = append(msgs, err.Error())
+		}
+
+		return &kwhvalidating.ValidatorResult{Valid: valid, Message: strings.Join(msgs, "\n")}, nil
+	},
+)
diff --git a/pkg/api/validation/utils.go b/pkg/api/validation/utils.go
index 21abebe..5d2145c 100644
--- a/pkg/api/validation/utils.go
+++ b/pkg/api/validation/utils.go
@@ -20,7 +20,10 @@ import (
 	"fmt"
 	"sync"
 
+	"github.com/gin-gonic/gin"
 	"github.com/hashicorp/go-multierror"
+	kwhhttp "github.com/slok/kubewebhook/v2/pkg/http"
+	kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating"
 	"github.com/xeipuuv/gojsonschema"
 
 	"github.com/apache/apisix-ingress-controller/pkg/apisix"
@@ -53,13 +56,30 @@ func GetSchemaClient(co *apisix.ClusterOptions) (apisix.Schema, error) {
 	return schemaClient, onceErr
 }
 
-// TODO: make this helper function more generic so that it can be used by other validating webhooks.
-func validateSchema(schema string, config interface{}) (bool, error) {
-	// TODO: cache the schema loader
-	schemaLoader := gojsonschema.NewStringLoader(schema)
-	configLoader := gojsonschema.NewGoLoader(config)
+// NewHandlerFunc returns a HandlerFunc to handle admission reviews using the given validator.
+func NewHandlerFunc(ID string, validator kwhvalidating.Validator) gin.HandlerFunc {
+	// Create a validating webhook.
+	wh, err := kwhvalidating.NewWebhook(kwhvalidating.WebhookConfig{
+		ID:        ID,
+		Validator: validator,
+	})
+	if err != nil {
+		log.Errorf("failed to create webhook: %s", err)
+	}
+
+	h, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh})
+	if err != nil {
+		log.Errorf("failed to create webhook handle: %s", err)
+	}
+
+	return gin.WrapH(h)
+}
+
+// validateSchema validates the schema of the given Go struct.
+func validateSchema(schemaLoader *gojsonschema.JSONLoader, obj interface{}) (bool, error) {
+	configLoader := gojsonschema.NewGoLoader(obj)
 
-	result, err := gojsonschema.Validate(schemaLoader, configLoader)
+	result, err := gojsonschema.Validate(*schemaLoader, configLoader)
 	if err != nil {
 		log.Errorf("failed to load and validate the schema: %s", err)
 		return false, err
@@ -71,9 +91,10 @@ func validateSchema(schema string, config interface{}) (bool, error) {
 
 	log.Warn("the given document is not valid. see errors:\n")
 	var resultErr error
+	resultErr = multierror.Append(resultErr, fmt.Errorf("the given document is not valid"))
 	for _, desc := range result.Errors() {
-		resultErr = multierror.Append(resultErr, fmt.Errorf("%s\n", desc.Description()))
-		log.Errorf("- %s\n", desc)
+		resultErr = multierror.Append(resultErr, fmt.Errorf("%s", desc.Description()))
+		log.Warnf("- %s", desc)
 	}
 
 	return false, resultErr
diff --git a/pkg/api/validation/utils_test.go b/pkg/api/validation/utils_test.go
new file mode 100644
index 0000000..17c3961
--- /dev/null
+++ b/pkg/api/validation/utils_test.go
@@ -0,0 +1,48 @@
+// 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 validation
+
+import (
+	"testing"
+
+	"github.com/xeipuuv/gojsonschema"
+
+	v1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+)
+
+func Test_validateSchema(t *testing.T) {
+	tests := []struct {
+		name         string
+		schemaLoader gojsonschema.JSONLoader
+		obj          interface{}
+		wantErr      bool
+	}{
+		{
+			name:         "",
+			schemaLoader: gojsonschema.NewStringLoader(`{"anyOf":[{"required":["plugins","uri"]},{"required":["upstream","uri"]},{"required":["upstream_id","uri"]},{"required":["service_id","uri"]},{"required":["plugins","uris"]},{"required":["upstream","uris"]},{"required":["upstream_id","uris"]},{"required":["service_id","uris"]},{"required":["script","uri"]},{"required":["script","uris"]}],"additionalProperties":false,"not":{"anyOf":[{"required":["script","plugins"]},{"required":["script","plu [...]
+			obj:          v1.ApisixRoute{},
+			wantErr:      true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := validateSchema(&tt.schemaLoader, tt.obj)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("validateSchema() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
diff --git a/pkg/apisix/apisix.go b/pkg/apisix/apisix.go
index 6a5a4e6..fe89c86 100644
--- a/pkg/apisix/apisix.go
+++ b/pkg/apisix/apisix.go
@@ -132,6 +132,7 @@ type Schema interface {
 	GetRouteSchema(context.Context) (*v1.Schema, error)
 	GetUpstreamSchema(context.Context) (*v1.Schema, error)
 	GetConsumerSchema(context.Context) (*v1.Schema, error)
+	GetSslSchema(context.Context) (*v1.Schema, error)
 }
 
 type apisix struct {
diff --git a/pkg/apisix/nonexistentclient.go b/pkg/apisix/nonexistentclient.go
index 35523bc..64ce7bb 100644
--- a/pkg/apisix/nonexistentclient.go
+++ b/pkg/apisix/nonexistentclient.go
@@ -208,6 +208,10 @@ func (f *dummySchema) GetConsumerSchema(_ context.Context) (*v1.Schema, error) {
 	return nil, ErrClusterNotExist
 }
 
+func (f *dummySchema) GetSslSchema(_ context.Context) (*v1.Schema, error) {
+	return nil, ErrClusterNotExist
+}
+
 func (nc *nonExistentCluster) Route() Route {
 	return nc.route
 }
diff --git a/pkg/apisix/schema.go b/pkg/apisix/schema.go
index 34ce2ce..97c6c97 100644
--- a/pkg/apisix/schema.go
+++ b/pkg/apisix/schema.go
@@ -105,3 +105,8 @@ func (sc schemaClient) GetUpstreamSchema(ctx context.Context) (*v1.Schema, error
 func (sc schemaClient) GetConsumerSchema(ctx context.Context) (*v1.Schema, error) {
 	return sc.getSchema(ctx, "consumer")
 }
+
+// GetSslSchema returns SSL's schema.
+func (sc schemaClient) GetSslSchema(ctx context.Context) (*v1.Schema, error) {
+	return sc.getSchema(ctx, "ssl")
+}
diff --git a/pkg/apisix/schema_test.go b/pkg/apisix/schema_test.go
index 3fe707b..74eccbd 100644
--- a/pkg/apisix/schema_test.go
+++ b/pkg/apisix/schema_test.go
@@ -38,6 +38,7 @@ var testData = map[string]string{
 	"route":    `{"anyOf":[{"required":["plugins","uri"]},{"required":["upstream","uri"]},{"required":["upstream_id","uri"]},{"required":["service_id","uri"]},{"required":["plugins","uris"]},{"required":["upstream","uris"]},{"required":["upstream_id","uris"]},{"required":["service_id","uris"]},{"required":["script","uri"]},{"required":["script","uris"]}],"additionalProperties":false,"not":{"anyOf":[{"required":["script","plugins"]},{"required":["script","plugin_config_id"]}]},"properties":{ [...]
 	"upstream": `{"oneOf":[{"required":["type","nodes"]},{"required":["type","service_name","discovery_type"]}],"properties":{"id":{"anyOf":[{"pattern":"^[a-zA-Z0-9-_.]+$","type":"string","minLength":1,"maxLength":64},{"minimum":1,"type":"integer"}]},"name":{"type":"string","minLength":1,"maxLength":100},"create_time":{"type":"integer"},"retries":{"minimum":0,"type":"integer"},"scheme":{"enum":["grpc","grpcs","http","https"],"default":"http"},"key":{"type":"string","description":"the key of [...]
 	"consumer": `{"type":"object","properties":{"desc":{"maxLength":256,"type":"string"},"username":{"pattern":"^[a-zA-Z0-9_]+$","type":"string","minLength":1,"maxLength":32},"plugins":{"type":"object"},"labels":{"maxProperties":16,"type":"object","patternProperties":{".*":{"pattern":"^\\S+$","description":"value of label","type":"string","minLength":1,"maxLength":64}},"description":"key\/value pairs to specify attributes"},"update_time":{"type":"integer"},"create_time":{"type":"integer"}}, [...]
+	"ssl":      `{"additionalProperties":false,"type":"object","oneOf":[{"required":["sni","key","cert"]},{"required":["snis","key","cert"]}],"properties":{"update_time":{"type":"integer"},"client":{"type":"object","required":["ca"],"properties":{"ca":{"minLength":128,"type":"string","maxLength":65536},"depth":{"default":1,"type":"integer","minimum":0}}},"status":{"default":1,"type":"integer","enum":[1,0],"description":"ssl status, 1 to enable, 0 to disable"},"sni":{"pattern":"^\\*?[0-9a-zA [...]
 }
 
 const errMsg = `{"error_msg":"not found schema"}`
@@ -144,4 +145,9 @@ func TestSchemaClient(t *testing.T) {
 	consumerSchema, err := cli.GetConsumerSchema(ctx)
 	assert.Nil(t, err)
 	assert.Equal(t, consumerSchema.Content, testData["consumer"])
+
+	// Test `GetSslSchema`
+	sslSchema, err := cli.GetSslSchema(ctx)
+	assert.Nil(t, err)
+	assert.Equal(t, sslSchema.Content, testData["ssl"])
 }
diff --git a/test/e2e/ingress/webhook.go b/test/e2e/ingress/webhook.go
index c7dc238..001e88a 100644
--- a/test/e2e/ingress/webhook.go
+++ b/test/e2e/ingress/webhook.go
@@ -67,6 +67,8 @@ spec:
 		err := s.CreateResourceFromString(ar)
 		assert.Error(ginkgo.GinkgoT(), err, "Failed to create ApisixRoute")
 		assert.Contains(ginkgo.GinkgoT(), err.Error(), "admission webhook")
-		assert.Contains(ginkgo.GinkgoT(), err.Error(), "denied the request: api-breaker plugin's config is invalid")
+		assert.Contains(ginkgo.GinkgoT(), err.Error(), "denied the request")
+		assert.Contains(ginkgo.GinkgoT(), err.Error(), "api-breaker plugin's config is invalid")
+		assert.Contains(ginkgo.GinkgoT(), err.Error(), "Must be greater than or equal to 200")
 	})
 })
diff --git a/test/e2e/scaffold/ingress.go b/test/e2e/scaffold/ingress.go
index a38263c..e65832f 100644
--- a/test/e2e/scaffold/ingress.go
+++ b/test/e2e/scaffold/ingress.go
@@ -310,13 +310,13 @@ kind: ValidatingWebhookConfiguration
 metadata:
   name: apisix-validation-webhooks-e2e-test
 webhooks:
-  - name: apisixroute-plugin-validator-webhook.apisix.apache.org
+  - name: apisixroute-validator-webhook.apisix.apache.org
     clientConfig:
       service:
         name: webhook
         namespace: %s
         port: 8443
-        path: "/validation/apisixroutes/plugin"
+        path: "/validation/apisixroutes"
       caBundle: %s
     rules:
       - operations: [ "CREATE", "UPDATE" ]
@@ -325,6 +325,51 @@ webhooks:
         resources: ["apisixroutes"]
     timeoutSeconds: 30
     failurePolicy: Fail
+  - name: apisixconsumer-validator-webhook.apisix.apache.org
+    clientConfig:
+      service:
+        name: webhook
+        namespace: %s
+        port: 8443
+        path: "/validation/apisixconsumers"
+      caBundle: %s
+    rules:
+      - operations: [ "CREATE", "UPDATE" ]
+        apiGroups: ["apisix.apache.org"]
+        apiVersions: ["*"]
+        resources: ["apisixconsumers"]
+    timeoutSeconds: 30
+    failurePolicy: Fail
+  - name: apisixtls-validator-webhook.apisix.apache.org
+    clientConfig:
+      service:
+        name: webhook
+        namespace: %s
+        port: 8443
+        path: "/validation/apisixtlses"
+      caBundle: %s
+    rules:
+      - operations: [ "CREATE", "UPDATE" ]
+        apiGroups: ["apisix.apache.org"]
+        apiVersions: ["*"]
+        resources: ["apisixtlses"]
+    timeoutSeconds: 30
+    failurePolicy: Fail
+  - name: apisixupstream-validator-webhook.apisix.apache.org
+    clientConfig:
+      service:
+        name: webhook
+        namespace: %s
+        port: 8443
+        path: "/validation/apisixupstreams"
+      caBundle: %s
+    rules:
+      - operations: [ "CREATE", "UPDATE" ]
+        apiGroups: ["apisix.apache.org"]
+        apiVersions: ["*"]
+        resources: ["apisixupstreams"]
+    timeoutSeconds: 30
+    failurePolicy: Fail
 `
 	_webhookCertSecret = "webhook-certs"
 	_volumeMounts      = `volumeMounts:
@@ -377,7 +422,8 @@ func (s *Scaffold) newIngressAPISIXController() error {
 		assert.True(s.t, ok, "get cert.pem from the secret")
 		caBundle := base64.StdEncoding.EncodeToString(cert)
 
-		webhookReg := fmt.Sprintf(_ingressAPISIXAdmissionWebhook, s.namespace, caBundle)
+		webhookReg := fmt.Sprintf(_ingressAPISIXAdmissionWebhook, s.namespace, caBundle, s.namespace, caBundle, s.namespace, caBundle, s.namespace, caBundle)
+		ginkgo.GinkgoT().Log(webhookReg)
 		err = k8s.KubectlApplyFromStringE(s.t, s.kubectlOptions, webhookReg)
 		assert.Nil(s.t, err, "create webhook registration")