Skip to content

Commit

Permalink
Use apimachinery conventions for ingress validation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
johngmyers committed Aug 5, 2023
1 parent 9b27d26 commit f3d0e82
Show file tree
Hide file tree
Showing 10 changed files with 94 additions and 164 deletions.
15 changes: 8 additions & 7 deletions pkg/annotations/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package annotations
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"strconv"
"strings"

"github.com/pkg/errors"
)

type ParseOptions struct {
Expand Down Expand Up @@ -49,8 +50,8 @@ type Parser interface {
ParseStringSliceAnnotation(annotation string, value *[]string, annotations map[string]string, opts ...ParseOption) bool

// ParseJSONAnnotation parses json value into the given interface
// returns true if the annotation exists and parser error if any
ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error)
// returns true and the matched annotation key if the annotation exists and parser error if any
ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, string, error)

// ParseStringMapAnnotation parses comma separated key=value pairs into a map
// returns true if the annotation exists
Expand Down Expand Up @@ -115,16 +116,16 @@ func (p *suffixAnnotationParser) ParseStringSliceAnnotation(annotation string, v
return true
}

func (p *suffixAnnotationParser) ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error) {
func (p *suffixAnnotationParser) ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, string, error) {
raw := ""
exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...)
if !exists {
return false, nil
return false, "", nil
}
if err := json.Unmarshal([]byte(raw), value); err != nil {
return true, errors.Wrapf(err, "failed to parse json annotation, %v: %v", matchedKey, raw)
return true, matchedKey, errors.Wrapf(err, "failed to parse json annotation, %v: %v", matchedKey, raw)
}
return true, nil
return true, matchedKey, nil
}

func (p *suffixAnnotationParser) ParseStringMapAnnotation(annotation string, value *map[string]string, annotations map[string]string, opts ...ParseOption) (bool, error) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/annotations/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package annotations

