Skip to content

Commit b669eae

Browse files
committed
wip: validation script initial code outline/implementation
Signed-off-by: Hunter Gregory <[email protected]>
1 parent c670d53 commit b669eae

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# TODO: block comment describing this script/usage
2+
3+
import os
4+
from kubernetes import client, config
5+
6+
# Load Kubernetes configuration
7+
config.load_kube_config()
8+
9+
# Initialize Kubernetes API clients
10+
v1 = client.CoreV1Api()
11+
networking_v1 = client.NetworkingV1Api()
12+
13+
# Get namespaces
14+
namespaces = [ns.metadata.name for ns in v1.list_namespace().items]
15+
16+
# Store policies and services in dictionaries
17+
policies = {}
18+
# FIXME: find services with externaltrafficpolicy=Cluster instead of all services. Modify the variable name and code that touch it
19+
services = {}
20+
21+
# Iterate over namespaces and store policies/services
22+
for ns in namespaces:
23+
print(f"Writing policies and services for namespace {ns}...")
24+
policies[ns] = networking_v1.list_namespaced_network_policy(ns).to_dict()
25+
services[ns] = v1.list_namespaced_service(ns).to_dict()
26+
27+
print("Policies and services have been stored in memory.")
28+
29+
# NetworkPolicy validation for Cilium differences
30+
print("In Cilium, some kinds of NetworkPolicy behave differently. Reviewing NetworkPolicy configuration...")
31+
32+
def check_endport_policies():
33+
for ns in namespaces:
34+
endport_policies = [policy for policy in policies[ns]['items'] if any(egress.get('ports', [{}])[0].get('endPort') for egress in policy['spec'].get('egress', []))]
35+
if endport_policies:
36+
print(f"❌ Found NetworkPolicies with endPort field in namespace {ns}:")
37+
for policy in endport_policies:
38+
print(f"{ns}/{policy['metadata']['name']}")
39+
else:
40+
print(f"✅ No NetworkPolicies with endPort field found in namespace {ns}.")
41+
42+
def check_cidr_policies():
43+
for ns in namespaces:
44+
cidr_policies = [policy for policy in policies[ns]['items'] if any(egress.get('to', [{}])[0].get('ipBlock') for egress in policy['spec'].get('egress', []))]
45+
if cidr_policies:
46+
print(f"❌ Found NetworkPolicies with CIDRs in namespace {ns}:")
47+
for policy in cidr_policies:
48+
print(f"{ns}/{policy['metadata']['name']}")
49+
else:
50+
print(f"✅ No NetworkPolicies with CIDRs found in namespace {ns}.")
51+
52+
check_endport_policies()
53+
check_cidr_policies()
54+
55+
# Service validation for Cilium differences
56+
print("In Cilium, NetworkPolicy behaves differently for some kinds of Service. Reviewing Service configuration...")
57+
58+
SERVICES_AT_RISK = []
59+
NO_SELECTOR_SERVICES = []
60+
SAFE_SERVICES = []
61+
62+
# is the policy guaranteed to target any pod that could be selected by the service selector?
63+
# TODO: eval if this logic is correct/complete
64+
def match_selector(service_selector, policy_selector):
65+
service_labels = service_selector.get('matchLabels', {})
66+
policy_labels = policy_selector.get('matchLabels', {})
67+
68+
if all(item in service_labels.items() for item in policy_labels.items()):
69+
return True
70+
71+
service_expressions = service_selector.get('matchExpressions', [])
72+
policy_expressions = policy_selector.get('matchExpressions', [])
73+
74+
for expr in service_expressions:
75+
key = expr['key']
76+
operator = expr['operator']
77+
values = expr.get('values', [])
78+
79+
matching_expr = next((e for e in policy_expressions if e['key'] == key), None)
80+
if not matching_expr:
81+
return False
82+
83+
matching_operator = matching_expr['operator']
84+
matching_values = matching_expr.get('values', [])
85+
86+
if operator == "In" and not any(value in matching_values for value in values):
87+
return False
88+
if operator == "NotIn" and any(value in matching_values for value in values):
89+
return False
90+
91+
return True
92+
93+
def check_service_risk(service_selector, namespace, service_ports):
94+
policies_ns = policies[namespace]['items']
95+
96+
for policy in policies_ns:
97+
if any(not ingress.get('from') and not ingress.get('ports') for ingress in policy['spec'].get('ingress', [])):
98+
if match_selector(service_selector, policy['spec'].get('podSelector', {})):
99+
SAFE_SERVICES.append(f"{namespace}/{service}")
100+
return
101+
102+
for policy in policies_ns:
103+
if any(not ingress.get('from') and ingress.get('ports') for ingress in policy['spec'].get('ingress', [])):
104+
if match_selector(service_selector, policy['spec'].get('podSelector', {})):
105+
matching_ports = [f"{port['port']}/{port['protocol']}" for ingress in policy['spec']['ingress'] for port in ingress.get('ports', [])]
106+
if any(svc_port in matching_ports for svc_port in service_ports):
107+
SAFE_SERVICES.append(f"{namespace}/{service}")
108+
return
109+
110+
for ns in namespaces:
111+
if not any(ingress.get('from') for policy in policies[ns]['items'] for ingress in policy['spec'].get('ingress', [])):
112+
print(f"Skipping namespace {ns} as it has no ingress NetworkPolicy rules.")
113+
continue
114+
115+
services_ns = services[ns]['items']
116+
print(f"Checking NetworkPolicy targeting services with externalTrafficPolicy=Cluster in namespace {ns}...")
117+
118+
for service in services_ns:
119+
if service['spec']['type'] in ["LoadBalancer", "NodePort"]:
120+
externalTrafficPolicy = service['spec'].get('externalTrafficPolicy')
121+
service_ports = [f"{port['port']}/{port['protocol']}" for port in service['spec'].get('ports', [])]
122+
if externalTrafficPolicy != "Local":
123+
SERVICES_AT_RISK.append(f"{ns}/{service['metadata']['name']}")
124+
selector = service['spec'].get('selector')
125+
if not selector:
126+
NO_SELECTOR_SERVICES.append(f"{ns}/{service['metadata']['name']}")
127+
else:
128+
check_service_risk(selector, ns, service_ports)
129+
130+
unsafe_services = list(set(SERVICES_AT_RISK) - set(SAFE_SERVICES) - set(NO_SELECTOR_SERVICES))
131+
132+
if not SERVICES_AT_RISK:
133+
print("\033[32m✔ No issues with service ingress.\033[0m")
134+
else:
135+
if NO_SELECTOR_SERVICES:
136+
print("\nFound services without selectors which could be impacted by migration. Manual investigation is required to evaluate if ingress is allowed to the service's backend Pods. Please evaluate if these services would be impacted:")
137+
for service in NO_SELECTOR_SERVICES:
138+
print(service)
139+
if unsafe_services:
140+
print("\nFound services with selectors which could be impacted by migration. Manual investigation is required to evaluate if ingress is allowed to the service's backend Pods. Please evaluate if these services would be impacted:")
141+
for service in unsafe_services:
142+
print(service)
143+
144+
if NO_SELECTOR_SERVICES or len(SAFE_SERVICES) < len(SERVICES_AT_RISK) or any("❌" in check_endport_policies()) or any("❌" in check_cidr_policies()):
145+
print("\033[31m✘ Review above issues before migration.\033[0m")
146+
else:
147+
print("\033[32m✔ Safe to migrate this cluster.\033[0m")
148+
149+
print("Warning: This script should be rerun if services or network policies are created, deleted, or edited.")

0 commit comments

Comments
 (0)