Skip to content

Commit

Permalink
feat(admission): validate path with regex supplied in Ingress (#4647)
Browse files Browse the repository at this point in the history
  • Loading branch information
programmer04 committed Sep 13, 2023
1 parent 896edff commit 832a7aa
Show file tree
Hide file tree
Showing 11 changed files with 477 additions and 47 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Adding a new version? You'll need three changes:
- Allow regex expressions in `HTTPRoute` configuration and provide validation in admission webhook.
Before this change admission webhook used to reject entirely such configurations incorrectly as not supported yet.
[#4608](https://github.com/Kong/kubernetes-ingress-controller/pull/4608)
- Provide validation in admission webhook for `Ingress` paths (validate regex expressions).
[#4647](https://github.com/Kong/kubernetes-ingress-controller/pull/4647)
- Add new feature gate `RewriteURIs` to enable/disable the `konghq.com/rewrite`
annotation (default disabled).
[#4360](https://github.com/Kong/kubernetes-ingress-controller/pull/4360)
Expand Down
22 changes: 22 additions & 0 deletions internal/admission/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/sirupsen/logrus"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

Expand Down Expand Up @@ -98,6 +99,11 @@ var (
Version: gatewayv1beta1.SchemeGroupVersion.Version,
Resource: "httproutes",
}
ingressGVResource = metav1.GroupVersionResource{
Group: netv1.SchemeGroupVersion.Group,
Version: netv1.SchemeGroupVersion.Version,
Resource: "ingresses",
}
)

func (h RequestHandler) handleValidation(ctx context.Context, request admissionv1.AdmissionRequest) (
Expand All @@ -122,6 +128,8 @@ func (h RequestHandler) handleValidation(ctx context.Context, request admissionv
return h.handleHTTPRoute(ctx, request, responseBuilder)
case kongIngressGVResource:
return h.handleKongIngress(ctx, request, responseBuilder)
case ingressGVResource:
return h.handleIngress(ctx, request, responseBuilder)
default:
return nil, fmt.Errorf("unknown resource type to validate: %s/%s %s",
request.Resource.Group, request.Resource.Version,
Expand Down Expand Up @@ -313,3 +321,17 @@ func (h RequestHandler) handleKongIngress(_ context.Context, request admissionv1

return responseBuilder.Build(), nil
}

func (h RequestHandler) handleIngress(ctx context.Context, request admissionv1.AdmissionRequest, responseBuilder *ResponseBuilder) (*admissionv1.AdmissionResponse, error) {
ingress := netv1.Ingress{}
_, _, err := codecs.UniversalDeserializer().Decode(request.Object.Raw, nil, &ingress)
if err != nil {
return nil, err
}
ok, message, err := h.Validator.ValidateIngress(ctx, ingress)
if err != nil {
return nil, err
}

return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil
}
5 changes: 5 additions & 0 deletions internal/admission/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

Expand Down Expand Up @@ -69,6 +70,10 @@ func (v KongFakeValidator) ValidateHTTPRoute(_ context.Context, _ gatewayv1beta1
return v.Result, v.Message, v.Error
}

func (v KongFakeValidator) ValidateIngress(_ context.Context, _ netv1.Ingress) (bool, string, error) {
return v.Result, v.Message, v.Error
}

func TestServeHTTPBasic(t *testing.T) {
assert := assert.New(t)
res := httptest.NewRecorder()
Expand Down
32 changes: 28 additions & 4 deletions internal/admission/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/kong/go-kong/kong"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -19,6 +20,7 @@ import (
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser"
credsvalidation "github.com/kong/kubernetes-ingress-controller/v2/internal/validation/consumers/credentials"
gatewayvalidators "github.com/kong/kubernetes-ingress-controller/v2/internal/validation/gateway"
ingressvalidator "github.com/kong/kubernetes-ingress-controller/v2/internal/validation/ingress"
"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"
Expand All @@ -33,6 +35,7 @@ type KongValidator interface {
ValidateCredential(ctx context.Context, secret corev1.Secret) (bool, string, error)
ValidateGateway(ctx context.Context, gateway gatewaycontroller.Gateway) (bool, string, error)
ValidateHTTPRoute(ctx context.Context, httproute gatewaycontroller.HTTPRoute) (bool, string, error)
ValidateIngress(ctx context.Context, ingress netv1.Ingress) (bool, string, error)
}

// AdminAPIServicesProvider provides KongHTTPValidator with Kong Admin API services that are needed to perform
Expand All @@ -55,7 +58,8 @@ type KongHTTPValidator struct {
ParserFeatures parser.FeatureFlags
KongVersion semver.Version

ingressClassMatcher func(*metav1.ObjectMeta, string, annotations.ClassMatching) bool
ingressClassMatcher func(*metav1.ObjectMeta, string, annotations.ClassMatching) bool
ingressV1ClassMatcher func(*netv1.Ingress, annotations.ClassMatching) bool
}

// NewKongHTTPValidator provides a new KongHTTPValidator object provided a
Expand All @@ -70,7 +74,6 @@ func NewKongHTTPValidator(
parserFeatures parser.FeatureFlags,
kongVersion semver.Version,
) KongHTTPValidator {
matcher := annotations.IngressClassValidatorFuncFromObjectMeta(ingressClass)
return KongHTTPValidator{
Logger: logger,
SecretGetter: &managerClientSecretGetter{managerClient: managerClient},
Expand All @@ -79,7 +82,8 @@ func NewKongHTTPValidator(
ParserFeatures: parserFeatures,
KongVersion: kongVersion,

ingressClassMatcher: matcher,
ingressClassMatcher: annotations.IngressClassValidatorFuncFromObjectMeta(ingressClass),
ingressV1ClassMatcher: annotations.IngressClassValidatorFuncFromV1Ingress(ingressClass),
}
}

Expand Down Expand Up @@ -423,7 +427,7 @@ func (validator KongHTTPValidator) ValidateHTTPRoute(

// Now that we know whether or not the HTTPRoute is linked to a managed
// Gateway we can run it through full validation.
var routeValidator gatewayvalidators.RouteValidator = noOpRoutesValidator{}
var routeValidator routeValidator = noOpRoutesValidator{}
if routesSvc, ok := validator.AdminAPIServicesProvider.GetRoutesService(); ok {
routeValidator = routesSvc
}
Expand All @@ -432,6 +436,26 @@ func (validator KongHTTPValidator) ValidateHTTPRoute(
)
}

func (validator KongHTTPValidator) ValidateIngress(
ctx context.Context, ingress netv1.Ingress,
) (bool, string, error) {
// Ignore Ingresses that are being managed by another controller.
if !validator.ingressClassMatcher(&ingress.ObjectMeta, annotations.IngressClassKey, annotations.ExactClassMatch) &&
!validator.ingressV1ClassMatcher(&ingress, annotations.ExactClassMatch) {
return true, "", nil
}

var routeValidator routeValidator = noOpRoutesValidator{}
if routesSvc, ok := validator.AdminAPIServicesProvider.GetRoutesService(); ok {
routeValidator = routesSvc
}
return ingressvalidator.ValidateIngress(ctx, routeValidator, validator.ParserFeatures, validator.KongVersion, &ingress)
}

type routeValidator interface {
Validate(context.Context, *kong.Route) (bool, string, error)
}

type noOpRoutesValidator struct{}

func (noOpRoutesValidator) Validate(_ context.Context, _ *kong.Route) (bool, string, error) {
Expand Down
67 changes: 42 additions & 25 deletions internal/dataplane/parser/translate_ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/kong/go-kong/kong"
netv1 "k8s.io/api/networking/v1"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/failures"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/atc"
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators"
Expand Down Expand Up @@ -57,7 +58,13 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules {
}

// Translate Ingress objects into Kong Services.
servicesCache := p.ingressesV1ToKongServices(ingressList, icp)
servicesCache := IngressesV1ToKongServices(
p.featureFlags,
ingressList,
icp,
p.parsedObjectsCollector,
p.failuresCollector,
)
for i := range servicesCache {
service := servicesCache[i]
if err := translators.MaybeRewriteURI(&service, p.featureFlags.RewriteURIs); err != nil {
Expand All @@ -79,40 +86,52 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules {
return result
}

// ingressV1ToKongServicesCache is a cache of Kong Services indexed by their name.
type kongServicesCache map[string]kongstate.Service
// KongServicesCache is a cache of Kong Services indexed by their name.
type KongServicesCache map[string]kongstate.Service

// ingressesV1ToKongServices translates IngressV1 object into Kong Service. It inserts the Kong Service into the passed servicesCache.
// Returns true if the passed servicesCache was updated.
func (p *Parser) ingressesV1ToKongServices(
// IngressesV1ToKongServices translates IngressV1 object into Kong Service, returns them indexed by name.
// Argument parsedObjectsCollector is used to register all successfully parsed objects. In case of a failure,
// the object is registered in failuresCollector.
func IngressesV1ToKongServices(
featureFlags FeatureFlags,
ingresses []*netv1.Ingress,
icp kongv1alpha1.IngressClassParametersSpec,
) kongServicesCache {
if p.featureFlags.CombinedServiceRoutes {
return p.ingressV1ToKongServiceCombinedRoutes(ingresses, icp)
parsedObjectsCollector *ObjectsCollector,
failuresCollector *failures.ResourceFailuresCollector,
) KongServicesCache {
if featureFlags.CombinedServiceRoutes {
return ingressV1ToKongServiceCombinedRoutes(featureFlags, ingresses, icp, parsedObjectsCollector)
}
return p.ingressV1ToKongServiceLegacy(ingresses, icp)
return ingressV1ToKongServiceLegacy(featureFlags, ingresses, icp, parsedObjectsCollector, failuresCollector)
}

// ingressV1ToKongServiceLegacy translates a slice of IngressV1 object into Kong Services.
func (p *Parser) ingressV1ToKongServiceCombinedRoutes(
func ingressV1ToKongServiceCombinedRoutes(
featureFlags FeatureFlags,
ingresses []*netv1.Ingress,
icp kongv1alpha1.IngressClassParametersSpec,
) kongServicesCache {
parsedObjectsCollector *ObjectsCollector,
) KongServicesCache {
return translators.TranslateIngresses(ingresses, icp, translators.TranslateIngressFeatureFlags{
RegexPathPrefix: p.featureFlags.RegexPathPrefix,
ExpressionRoutes: p.featureFlags.ExpressionRoutes,
CombinedServices: p.featureFlags.CombinedServices,
}, p.parsedObjectsCollector)
RegexPathPrefix: featureFlags.RegexPathPrefix,
ExpressionRoutes: featureFlags.ExpressionRoutes,
CombinedServices: featureFlags.CombinedServices,
}, parsedObjectsCollector)
}

// ingressV1ToKongServiceLegacy translates a slice IngressV1 object into Kong Services.
func (p *Parser) ingressV1ToKongServiceLegacy(ingresses []*netv1.Ingress, icp kongv1alpha1.IngressClassParametersSpec) kongServicesCache {
servicesCache := make(kongServicesCache)
func ingressV1ToKongServiceLegacy(
featureFlags FeatureFlags,
ingresses []*netv1.Ingress,
icp kongv1alpha1.IngressClassParametersSpec,
parsedObjectsCollector *ObjectsCollector,
failuresCollector *failures.ResourceFailuresCollector,
) KongServicesCache {
servicesCache := make(KongServicesCache)

for _, ingress := range ingresses {
ingressSpec := ingress.Spec
maybePrependRegexPrefixFn := translators.MaybePrependRegexPrefixForIngressV1Fn(ingress, icp.EnableLegacyRegexDetection && p.featureFlags.RegexPathPrefix)
maybePrependRegexPrefixFn := translators.MaybePrependRegexPrefixForIngressV1Fn(ingress, icp.EnableLegacyRegexDetection && featureFlags.RegexPathPrefix)
for i, rule := range ingressSpec.Rules {
if rule.HTTP == nil {
continue
Expand All @@ -124,12 +143,10 @@ func (p *Parser) ingressV1ToKongServiceLegacy(ingresses []*netv1.Ingress, icp ko
rulePath.PathType = &pathTypeImplementationSpecific
}

paths := translators.PathsFromIngressPaths(rulePath, p.featureFlags.RegexPathPrefix)
paths := translators.PathsFromIngressPaths(rulePath, featureFlags.RegexPathPrefix)
if paths == nil {
// registering a failure, but technically it should never happen thanks to Kubernetes API validations
p.registerTranslationFailure(
fmt.Sprintf("could not translate Ingress Path %s to Kong paths", rulePath.Path), ingress,
)
// Registering a failure, but technically it should never happen thanks to Kubernetes API validations.
failuresCollector.PushResourceFailure(fmt.Sprintf("could not translate Ingress Path %s to Kong paths", rulePath.Path), ingress)
continue
}

Expand Down Expand Up @@ -192,7 +209,7 @@ func (p *Parser) ingressV1ToKongServiceLegacy(ingresses []*netv1.Ingress, icp ko

service.Routes = append(service.Routes, r)
servicesCache[serviceName] = service
p.registerSuccessfullyParsedObject(ingress)
parsedObjectsCollector.Add(ingress) // Register successfully parsed object.
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions internal/util/builder/ingress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package builder

import (
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/kong/kubernetes-ingress-controller/v2/internal/annotations"
)

type IngressBuilder struct {
ingress netv1.Ingress
}

// NewIngress builds an Ingress object with the given name and class, when "" is passed as class parameter
// the field .Spec.IngressClassName is not set.
func NewIngress(name string, class string) *IngressBuilder {
var classToSet *string
if class != "" {
classToSet = &class
}
return &IngressBuilder{
ingress: netv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: make(map[string]string),
},
Spec: netv1.IngressSpec{
IngressClassName: classToSet,
},
},
}
}

func (b *IngressBuilder) Build() *netv1.Ingress {
return &b.ingress
}

func (b *IngressBuilder) WithLegacyClassAnnotation(class string) *IngressBuilder {
b.ingress.Annotations[annotations.IngressClassKey] = class
return b
}

func (b *IngressBuilder) WithRules(rules ...netv1.IngressRule) *IngressBuilder {
b.ingress.Spec.Rules = append(b.ingress.Spec.Rules, rules...)
return b
}
6 changes: 3 additions & 3 deletions internal/validation/gateway/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators"
)

type RouteValidator interface {
type routeValidator interface {
Validate(context.Context, *kong.Route) (bool, string, error)
}

Expand All @@ -28,7 +28,7 @@ type RouteValidator interface {
// validation endpoint.
func ValidateHTTPRoute(
ctx context.Context,
routesValidator RouteValidator,
routesValidator routeValidator,
parserFeatures parser.FeatureFlags,
kongVersion semver.Version,
httproute *gatewayv1beta1.HTTPRoute,
Expand Down Expand Up @@ -188,7 +188,7 @@ func getListenersForHTTPRouteValidation(sectionName *gatewayv1beta1.SectionName,
}

func validateWithKongGateway(
ctx context.Context, routesValidator RouteValidator, parserFeatures parser.FeatureFlags, kongVersion semver.Version, httproute *gatewayv1beta1.HTTPRoute,
ctx context.Context, routesValidator routeValidator, parserFeatures parser.FeatureFlags, kongVersion semver.Version, httproute *gatewayv1beta1.HTTPRoute,
) (bool, string, error) {
// Translate HTTPRoute to Kong Route object(s) that can be sent directly to the Admin API for validation.
// Use KIC parser that works both for traditional and expressions based routes.
Expand Down
Loading

0 comments on commit 832a7aa

Please sign in to comment.