Skip to content

Commit

Permalink
feat(consumer group): add validation admission webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
programmer04 committed Aug 1, 2023
1 parent e686d34 commit b642b95
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 5 deletions.
3 changes: 2 additions & 1 deletion hack/deploy-admission-controller.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ webhooks:
- UPDATE
resources:
- kongconsumers
- kongconsumergroups
- kongplugins
- kongclusterplugins
- kongingresses
Expand All @@ -74,6 +75,6 @@ webhooks:
service:
namespace: kong
name: kong-validation-webhook
caBundle: $(base64 ${BASE64_OPTIONS:+${BASE64_OPTIONS}} "${TMPDIR}/tls.crt")
caBundle: $(base64 ${BASE64_OPTIONS:+${BASE64_OPTIONS}} "${TMPDIR}/tls.crt")
EOF
) | kubectl apply -f -
16 changes: 16 additions & 0 deletions internal/admission/adminapi_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ func (p DefaultAdminAPIServicesProvider) GetPluginsService() (kong.AbstractPlugi
return c.Plugins, true
}

func (p DefaultAdminAPIServicesProvider) GetConsumerGroupsService() (kong.AbstractConsumerGroupService, bool) {
c, ok := p.designatedAdminAPIClient()
if !ok {
return nil, ok
}
return c.ConsumerGroups, true
}

func (p DefaultAdminAPIServicesProvider) GetInfoService() (kong.AbstractInfoService, bool) {
c, ok := p.designatedAdminAPIClient()
if !ok {
return nil, ok
}
return c.Info, true
}

