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")