Skip to content

Commit 6091ffa

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

File tree

1 file changed

+251
-0
lines changed

1 file changed

+251
-0
lines changed

test/e2e/network_policy_test.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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
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 rule %d in policy %q is too short: %q", i, name, pwj.Justification)
121+
}
122+
for i, rule := range policyDef.EgressRule.Ports {
123+
assert.GreaterOrEqualf(t, len(rule.Justification), minJustificationLength,
124+
"Justification for egress rule %d in policy %q is too short: %q", i, name, rule.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+
if len(clusterPolicy.Spec.Ingress) > 0 {
161+
if len(clusterPolicy.Spec.Ingress[0].Ports) == 0 { // Cluster policy denies all ingress
162+
// Expect DenyAll justification and no specific IngressRule.Ports in our definition
163+
require.Emptyf(t, expectedPolicy.IngressRule.Ports,
164+
"Policy %q: cluster has no ingress rules (denies all), but registry IngressRule.Ports is not empty.", policyName)
165+
require.Emptyf(t, expectedPolicy.IngressRule.From,
166+
"Policy %q: cluster has no ingress rules (denies all), but registry IngressRule.From is not empty.", policyName)
167+
require.NotEmptyf(t, expectedPolicy.DenyAllIngressByTypeJustification,
168+
"Policy %q: cluster has no ingress rules (denies all), but registry has no DenyAllIngressByTypeJustification.", policyName)
169+
} else { // Cluster policy has ingress rule with ports defined
170+
clusterIngressRule := clusterPolicy.Spec.Ingress[0]
171+
expectedIngressRule := expectedPolicy.IngressRule
172+
173+
require.Emptyf(t, expectedPolicy.DenyAllIngressByTypeJustification,
174+
"Policy %q: cluster has an ingress rule, but registry also has DenyAllIngressByTypeJustification.", policyName)
175+
176+
// Compare 'From'
177+
assert.True(t, equality.Semantic.DeepEqual(expectedIngressRule.From, clusterIngressRule.From),
178+
"Policy %q, Ingress Rule: 'From' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedIngressRule.From, clusterIngressRule.From)
179+
180+
// Compare 'Ports'
181+
var allExpectedPorts []networkingv1.NetworkPolicyPort
182+
for _, pwj := range expectedIngressRule.Ports {
183+
allExpectedPorts = append(allExpectedPorts, pwj.Port...)
184+
}
185+
require.ElementsMatchf(t, allExpectedPorts, clusterIngressRule.Ports,
186+
"Policy %q, Ingress Rule: 'Ports' mismatch based on PortWithJustification aggregation.", policyName)
187+
}
188+
} else { // No Ingress policy type
189+
require.Emptyf(t, clusterPolicy.Spec.Ingress, "Policy %q does not include Ingress in PolicyTypes, but has Ingress rules defined.", policyName)
190+
require.Emptyf(t, expectedPolicy.IngressRule.Ports, "Policy %q does not include Ingress in PolicyTypes, but registry IngressRule.Ports is not empty.", policyName)
191+
require.Emptyf(t, expectedPolicy.IngressRule.From, "Policy %q does not include Ingress in PolicyTypes, but registry IngressRule.From is not empty.", policyName)
192+
require.Emptyf(t, expectedPolicy.DenyAllIngressByTypeJustification, "Policy %q does not include Ingress in PolicyTypes, but registry has DenyAllIngressByTypeJustification.", policyName)
193+
}
194+
195+
// 4. Validate Egress Rules
196+
if len(clusterPolicy.Spec.Egress) > 0 {
197+
if len(clusterPolicy.Spec.Egress[0].Ports) == 0 { // Cluster policy denies all egress
198+
require.Emptyf(t, expectedPolicy.EgressRule.Ports,
199+
"Policy %q: cluster has no egress rules (denies all), but registry EgressRule.Ports is not empty.", policyName)
200+
require.Emptyf(t, expectedPolicy.EgressRule.To,
201+
"Policy %q: cluster has no egress rules (denies all), but registry EgressRule.To is not empty.", policyName)
202+
require.NotEmptyf(t, expectedPolicy.DenyAllEgressByTypeJustification,
203+
"Policy %q: cluster has no egress rules (denies all), but registry has no DenyAllEgressByTypeJustification.", policyName)
204+
} else { // Cluster policy has an egress rule with ports defined
205+
clusterEgressRule := clusterPolicy.Spec.Egress[0]
206+
expectedEgressRule := expectedPolicy.EgressRule
207+
208+
require.Emptyf(t, expectedPolicy.DenyAllEgressByTypeJustification,
209+
"Policy %q: cluster has an egress rule, but registry also has DenyAllEgressByTypeJustification.", policyName)
210+
211+
// Compare 'To'
212+
assert.True(t, equality.Semantic.DeepEqual(expectedEgressRule.To, clusterEgressRule.To),
213+
"Policy %q, Egress Rule: 'To' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedEgressRule.To, clusterEgressRule.To)
214+
215+
// Compare 'Ports'
216+
var allExpectedPorts []networkingv1.NetworkPolicyPort
217+
for _, pwj := range expectedEgressRule.Ports {
218+
allExpectedPorts = append(allExpectedPorts, pwj.Port...)
219+
}
220+
require.ElementsMatchf(t, allExpectedPorts, clusterEgressRule.Ports,
221+
"Policy %q, Egress Rule: 'Ports' mismatch based on PortWithJustification aggregation.", policyName)
222+
}
223+
} else { // No Egress policy type
224+
require.Emptyf(t, clusterPolicy.Spec.Egress, "Policy %q does not include Egress in PolicyTypes, but has Egress rules defined.", policyName)
225+
require.Emptyf(t, expectedPolicy.EgressRule.Ports, "Policy %q does not include Egress in PolicyTypes, but registry EgressRule.Ports is not empty.", policyName)
226+
require.Emptyf(t, expectedPolicy.EgressRule.To, "Policy %q does not include Egress in PolicyTypes, but registry EgressRule.To is not empty.", policyName)
227+
require.Emptyf(t, expectedPolicy.DenyAllEgressByTypeJustification, "Policy %q does not include Egress in PolicyTypes, but registry has DenyAllEgressByTypeJustification.", policyName)
228+
}
229+
})
230+
}
231+
232+
// 5. Ensure all policies in the registry were found in the cluster
233+
assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
234+
"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))
235+
}
236+
237+
// Helper function to create a pointer to intstr.IntOrString from an int
238+
func intOrStrPtr(port int32) *intstr.IntOrString {
239+
val := intstr.FromInt(int(port))
240+
return &val
241+
}
242+
243+
func missingPolicies(expected map[string]AllowedPolicyDefinition, actual map[string]bool) []string {
244+
missing := []string{}
245+
for k := range expected {
246+
if !actual[k] {
247+
missing = append(missing, k)
248+
}
249+
}
250+
return missing
251+
}

0 commit comments

Comments
 (0)