feat: add externalip to rules api (#2099)

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
This commit is contained in:
Oliver Bähler
2026-08-24 08:37:50 +02:00
committed by GitHub
parent 7f56e957d6
commit 7b146871d6
10 changed files with 789 additions and 0 deletions
@@ -295,6 +295,19 @@ spec:
services:
description: Enforcement for Services.
properties:
externalIPs:
description: ExternalIPs defines constraints for spec.externalIPs.
properties:
cidrs:
description: |-
CIDRs restricts spec.externalIPs. Individual IP addresses are treated as
host CIDRs (/32 for IPv4 and /128 for IPv6).
For deny rules, empty means all external IPs are denied. For allow and
audit rules, empty means no external IP restriction.
items:
type: string
type: array
type: object
externalNames:
description: ExternalNames defines additional constraints
for Services of type ExternalName.
@@ -972,6 +985,19 @@ spec:
services:
description: Enforcement for Services.
properties:
externalIPs:
description: ExternalIPs defines constraints for spec.externalIPs.
properties:
cidrs:
description: |-
CIDRs restricts spec.externalIPs. Individual IP addresses are treated as
host CIDRs (/32 for IPv4 and /128 for IPv6).
For deny rules, empty means all external IPs are denied. For allow and
audit rules, empty means no external IP restriction.
items:
type: string
type: array
type: object
externalNames:
description: ExternalNames defines additional constraints
for Services of type ExternalName.
@@ -1588,6 +1614,19 @@ spec:
services:
description: Enforcement for Services.
properties:
externalIPs:
description: ExternalIPs defines constraints for spec.externalIPs.
properties:
cidrs:
description: |-
CIDRs restricts spec.externalIPs. Individual IP addresses are treated as
host CIDRs (/32 for IPv4 and /128 for IPv6).
For deny rules, empty means all external IPs are denied. For allow and
audit rules, empty means no external IP restriction.
items:
type: string
type: array
type: object
externalNames:
description: ExternalNames defines additional constraints
for Services of type ExternalName.
@@ -2752,6 +2752,19 @@ spec:
services:
description: Enforcement for Services.
properties:
externalIPs:
description: ExternalIPs defines constraints for spec.externalIPs.
properties:
cidrs:
description: |-
CIDRs restricts spec.externalIPs. Individual IP addresses are treated as
host CIDRs (/32 for IPv4 and /128 for IPv6).
For deny rules, empty means all external IPs are denied. For allow and
audit rules, empty means no external IP restriction.
items:
type: string
type: array
type: object
externalNames:
description: ExternalNames defines additional constraints
for Services of type ExternalName.
+130
View File
@@ -96,6 +96,21 @@ var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "
}
}
externalIPCIDRRule := func(action rules.ActionType, cidrs ...string) *rules.NamespaceRuleBodyTenant {
return &rules.NamespaceRuleBodyTenant{
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: action,
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &rules.ServiceExternalIPRule{
CIDRs: cidrs,
},
},
},
},
}
}
externalNameRule := func(action rules.ActionType, hostnames ...runtime.ExpressionMatch) *rules.NamespaceRuleBodyTenant {
return &rules.NamespaceRuleBodyTenant{
NamespaceRuleBodyNamespace: &rules.NamespaceRuleBodyNamespace{
@@ -260,6 +275,8 @@ var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "
action rules.ActionType
types []rules.ServiceType
loadBalancerCIDRs []string
externalIPs bool
externalIPCIDRs []string
nodePortRanges []rules.ServiceNodePortRange
externalExpressions []string
externalExact [][]string
@@ -303,6 +320,13 @@ var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "
g.Expect(got.Enforce.Services.LoadBalancers.CIDRs).To(Equal(expected.loadBalancerCIDRs))
}
if expected.externalIPs {
g.Expect(got.Enforce.Services.ExternalIPs).NotTo(BeNil())
g.Expect(got.Enforce.Services.ExternalIPs.CIDRs).To(Equal(expected.externalIPCIDRs))
} else {
g.Expect(got.Enforce.Services.ExternalIPs).To(BeNil())
}
if len(expected.nodePortRanges) == 0 {
if got.Enforce.Services.NodePorts != nil {
g.Expect(got.Enforce.Services.NodePorts.Ports).To(BeEmpty())
@@ -593,6 +617,13 @@ var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "
}
}
externalIPService := func(name string, externalIPs ...string) *corev1.Service {
svc := clusterIPService(name)
svc.Spec.ExternalIPs = externalIPs
return svc
}
nodePortService := func(name string, nodePort int32) *corev1.Service {
port := servicePort()
port.NodePort = nodePort
@@ -859,6 +890,105 @@ var _ = Describe("enforcing service namespace rules", Ordered, Label("tenant", "
)
})
It("allows only Service external IPs contained in configured CIDRs", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
serviceTypeRule(
rules.ActionTypeAllow,
rules.ServiceTypeClusterIP,
),
externalIPCIDRRule(
rules.ActionTypeAllow,
"10.20.0.0/16",
"192.168.1.2",
),
})
ns := createNamespace(nil)
expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
{
action: rules.ActionTypeAllow,
types: []rules.ServiceType{
rules.ServiceTypeClusterIP,
},
},
{
action: rules.ActionTypeAllow,
externalIPs: true,
externalIPCIDRs: []string{
"10.20.0.0/16",
"192.168.1.2",
},
},
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createServiceAndExpectAllowed(cs, ns.Name, clusterIPService("external-ip-omitted-allowed"))
allowed := externalIPService("external-ip-cidr-allowed", "10.20.1.44")
createServiceAndExpectAllowed(cs, ns.Name, allowed)
createServiceAndExpectAllowed(cs, ns.Name, externalIPService("external-ip-host-allowed", "192.168.1.2"))
createServiceAndExpectDenied(cs, ns.Name, externalIPService("external-ip-denied", "8.8.8.8"),
"external IP",
"8.8.8.8",
"spec.externalIPs[0]",
"not allowed",
"Allowed CIDRs",
"10.20.0.0/16",
"192.168.1.2",
)
updateServiceAndExpectDenied(
cs,
ns.Name,
allowed.Name,
func(svc *corev1.Service) {
svc.Spec.ExternalIPs = []string{"8.8.4.4"}
},
"external IP",
"8.8.4.4",
"spec.externalIPs[0]",
"not allowed",
)
})
It("denies every specified Service external IP when a deny rule has empty CIDRs", func() {
updateTenantRules([]*rules.NamespaceRuleBodyTenant{
serviceTypeRule(
rules.ActionTypeAllow,
rules.ServiceTypeClusterIP,
),
externalIPCIDRRule(rules.ActionTypeDeny),
})
ns := createNamespace(nil)
expectNamespaceStatusRules(ns.Name, []expectedServiceStatusRule{
{
action: rules.ActionTypeAllow,
types: []rules.ServiceType{
rules.ServiceTypeClusterIP,
},
},
{
action: rules.ActionTypeDeny,
externalIPs: true,
},
})
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
createServiceAndExpectAllowed(cs, ns.Name, clusterIPService("external-ip-empty-deny-omitted"))
createServiceAndExpectDenied(cs, ns.Name, externalIPService("external-ip-empty-denied", "10.20.1.44"),
"external IP",
"10.20.1.44",
"denied",
"all external IPs",
)
})
It("allows exact, regex, and combined ExternalName hostname matchers", func() {
ns := createNamespace(nil)
cs := ownerClient(tnt.Spec.Owners[0].UserSpec)
@@ -0,0 +1,131 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package validation
import (
"fmt"
"net"
"strings"
corev1 "k8s.io/api/core/v1"
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
ruleengine "github.com/projectcapsule/capsule/pkg/ruleengine"
"github.com/projectcapsule/capsule/pkg/runtime/events"
)
type externalIPCIDRRule struct {
CIDRs []string
MatchAll bool
}
func (h *serviceRules) validateExternalIPs(
svc *corev1.Service,
enforceBodies []*apirules.NamespaceRuleEnforceBody,
) (*ruleengine.Evaluation, error) {
return evaluateServiceRules[externalIPCIDRRule](
svc,
enforceBodies,
serviceRuleSet[externalIPCIDRRule]{
Name: "external IP",
EventReason: events.ReasonForbiddenExternalServiceIP,
Values: externalIPValues,
Rules: func(enforce *apirules.NamespaceRuleEnforceBody) []externalIPCIDRRule {
if enforce == nil || enforce.Services.ExternalIPs == nil {
return nil
}
if len(enforce.Services.ExternalIPs.CIDRs) == 0 {
if enforce.Action.OrDefault() == apirules.ActionTypeDeny {
return []externalIPCIDRRule{
{
MatchAll: true,
},
}
}
return nil
}
cidrs := make([]string, 0, len(enforce.Services.ExternalIPs.CIDRs))
for _, cidr := range enforce.Services.ExternalIPs.CIDRs {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
cidrs = append(cidrs, cidr)
}
if len(cidrs) == 0 {
return nil
}
return []externalIPCIDRRule{
{
CIDRs: cidrs,
},
}
},
Matches: func(rule externalIPCIDRRule, value ruleengine.Value) (ruleengine.Match, error) {
if rule.MatchAll {
return ruleengine.Match{
Matched: true,
MatchedValue: "all external IPs",
Detail: "deny rule applies to all external IPs",
}, nil
}
ip := net.ParseIP(value.Value)
if ip == nil {
return ruleengine.Match{}, fmt.Errorf(
"%s contains invalid IP %q",
value.Path,
value.Value,
)
}
for _, rawCIDR := range rule.CIDRs {
allowedCIDR, err := parseCIDR(rawCIDR)
if err != nil {
return ruleengine.Match{}, fmt.Errorf("invalid external IP CIDR %q: %w", rawCIDR, err)
}
if !cidrContainsIP(allowedCIDR, ip) {
continue
}
return ruleengine.Match{
Matched: true,
MatchedValue: rawCIDR,
Detail: fmt.Sprintf("%s is contained in %s", value.Value, rawCIDR),
}, nil
}
return ruleengine.Match{}, nil
},
RuleDescription: func(rule externalIPCIDRRule) string {
if rule.MatchAll {
return "all external IPs"
}
return strings.Join(rule.CIDRs, ", ")
},
AllowedDescription: "Allowed CIDRs",
},
)
}
func externalIPValues(svc *corev1.Service) []ruleengine.Value {
out := make([]ruleengine.Value, 0, len(svc.Spec.ExternalIPs))
for i, externalIP := range svc.Spec.ExternalIPs {
out = append(out, ruleengine.Value{
Value: externalIP,
Path: fmt.Sprintf("spec.externalIPs[%d]", i),
})
}
return out
}
@@ -0,0 +1,350 @@
// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package validation
import (
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
apirules "github.com/projectcapsule/capsule/pkg/api/rules"
ruleengine "github.com/projectcapsule/capsule/pkg/ruleengine"
"github.com/projectcapsule/capsule/pkg/runtime/events"
)
func TestServiceRulesValidateExternalIPs(t *testing.T) {
tests := []struct {
name string
svc *corev1.Service
enforceBodies []*apirules.NamespaceRuleEnforceBody
wantNil bool
wantBlocking bool
wantFinal bool
wantAudits int
wantErr string
wantMessage []string
}{
{
name: "nil service returns nil evaluation",
svc: nil,
wantNil: true,
},
{
name: "service without external IPs remains unrestricted",
svc: externalIPServiceForTest(),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.0.0.0/8"),
},
},
{
name: "external IP without external IP rules remains unrestricted",
svc: externalIPServiceForTest("10.0.0.2"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
{Action: apirules.ActionTypeAllow},
},
},
{
name: "allows IPv4 address inside configured CIDR",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/16"),
},
wantFinal: true,
wantMessage: []string{
`external IP "10.20.1.44" at spec.externalIPs[0] is allowed by namespace rule`,
"10.20.1.44 is contained in 10.20.0.0/16",
},
},
{
name: "allows plain IPv4 rule as a host CIDR",
svc: externalIPServiceForTest("192.168.1.2"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "192.168.1.2"),
},
wantFinal: true,
},
{
name: "allows IPv6 address inside configured CIDR",
svc: externalIPServiceForTest("2001:db8::2"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "2001:db8::/32"),
},
wantFinal: true,
wantMessage: []string{
"2001:db8::2 is contained in 2001:db8::/32",
},
},
{
name: "allow miss denies address outside configured CIDRs",
svc: externalIPServiceForTest("8.8.8.8"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/16", "192.168.1.2"),
},
wantBlocking: true,
wantMessage: []string{
`external IP "8.8.8.8" at spec.externalIPs[0] is not allowed by namespace rule`,
"Allowed CIDRs",
"10.20.0.0/16",
"192.168.1.2",
},
},
{
name: "all external IPs must match an allow rule",
svc: externalIPServiceForTest("10.20.1.44", "8.8.8.8"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/16"),
},
wantFinal: true,
wantBlocking: true,
wantMessage: []string{
`external IP "8.8.8.8" at spec.externalIPs[1] is not allowed by namespace rule`,
},
},
{
name: "matching deny rule blocks address",
svc: externalIPServiceForTest("10.20.66.4"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeDeny, "10.20.66.0/24"),
},
wantFinal: true,
wantBlocking: true,
wantMessage: []string{
`external IP "10.20.66.4" at spec.externalIPs[0] is denied by namespace rule`,
},
},
{
name: "deny with omitted CIDRs blocks every external IP",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeDeny),
},
wantFinal: true,
wantBlocking: true,
wantMessage: []string{
`external IP "10.20.1.44" at spec.externalIPs[0] is denied by namespace rule`,
"all external IPs",
"deny rule applies to all external IPs",
},
},
{
name: "deny with explicit empty CIDRs blocks every external IP",
svc: externalIPServiceForTest("2001:db8::2"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
{
Action: apirules.ActionTypeDeny,
Services: apirules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &apirules.ServiceExternalIPRule{
CIDRs: []string{},
},
},
},
},
wantFinal: true,
wantBlocking: true,
},
{
name: "default deny action with empty CIDRs blocks every external IP",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(""),
},
wantFinal: true,
wantBlocking: true,
},
{
name: "deny with empty CIDRs does not require an external IP",
svc: externalIPServiceForTest(),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeDeny),
},
},
{
name: "later allow overrides earlier matching deny",
svc: externalIPServiceForTest("10.20.66.4"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeDeny, "10.20.0.0/16"),
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.66.0/24"),
},
wantFinal: true,
},
{
name: "matching audit rule is observational",
svc: externalIPServiceForTest("10.20.66.4"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAudit, "10.20.66.0/24"),
},
wantAudits: 1,
wantMessage: []string{
`external IP "10.20.66.4" at spec.externalIPs[0] matched audit namespace rule`,
},
},
{
name: "audit does not satisfy a non-matching allow rule",
svc: externalIPServiceForTest("10.20.66.4"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAudit, "10.20.66.0/24"),
externalIPEnforceForTest(apirules.ActionTypeAllow, "192.168.0.0/16"),
},
wantAudits: 1,
wantBlocking: true,
},
{
name: "blank configured CIDRs are ignored",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "", " "),
},
},
{
name: "invalid configured CIDR returns matcher error",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/33"),
},
wantErr: `external IP: invalid rule: invalid external IP CIDR "10.20.0.0/33"`,
},
{
name: "invalid requested external IP returns matcher error",
svc: externalIPServiceForTest("not-an-ip"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/16"),
},
wantErr: `external IP: invalid rule: spec.externalIPs[0] contains invalid IP "not-an-ip"`,
},
{
name: "nil enforce body is ignored",
svc: externalIPServiceForTest("10.20.1.44"),
enforceBodies: []*apirules.NamespaceRuleEnforceBody{
nil,
externalIPEnforceForTest(apirules.ActionTypeAllow, "10.20.0.0/16"),
},
wantFinal: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := serviceRulesForTest()
evaluation, err := h.validateExternalIPs(tt.svc, tt.enforceBodies)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error())
}
return
}
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if tt.wantNil {
if evaluation != nil {
t.Fatalf("expected nil evaluation, got %#v", evaluation)
}
return
}
if evaluation == nil {
t.Fatalf("expected evaluation, got nil")
}
if (evaluation.Blocking != nil) != tt.wantBlocking {
t.Fatalf("blocking decision = %#v, want presence %t", evaluation.Blocking, tt.wantBlocking)
}
if (evaluation.Final != nil) != tt.wantFinal {
t.Fatalf("final decision = %#v, want presence %t", evaluation.Final, tt.wantFinal)
}
if len(evaluation.Audits) != tt.wantAudits {
t.Fatalf("audit decisions = %d, want %d", len(evaluation.Audits), tt.wantAudits)
}
message := externalIPDecisionMessagesForTest(evaluation)
for _, expected := range tt.wantMessage {
if !strings.Contains(message, expected) {
t.Fatalf("expected message %q to contain %q", message, expected)
}
}
if evaluation.Final != nil && evaluation.Final.EventReason != events.ReasonForbiddenExternalServiceIP {
t.Fatalf("final event reason = %q, want %q", evaluation.Final.EventReason, events.ReasonForbiddenExternalServiceIP)
}
if evaluation.Blocking != nil && evaluation.Blocking.EventReason != events.ReasonForbiddenExternalServiceIP {
t.Fatalf("blocking event reason = %q, want %q", evaluation.Blocking.EventReason, events.ReasonForbiddenExternalServiceIP)
}
for _, audit := range evaluation.Audits {
if audit.EventReason != events.ReasonForbiddenExternalServiceIP {
t.Fatalf("audit event reason = %q, want %q", audit.EventReason, events.ReasonForbiddenExternalServiceIP)
}
}
})
}
}
func TestExternalIPValues(t *testing.T) {
values := externalIPValues(externalIPServiceForTest("10.20.1.44", "2001:db8::2"))
if len(values) != 2 {
t.Fatalf("expected 2 values, got %d", len(values))
}
if values[0].Value != "10.20.1.44" || values[0].Path != "spec.externalIPs[0]" {
t.Fatalf("unexpected first value: %#v", values[0])
}
if values[1].Value != "2001:db8::2" || values[1].Path != "spec.externalIPs[1]" {
t.Fatalf("unexpected second value: %#v", values[1])
}
}
func externalIPEnforceForTest(
action apirules.ActionType,
cidrs ...string,
) *apirules.NamespaceRuleEnforceBody {
return &apirules.NamespaceRuleEnforceBody{
Action: action,
Services: apirules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &apirules.ServiceExternalIPRule{
CIDRs: cidrs,
},
},
}
}
func externalIPServiceForTest(externalIPs ...string) *corev1.Service {
return &corev1.Service{
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
ExternalIPs: externalIPs,
},
}
}
func externalIPDecisionMessagesForTest(evaluation *ruleengine.Evaluation) string {
messages := make([]string, 0, len(evaluation.Audits)+2)
for _, audit := range evaluation.Audits {
messages = append(messages, audit.Message)
}
if evaluation.Final != nil {
messages = append(messages, evaluation.Final.Message)
}
if evaluation.Blocking != nil && evaluation.Blocking != evaluation.Final {
messages = append(messages, evaluation.Blocking.Message)
}
return strings.Join(messages, "\n")
}
@@ -62,6 +62,7 @@ func ServiceRules(
h.rules = []serviceRuleValidator{
h.validateServiceTypes,
h.validateLoadBalancers,
h.validateExternalIPs,
h.validateExternalNames,
h.validateNodePorts,
}
+14
View File
@@ -23,6 +23,10 @@ type NamespaceRuleEnforceServicesBody struct {
// +optional
LoadBalancers *ServiceLoadBalancerRule `json:"loadBalancers,omitempty"`
// ExternalIPs defines constraints for spec.externalIPs.
// +optional
ExternalIPs *ServiceExternalIPRule `json:"externalIPs,omitempty"`
// ExternalNames defines additional constraints for Services of type ExternalName.
// +optional
ExternalNames *ServiceExternalNameRule `json:"externalNames,omitempty"`
@@ -50,6 +54,16 @@ type ServiceLoadBalancerRule struct {
CIDRs []string `json:"cidrs,omitempty"`
}
// +kubebuilder:object:generate=true
type ServiceExternalIPRule struct {
// CIDRs restricts spec.externalIPs. Individual IP addresses are treated as
// host CIDRs (/32 for IPv4 and /128 for IPv6).
// For deny rules, empty means all external IPs are denied. For allow and
// audit rules, empty means no external IP restriction.
// +optional
CIDRs []string `json:"cidrs,omitempty"`
}
// +kubebuilder:object:generate=true
type ServiceExternalNameRule struct {
// Hostnames restricts spec.externalName.
+25
View File
@@ -214,6 +214,11 @@ func (in *NamespaceRuleEnforceServicesBody) DeepCopyInto(out *NamespaceRuleEnfor
*out = new(ServiceLoadBalancerRule)
(*in).DeepCopyInto(*out)
}
if in.ExternalIPs != nil {
in, out := &in.ExternalIPs, &out.ExternalIPs
*out = new(ServiceExternalIPRule)
(*in).DeepCopyInto(*out)
}
if in.ExternalNames != nil {
in, out := &in.ExternalNames, &out.ExternalNames
*out = new(ServiceExternalNameRule)
@@ -375,6 +380,26 @@ func (in *ResourceQuotaRule) DeepCopy() *ResourceQuotaRule {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceExternalIPRule) DeepCopyInto(out *ServiceExternalIPRule) {
*out = *in
if in.CIDRs != nil {
in, out := &in.CIDRs, &out.CIDRs
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExternalIPRule.
func (in *ServiceExternalIPRule) DeepCopy() *ServiceExternalIPRule {
if in == nil {
return nil
}
out := new(ServiceExternalIPRule)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ServiceExternalNameRule) DeepCopyInto(out *ServiceExternalNameRule) {
*out = *in
+14
View File
@@ -415,6 +415,20 @@ func validateServiceRules(
}
}
if services.ExternalIPs != nil {
for j, cidr := range services.ExternalIPs.CIDRs {
if err := validateCIDR(cidr); err != nil {
return fmt.Errorf(
"rules[%d].enforce.services.externalIPs.cidrs[%d] %q is invalid: %w",
ruleIndex,
j,
cidr,
err,
)
}
}
}
if services.ExternalNames != nil {
for j, hostname := range services.ExternalNames.Hostnames {
if err := validateExpressionMatch(
+72
View File
@@ -647,6 +647,78 @@ func TestValidateRuleStatusBody(t *testing.T) {
},
wantErr: `rules[0].enforce.services.loadBalancers.cidrs[0] "" is invalid: CIDR is empty`,
},
{
name: "invalid external IP CIDR",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &rules.ServiceExternalIPRule{
CIDRs: []string{
"10.20.0.0/33",
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.externalIPs.cidrs[0] "10.20.0.0/33" is invalid`,
},
{
name: "empty external IP CIDR entry",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &rules.ServiceExternalIPRule{
CIDRs: []string{
"",
},
},
},
},
},
},
wantErr: `rules[0].enforce.services.externalIPs.cidrs[0] "" is invalid: CIDR is empty`,
},
{
name: "external IP CIDRs are valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeAllow,
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &rules.ServiceExternalIPRule{
CIDRs: []string{
"10.20.0.0/16",
"192.168.1.2",
"2001:db8::/32",
},
},
},
},
},
},
},
{
name: "empty external IP CIDR deny rule is valid",
mapper: mapper,
bodies: []*rules.NamespaceRuleBodyNamespace{
{
Enforce: &rules.NamespaceRuleEnforceBody{
Action: rules.ActionTypeDeny,
Services: rules.NamespaceRuleEnforceServicesBody{
ExternalIPs: &rules.ServiceExternalIPRule{
CIDRs: []string{},
},
},
},
},
},
},
{
name: "invalid externalName hostname regex",
mapper: mapper,