Skip to content

Commit 98150b6

Browse files
committed
OPRUN-3873: Add e2e tests for NetworkPolicies
1 parent 8f81c23 commit 98150b6

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

test/e2e/network_policy_test.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
corev1 "k8s.io/api/core/v1"
12+
networkingv1 "k8s.io/api/networking/v1"
13+
"k8s.io/apimachinery/pkg/api/equality"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/util/intstr"
16+
"k8s.io/utils/ptr"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
18+
)
19+
20+
const (
21+
olmSystemNamespace = "olmv1-system"
22+
minJustificationLength = 40
23+
catalogdManagerSelector = "control-plane=catalogd-controller-manager"
24+
operatorManagerSelector = "control-plane=operator-controller-controller-manager"
25+
)
26+
27+
// IngressRule defines a k8s IngressRule, along with a justification.
28+
type IngressRule struct {
29+
Ports []networkingv1.NetworkPolicyPort
30+
From []networkingv1.NetworkPolicyPeer
31+
Justification string
32+
}
33+
34+
// EgressRule defines a k8s EgressRule, along with a justification.
35+
type EgressRule struct {
36+
Ports []networkingv1.NetworkPolicyPort
37+
To []networkingv1.NetworkPolicyPeer
38+
Justification string
39+
}
40+
41+
// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy.
42+
type AllowedPolicyDefinition struct {
43+
Selector metav1.LabelSelector
44+
PolicyTypes []networkingv1.PolicyType
45+
IngressRules []IngressRule
46+
EgressRules []EgressRule
47+
DenyAllIngressByTypeJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty
48+
DenyAllEgressByTypeJustification string // Justification if Egress is in PolicyTypes and EgressRules is empty
49+
}
50+
51+
var allowedNetworkPolicies = map[string]AllowedPolicyDefinition{
52+
"controller-manager.catalogd": {
53+
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}},
54+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
55+
IngressRules: []IngressRule{
56+
{
57+
Ports: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(7443)}},
58+
Justification: "Allows Prometheus to scrape metrics from catalogd, providing observability into its performance and health. This is essential for monitoring.",
59+
},
60+
{
61+
Ports: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(8443)}},
62+
Justification: "Enables operator-controller to query catalog metadata from catalogd. This is a core function for bundle resolution and operator discovery.",
63+
},
64+
{
65+
Ports: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(9443)}},
66+
Justification: "Permits Kubernetes API server to reach catalogd's admission webhook for CRD validation, ensuring integrity of catalog resources.",
67+
},
68+
},
69+
EgressRules: []EgressRule{
70+
{
71+
// Empty Ports and To means allow all egress
72+
Justification: "Permits catalogd to fetch catalog images from various container registries and communicate with the Kubernetes API server for its operational needs.",
73+
},
74+
},
75+
},
76+
"controller-manager.operator-controller": {
77+
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}},
78+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
79+
IngressRules: []IngressRule{
80+
{
81+
Ports: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(8443)}},
82+
Justification: "Allows Prometheus to scrape metrics from operator-controller, crucial for monitoring its activity, reconciliations, and overall health.",
83+
},
84+
},
85+
EgressRules: []EgressRule{
86+
{
87+
// Empty Ports and To means allow all egress
88+
Justification: "Enables operator-controller to pull bundle images, connect to catalogd's HTTPS server for metadata, and interact with the Kubernetes API server.",
89+
},
90+
},
91+
},
92+
"default-deny-all-traffic": {
93+
Selector: metav1.LabelSelector{}, // Empty selector, matches all pods
94+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
95+
// No IngressRules means deny all ingress if PolicyTypeIngress is present
96+
// No EgressRules means deny all egress if PolicyTypeEgress is present
97+
DenyAllIngressByTypeJustification: "Denies all ingress traffic to pods selected by this policy by default, unless explicitly allowed by other policy rules, ensuring a baseline secure posture.",
98+
DenyAllEgressByTypeJustification: "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.",
99+
},
100+
}
101+
102+
func TestNetworkPolicyJustifications(t *testing.T) {
103+
ctx := context.Background()
104+
105+
// Validate justifications have min length
106+
for name, policyDef := range allowedNetworkPolicies {
107+
for i, rule := range policyDef.IngressRules {
108+
assert.GreaterOrEqualf(t, len(rule.Justification), minJustificationLength,
109+
"Justification for ingress rule %d in policy %q is too short: %q", i, name, rule.Justification)
110+
}
111+
for i, rule := range policyDef.EgressRules {
112+
assert.GreaterOrEqualf(t, len(rule.Justification), minJustificationLength,
113+
"Justification for egress rule %d in policy %q is too short: %q", i, name, rule.Justification)
114+
}
115+
if policyDef.DenyAllIngressByTypeJustification != "" {
116+
assert.GreaterOrEqualf(t, len(policyDef.DenyAllIngressByTypeJustification), minJustificationLength,
117+
"DenyAllIngressByTypeJustification for policy %q is too short: %q", name, policyDef.DenyAllIngressByTypeJustification)
118+
}
119+
if policyDef.DenyAllEgressByTypeJustification != "" {
120+
assert.GreaterOrEqualf(t, len(policyDef.DenyAllEgressByTypeJustification), minJustificationLength,
121+
"DenyAllEgressByTypeJustification for policy %q is too short: %q", name, policyDef.DenyAllEgressByTypeJustification)
122+
}
123+
}
124+
125+
networkPolicyList := &networkingv1.NetworkPolicyList{}
126+
err := c.List(ctx, networkPolicyList, client.InNamespace(olmSystemNamespace))
127+
require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", olmSystemNamespace)
128+
129+
validatedRegistryPolicies := make(map[string]bool)
130+
131+
for _, clusterPolicyItem := range networkPolicyList.Items {
132+
clusterPolicy := clusterPolicyItem
133+
policyName := clusterPolicy.Name
134+
135+
t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policyName, "-", "_")), func(t *testing.T) {
136+
expectedPolicy, found := allowedNetworkPolicies[policyName]
137+
require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policyName, clusterPolicy.Namespace)
138+
validatedRegistryPolicies[policyName] = true
139+
140+
// 1. Compare PodSelector
141+
assert.True(t, equality.Semantic.DeepEqual(expectedPolicy.Selector, clusterPolicy.Spec.PodSelector),
142+
"PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policyName, expectedPolicy.Selector, clusterPolicy.Spec.PodSelector)
143+
144+
// 2. Compare PolicyTypes
145+
require.ElementsMatchf(t, expectedPolicy.PolicyTypes, clusterPolicy.Spec.PolicyTypes,
146+
"PolicyTypes mismatch for policy %q.", policyName)
147+
148+
// 3. Validate Ingress Rules
149+
hasIngressPolicyType := false
150+
for _, pt := range clusterPolicy.Spec.PolicyTypes {
151+
if pt == networkingv1.PolicyTypeIngress {
152+
hasIngressPolicyType = true
153+
break
154+
}
155+
}
156+
157+
if hasIngressPolicyType {
158+
if len(clusterPolicy.Spec.Ingress) == 0 {
159+
require.Emptyf(t, expectedPolicy.IngressRules,
160+
"Policy %q: cluster has no ingress rules (deny all), but registry expects specific ingress rules.", policyName)
161+
require.NotEmptyf(t, expectedPolicy.DenyAllIngressByTypeJustification,
162+
"Policy %q: cluster has no ingress rules (deny all), but registry has no DenyAllIngressByTypeJustification.", policyName)
163+
} else {
164+
require.NotEmptyf(t, expectedPolicy.IngressRules,
165+
"Policy %q: cluster has ingress rules, but registry expects no specific ingress rules.", policyName)
166+
require.Emptyf(t, expectedPolicy.DenyAllIngressByTypeJustification,
167+
"Policy %q: cluster has ingress rules, but registry also has DenyAllIngressByTypeJustification.", policyName)
168+
require.Lenf(t, clusterPolicy.Spec.Ingress, len(expectedPolicy.IngressRules),
169+
"Policy %q: mismatch in number of ingress rules.", policyName)
170+
171+
for i, clusterRule := range clusterPolicy.Spec.Ingress {
172+
expectedRule := expectedPolicy.IngressRules[i]
173+
assert.True(t, equality.Semantic.DeepEqual(expectedRule.Ports, clusterRule.Ports),
174+
"Policy %q, Ingress Rule %d: Ports mismatch.\nExpected: %+v\nGot: %+v", policyName, i, expectedRule.Ports, clusterRule.Ports)
175+
assert.True(t, equality.Semantic.DeepEqual(expectedRule.From, clusterRule.From),
176+
"Policy %q, Ingress Rule %d: From mismatch.\nExpected: %+v\nGot: %+v", policyName, i, expectedRule.From, clusterRule.From)
177+
assert.GreaterOrEqualf(t, len(expectedRule.Justification), minJustificationLength,
178+
"Policy %q, Ingress Rule %d: Justification is too short: %q", policyName, i, expectedRule.Justification)
179+
}
180+
}
181+
} else {
182+
require.Emptyf(t, clusterPolicy.Spec.Ingress, "Policy %q does not include Ingress in PolicyTypes, but has Ingress rules defined.", policyName)
183+
require.Emptyf(t, expectedPolicy.IngressRules, "Policy %q does not include Ingress in PolicyTypes, but registry expects ingress rules.", policyName)
184+
require.Emptyf(t, expectedPolicy.DenyAllIngressByTypeJustification, "Policy %q does not include Ingress in PolicyTypes, but registry has DenyAllIngressByTypeJustification.", policyName)
185+
}
186+
187+
// 4. Validate Egress Rules
188+
hasEgressPolicyType := false
189+
for _, pt := range clusterPolicy.Spec.PolicyTypes {
190+
if pt == networkingv1.PolicyTypeEgress {
191+
hasEgressPolicyType = true
192+
break
193+
}
194+
}
195+
196+
if hasEgressPolicyType {
197+
if len(clusterPolicy.Spec.Egress) == 0 {
198+
require.Emptyf(t, expectedPolicy.EgressRules,
199+
"Policy %q: cluster has no egress rules (deny all), but registry expects specific egress rules.", policyName)
200+
require.NotEmptyf(t, expectedPolicy.DenyAllEgressByTypeJustification,
201+
"Policy %q: cluster has no egress rules (deny all), but registry has no DenyAllEgressByTypeJustification.", policyName)
202+
} else {
203+
require.NotEmptyf(t, expectedPolicy.EgressRules,
204+
"Policy %q: cluster has egress rules, but registry expects no specific egress rules.", policyName)
205+
require.Emptyf(t, expectedPolicy.DenyAllEgressByTypeJustification,
206+
"Policy %q: cluster has egress rules, but registry also has DenyAllEgressByTypeJustification.", policyName)
207+
require.Lenf(t, clusterPolicy.Spec.Egress, len(expectedPolicy.EgressRules),
208+
"Policy %q: mismatch in number of egress rules.", policyName)
209+
210+
for i, clusterRule := range clusterPolicy.Spec.Egress {
211+
expectedRule := expectedPolicy.EgressRules[i]
212+
assert.True(t, equality.Semantic.DeepEqual(expectedRule.Ports, clusterRule.Ports),
213+
"Policy %q, Egress Rule %d: Ports mismatch.\nExpected: %+v\nGot: %+v", policyName, i, expectedRule.Ports, clusterRule.Ports)
214+
assert.True(t, equality.Semantic.DeepEqual(expectedRule.To, clusterRule.To),
215+
"Policy %q, Egress Rule %d: To mismatch.\nExpected: %+v\nGot: %+v", policyName, i, expectedRule.To, clusterRule.To)
216+
assert.GreaterOrEqualf(t, len(expectedRule.Justification), minJustificationLength,
217+
"Policy %q, Egress Rule %d: Justification is too short: %q", policyName, i, expectedRule.Justification)
218+
}
219+
}
220+
} else {
221+
require.Emptyf(t, clusterPolicy.Spec.Egress, "Policy %q does not include Egress in PolicyTypes, but has Egress rules defined.", policyName)
222+
require.Emptyf(t, expectedPolicy.EgressRules, "Policy %q does not include Egress in PolicyTypes, but registry expects egress rules.", policyName)
223+
require.Emptyf(t, expectedPolicy.DenyAllEgressByTypeJustification, "Policy %q does not include Egress in PolicyTypes, but registry has DenyAllEgressByTypeJustification.", policyName)
224+
}
225+
})
226+
}
227+
228+
// 5. Ensure all policies in the registry were found in the cluster
229+
assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
230+
"Mismatch between number of expected policies in registry (%d) and number of policies found & validated in cluster (%d). Missing policies from registry: %v", len(allowedNetworkPolicies), len(validatedRegistryPolicies), missingPolicies(allowedNetworkPolicies, validatedRegistryPolicies))
231+
}
232+
233+
// Helper function to create a pointer to intstr.IntOrString from an int
234+
func intOrStrPtr(port int32) *intstr.IntOrString {
235+
val := intstr.FromInt(int(port))
236+
return &val
237+
}
238+
239+
func missingPolicies(expected map[string]AllowedPolicyDefinition, actual map[string]bool) []string {
240+
missing := []string{}
241+
for k := range expected {
242+
if !actual[k] {
243+
missing = append(missing, k)
244+
}
245+
}
246+
return missing
247+
}

0 commit comments

Comments
 (0)