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 <[email protected]>
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")
 

Reply via email to