Skip to content

Commit ecc156b

Browse files
anik120Per Goncalves da Silva
authored and
Per Goncalves da Silva
committed
OPRUN-3873: Add e2e tests for NetworkPolicies
1 parent efc6657 commit ecc156b

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+
"github.com/operator-framework/operator-controller/test/utils"
20+
)
21+
22+
const (
23+
minJustificationLength = 40
24+
catalogdManagerSelector = "control-plane=catalogd-controller-manager"
25+
operatorManagerSelector = "control-plane=operator-controller-controller-manager"
26+
catalogdMetricsPort = 7443
27+
catalogdWebhookPort = 9443
28+
catalogServerPort = 8443
29+
operatorControllerMetricsPort = 8443
30+
)
31+
32+
type portWithJustification struct {
33+
port []networkingv1.NetworkPolicyPort
34+
justification string
35+
}
36+
37+
// ingressRule defines a k8s IngressRule, along with a justification.
38+
type ingressRule struct {
39+
ports []portWithJustification
40+
from []networkingv1.NetworkPolicyPeer
41+
}
42+
43+
// egressRule defines a k8s egressRule, along with a justification.
44+
type egressRule struct {
45+
ports []portWithJustification
46+
to []networkingv1.NetworkPolicyPeer
47+
}
48+
49+
// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy.
50+
type allowedPolicyDefinition struct {
51+
selector metav1.LabelSelector
52+
policyTypes []networkingv1.PolicyType
53+
ingressRule ingressRule
54+
egressRule egressRule
55+
denyAllIngressJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty
56+
denyAllEgressJustification string // Justification if Egress is in PolicyTypes and EgressRules is empty
57+
}
58+
59+
// Ref: https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?usp=sharing
60+
var allowedNetworkPolicies = map[string]allowedPolicyDefinition{
61+
"catalogd-controller-manager": {
62+
selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}},
63+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
64+
ingressRule: ingressRule{
65+
ports: []portWithJustification{
66+
{
67+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdMetricsPort}}},
68+
justification: "Allows Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health.",
69+
},
70+
{
71+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogdWebhookPort}}},
72+
justification: "Permits Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources.",
73+
},
74+
{
75+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: catalogServerPort}}},
76+
justification: "Enables clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery.",
77+
},
78+
},
79+
},
80+
egressRule: egressRule{
81+
ports: []portWithJustification{
82+
{
83+
port: nil, // Empty Ports means allow all egress
84+
justification: "Permits catalogd to fetch catalog images from arbitrary container registries and communicate with the Kubernetes API server for its operational needs.",
85+
},
86+
},
87+
},
88+
},
89+
"operator-controller-controller-manager": {
90+
selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}},
91+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
92+
ingressRule: ingressRule{
93+
ports: []portWithJustification{
94+
{
95+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: &intstr.IntOrString{Type: intstr.Int, IntVal: operatorControllerMetricsPort}}},
96+
justification: "Allows Prometheus to scrape metrics from operator-controller, which is crucial for monitoring its activity, reconciliations, and overall health.",
97+
},
98+
},
99+
},
100+
egressRule: egressRule{
101+
ports: []portWithJustification{
102+
{
103+
port: nil, // Empty Ports means allow all egress
104+
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.",
105+
},
106+
},
107+
},
108+
},
109+
"default-deny-all-traffic": {
110+
selector: metav1.LabelSelector{}, // Empty selector, matches all pods
111+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
112+
// No IngressRules means deny all ingress if PolicyTypeIngress is present
113+
// No EgressRules means deny all egress if PolicyTypeEgress is present
114+
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.",
115+
denyAllEgressJustification: "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.",
116+
},
117+
}
118+
119+
func TestNetworkPolicyJustifications(t *testing.T) {
120+
ctx := context.Background()
121+
122+
// Validate justifications have min length in the allowedNetworkPolicies definition
123+
for name, policyDef := range allowedNetworkPolicies {
124+
for i, pwj := range policyDef.ingressRule.ports {
125+
assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
126+
"Justification for ingress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
127+
}
128+
for i, pwj := range policyDef.egressRule.ports { // Corrected variable name from 'rule' to 'pwj'
129+
assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
130+
"Justification for egress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
131+
}
132+
if policyDef.denyAllIngressJustification != "" {
133+
assert.GreaterOrEqualf(t, len(policyDef.denyAllIngressJustification), minJustificationLength,
134+
"DenyAllIngressJustification for policy %q is too short: %q", name, policyDef.denyAllIngressJustification)
135+
}
136+
if policyDef.denyAllEgressJustification != "" {
137+
assert.GreaterOrEqualf(t, len(policyDef.denyAllEgressJustification), minJustificationLength,
138+
"DenyAllEgressJustification for policy %q is too short: %q", name, policyDef.denyAllEgressJustification)
139+
}
140+
}
141+
142+
clientForComponent := utils.FindK8sClient(t)
143+
componentNamespace := getComponentNamespace(t, clientForComponent, operatorManagerSelector)
144+
clusterPolicies := &networkingv1.NetworkPolicyList{}
145+
err := c.List(ctx, clusterPolicies, client.InNamespace(componentNamespace))
146+
require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", componentNamespace)
147+
148+
validatedRegistryPolicies := make(map[string]bool)
149+
150+
for _, policy := range clusterPolicies.Items {
151+
t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policy.Name, "-", "_")), func(t *testing.T) {
152+
expectedPolicy, found := allowedNetworkPolicies[policy.Name]
153+
require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policy.Name, policy.Namespace)
154+
validatedRegistryPolicies[policy.Name] = true
155+
156+
// 1. Compare PodSelector
157+
assert.True(t, equality.Semantic.DeepEqual(expectedPolicy.selector, policy.Spec.PodSelector),
158+
"PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policy.Name, expectedPolicy.selector, policy.Spec.PodSelector)
159+
160+
// 2. Compare PolicyTypes
161+
require.ElementsMatchf(t, expectedPolicy.policyTypes, policy.Spec.PolicyTypes,
162+
"PolicyTypes mismatch for policy %q.", policy.Name)
163+
164+
// 3. Validate Ingress Rules
165+
hasIngressPolicyType := false
166+
for _, pt := range policy.Spec.PolicyTypes {
167+
if pt == networkingv1.PolicyTypeIngress {
168+
hasIngressPolicyType = true
169+
break
170+
}
171+
}
172+
173+
if hasIngressPolicyType {
174+
switch len(policy.Spec.Ingress) {
175+
case 0:
176+
validateDenyAllIngress(t, policy.Name, expectedPolicy)
177+
case 1:
178+
validateSingleIngressRule(t, policy.Name, policy.Spec.Ingress[0], expectedPolicy)
179+
default:
180+
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))
181+
}
182+
} else {
183+
validateNoIngress(t, policy.Name, policy, expectedPolicy)
184+
}
185+
186+
// 4. Validate Egress Rules
187+
hasEgressPolicyType := false
188+
for _, pt := range policy.Spec.PolicyTypes {
189+
if pt == networkingv1.PolicyTypeEgress {
190+
hasEgressPolicyType = true
191+
break
192+
}
193+
}
194+
195+
if hasEgressPolicyType {
196+
switch len(policy.Spec.Egress) {
197+
case 0:
198+
validateDenyAllEgress(t, policy.Name, expectedPolicy)
199+
case 1:
200+
validateSingleEgressRule(t, policy.Name, policy.Spec.Egress[0], expectedPolicy)
201+
default:
202+
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))
203+
}
204+
} else {
205+
validateNoEgress(t, policy, expectedPolicy)
206+
}
207+
})
208+
}
209+
210+
// 5. Ensure all policies in the registry were found in the cluster
211+
assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
212+
"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))
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)