Skip to content

Commit 087b1c1

Browse files
committed
npm to cilium validator script
testing not done update added new check still debugging service check fixed logic on services with allow all ingress polcies added checks for allow all ingress policies added checks for services with allow all policys with empty and label selectors
1 parent c670d53 commit 087b1c1

File tree

1 file changed

+397
-0
lines changed

1 file changed

+397
-0
lines changed
Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"strings"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
v1 "k8s.io/api/core/v1"
12+
networkingv1 "k8s.io/api/networking/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/client-go/kubernetes"
15+
"k8s.io/client-go/tools/clientcmd"
16+
)
17+
18+
// Use this tool to validate if your cluster is ready to migrate from Azure Network Policy Manager (NPM) to Cilium.
19+
// go run azure-npm-to-cilium-validator.go --kubeconfig ~/.kube/config
20+
21+
func main() {
22+
// Parse the kubeconfig flag
23+
kubeconfig := flag.String("kubeconfig", "~/.kube/config", "absolute path to the kubeconfig file")
24+
flag.Parse()
25+
26+
// Build the Kubernetes client config
27+
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
28+
if err != nil {
29+
log.Fatalf("Error building kubeconfig: %v", err)
30+
}
31+
32+
// Create a Kubernetes client
33+
clientset, err := kubernetes.NewForConfig(config)
34+
if err != nil {
35+
log.Fatalf("Error creating Kubernetes client: %v", err)
36+
}
37+
38+
// Get namespaces
39+
namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
40+
if err != nil {
41+
log.Fatalf("Error getting namespaces: %v\n", err)
42+
}
43+
44+
// Store network policies and services in maps
45+
policiesByNamespace := make(map[string][]networkingv1.NetworkPolicy)
46+
servicesByNamespace := make(map[string][]corev1.Service)
47+
48+
// Iterate over namespaces and store policies/services
49+
for _, ns := range namespaces.Items {
50+
fmt.Printf("Writing policies and services for namespace %s...\n", ns.Name)
51+
52+
// Get network policies
53+
networkPolicies, err := clientset.NetworkingV1().NetworkPolicies(ns.Name).List(context.TODO(), metav1.ListOptions{})
54+
if err != nil {
55+
fmt.Printf("Error getting network policies in namespace %s: %v\n", ns.Name, err)
56+
continue
57+
}
58+
policiesByNamespace[ns.Name] = networkPolicies.Items
59+
60+
// Get services
61+
services, err := clientset.CoreV1().Services(ns.Name).List(context.TODO(), metav1.ListOptions{})
62+
if err != nil {
63+
fmt.Printf("Error getting services in namespace %s: %v\n", ns.Name, err)
64+
continue
65+
}
66+
servicesByNamespace[ns.Name] = services.Items
67+
}
68+
69+
fmt.Println("Migration Summary:")
70+
fmt.Println("+------------------------------+-------------------------------+")
71+
fmt.Printf("%-30s | %-30s \n", "Breaking Change", "No Impact / Safe to Migrate")
72+
fmt.Println("+------------------------------+-------------------------------+")
73+
74+
// Check the endports of the network policies
75+
foundEnportNetworkPolicy := checkEndportNetworkPolicies(policiesByNamespace)
76+
77+
fmt.Println("+------------------------------+-------------------------------+")
78+
79+
// Check the cidr of the network policies
80+
foundCIDRNetworkPolicy := checkCIDRNetworkPolicies(policiesByNamespace)
81+
82+
fmt.Println("+------------------------------+-------------------------------+")
83+
84+
// Check the egress of the network policies
85+
foundEgressPolicy := checkForEgressPolicies(policiesByNamespace)
86+
87+
fmt.Println("+------------------------------+-------------------------------+")
88+
89+
// Check services that have externalTrafficPolicy!=Local
90+
foundServiceDispruption := checkExternalTrafficPolicyServices(namespaces, servicesByNamespace, policiesByNamespace)
91+
92+
fmt.Println("+------------------------------+-------------------------------+")
93+
if foundEnportNetworkPolicy || foundCIDRNetworkPolicy || foundEgressPolicy || foundServiceDispruption {
94+
fmt.Println("\033[31m✘ Review above issues before migration.\033[0m")
95+
fmt.Println("Please see \033[32maka.ms/azurenpmtocilium\033[0m for instructions on how to evaluate/assess the above warnings marked by ❌.")
96+
fmt.Println("NOTE: rerun this script if any modifications (create/update/delete) are made to services or policies.")
97+
} else {
98+
fmt.Println("\033[32m✔ Safe to migrate this cluster.\033[0m")
99+
fmt.Println("For more details please see \033[32maka.ms/azurenpmtocilium\033[0m.")
100+
}
101+
}
102+
103+
func checkEndportNetworkPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool {
104+
networkPolicyWithEndport := false
105+
for namespace, policies := range policiesByNamespace {
106+
for _, policy := range policies {
107+
foundEndPort := false
108+
for _, egress := range policy.Spec.Egress {
109+
for _, port := range egress.Ports {
110+
if port.EndPort != nil {
111+
foundEndPort = true
112+
if !networkPolicyWithEndport {
113+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with endPort", "❌")
114+
fmt.Println("Policies affected:")
115+
networkPolicyWithEndport = true
116+
}
117+
fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with endPort field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace)
118+
// Exit egress.port loop
119+
break
120+
}
121+
}
122+
if foundEndPort {
123+
// Exit egress loop
124+
break
125+
}
126+
}
127+
}
128+
}
129+
// Print no impact if no network policy has endport
130+
if !networkPolicyWithEndport {
131+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with endPort", "✅")
132+
return false
133+
}
134+
return true
135+
}
136+
137+
func checkCIDRNetworkPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool {
138+
networkPolicyWithCIDR := false
139+
for namespace, policies := range policiesByNamespace {
140+
for _, policy := range policies {
141+
foundCIDRIngress := false
142+
foundCIDREgress := false
143+
// Check the ingress field for cidr
144+
for _, ingress := range policy.Spec.Ingress {
145+
for _, from := range ingress.From {
146+
if from.IPBlock != nil {
147+
if from.IPBlock.CIDR != "" {
148+
foundCIDRIngress = true
149+
// Print the network policy if it has an ingress cidr
150+
if !networkPolicyWithCIDR {
151+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "❌")
152+
fmt.Println("Policies affected:")
153+
networkPolicyWithCIDR = true
154+
}
155+
fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with ingress cidr field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace)
156+
157+
// Exit ingress.from.ipBlock loop
158+
break
159+
}
160+
}
161+
}
162+
if foundCIDRIngress {
163+
// Exit ingress loop
164+
break
165+
}
166+
}
167+
// Check the egress field for cidr
168+
for _, egress := range policy.Spec.Egress {
169+
for _, to := range egress.To {
170+
if to.IPBlock != nil {
171+
if to.IPBlock.CIDR != "" {
172+
foundCIDREgress = true
173+
// Print the network policy if it has an egress cidr
174+
if !networkPolicyWithCIDR {
175+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "❌")
176+
fmt.Println("Policies affected:")
177+
networkPolicyWithCIDR = true
178+
}
179+
fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with egress cidr field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace)
180+
181+
// Exit egress.to.ipBlock loop
182+
break
183+
}
184+
}
185+
}
186+
if foundCIDREgress {
187+
// Exit egress loop
188+
break
189+
}
190+
}
191+
}
192+
}
193+
// Print no impact if no network policy has cidr
194+
if !networkPolicyWithCIDR {
195+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "✅")
196+
return false
197+
}
198+
return true
199+
}
200+
201+
func checkForEgressPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool {
202+
networkPolicyWithEgress := false
203+
for namespace, policies := range policiesByNamespace {
204+
for _, policy := range policies {
205+
for _, egress := range policy.Spec.Egress {
206+
// If the policy has a egress field thats not an egress allow all flag it
207+
if len(egress.To) > 0 || len(egress.Ports) > 0 {
208+
if !networkPolicyWithEgress {
209+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with egress", "❌")
210+
fmt.Printf("%-30s | %-30s \n", "(Not allow all egress)", "")
211+
fmt.Println("Policies affected:")
212+
networkPolicyWithEgress = true
213+
}
214+
fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with egress field (non-allow all) in namespace: \033[31m%s\033[0m\n", policy.Name, namespace)
215+
216+
// Exit egress loop
217+
break
218+
}
219+
}
220+
}
221+
}
222+
if !networkPolicyWithEgress {
223+
fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with egress", "✅")
224+
return false
225+
}
226+
return true
227+
}
228+
229+
func checkExternalTrafficPolicyServices(namespaces *corev1.NamespaceList, servicesByNamespace map[string][]corev1.Service, policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool {
230+
var servicesAtRisk, noSelectorServices, safeServices []string
231+
232+
for _, namespace := range namespaces.Items {
233+
// Check if are there ingress policies in the namespace if not skip
234+
if !hasIngressPolicies(policiesByNamespace[namespace.Name]) {
235+
continue
236+
}
237+
serviceListAtNamespace := servicesByNamespace[namespace.Name]
238+
239+
// Check if are there services with externalTrafficPolicy=Cluster (applicable if Type=NodePort or Type=LoadBalancer)
240+
for _, service := range serviceListAtNamespace {
241+
if service.Spec.Type == v1.ServiceTypeLoadBalancer || service.Spec.Type == v1.ServiceTypeNodePort {
242+
servicePorts := []string{}
243+
// get the Port and Protocol of the service
244+
for _, port := range service.Spec.Ports {
245+
servicePorts = append(servicePorts, fmt.Sprintf("%d/%s", port.Port, port.Protocol))
246+
}
247+
externalTrafficPolicy := service.Spec.ExternalTrafficPolicy
248+
// If the service has externalTrafficPolicy is set to "Cluster" add it to the servicesAtRisk list (ExternalTrafficPolicy: "" defaults to Cluster)
249+
if externalTrafficPolicy != v1.ServiceExternalTrafficPolicyTypeLocal {
250+
// Any service with externalTrafficPolicy=Cluster is at risk so need to elimate any services that are incorrectly flagged
251+
servicesAtRisk = append(servicesAtRisk, fmt.Sprintf("%s/%s", namespace.Name, service.Name))
252+
// If the service has no selector add it to the noSelectorServices list
253+
if service.Spec.Selector == nil {
254+
noSelectorServices = append(noSelectorServices, fmt.Sprintf("%s/%s", namespace.Name, service.Name))
255+
} else {
256+
// Check if are there services with selector that match the network policy
257+
safeServices = checkServiceRisk(service, namespace.Name, servicePorts, policiesByNamespace[namespace.Name], safeServices)
258+
}
259+
}
260+
}
261+
}
262+
}
263+
264+
// Get the services that are at risk but not in the safe services or no selector services lists
265+
unsafeServices := difference(servicesAtRisk, safeServices, noSelectorServices)
266+
267+
// If there is no unsafe services then migration is safe for services with extranalTrafficPolicy=Cluster
268+
if len(unsafeServices) == 0 {
269+
fmt.Printf("%-30s | %-30s \n", "Disruption for some", "✅")
270+
fmt.Printf("%-30s | %-30s \n", "Services with", "")
271+
fmt.Printf("%-30s | %-30s \n", "externalTrafficPolicy=Cluster", "")
272+
return false
273+
} else {
274+
fmt.Printf("%-30s | %-30s \n", "Disruption for some", "❌")
275+
fmt.Printf("%-30s | %-30s \n", "Services with", "")
276+
fmt.Printf("%-30s | %-30s \n", "externalTrafficPolicy=Cluster", "")
277+
fmt.Println("Services affected:")
278+
// If there are any no selector services or unsafe services then print them as they could be impacted by migration
279+
if len(noSelectorServices) > 0 {
280+
for _, service := range noSelectorServices {
281+
serviceName := strings.Split(service, "/")[1]
282+
serviceNamespace := strings.Split(service, "/")[0]
283+
fmt.Printf("❌ Found Service: \033[31m%s\033[0m without selectors in namespace: \033[31m%s\033[0m\n", serviceName, serviceNamespace)
284+
}
285+
}
286+
if len(unsafeServices) > 0 {
287+
for _, service := range unsafeServices {
288+
serviceName := strings.Split(service, "/")[1]
289+
serviceNamespace := strings.Split(service, "/")[0]
290+
fmt.Printf("❌ Found Service: \033[31m%s\033[0m with selectors in namespace: \033[31m%s\033[0m\n", serviceName, serviceNamespace)
291+
}
292+
}
293+
fmt.Println("Manual investigation is required to evaluate if ingress is allowed to the service's backend Pods.")
294+
fmt.Println("Please evaluate if these services would be impacted by migration.")
295+
return true
296+
}
297+
298+
}
299+
300+
func hasIngressPolicies(policies []networkingv1.NetworkPolicy) bool {
301+
// Check if any policy is ingress
302+
for _, policy := range policies {
303+
for _, ingress := range policy.Spec.Ingress {
304+
if len(ingress.From) > 0 {
305+
return true
306+
}
307+
}
308+
}
309+
return false
310+
}
311+
312+
func checkServiceRisk(service v1.Service, namespace string, servicePorts []string, policiesListAtNamespace []networkingv1.NetworkPolicy, safeServices []string) []string {
313+
for _, policy := range policiesListAtNamespace {
314+
for _, ingress := range policy.Spec.Ingress {
315+
// Check if there is an allow all ingress policy that matches labels the service is safe
316+
if len(ingress.From) == 0 && len(ingress.Ports) == 0 {
317+
// Check if there is an allow all ingress policy with empty selectors return true as the policy allows all services in the namespace
318+
if len(policy.Spec.PodSelector.MatchLabels) == 0 {
319+
fmt.Printf("found an allow all ingress policy: %s with empty selectors so service %s in the namespace %s is safe\n", policy.Name, service.Name, namespace)
320+
safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name))
321+
return safeServices
322+
}
323+
// Check if there is an allow all ingress policy that matches the service labels
324+
if checkPolicyMatchServiceLabels(service.Spec.Selector, policy.Spec.PodSelector.MatchLabels) {
325+
fmt.Printf("found an allow all ingress policy: %s with matching selectors so service %s in the namespace %s is safe\n", policy.Name, service.Name, namespace)
326+
safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name))
327+
return safeServices
328+
}
329+
}
330+
// Check if all the labels in
331+
// // If there are no ingress from but there are ports in the policy; check if the service is safe
332+
// if len(ingress.From) == 0 && len(ingress.Ports) > 0 {
333+
// if matchAllServiceSelector(&metav1.LabelSelector{MatchLabels: service.Spec.Selector}, &policy.Spec.PodSelector) {
334+
// matchingPorts := []string{}
335+
// for _, port := range ingress.Ports {
336+
// matchingPorts = append(matchingPorts, fmt.Sprintf("%d/%s", port.Port.IntVal, string(*port.Protocol)))
337+
// }
338+
// for _, sevicePort := range servicePorts {
339+
// if contains(matchingPorts, sevicePort) {
340+
// safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name))
341+
// return
342+
// }
343+
// }
344+
// }
345+
// }
346+
}
347+
}
348+
return safeServices
349+
}
350+
351+
func checkPolicyMatchServiceLabels(serviceLabels, policyLabels map[string]string) bool {
352+
// Return false if the policy has more labels than the service
353+
if len(policyLabels) > len(serviceLabels) {
354+
return false
355+
}
356+
357+
// Check for each policy label that that label is present in the service labels
358+
for policyKey, policyValue := range policyLabels {
359+
matchedPolicyLabelToServiceLabel := false
360+
for serviceKey, serviceValue := range serviceLabels {
361+
if policyKey == serviceKey && policyValue == serviceValue {
362+
matchedPolicyLabelToServiceLabel = true
363+
break
364+
}
365+
}
366+
if !matchedPolicyLabelToServiceLabel {
367+
return false
368+
}
369+
}
370+
return true
371+
}
372+
373+
func contains(slice []string, item string) bool {
374+
for _, s := range slice {
375+
if s == item {
376+
return true
377+
}
378+
}
379+
return false
380+
}
381+
382+
func difference(slice1, slice2, slice3 []string) []string {
383+
m := make(map[string]bool)
384+
for _, s := range slice2 {
385+
m[s] = true
386+
}
387+
for _, s := range slice3 {
388+
m[s] = true
389+
}
390+
var diff []string
391+
for _, s := range slice1 {
392+
if !m[s] {
393+
diff = append(diff, s)
394+
}
395+
}
396+
return diff
397+
}

0 commit comments

Comments
 (0)