Skip to content

Commit 5496cd9

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

File tree

1 file changed

+337
-0
lines changed

1 file changed

+337
-0
lines changed

test/e2e/network_policy_test.go

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
type PortWithJustification struct {
28+
Port []networkingv1.NetworkPolicyPort
29+
Justification string
30+
}
31+
32+
// IngressRule defines a k8s IngressRule, along with a justification.
33+
type IngressRule struct {
34+
Ports []PortWithJustification
35+
From []networkingv1.NetworkPolicyPeer
36+
}
37+
38+
// EgressRule defines a k8s EgressRule, along with a justification.
39+
type EgressRule struct {
40+
Ports []PortWithJustification
41+
To []networkingv1.NetworkPolicyPeer
42+
}
43+
44+
// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy.
45+
type AllowedPolicyDefinition struct {
46+
Selector metav1.LabelSelector
47+
PolicyTypes []networkingv1.PolicyType
48+
IngressRule IngressRule
49+
EgressRule EgressRule
50+
DenyAllIngressJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty
51+
DenyAllEgressJustification string // Justification if Egress is in PolicyTypes and EgressRules is empty
52+
}
53+
54+
// Ref: https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?usp=sharing
55+
var allowedNetworkPolicies = map[string]AllowedPolicyDefinition{
56+
"catalogd-controller-manager": {
57+
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}},
58+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
59+
IngressRule: IngressRule{
60+
Ports: []PortWithJustification{
61+
{
62+
Port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(8443)}},
63+
Justification: "Allows Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health.",
64+
},
65+
{
66+
Port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(9443)}},
67+
Justification: "Permits Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources.",
68+
},
69+
{
70+
Port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(7443)}},
71+
Justification: "Enables clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery.",
72+
},
73+
},
74+
},
75+
EgressRule: EgressRule{
76+
Ports: []PortWithJustification{
77+
{
78+
Port: nil, // Empty Ports means allow all egress
79+
Justification: "Permits catalogd to fetch catalog images from arbitrary container registries and communicate with the Kubernetes API server for its operational needs.",
80+
},
81+
},
82+
},
83+
},
84+
"operator-controller-controller-manager": {
85+
Selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}},
86+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
87+
IngressRule: IngressRule{
88+
Ports: []PortWithJustification{
89+
{
90+
Port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(8443)}},
91+
Justification: "Allows Prometheus to scrape metrics from operator-controller, which is crucial for monitoring its activity, reconciliations, and overall health.",
92+
},
93+
},
94+
},
95+
EgressRule: EgressRule{
96+
Ports: []PortWithJustification{
97+
{
98+
Port: nil, // Empty Ports means allow all egress
99+
Justification: "Enables operator-controller to pull bundle images from arbitrary image registries, connect to catalogd's HTTPS server for metadata, and interact with the Kubernetes API server.",
100+
},
101+
},
102+
},
103+
},
104+
"default-deny-all-traffic": {
105+
Selector: metav1.LabelSelector{}, // Empty selector, matches all pods
106+
PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
107+
// No IngressRules means deny all ingress if PolicyTypeIngress is present
108+
// No EgressRules means deny all egress if PolicyTypeEgress is present
109+
DenyAllIngressJustification: "Denies all ingress traffic to pods selected by this policy by default, unless explicitly allowed by other policy rules, ensuring a baseline secure posture.",
110+
DenyAllEgressJustification: "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.",
111+
},
112+
}
113+
114+
func TestNetworkPolicyJustifications(t *testing.T) {
115+
ctx := context.Background()
116+
117+
// Validate justifications have min length in the allowedNetworkPolicies definition
118+
for name, policyDef := range allowedNetworkPolicies {
119+
for i, pwj := range policyDef.IngressRule.Ports {
120+
assert.GreaterOrEqualf(t, len(pwj.Justification), minJustificationLength,
121+
"Justification for ingress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.Justification)
122+
}
123+
for i, pwj := range policyDef.EgressRule.Ports { // Corrected variable name from 'rule' to 'pwj'
124+
assert.GreaterOrEqualf(t, len(pwj.Justification), minJustificationLength,
125+
"Justification for egress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.Justification)
126+
}
127+
if policyDef.DenyAllIngressJustification != "" {
128+
assert.GreaterOrEqualf(t, len(policyDef.DenyAllIngressJustification), minJustificationLength,
129+
"DenyAllIngressJustification for policy %q is too short: %q", name, policyDef.DenyAllIngressJustification)
130+
}
131+
if policyDef.DenyAllEgressJustification != "" {
132+
assert.GreaterOrEqualf(t, len(policyDef.DenyAllEgressJustification), minJustificationLength,
133+
"DenyAllEgressJustification for policy %q is too short: %q", name, policyDef.DenyAllEgressJustification)
134+
}
135+
}
136+
137+
clusterPolicies := &networkingv1.NetworkPolicyList{}
138+
err := c.List(ctx, clusterPolicies, client.InNamespace(olmSystemNamespace))
139+
require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", olmSystemNamespace)
140+
141+
validatedRegistryPolicies := make(map[string]bool)
142+
143+
for _, policy := range clusterPolicies.Items {
144+
145+
t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policy.Name, "-", "_")), func(t *testing.T) {
146+
expectedPolicy, found := allowedNetworkPolicies[policy.Name]
147+
require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policy.Name, policy.Namespace)
148+
validatedRegistryPolicies[policy.Name] = true
149+
150+
// 1. Compare PodSelector
151+
assert.True(t, equality.Semantic.DeepEqual(expectedPolicy.Selector, policy.Spec.PodSelector),
152+
"PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policy.Name, expectedPolicy.Selector, policy.Spec.PodSelector)
153+
154+
// 2. Compare PolicyTypes
155+
require.ElementsMatchf(t, expectedPolicy.PolicyTypes, policy.Spec.PolicyTypes,
156+
"PolicyTypes mismatch for policy %q.", policy.Name)
157+
158+
// 3. Validate Ingress Rules
159+
hasIngressPolicyType := false
160+
for _, pt := range policy.Spec.PolicyTypes {
161+
if pt == networkingv1.PolicyTypeIngress {
162+
hasIngressPolicyType = true
163+
break
164+
}
165+
}
166+
167+
if hasIngressPolicyType {
168+
switch len(policy.Spec.Ingress) {
169+
case 0:
170+
validateDenyAllIngress(t, policy.Name, expectedPolicy)
171+
case 1:
172+
validateSingleIngressRule(t, policy.Name, policy.Spec.Ingress[0], expectedPolicy)
173+
default:
174+
assert.Failf(t, "Policy %q in cluster has %d ingress rules. Allowed definition supports at most 1 explicit ingress rule.", policy.Name, len(policy.Spec.Ingress))
175+
}
176+
} else {
177+
validateNoIngress(t, policy.Name, policy, expectedPolicy)
178+
}
179+
180+
// 4. Validate Egress Rules
181+
hasEgressPolicyType := false
182+
for _, pt := range policy.Spec.PolicyTypes {
183+
if pt == networkingv1.PolicyTypeEgress {
184+
hasEgressPolicyType = true
185+
break
186+
}
187+
}
188+
189+
if hasEgressPolicyType {
190+
switch len(policy.Spec.Egress) {
191+
case 0:
192+
validateDenyAllEgress(t, policy.Name, expectedPolicy)
193+
case 1:
194+
validateSingleEgressRule(t, policy.Name, policy.Spec.Egress[0], expectedPolicy)
195+
default:
196+
assert.Failf(t, "Policy %q in cluster has %d egress rules. Allowed definition supports at most 1 explicit egress rule.", policy.Name, len(policy.Spec.Egress))
197+
}
198+
} else {
199+
validateNoEgress(t, policy, expectedPolicy)
200+
}
201+
})
202+
}
203+
204+
// 5. Ensure all policies in the registry were found in the cluster
205+
assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
206+
"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))
207+
}
208+
209+
// Helper function to create a pointer to intstr.IntOrString from an int
210+
func intOrStrPtr(port int32) *intstr.IntOrString {
211+
val := intstr.FromInt(int(port))
212+
return &val
213+
}
214+
215+
func missingPolicies(expected map[string]AllowedPolicyDefinition, actual map[string]bool) []string {
216+
missing := []string{}
217+
for k := range expected {
218+
if !actual[k] {
219+
missing = append(missing, k)
220+
}
221+
}
222+
return missing
223+
}
224+
225+
// validateNoEgress confirms that a policy which does not have spec.PolicyType=Egress specified
226+
// has no corresponding egress rules or expectations defined.
227+
func validateNoEgress(t *testing.T, policy networkingv1.NetworkPolicy, expectedPolicy AllowedPolicyDefinition) {
228+
// Policy is NOT expected to affect Egress traffic (no Egress in PolicyTypes)
229+
// Expected: Cluster has no egress rules; Registry has no DenyAllEgressJustification and empty EgressRule.
230+
require.Emptyf(t, policy.Spec.Egress,
231+
"Policy %q: Cluster does not have Egress PolicyType, but has Egress rules defined.", policy.Name)
232+
require.Emptyf(t, expectedPolicy.DenyAllEgressJustification,
233+
"Policy %q: Cluster does not have Egress PolicyType. Registry's DenyAllEgressJustification is not empty.", policy.Name)
234+
require.Emptyf(t, expectedPolicy.EgressRule.Ports,
235+
"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.Ports is not empty.", policy.Name)
236+
require.Emptyf(t, expectedPolicy.EgressRule.To,
237+
"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.To is not empty.", policy.Name)
238+
}
239+
240+
// validateDenyAllEgress confirms that a policy with Egress PolicyType but no explicit rules
241+
// correctly corresponds to a "deny all" expectation.
242+
func validateDenyAllEgress(t *testing.T, policyName string, expectedPolicy AllowedPolicyDefinition) {
243+
// Cluster: PolicyType Egress is present, but no explicit egress rules -> Deny All Egress by this policy.
244+
// Expected: DenyAllEgressJustification is set; EgressRule.Ports and .To are empty.
245+
require.NotEmptyf(t, expectedPolicy.DenyAllEgressJustification,
246+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's DenyAllEgressJustification is empty.", policyName)
247+
require.Emptyf(t, expectedPolicy.EgressRule.Ports,
248+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.Ports is not empty.", policyName)
249+
require.Emptyf(t, expectedPolicy.EgressRule.To,
250+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.To is not empty.", policyName)
251+
}
252+
253+
// validateSingleEgressRule validates a policy that has exactly one explicit egress rule,
254+
// distinguishing between "allow-all" and more specific rules.
255+
func validateSingleEgressRule(t *testing.T, policyName string, clusterEgressRule networkingv1.NetworkPolicyEgressRule, expectedPolicy AllowedPolicyDefinition) {
256+
// Cluster: PolicyType Egress is present, and there's one explicit egress rule.
257+
// Expected: DenyAllEgressJustification is empty; EgressRule matches the cluster's rule.
258+
expectedEgressRule := expectedPolicy.EgressRule
259+
260+
require.Emptyf(t, expectedPolicy.DenyAllEgressJustification,
261+
"Policy %q: Cluster has a specific Egress rule. Registry's DenyAllEgressJustification should be empty.", policyName)
262+
263+
isClusterRuleAllowAllPorts := len(clusterEgressRule.Ports) == 0
264+
isClusterRuleAllowAllPeers := len(clusterEgressRule.To) == 0
265+
266+
if isClusterRuleAllowAllPorts && isClusterRuleAllowAllPeers { // Handles egress: [{}] - allow all ports to all peers
267+
require.Lenf(t, expectedEgressRule.Ports, 1,
268+
"Policy %q (allow-all egress): Expected EgressRule.Ports to have 1 justification entry, got %d", policyName, len(expectedEgressRule.Ports))
269+
if len(expectedEgressRule.Ports) == 1 { // Guard against panic
270+
assert.Nilf(t, expectedEgressRule.Ports[0].Port,
271+
"Policy %q (allow-all egress): Expected EgressRule.Ports[0].Port to be nil, got %+v", policyName, expectedEgressRule.Ports[0].Port)
272+
}
273+
assert.Conditionf(t, func() bool { return len(expectedEgressRule.To) == 0 },
274+
"Policy %q (allow-all egress): Expected EgressRule.To to be empty for allow-all peers, got %+v", policyName, expectedEgressRule.To)
275+
} else {
276+
// Specific egress rule (not the simple allow-all ports and allow-all peers)
277+
assert.True(t, equality.Semantic.DeepEqual(expectedEgressRule.To, clusterEgressRule.To),
278+
"Policy %q, Egress Rule: 'To' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedEgressRule.To, clusterEgressRule.To)
279+
280+
var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
281+
for _, pwj := range expectedEgressRule.Ports {
282+
allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.Port...)
283+
}
284+
require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterEgressRule.Ports,
285+
"Policy %q, Egress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterEgressRule.Ports)
286+
}
287+
}
288+
289+
// validateNoIngress confirms that a policy which does not have the Ingress PolicyType
290+
// has no corresponding ingress rules or expectations defined.
291+
func validateNoIngress(t *testing.T, policyName string, clusterPolicy networkingv1.NetworkPolicy, expectedPolicy AllowedPolicyDefinition) {
292+
// Policy is NOT expected to affect Ingress traffic (no Ingress in PolicyTypes)
293+
// Expected: Cluster has no ingress rules; Registry has no DenyAllIngressJustification and empty IngressRule.
294+
require.Emptyf(t, clusterPolicy.Spec.Ingress,
295+
"Policy %q: Cluster does not have Ingress PolicyType, but has Ingress rules defined.", policyName)
296+
require.Emptyf(t, expectedPolicy.DenyAllIngressJustification,
297+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's DenyAllIngressJustification is not empty.", policyName)
298+
require.Emptyf(t, expectedPolicy.IngressRule.Ports,
299+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.Ports is not empty.", policyName)
300+
require.Emptyf(t, expectedPolicy.IngressRule.From,
301+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.From is not empty.", policyName)
302+
}
303+
304+
// validateDenyAllIngress confirms that a policy with Ingress PolicyType but no explicit rules
305+
// correctly corresponds to a "deny all" expectation.
306+
func validateDenyAllIngress(t *testing.T, policyName string, expectedPolicy AllowedPolicyDefinition) {
307+
// Cluster: PolicyType Ingress is present, but no explicit ingress rules -> Deny All Ingress by this policy.
308+
// Expected: DenyAllIngressJustification is set; IngressRule.Ports and .From are empty.
309+
require.NotEmptyf(t, expectedPolicy.DenyAllIngressJustification,
310+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's DenyAllIngressJustification is empty.", policyName)
311+
require.Emptyf(t, expectedPolicy.IngressRule.Ports,
312+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.Ports is not empty.", policyName)
313+
require.Emptyf(t, expectedPolicy.IngressRule.From,
314+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.From is not empty.", policyName)
315+
}
316+
317+
// validateSingleIngressRule validates a policy that has exactly one explicit ingress rule.
318+
func validateSingleIngressRule(t *testing.T, policyName string, clusterIngressRule networkingv1.NetworkPolicyIngressRule, expectedPolicy AllowedPolicyDefinition) {
319+
// Cluster: PolicyType Ingress is present, and there's one explicit ingress rule.
320+
// Expected: DenyAllIngressJustification is empty; IngressRule matches the cluster's rule.
321+
expectedIngressRule := expectedPolicy.IngressRule
322+
323+
require.Emptyf(t, expectedPolicy.DenyAllIngressJustification,
324+
"Policy %q: Cluster has a specific Ingress rule. Registry's DenyAllIngressJustification should be empty.", policyName)
325+
326+
// Compare 'From'
327+
assert.True(t, equality.Semantic.DeepEqual(expectedIngressRule.From, clusterIngressRule.From),
328+
"Policy %q, Ingress Rule: 'From' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedIngressRule.From, clusterIngressRule.From)
329+
330+
// Compare 'Ports' by aggregating the ports from our justified structure
331+
var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
332+
for _, pwj := range expectedIngressRule.Ports {
333+
allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.Port...)
334+
}
335+
require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterIngressRule.Ports,
336+
"Policy %q, Ingress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterIngressRule.Ports)
337+
}

0 commit comments

Comments
 (0)