import (
"errors"
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_annotationParser_ParseStringAnnotation(t *testing.T) {
Expand Down Expand Up @@ -379,7 +380,7 @@ func Test_serviceAnnotationParser_ParseJSONAnnotation(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
parser := NewSuffixAnnotationParser(tt.prefix)
value := objStruct{}
exists, err := parser.ParseJSONAnnotation(tt.suffix, &value, tt.annotations, tt.opts...)
exists, _, err := parser.ParseJSONAnnotation(tt.suffix, &value, tt.annotations, tt.opts...)
if tt.wantError {
assert.True(t, err != nil)
} else {
Expand Down Expand Up @@ -434,7 +435,7 @@ func Test_serviceAnnotationParser_ParseBoolAnnotation(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
parser := NewSuffixAnnotationParser(tt.prefix)
value := false
exists, err := parser.ParseJSONAnnotation(tt.suffix, &value, tt.annotations, tt.opts...)
exists, _, err := parser.ParseJSONAnnotation(tt.suffix, &value, tt.annotations, tt.opts...)
if tt.wantError {
assert.True(t, err != nil)
} else {
Expand Down
5 changes: 3 additions & 2 deletions pkg/ingress/auth_config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ingress

import (
"context"

"github.com/pkg/errors"
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
)
Expand Down Expand Up @@ -95,7 +96,7 @@ func (b *defaultAuthConfigBuilder) buildAuthType(_ context.Context, svcAndIngAnn

func (b *defaultAuthConfigBuilder) buildAuthIDPConfigCognito(_ context.Context, svcAndIngAnnotations map[string]string) (*AuthIDPConfigCognito, error) {
authIDP := AuthIDPConfigCognito{}
exists, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPCognito, &authIDP, svcAndIngAnnotations)
exists, _, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPCognito, &authIDP, svcAndIngAnnotations)
if err != nil {
return nil, err
}
Expand All @@ -107,7 +108,7 @@ func (b *defaultAuthConfigBuilder) buildAuthIDPConfigCognito(_ context.Context,

func (b *defaultAuthConfigBuilder) buildAuthIDPConfigOIDC(_ context.Context, svcAndIngAnnotations map[string]string) (*AuthIDPConfigOIDC, error) {
authIDP := AuthIDPConfigOIDC{}
exists, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPOIDC, &authIDP, svcAndIngAnnotations)
exists, _, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPOIDC, &authIDP, svcAndIngAnnotations)
if err != nil {
return nil, err
}
Expand Down
7 changes: 5 additions & 2 deletions pkg/ingress/class_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ const (
defaultClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
)

// ErrInvalidIngressClass is an sentinel error that represents the IngressClass configuration for Ingress is invalid.
// ErrInvalidIngressClass is a sentinel error that represents the IngressClass configuration for Ingress is invalid.
var ErrInvalidIngressClass = errors.New("invalid ingress class")

// ErrIngressClassNotFound is a sentinel error that indicates the IngressClass for an Ingress is not found.
var ErrIngressClassNotFound = errors.New("ingress class not found")

// ClassLoader loads IngressClass configurations for Ingress.
type ClassLoader interface {
// Load loads the ClassConfiguration for Ingress with IngressClassName.
Expand Down Expand Up @@ -89,7 +92,7 @@ func (l *defaultClassLoader) Load(ctx context.Context, ing *networking.Ingress)
ingClass := &networking.IngressClass{}
if err := l.client.Get(ctx, ingClassKey, ingClass); err != nil {
if apierrors.IsNotFound(err) {
return ClassConfiguration{}, fmt.Errorf("%w: %v", ErrInvalidIngressClass, err.Error())
return ClassConfiguration{}, fmt.Errorf("%w: %v", ErrIngressClassNotFound, err.Error())
}
return ClassConfiguration{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/ingress/class_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ func Test_defaultClassLoader_Load(t *testing.T) {
},
},
},
wantErr: errors.New("invalid ingress class: ingressclasses.networking.k8s.io \"awesome-class\" not found"),
wantErr: errors.New("ingress class not found: ingressclasses.networking.k8s.io \"awesome-class\" not found"),
},
{
name: "when IngressClass found and belong to other controller",
Expand Down
4 changes: 2 additions & 2 deletions pkg/ingress/enhanced_backend_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (b *defaultEnhancedBackendBuilder) Build(ctx context.Context, ing *networki
func (b *defaultEnhancedBackendBuilder) buildConditions(_ context.Context, ingAnnotation map[string]string, svcName string) ([]RuleCondition, error) {
var conditions []RuleCondition
annotationKey := fmt.Sprintf("conditions.%v", svcName)
_, err := b.annotationParser.ParseJSONAnnotation(annotationKey, &conditions, ingAnnotation)
_, _, err := b.annotationParser.ParseJSONAnnotation(annotationKey, &conditions, ingAnnotation)
if err != nil {
return nil, err
}
Expand All @@ -176,7 +176,7 @@ func (b *defaultEnhancedBackendBuilder) buildConditions(_ context.Context, ingAn
func (b *defaultEnhancedBackendBuilder) buildActionViaAnnotation(ctx context.Context, ingAnnotation map[string]string, svcName string) (Action, error) {
action := Action{}
annotationKey := fmt.Sprintf("actions.%v", svcName)
exists, err := b.annotationParser.ParseJSONAnnotation(annotationKey, &action, ingAnnotation)
exists, _, err := b.annotationParser.ParseJSONAnnotation(annotationKey, &action, ingAnnotation)
if err != nil {
return Action{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/ingress/group_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func (m *defaultGroupLoader) checkGroupMembershipType(ctx context.Context, group
// tolerate ErrInvalidIngressClass for Ingresses that hasn't belongs to the group yet.
// for Ingresses that was belongs to the group, we error out to avoid remove the Ingress from IngressGroup since the IngressClass might be accidentally modified.
// see https://github.com/kubernetes-sigs/aws-load-balancer-controller/issues/2731
if errors.Is(err, ErrInvalidIngressClass) {
if errors.Is(err, ErrInvalidIngressClass) || errors.Is(err, ErrIngressClassNotFound) {
if hasGroupFinalizer {
return groupMembershipTypeNone, ClassifiedIngress{}, errors.Wrapf(err, "Ingress has invalid IngressClass configuration")
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/ingress/group_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1704,7 +1704,7 @@ func Test_defaultGroupLoader_loadGroupIDIfAnyHelper(t *testing.T) {
},
wantClassifiedIng: ClassifiedIngress{},
wantGroupID: nil,
wantErr: errors.New("invalid ingress class: ingressclasses.networking.k8s.io \"ing-class\" not found"),
wantErr: errors.New("ingress class not found: ingressclasses.networking.k8s.io \"ing-class\" not found"),
},
{
name: "ingress isn't matched by controller's class",
Expand Down Expand Up @@ -2051,7 +2051,7 @@ func Test_defaultGroupLoader_classifyIngress(t *testing.T) {
},
IngClassConfig: ClassConfiguration{},
},
wantErr: errors.New("invalid ingress class: ingressclasses.networking.k8s.io \"ing-class\" not found"),
wantErr: errors.New("ingress class not found: ingressclasses.networking.k8s.io \"ing-class\" not found"),
},
{
name: "no class specified - manageIngressesWithoutIngressClass is set",
Expand Down
105 changes: 33 additions & 72 deletions webhooks/networking/ingress_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"context"
"fmt"

awssdk "github.com/aws/aws-sdk-go/aws"
"github.com/go-logr/logr"
"github.com/pkg/errors"
networking "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
"sigs.k8s.io/aws-load-balancer-controller/pkg/config"
"sigs.k8s.io/aws-load-balancer-controller/pkg/ingress"
Expand Down Expand Up @@ -58,19 +58,12 @@ func (v *ingressValidator) ValidateCreate(ctx context.Context, obj runtime.Objec
if skip, err := v.checkIngressClass(ctx, ing); skip || err != nil {
return err
}
if err := v.checkIngressClassAnnotationUsage(ing, nil); err != nil {
return err
}
if err := v.checkGroupNameAnnotationUsage(ing, nil); err != nil {
return err
}
if err := v.checkIngressClassUsage(ctx, ing, nil); err != nil {
return err
}
if err := v.checkIngressAnnotationConditions(ing); err != nil {
return err
}
return nil
allErrs := field.ErrorList{}
allErrs = append(allErrs, v.checkIngressClassAnnotationUsage(ing, nil)...)
allErrs = append(allErrs, v.checkGroupNameAnnotationUsage(ing, nil)...)
allErrs = append(allErrs, v.checkIngressAnnotationConditions(ing)...)

return allErrs.ToAggregate()
}

func (v *ingressValidator) ValidateUpdate(ctx context.Context, obj runtime.Object, oldObj runtime.Object) error {
Expand All @@ -79,19 +72,11 @@ func (v *ingressValidator) ValidateUpdate(ctx context.Context, obj runtime.Objec
if skip, err := v.checkIngressClass(ctx, ing); skip || err != nil {
return err
}
if err := v.checkIngressClassAnnotationUsage(ing, oldIng); err != nil {
return err
}
if err := v.checkGroupNameAnnotationUsage(ing, oldIng); err != nil {
return err
}
if err := v.checkIngressClassUsage(ctx, ing, oldIng); err != nil {
return err
}
if err := v.checkIngressAnnotationConditions(ing); err != nil {
return err
}
return nil
allErrs := field.ErrorList{}
allErrs = append(allErrs, v.checkIngressClassAnnotationUsage(ing, oldIng)...)
allErrs = append(allErrs, v.checkGroupNameAnnotationUsage(ing, oldIng)...)
allErrs = append(allErrs, v.checkIngressAnnotationConditions(ing)...)
return allErrs.ToAggregate()
}

func (v *ingressValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
Expand All @@ -105,7 +90,10 @@ func (v *ingressValidator) checkIngressClass(ctx context.Context, ing *networkin
}
classConfiguration, err := v.classLoader.Load(ctx, ing)
if err != nil {
return false, err
if errors.Is(err, ingress.ErrIngressClassNotFound) {
return false, field.NotFound(field.NewPath("spec", "ingressClassName"), ing.Spec.IngressClassName)
}
return false, field.Forbidden(field.NewPath("spec", "ingressClassName"), err.Error())
}
if classConfiguration.IngClass != nil {
return classConfiguration.IngClass.Spec.Controller != ingress.IngressClassControllerALB, nil
Expand All @@ -116,7 +104,7 @@ func (v *ingressValidator) checkIngressClass(ctx context.Context, ing *networkin
// checkIngressClassAnnotationUsage checks the usage of kubernetes.io/ingress.class annotation.
// kubernetes.io/ingress.class annotation cannot be set to the ingress class for this controller once disabled,
// so that we enforce users to use spec.ingressClassName in Ingress and IngressClass resource instead.
func (v *ingressValidator) checkIngressClassAnnotationUsage(ing *networking.Ingress, oldIng *networking.Ingress) error {
func (v *ingressValidator) checkIngressClassAnnotationUsage(ing *networking.Ingress, oldIng *networking.Ingress) (allErrs field.ErrorList) {
if !v.disableIngressClassAnnotation {
return nil
}
Expand All @@ -135,15 +123,17 @@ func (v *ingressValidator) checkIngressClassAnnotationUsage(ing *networking.Ingr
}
}
if !usedInOldIng && usedInNewIng {
return errors.Errorf("new usage of `%s` annotation is forbidden", annotations.IngressClass)
fieldPath := field.NewPath("metadata", "annotations").Key(annotations.IngressClass)
allErrs = append(allErrs, field.Forbidden(fieldPath, "new usage is forbidden"))
}
return nil

return allErrs
}

// checkGroupNameAnnotationUsage checks the usage of "group.name" annotation.
// "group.name" annotation cannot be set once disabled,
// so that we enforce users to use spec.group in IngressClassParams resource instead.
func (v *ingressValidator) checkGroupNameAnnotationUsage(ing *networking.Ingress, oldIng *networking.Ingress) error {
func (v *ingressValidator) checkGroupNameAnnotationUsage(ing *networking.Ingress, oldIng *networking.Ingress) (allErrs field.ErrorList) {
if !v.disableIngressGroupAnnotation {
return nil
}
Expand All @@ -162,68 +152,39 @@ func (v *ingressValidator) checkGroupNameAnnotationUsage(ing *networking.Ingress

if usedInNewIng {
if !usedInOldIng || (newGroupName != oldGroupName) {
return errors.Errorf("new usage of `%s/%s` annotation is forbidden", annotations.AnnotationPrefixIngress, annotations.IngressSuffixGroupName)
fieldPath := field.NewPath("metadata", "annotations").Key(annotations.AnnotationPrefixIngress + "/" + annotations.IngressSuffixGroupName)
allErrs = append(allErrs, field.Forbidden(fieldPath, "new usage is forbidden"))
}
}
return nil
}

// checkIngressClassUsage checks the usage of "ingressClassName" field.
// if ingressClassName is mutated, it must refer to a existing & valid IngressClass.
func (v *ingressValidator) checkIngressClassUsage(ctx context.Context, ing *networking.Ingress, oldIng *networking.Ingress) error {
usedInNewIng := false
usedInOldIng := false
newIngressClassName := ""
oldIngressClassName := ""

if ing.Spec.IngressClassName != nil {
usedInNewIng = true
newIngressClassName = awssdk.StringValue(ing.Spec.IngressClassName)
}
if oldIng != nil && oldIng.Spec.IngressClassName != nil {
usedInOldIng = true
oldIngressClassName = awssdk.StringValue(oldIng.Spec.IngressClassName)
}

if usedInNewIng {
if !usedInOldIng || (newIngressClassName != oldIngressClassName) {
_, err := v.classLoader.Load(ctx, ing)
if err != nil {
return err
}
}
}
return nil
return allErrs
}

// checkGroupNameAnnotationUsage checks the validity of "conditions.${conditions-name}" annotation.
func (v *ingressValidator) checkIngressAnnotationConditions(ing *networking.Ingress) error {
func (v *ingressValidator) checkIngressAnnotationConditions(ing *networking.Ingress) (allErrs field.ErrorList) {
for _, rule := range ing.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
var conditions []ingress.RuleCondition
annotationKey := fmt.Sprintf("conditions.%v", path.Backend.Service.Name)
_, err := v.annotationParser.ParseJSONAnnotation(annotationKey, &conditions, ing.Annotations)
_, matchedKey, err := v.annotationParser.ParseJSONAnnotation(annotationKey, &conditions, ing.Annotations)
fieldPath := field.NewPath("metadata", "annotations").Key(matchedKey)
if err != nil {
return err
allErrs = append(allErrs, field.Invalid(fieldPath, ing.Annotations[matchedKey], err.Error()))
continue
}

for _, condition := range conditions {
if err := condition.Validate(); err != nil {
return fmt.Errorf("ignoring Ingress %s/%s since invalid alb.ingress.kubernetes.io/conditions.%s annotation: %w",
ing.Namespace,
ing.Name,
path.Backend.Service.Name,
err,
)
allErrs = append(allErrs, field.Invalid(fieldPath, ing.Annotations[matchedKey], err.Error()))
continue
}
}
}
}

return nil
return allErrs
}

// +kubebuilder:webhook:path=/validate-networking-v1-ingress,mutating=false,failurePolicy=fail,groups=networking.k8s.io,resources=ingresses,verbs=create;update,versions=v1,name=vingress.elbv2.k8s.aws,sideEffects=None,matchPolicy=Equivalent,webhookVersions=v1,admissionReviewVersions=v1beta1
Expand Down
Loading

0 comments on commit f3d0e82

Please sign in to comment.