Skip to content

Commit

Permalink
feat: add webhooks for consumer/tls/upstream (#667)
Browse files Browse the repository at this point in the history
  • Loading branch information
fgksgf committed Oct 8, 2021
1 parent 657a1fd commit 9dd4f40
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 56 deletions.
10 changes: 9 additions & 1 deletion pkg/api/router/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
80 changes: 80 additions & 0 deletions pkg/api/validation/apisix_consumer.go
Original file line number Diff line number Diff line change
@@ -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
},
)
78 changes: 41 additions & 37 deletions pkg/api/validation/apisix_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -106,44 +94,60 @@ 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)
if err != nil {
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
Expand Down
18 changes: 12 additions & 6 deletions pkg/api/validation/apisix_route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}
Expand Down
80 changes: 80 additions & 0 deletions pkg/api/validation/apisix_tls.go
Original file line number Diff line number Diff line change
@@ -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
},
)
Loading

0 comments on commit 9dd4f40

Please sign in to comment.