func (p DefaultAdminAPIServicesProvider) designatedAdminAPIClient() (*kong.Client, bool) {
gwClients := p.gatewayClientsProvider.GatewayClients()
if len(gwClients) == 0 {
Expand Down
10 changes: 10 additions & 0 deletions internal/admission/adminapi_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ func TestDefaultAdminAPIServicesProvider(t *testing.T) {

_, ok = p.GetPluginsService()
require.False(t, ok)

_, ok = p.GetConsumerGroupsService()
require.False(t, ok)

_, ok = p.GetInfoService()
require.False(t, ok)
})

t.Run("when clients available should return first one", func(t *testing.T) {
Expand All @@ -45,5 +51,9 @@ func TestDefaultAdminAPIServicesProvider(t *testing.T) {
pluginsSvc, ok := p.GetPluginsService()
require.True(t, ok)
require.Equal(t, firstClient.AdminAPIClient().Plugins, pluginsSvc)

consumerGroupsSvc, ok := p.GetConsumerGroupsService()
require.True(t, ok)
require.Equal(t, firstClient.AdminAPIClient().ConsumerGroups, consumerGroupsSvc)
})
}
6 changes: 5 additions & 1 deletion internal/admission/errors.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package admission

const (
ErrTextAdminAPIUnavailable = "could not talk to Kong admin API"
ErrTextConsumerCredentialSecretNotFound = "consumer referenced non-existent credentials secret"
ErrTextConsumerCredentialValidationFailed = "consumer credential failed validation"
ErrTextConsumerExists = "consumer already exists"
ErrTextConsumerUnretrievable = "failed to fetch consumer from kong"
ErrTextConsumerGroupUnsupported = "consumer group support requires Kong Enterprise 3.4+"
ErrTextConsumerGroupUnlicensed = "consumer group support requires a valid Kong Enterprise license"
ErrTextConsumerGroupUnexpected = "unexpected error during checking support for consumer group"
ErrTextConsumerUsernameEmpty = "username cannot be empty"
ErrTextFailedToRetrieveSecret = "could not retrieve secrets from the kubernets API" //nolint:gosec
ErrTextFailedToRetrieveSecret = "could not retrieve secrets from the kubernetes API" //nolint:gosec
ErrTextPluginConfigInvalid = "could not parse plugin configuration"
ErrTextPluginConfigValidationFailed = "unable to validate plugin schema"
ErrTextPluginConfigViolatesSchema = "plugin failed schema validation: %s"
Expand Down
25 changes: 25 additions & 0 deletions internal/admission/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

configuration "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1beta1"
)

// RequestHandler is an HTTP server that can validate Kong Ingress Controllers'
Expand Down Expand Up @@ -62,6 +63,11 @@ var (
Version: configuration.SchemeGroupVersion.Version,
Resource: "kongconsumers",
}
consumerGroupGVResource = metav1.GroupVersionResource{
Group: configurationv1beta1.SchemeGroupVersion.Group,
Version: configurationv1beta1.SchemeGroupVersion.Version,
Resource: "kongconsumergroups",
}
pluginGVResource = metav1.GroupVersionResource{
Group: configuration.SchemeGroupVersion.Group,
Version: configuration.SchemeGroupVersion.Version,
Expand Down Expand Up @@ -102,6 +108,8 @@ func (h RequestHandler) handleValidation(ctx context.Context, request admissionv
switch request.Resource {
case consumerGVResource:
return h.handleKongConsumer(ctx, request, responseBuilder)
case consumerGroupGVResource:
return h.handleKongConsumerGroup(ctx, request, responseBuilder)
case pluginGVResource:
return h.handleKongPlugin(ctx, request, responseBuilder)
case clusterPluginGVResource:
Expand Down Expand Up @@ -160,6 +168,23 @@ func (h RequestHandler) handleKongConsumer(
}
}

func (h RequestHandler) handleKongConsumerGroup(
ctx context.Context,
request admissionv1.AdmissionRequest,
responseBuilder *ResponseBuilder,
) (*admissionv1.AdmissionResponse, error) {
var consumerGroup configurationv1beta1.KongConsumerGroup
if _, _, err := codecs.UniversalDeserializer().Decode(request.Object.Raw, nil, &consumerGroup); err != nil {
return nil, err
}
ok, message, err := h.Validator.ValidateConsumerGroup(ctx, consumerGroup)
if err != nil {
return nil, err
}

return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil
}

func (h RequestHandler) handleKongPlugin(
ctx context.Context,
request admissionv1.AdmissionRequest,
Expand Down
8 changes: 8 additions & 0 deletions internal/admission/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

configuration "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1beta1"
)

var decoder = codecs.UniversalDeserializer()
Expand All @@ -35,6 +36,13 @@ func (v KongFakeValidator) ValidateConsumer(
return v.Result, v.Message, v.Error
}

func (v KongFakeValidator) ValidateConsumerGroup(
_ context.Context,
_ configurationv1beta1.KongConsumerGroup,
) (bool, string, error) {
return v.Result, v.Message, v.Error
}

func (v KongFakeValidator) ValidatePlugin(
_ context.Context,
_ configuration.KongPlugin,
Expand Down
59 changes: 58 additions & 1 deletion internal/admission/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/blang/semver/v4"
"github.com/kong/go-kong/kong"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
Expand All @@ -17,12 +18,15 @@ import (
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
credsvalidation "github.com/kong/kubernetes-ingress-controller/v2/internal/validation/consumers/credentials"
gatewayvalidators "github.com/kong/kubernetes-ingress-controller/v2/internal/validation/gateway"
"github.com/kong/kubernetes-ingress-controller/v2/internal/versions"
kongv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1beta1"
)

// KongValidator validates Kong entities.
type KongValidator interface {
ValidateConsumer(ctx context.Context, consumer kongv1.KongConsumer) (bool, string, error)
ValidateConsumerGroup(ctx context.Context, consumerGroup kongv1beta1.KongConsumerGroup) (bool, string, error)
ValidatePlugin(ctx context.Context, plugin kongv1.KongPlugin) (bool, string, error)
ValidateClusterPlugin(ctx context.Context, plugin kongv1.KongClusterPlugin) (bool, string, error)
ValidateCredential(ctx context.Context, secret corev1.Secret) (bool, string, error)
Expand All @@ -35,6 +39,8 @@ type KongValidator interface {
type AdminAPIServicesProvider interface {
GetConsumersService() (kong.AbstractConsumerService, bool)
GetPluginsService() (kong.AbstractPluginService, bool)
GetConsumerGroupsService() (kong.AbstractConsumerGroupService, bool)
GetInfoService() (kong.AbstractInfoService, bool)
}

// KongHTTPValidator implements KongValidator interface to validate Kong
Expand Down Expand Up @@ -157,10 +163,61 @@ func (validator KongHTTPValidator) ValidateConsumer(
return true, "", nil
}

func (validator KongHTTPValidator) ValidateConsumerGroup(
ctx context.Context,
consumerGroup kongv1beta1.KongConsumerGroup,
) (bool, string, error) {
// Ignore ConsumerGroups that are being managed by another controller.
if !validator.ingressClassMatcher(&consumerGroup.ObjectMeta, annotations.IngressClassKey, annotations.ExactClassMatch) {
return true, "", nil
}

// Consumer groups work only for Kong Enterprise >=3.4.
infoSvc, ok := validator.AdminAPIServicesProvider.GetInfoService()
if !ok {
return true, "", nil
}
info, err := infoSvc.Get(ctx)
if err != nil {
validator.Logger.Debugf("failed to fetch Kong info: %v", err)
return false, ErrTextAdminAPIUnavailable, nil
}
version, err := kong.NewVersion(info.Version)
if err != nil {
validator.Logger.Debugf("failed to parse Kong version: %v", err)
} else {
kongVer := semver.Version{Major: version.Major(), Minor: version.Minor()}
if !version.IsKongGatewayEnterprise() || !kongVer.GTE(versions.ConsumerGroupsVersionCutoff) {
return false, ErrTextConsumerGroupUnsupported, nil
}
}

// This check forbids consumer group creation if the license is invalid or missing.
cgs, ok := validator.AdminAPIServicesProvider.GetConsumerGroupsService()
if !ok {
return true, "", nil
}
// There is no way to robustly check the validity of a license than actually trying an enterprise feature.
if _, _, err := cgs.List(ctx, &kong.ListOpt{Size: 0}); err != nil {
fmt.Printf(">>>>>>>>>> err: %v\n", err)
switch {
case kong.IsNotFoundErr(err):
// This is the case when consumer group is not supported (Kong OSS).
// And previous version check has been omitted due to an error.
return false, ErrTextConsumerGroupUnsupported, nil
case kong.IsForbiddenErr(err):
return false, ErrTextConsumerGroupUnlicensed, nil
default:
return false, fmt.Sprintf("%s: %s", ErrTextConsumerGroupUnexpected, err), nil
}
}
return true, "", nil
}

// ValidateCredential checks if the secret contains a credential meant to
// be installed in Kong. If so, then it verifies if all the required fields
// are present in it or not. If valid, it returns true with an empty string,
// else it returns false with the error messsage. If an error happens during
// else it returns false with the error message. If an error happens during
// validation, error is returned.
func (validator KongHTTPValidator) ValidateCredential(
ctx context.Context,
Expand Down
Loading

0 comments on commit b642b95

Please sign in to comment.