Skip to content

Commit e8d954d

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

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed

test/e2e/network_policy_test.go

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

0 commit comments

Comments
 (0)