Skip to content

Commit

Permalink
Support grouping internet network policies by /24 CIDR to reduce numb…
Browse files Browse the repository at this point in the history
…er of IP addresses per policy (#513)

Co-authored-by: Amit Lichtenberg <[email protected]>
  • Loading branch information
evyatarmeged and amitlicht authored Nov 13, 2024
1 parent 1152f32 commit 46abcae
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/go.mod

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

Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package builders
import (
"context"
"fmt"
"github.com/amit7itz/goset"
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
"github.com/otterize/intents-operator/src/operator/controllers/intents_reconcilers/consts"
"github.com/otterize/intents-operator/src/operator/effectivepolicy"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/otterize/intents-operator/src/shared/injectablerecorder"
"github.com/otterize/intents-operator/src/shared/operatorconfig"
"github.com/samber/lo"
"github.com/spf13/viper"
"golang.org/x/exp/slices"
v1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/util/intstr"
Expand Down Expand Up @@ -65,17 +68,16 @@ func (r *InternetEgressRulesBuilder) buildRuleForIntent(intent otterizev2alpha1.
return nil, nil, false, nil
}

peers := make([]v1.NetworkPolicyPeer, 0)
for ip := range ips {
cidr, err := getCIDR(ip)
peers, err := getIPsAsPeers(ips, false)
if err != nil {
return nil, nil, false, errors.Wrap(err)
}

if viper.GetBool(operatorconfig.EnableGroupInternetIPsByCIDRKey) && len(peers) > viper.GetInt(operatorconfig.EnableGroupInternetIPsByCIDRPeersLimitKey) {
peers, err = getIPsAsPeers(ips, true)
if err != nil {
return nil, nil, false, errors.Wrap(err)
}
peers = append(peers, v1.NetworkPolicyPeer{
IPBlock: &v1.IPBlock{
CIDR: cidr,
},
})
}

slices.SortFunc(peers, func(a, b v1.NetworkPolicyPeer) bool {
Expand Down Expand Up @@ -118,25 +120,51 @@ func (r *InternetEgressRulesBuilder) Build(_ context.Context, ep effectivepolicy
return r.buildEgressRules(ep)
}

func getCIDR(ipStr string) (string, error) {
func getIPsAsPeers(ips map[string]struct{}, groupBySubnet bool) ([]v1.NetworkPolicyPeer, error) {
cidrSet := goset.NewSet[string]()
for ip := range ips {
cidr, err := getCIDR(ip, groupBySubnet)
if err != nil {
return nil, errors.Wrap(err)
}
cidrSet.Add(cidr.String())
}

peers := lo.Map(cidrSet.Items(), func(cidrStr string, _ int) v1.NetworkPolicyPeer {
return v1.NetworkPolicyPeer{
IPBlock: &v1.IPBlock{
CIDR: cidrStr,
},
}
})

return peers, nil
}

func getCIDR(ipStr string, groupBySubnet bool) (*net.IPNet, error) {
cidr := ipStr
if !strings.Contains(ipStr, "/") {
ip := net.ParseIP(ipStr)
if ip == nil {
return "", errors.New(fmt.Sprintf("invalid IP: %s", ipStr))
return nil, errors.New(fmt.Sprintf("invalid IP: %s", ipStr))
}
isV6 := ip.To4() == nil

if isV6 {
// groupBySubnet currently not supported for ipv6
cidr = fmt.Sprintf("%s/128", ip)
} else {
cidr = fmt.Sprintf("%s/32", ip)
if groupBySubnet {
cidr = fmt.Sprintf("%s/24", ip)
} else {
cidr = fmt.Sprintf("%s/32", ip)
}
}
}

_, _, err := net.ParseCIDR(cidr)
_, network, err := net.ParseCIDR(cidr)
if err != nil {
return "", errors.Wrap(err)
return nil, errors.Wrap(err)
}
return cidr, nil
return network, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
"github.com/otterize/intents-operator/src/operator/controllers/intents_reconcilers"
"github.com/otterize/intents-operator/src/operator/controllers/intents_reconcilers/consts"
"github.com/otterize/intents-operator/src/shared/operatorconfig"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
v1 "k8s.io/api/networking/v1"
Expand Down Expand Up @@ -44,7 +46,7 @@ func (s *InternetNetworkPolicyReconcilerTestSuite) TestCreateNetworkPolicySingle
serviceName := "test-client"
clientNamespace := testClientNamespace
formattedTargetClient := "test-client-test-client-namespac-edb3a2"
ips := []string{"10.1.2.2", "254.3.4.0/24", "2620:0:860:ed1a::1", "2607:f8b0:4001:c05::63/64"}
ips := []string{"10.1.2.2", "254.3.4.0/24", "2620:0:860:ed1a::1", "2607:f8b0:4001:c05::/64"}

namespacedName := types.NamespacedName{
Namespace: testClientNamespace,
Expand Down Expand Up @@ -1009,6 +1011,104 @@ func (s *InternetNetworkPolicyReconcilerTestSuite) TestNoIpFoundForAnyDNS() {
s.ExpectEvent(consts.ReasonInternetEgressNetworkPolicyCreationWaitingUnresolvedDNS)
}

func (s *InternetNetworkPolicyReconcilerTestSuite) TestIPsToCIDRConsolidation() {
viper.Set(operatorconfig.EnableGroupInternetIPsByCIDRKey, true)
viper.Set(operatorconfig.EnableGroupInternetIPsByCIDRPeersLimitKey, 2) // just under len(ips)
clientIntentsName := "client-intents"
policyName := "test-client-access"
serviceName := "test-client"
clientNamespace := testClientNamespace
formattedTargetClient := "test-client-test-client-namespac-edb3a2"
dns := "wiki.otters.com"
ips := []string{"10.1.2.4", "10.1.2.5", "10.1.2.6"}
expectedCIDRs := []string{"10.1.2.0"}

namespacedName := types.NamespacedName{
Namespace: testClientNamespace,
Name: clientIntentsName,
}
req := ctrl.Request{
NamespacedName: namespacedName,
}

intentsSpec := &otterizev2alpha1.IntentsSpec{
Workload: otterizev2alpha1.Workload{Name: serviceName},
Targets: []otterizev2alpha1.Target{
{
Internet: &otterizev2alpha1.Internet{
Domains: []string{dns},
},
},
},
}

intentsStatus := otterizev2alpha1.IntentsStatus{
ResolvedIPs: []otterizev2alpha1.ResolvedIPs{
{
DNS: dns,
IPs: ips,
},
},
}
clientIntents := otterizev2alpha1.ClientIntents{
Spec: intentsSpec,
Status: intentsStatus,
}
clientIntents.Namespace = clientNamespace
clientIntents.Name = clientIntentsName
s.expectGetAllEffectivePolicies([]otterizev2alpha1.ClientIntents{clientIntents})

// Search for existing NetworkPolicy
emptyNetworkPolicy := &v1.NetworkPolicy{}
networkPolicyNamespacedName := types.NamespacedName{
Namespace: clientNamespace,
Name: policyName,
}
s.Client.EXPECT().Get(gomock.Any(), networkPolicyNamespacedName, gomock.Eq(emptyNetworkPolicy)).DoAndReturn(
func(ctx context.Context, name types.NamespacedName, networkPolicy *v1.NetworkPolicy, options ...client.ListOption) error {
return apierrors.NewNotFound(v1.Resource("networkpolicy"), name.Name)
})

newPolicy := &v1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: policyName,
Namespace: clientNamespace,
Labels: map[string]string{
otterizev2alpha1.OtterizeNetworkPolicy: formattedTargetClient,
},
},
Spec: v1.NetworkPolicySpec{
PolicyTypes: []v1.PolicyType{v1.PolicyTypeEgress},
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
otterizev2alpha1.OtterizeServiceLabelKey: formattedTargetClient,
},
},
Egress: []v1.NetworkPolicyEgressRule{
{
To: []v1.NetworkPolicyPeer{
{
IPBlock: &v1.IPBlock{
CIDR: expectedCIDRs[0] + "/24",
},
},
},
Ports: []v1.NetworkPolicyPort{},
},
},
},
}
s.Client.EXPECT().Create(gomock.Any(), gomock.Eq(newPolicy))
s.externalNetpolHandler.EXPECT().HandlePodsByLabelSelector(gomock.Any(), gomock.Any(), gomock.Any())

s.ignoreRemoveOrphan()

res, err := s.EPIntentsReconciler.Reconcile(context.Background(), req)
s.Require().NoError(err)
s.Require().Empty(res)
s.ExpectEvent(consts.ReasonCreatedEgressNetworkPolicies)
}

func TestInternetNetworkPolicyReconcilerTestSuite(t *testing.T) {
suite.Run(t, new(InternetNetworkPolicyReconcilerTestSuite))
}
68 changes: 37 additions & 31 deletions src/shared/operatorconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,41 @@ const (
DisableWebhookServerKey = "disable-webhook-server" // Disable webhook validator server
DisableWebhookServerDefault = false

IntentsOperatorPodNameKey = "pod-name"
IntentsOperatorPodNamespaceKey = "pod-namespace"
EnvPrefix = "OTTERIZE"
RetryDelayTimeKey = "retry-delay-time" // Default retry delay time for retrying failed requests
RetryDelayTimeDefault = 5 * time.Second
DebugLogKey = "debug" // Whether to enable debug logging
DebugLogDefault = false
EnableEgressAutoallowDNSTrafficKey = "enable-egress-autoallow-dns-traffic" // Whether to automatically allow DNS traffic in egress network policies
EnableEgressAutoallowDNSTrafficDefault = true
EnableAWSRolesAnywhereKey = "enable-aws-iam-rolesanywhere"
EnableAWSRolesAnywhereDefault = false
AzureSubscriptionIDKey = "azure-subscription-id"
AzureResourceGroupKey = "azure-resource-group"
AzureAKSClusterNameKey = "azure-aks-cluster-name"
EKSClusterNameOverrideKey = "eks-cluster-name-override"
AWSRolesAnywhereClusterNameKey = "rolesanywhere-cluster-name"
AWSRolesAnywhereCertDirKey = "rolesanywhere-cert-dir"
AWSRolesAnywhereCertDirDefault = "/aws-config"
AWSRolesAnywherePrivKeyFilenameKey = "rolesanywhere-priv-key-filename"
AWSRolesAnywhereCertFilenameKey = "rolesanywhere-cert-filename"
AWSRolesAnywherePrivKeyFilenameDefault = "tls.key"
AWSRolesAnywhereCertFilenameDefault = "tls.crt"
TelemetryErrorsAPIKeyKey = "telemetry-errors-api-key"
TelemetryErrorsAPIKeyDefault = "60a78208a2b4fe714ef9fb3d3fdc0714"
AWSAccountsKey = "aws"
IngressControllerALBExemptKey = "ingress-controllers-exempt-alb"
IngressControllerALBExemptDefault = false
IngressControllerConfigKey = "ingressControllers"
SeparateNetpolsForIngressAndEgress = "separate-netpols-for-ingress-and-egress"
SeparateNetpolsForIngressAndEgressDefault = false
ExternallyManagedPolicyWorkloadsKey = "externallyManagedPolicyWorkloads"
IntentsOperatorPodNameKey = "pod-name"
IntentsOperatorPodNamespaceKey = "pod-namespace"
EnvPrefix = "OTTERIZE"
RetryDelayTimeKey = "retry-delay-time" // Default retry delay time for retrying failed requests
RetryDelayTimeDefault = 5 * time.Second
DebugLogKey = "debug" // Whether to enable debug logging
DebugLogDefault = false
EnableEgressAutoallowDNSTrafficKey = "enable-egress-autoallow-dns-traffic" // Whether to automatically allow DNS traffic in egress network policies
EnableEgressAutoallowDNSTrafficDefault = true
EnableGroupInternetIPsByCIDRKey = "enable-group-internet-ips-by-cidr"
EnableGroupInternetIPsByCIDRDefault = false
EnableGroupInternetIPsByCIDRPeersLimitKey = "enable-group-by-cidr-peers-limit"
EnableGroupInternetIPsByCIDRPeersLimitDefault = 50
EnableAWSRolesAnywhereKey = "enable-aws-iam-rolesanywhere"
EnableAWSRolesAnywhereDefault = false
AzureSubscriptionIDKey = "azure-subscription-id"
AzureResourceGroupKey = "azure-resource-group"
AzureAKSClusterNameKey = "azure-aks-cluster-name"
EKSClusterNameOverrideKey = "eks-cluster-name-override"
AWSRolesAnywhereClusterNameKey = "rolesanywhere-cluster-name"
AWSRolesAnywhereCertDirKey = "rolesanywhere-cert-dir"
AWSRolesAnywhereCertDirDefault = "/aws-config"
AWSRolesAnywherePrivKeyFilenameKey = "rolesanywhere-priv-key-filename"
AWSRolesAnywhereCertFilenameKey = "rolesanywhere-cert-filename"
AWSRolesAnywherePrivKeyFilenameDefault = "tls.key"
AWSRolesAnywhereCertFilenameDefault = "tls.crt"
TelemetryErrorsAPIKeyKey = "telemetry-errors-api-key"
TelemetryErrorsAPIKeyDefault = "60a78208a2b4fe714ef9fb3d3fdc0714"
AWSAccountsKey = "aws"
IngressControllerALBExemptKey = "ingress-controllers-exempt-alb"
IngressControllerALBExemptDefault = false
IngressControllerConfigKey = "ingressControllers"
SeparateNetpolsForIngressAndEgress = "separate-netpols-for-ingress-and-egress"
SeparateNetpolsForIngressAndEgressDefault = false
ExternallyManagedPolicyWorkloadsKey = "externallyManagedPolicyWorkloads"
)

func init() {
Expand All @@ -86,6 +90,8 @@ func init() {
viper.SetDefault(RetryDelayTimeKey, RetryDelayTimeDefault)
viper.SetDefault(DebugLogKey, DebugLogDefault)
viper.SetDefault(SeparateNetpolsForIngressAndEgress, SeparateNetpolsForIngressAndEgressDefault)
viper.SetDefault(EnableGroupInternetIPsByCIDRKey, EnableGroupInternetIPsByCIDRDefault)
viper.SetDefault(EnableGroupInternetIPsByCIDRPeersLimitKey, EnableGroupInternetIPsByCIDRPeersLimitDefault)
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()

Expand Down

0 comments on commit 46abcae

Please sign in to comment.