Skip to content

Commit

Permalink
Add InboundCIDRs field to IngressClassParams (kubernetes-sigs#3089)
Browse files Browse the repository at this point in the history
  • Loading branch information
johngmyers authored Mar 11, 2023
1 parent 1392585 commit 238416e
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 4 deletions.
4 changes: 4 additions & 0 deletions apis/elbv2/v1beta1/ingressclassparams_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ type IngressClassParamsSpec struct {
// +optional
Scheme *LoadBalancerScheme `json:"scheme,omitempty"`

// InboundCIDRs specifies the CIDRs that are allowed to access the Ingresses that belong to IngressClass with this IngressClassParams.
// +optional
InboundCIDRs []string `json:"inboundCIDRs,omitempty"`

// SSLPolicy specifies the SSL Policy for all Ingresses that belong to IngressClass with this IngressClassParams.
// +optional
SSLPolicy string `json:"sslPolicy,omitEmpty"`
Expand Down
5 changes: 5 additions & 0 deletions apis/elbv2/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ spec:
required:
- name
type: object
inboundCIDRs:
description: InboundCIDRs specifies the CIDRs that are allowed to
access the Ingresses that belong to IngressClass with this IngressClassParams.
items:
type: string
type: array
ipAddressType:
description: IPAddressType defines the ip address type for all Ingresses
that belong to IngressClass with this IngressClassParams.
Expand Down
5 changes: 5 additions & 0 deletions docs/guide/ingress/ingress_class.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ Cluster administrators can use the `scheme` field to restrict the scheme for all
1. If `scheme` specified, all Ingresses with this IngressClass will have the specified scheme.
2. If `scheme` un-specified, Ingresses with this IngressClass can continue to use `alb.ingress.kubernetes.io/scheme annotation` to specify scheme.

#### spec.inboundCIDRs

Cluster administrators can use the optional `inboundCIDRs` field to specify the CIDRs that are allowed to access the load balancers that belong to this IngressClass.
If the field is specified, LBC will ignore the `alb.ingress.kubernetes.io/inbound-cidrs` annotation.

#### spec.sslPolicy

Cluster administrators can use the optional `sslPolicy` field to specify the SSL policy for the load balancers that belong to this IngressClass.
Expand Down
6 changes: 6 additions & 0 deletions helm/aws-load-balancer-controller/crds/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ spec:
required:
- name
type: object
inboundCIDRs:
description: InboundCIDRs specifies the CIDRs that are allowed to
access the Ingresses that belong to IngressClass with this IngressClassParams.
items:
type: string
type: array
ipAddressType:
description: IPAddressType defines the ip address type for all Ingresses
that belong to IngressClass with this IngressClassParams.
Expand Down
17 changes: 13 additions & 4 deletions pkg/ingress/model_build_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ type listenPortConfig struct {
func (t *defaultModelBuildTask) computeIngressListenPortConfigByPort(ctx context.Context, ing *ClassifiedIngress) (map[int64]listenPortConfig, error) {
explicitTLSCertARNs := t.computeIngressExplicitTLSCertARNs(ctx, ing.Ing)
explicitSSLPolicy := t.computeIngressExplicitSSLPolicy(ctx, ing)
inboundCIDRv4s, inboundCIDRV6s, err := t.computeIngressExplicitInboundCIDRs(ctx, ing.Ing)
inboundCIDRv4s, inboundCIDRV6s, err := t.computeIngressExplicitInboundCIDRs(ctx, ing)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -209,15 +209,24 @@ func (t *defaultModelBuildTask) computeIngressListenPorts(_ context.Context, ing
return portAndProtocols, nil
}

func (t *defaultModelBuildTask) computeIngressExplicitInboundCIDRs(_ context.Context, ing *networking.Ingress) ([]string, []string, error) {
func (t *defaultModelBuildTask) computeIngressExplicitInboundCIDRs(_ context.Context, ing *ClassifiedIngress) ([]string, []string, error) {
var rawInboundCIDRs []string
_ = t.annotationParser.ParseStringSliceAnnotation(annotations.IngressSuffixInboundCIDRs, &rawInboundCIDRs, ing.Annotations)
fromIngressClassParams := false
if ing.IngClassConfig.IngClassParams != nil && len(ing.IngClassConfig.IngClassParams.Spec.InboundCIDRs) != 0 {
rawInboundCIDRs = ing.IngClassConfig.IngClassParams.Spec.InboundCIDRs
fromIngressClassParams = true
} else {
_ = t.annotationParser.ParseStringSliceAnnotation(annotations.IngressSuffixInboundCIDRs, &rawInboundCIDRs, ing.Ing.Annotations)
}

var inboundCIDRv4s, inboundCIDRv6s []string
for _, cidr := range rawInboundCIDRs {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return nil, nil, errors.Wrapf(err, "invalid %v settings on Ingress: %v", annotations.IngressSuffixInboundCIDRs, k8s.NamespacedName(ing))
if fromIngressClassParams {
return nil, nil, fmt.Errorf("invalid CIDR in IngressClassParams InboundCIDR %s: %w", cidr, err)
}
return nil, nil, fmt.Errorf("invalid %v settings on Ingress: %v: %w", annotations.IngressSuffixInboundCIDRs, k8s.NamespacedName(ing.Ing), err)
}
if strings.Contains(cidr, ":") {
inboundCIDRv6s = append(inboundCIDRv6s, cidr)
Expand Down
126 changes: 126 additions & 0 deletions pkg/ingress/model_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,132 @@ func Test_defaultModelBuilder_Build(t *testing.T) {
"ns-1/ing-1-svc-3:https": null
}
}
}`,
},
{
name: "Ingress - inboundCIDRs in IngressClassParams",
env: env{
svcs: []*corev1.Service{ns_1_svc_1, ns_1_svc_2, ns_1_svc_3},
},
fields: fields{
resolveViaDiscoveryCalls: []resolveViaDiscoveryCall{resolveViaDiscoveryCallForInternalLB},
listLoadBalancersCalls: []listLoadBalancersCall{listLoadBalancerCallForEmptyLB},
enableBackendSG: true,
},
args: args{
ingGroup: Group{
ID: GroupID{Namespace: "ns-1", Name: "ing-1"},
Members: []ClassifiedIngress{
{
IngClassConfig: ClassConfiguration{
IngClassParams: &v1beta1.IngressClassParams{
Spec: v1beta1.IngressClassParamsSpec{
InboundCIDRs: []string{
"10.0.0.0/8",
"172.16.0.0/12",
},
},
},
},
Ing: &networking.Ingress{ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-1",
Name: "ing-1",
Annotations: map[string]string{
"alb.ingress.kubernetes.io/inbound-cidrs": "20.0.0.0/8",
},
},
Spec: networking.IngressSpec{
Rules: []networking.IngressRule{
{
Host: "app-1.example.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/svc-1",
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: ns_1_svc_1.Name,
Port: networking.ServiceBackendPort{
Name: "http",
},
},
},
},
{
Path: "/svc-2",
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: ns_1_svc_2.Name,
Port: networking.ServiceBackendPort{
Name: "http",
},
},
},
},
},
},
},
},
{
Host: "app-2.example.com",
IngressRuleValue: networking.IngressRuleValue{
HTTP: &networking.HTTPIngressRuleValue{
Paths: []networking.HTTPIngressPath{
{
Path: "/svc-3",
Backend: networking.IngressBackend{
Service: &networking.IngressServiceBackend{
Name: ns_1_svc_3.Name,
Port: networking.ServiceBackendPort{
Name: "https",
},
},
},
},
},
},
},
},
},
},
},
},
},
},
},
wantStackPatch: `
{
"resources": {
"AWS::EC2::SecurityGroup": {
"ManagedLBSecurityGroup": {
"spec": {
"ingress": [
{
"fromPort": 80,
"ipProtocol": "tcp",
"ipRanges": [
{
"cidrIP": "10.0.0.0/8"
}
],
"toPort": 80
},
{
"fromPort": 80,
"ipProtocol": "tcp",
"ipRanges": [
{
"cidrIP": "172.16.0.0/12"
}
],
"toPort": 80
}
]
}
}
}
}
}`,
},
{
Expand Down
42 changes: 42 additions & 0 deletions webhooks/elbv2/ingressclassparams_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package elbv2

import (
"context"
"fmt"
"net"
"strings"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -30,6 +33,7 @@ func (v *ingressClassParamsValidator) Prototype(_ admission.Request) (runtime.Ob
func (v *ingressClassParamsValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
icp := obj.(*elbv2api.IngressClassParams)
allErrs := field.ErrorList{}
allErrs = append(allErrs, v.checkInboundCIDRs(icp)...)
allErrs = append(allErrs, v.checkSubnetSelectors(icp)...)

return allErrs.ToAggregate()
Expand All @@ -38,6 +42,7 @@ func (v *ingressClassParamsValidator) ValidateCreate(ctx context.Context, obj ru
func (v *ingressClassParamsValidator) ValidateUpdate(ctx context.Context, obj runtime.Object, oldObj runtime.Object) error {
icp := obj.(*elbv2api.IngressClassParams)
allErrs := field.ErrorList{}
allErrs = append(allErrs, v.checkInboundCIDRs(icp)...)
allErrs = append(allErrs, v.checkSubnetSelectors(icp)...)

return allErrs.ToAggregate()
Expand All @@ -47,6 +52,43 @@ func (v *ingressClassParamsValidator) ValidateDelete(ctx context.Context, obj ru
return nil
}

// checkInboundCIDRs will check for valid inboundCIDRs.
func (v *ingressClassParamsValidator) checkInboundCIDRs(icp *elbv2api.IngressClassParams) (allErrs field.ErrorList) {
for idx, cidr := range icp.Spec.InboundCIDRs {
fieldPath := field.NewPath("spec", "inboundCIDRs").Index(idx)
allErrs = append(allErrs, validateCIDR(cidr, fieldPath)...)
}

return allErrs
}

// validateCIDR will check for a valid CIDR.
func validateCIDR(cidr string, fieldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

ip, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
detail := "Could not be parsed as a CIDR"
if !strings.Contains(cidr, "/") {
ip := net.ParseIP(cidr)
if ip != nil {
if ip.To4() != nil && !strings.Contains(cidr, ":") {
detail += fmt.Sprintf(" (did you mean \"%s/32\")", cidr)
} else {
detail += fmt.Sprintf(" (did you mean \"%s/64\")", cidr)
}
}
}
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, detail))
} else if !ip.Equal(ipNet.IP) {
maskSize, _ := ipNet.Mask.Size()
detail := fmt.Sprintf("Network contains bits outside prefix (did you mean \"%s/%d\")", ipNet.IP, maskSize)
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, detail))
}

return allErrs
}

// checkSubnetSelectors will check for valid SubnetSelectors
func (v *ingressClassParamsValidator) checkSubnetSelectors(icp *elbv2api.IngressClassParams) (allErrs field.ErrorList) {
if icp.Spec.Subnets != nil {
Expand Down
66 changes: 66 additions & 0 deletions webhooks/elbv2/ingressclassparams_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,72 @@ func Test_ingressClassParamsValidator_ValidateCreate(t *testing.T) {
name: "empty",
obj: &elbv2api.IngressClassParams{},
},
{
name: "inboundCIDRs is valid CIDR list",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"10.0.0.0/8",
"2001:DB8::/32",
},
},
},
},
{
name: "inboundCIDRs IPv4 no length",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"192.168.0.1",
},
},
},
wantErr: "spec.inboundCIDRs[0]: Invalid value: \"192.168.0.1\": Could not be parsed as a CIDR (did you mean \"192.168.0.1/32\")",
},
{
name: "inboundCIDRs IPv6 no length",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"2001:DB8::",
},
},
},
wantErr: "spec.inboundCIDRs[0]: Invalid value: \"2001:DB8::\": Could not be parsed as a CIDR (did you mean \"2001:DB8::/64\")",
},
{
name: "inboundCIDRs bits outside prefix",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"10.128.0.0/8",
},
},
},
wantErr: "spec.inboundCIDRs[0]: Invalid value: \"10.128.0.0/8\": Network contains bits outside prefix (did you mean \"10.0.0.0/8\")",
},
{
name: "inboundCIDRs empty string",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"",
},
},
},
wantErr: "spec.inboundCIDRs[0]: Invalid value: \"\": Could not be parsed as a CIDR",
},
{
name: "inboundCIDRs domain",
obj: &elbv2api.IngressClassParams{
Spec: elbv2api.IngressClassParamsSpec{
InboundCIDRs: []string{
"invalid.example.com",
},
},
},
wantErr: "spec.inboundCIDRs[0]: Invalid value: \"invalid.example.com\": Could not be parsed as a CIDR",
},
{
name: "subnet is valid ID list",
obj: &elbv2api.IngressClassParams{
Expand Down

0 comments on commit 238416e

Please sign in to comment.