-
Notifications
You must be signed in to change notification settings - Fork 260
tool: validator script for Azure NPM to Cilium migration #3372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
087b1c1
eb5f727
69a71be
843741b
49fa49a
079e7c0
07690ae
5b5d2a7
d6ec15e
dd25cc8
afa4e9d
7625ef5
b9231e2
2f7d338
cd15f4a
c8bd575
f2cba91
1b64afb
a2d413f
c329191
f79188b
593f29e
840feab
894da23
bbe17e6
3ee1deb
c4676cf
c72b33c
51e0d16
ede206d
5805952
1689071
f35989c
160fd47
979a8b3
eac2c66
4312ba8
c1b4d4a
c74511e
dc2af8d
6873113
246965d
dabc25e
809fc90
ee59a88
4fe7634
62df911
1140a70
118f98a
cd0c2d5
6cb46db
2c92609
e08e97e
012c963
f0f8aa7
0d80410
b203bde
ce049e0
9b68cc0
c889e1d
5e9ae22
3c60e69
9f60f68
73ff864
ed71c9a
6cec80a
70556da
9de5cb8
c244424
0295b63
3ee497b
a11ffd9
2bb7a07
92b366e
01a7b51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,397 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "fmt" | ||
| "log" | ||
| "strings" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
|
Check failure on line 10 in tools/azure-npm-to-cilium-validator.go
|
||
| v1 "k8s.io/api/core/v1" | ||
|
Check failure on line 11 in tools/azure-npm-to-cilium-validator.go
|
||
| networkingv1 "k8s.io/api/networking/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/tools/clientcmd" | ||
| ) | ||
|
|
||
| // Use this tool to validate if your cluster is ready to migrate from Azure Network Policy Manager (NPM) to Cilium. | ||
| // go run azure-npm-to-cilium-validator.go --kubeconfig ~/.kube/config | ||
|
|
||
| func main() { | ||
| // Parse the kubeconfig flag | ||
| kubeconfig := flag.String("kubeconfig", "~/.kube/config", "absolute path to the kubeconfig file") | ||
| flag.Parse() | ||
|
|
||
| // Build the Kubernetes client config | ||
| config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) | ||
| if err != nil { | ||
| log.Fatalf("Error building kubeconfig: %v", err) | ||
| } | ||
|
|
||
| // Create a Kubernetes client | ||
| clientset, err := kubernetes.NewForConfig(config) | ||
| if err != nil { | ||
| log.Fatalf("Error creating Kubernetes client: %v", err) | ||
| } | ||
|
|
||
| // Get namespaces | ||
| namespaces, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) | ||
| if err != nil { | ||
| log.Fatalf("Error getting namespaces: %v\n", err) | ||
| } | ||
|
|
||
| // Store network policies and services in maps | ||
| policiesByNamespace := make(map[string][]networkingv1.NetworkPolicy) | ||
| servicesByNamespace := make(map[string][]corev1.Service) | ||
|
|
||
| // Iterate over namespaces and store policies/services | ||
| for _, ns := range namespaces.Items { | ||
|
Check failure on line 49 in tools/azure-npm-to-cilium-validator.go
|
||
| fmt.Printf("Writing policies and services for namespace %s...\n", ns.Name) | ||
|
|
||
| // Get network policies | ||
| networkPolicies, err := clientset.NetworkingV1().NetworkPolicies(ns.Name).List(context.TODO(), metav1.ListOptions{}) | ||
| if err != nil { | ||
| fmt.Printf("Error getting network policies in namespace %s: %v\n", ns.Name, err) | ||
| continue | ||
| } | ||
| policiesByNamespace[ns.Name] = networkPolicies.Items | ||
|
|
||
| // Get services | ||
| services, err := clientset.CoreV1().Services(ns.Name).List(context.TODO(), metav1.ListOptions{}) | ||
| if err != nil { | ||
| fmt.Printf("Error getting services in namespace %s: %v\n", ns.Name, err) | ||
| continue | ||
| } | ||
| servicesByNamespace[ns.Name] = services.Items | ||
| } | ||
|
|
||
| fmt.Println("Migration Summary:") | ||
| fmt.Println("+------------------------------+-------------------------------+") | ||
| fmt.Printf("%-30s | %-30s \n", "Breaking Change", "No Impact / Safe to Migrate") | ||
| fmt.Println("+------------------------------+-------------------------------+") | ||
|
|
||
| // Check the endports of the network policies | ||
| foundEnportNetworkPolicy := checkEndportNetworkPolicies(policiesByNamespace) | ||
|
|
||
| fmt.Println("+------------------------------+-------------------------------+") | ||
|
|
||
| // Check the cidr of the network policies | ||
| foundCIDRNetworkPolicy := checkCIDRNetworkPolicies(policiesByNamespace) | ||
|
|
||
| fmt.Println("+------------------------------+-------------------------------+") | ||
|
|
||
| // Check the egress of the network policies | ||
| foundEgressPolicy := checkForEgressPolicies(policiesByNamespace) | ||
|
|
||
| fmt.Println("+------------------------------+-------------------------------+") | ||
|
|
||
| // Check services that have externalTrafficPolicy!=Local | ||
| foundServiceDispruption := checkExternalTrafficPolicyServices(namespaces, servicesByNamespace, policiesByNamespace) | ||
|
|
||
| fmt.Println("+------------------------------+-------------------------------+") | ||
| if foundEnportNetworkPolicy || foundCIDRNetworkPolicy || foundEgressPolicy || foundServiceDispruption { | ||
| fmt.Println("\033[31m✘ Review above issues before migration.\033[0m") | ||
| fmt.Println("Please see \033[32maka.ms/azurenpmtocilium\033[0m for instructions on how to evaluate/assess the above warnings marked by ❌.") | ||
| fmt.Println("NOTE: rerun this script if any modifications (create/update/delete) are made to services or policies.") | ||
| } else { | ||
| fmt.Println("\033[32m✔ Safe to migrate this cluster.\033[0m") | ||
| fmt.Println("For more details please see \033[32maka.ms/azurenpmtocilium\033[0m.") | ||
| } | ||
| } | ||
|
|
||
| func checkEndportNetworkPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool { | ||
| networkPolicyWithEndport := false | ||
| for namespace, policies := range policiesByNamespace { | ||
| for _, policy := range policies { | ||
|
Check failure on line 106 in tools/azure-npm-to-cilium-validator.go
|
||
| foundEndPort := false | ||
| for _, egress := range policy.Spec.Egress { | ||
huntergregory marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for _, port := range egress.Ports { | ||
| if port.EndPort != nil { | ||
| foundEndPort = true | ||
| if !networkPolicyWithEndport { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with endPort", "❌") | ||
| fmt.Println("Policies affected:") | ||
| networkPolicyWithEndport = true | ||
| } | ||
| fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with endPort field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace) | ||
| // Exit egress.port loop | ||
| break | ||
huntergregory marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| if foundEndPort { | ||
| // Exit egress loop | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Print no impact if no network policy has endport | ||
| if !networkPolicyWithEndport { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with endPort", "✅") | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func checkCIDRNetworkPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool { | ||
| networkPolicyWithCIDR := false | ||
| for namespace, policies := range policiesByNamespace { | ||
| for _, policy := range policies { | ||
|
Check failure on line 140 in tools/azure-npm-to-cilium-validator.go
|
||
| foundCIDRIngress := false | ||
| foundCIDREgress := false | ||
| // Check the ingress field for cidr | ||
| for _, ingress := range policy.Spec.Ingress { | ||
| for _, from := range ingress.From { | ||
| if from.IPBlock != nil { | ||
huntergregory marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if from.IPBlock.CIDR != "" { | ||
| foundCIDRIngress = true | ||
| // Print the network policy if it has an ingress cidr | ||
| if !networkPolicyWithCIDR { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "❌") | ||
| fmt.Println("Policies affected:") | ||
| networkPolicyWithCIDR = true | ||
| } | ||
| fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with ingress cidr field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace) | ||
|
|
||
| // Exit ingress.from.ipBlock loop | ||
| break | ||
| } | ||
| } | ||
| } | ||
| if foundCIDRIngress { | ||
| // Exit ingress loop | ||
| break | ||
| } | ||
| } | ||
| // Check the egress field for cidr | ||
| for _, egress := range policy.Spec.Egress { | ||
| for _, to := range egress.To { | ||
| if to.IPBlock != nil { | ||
| if to.IPBlock.CIDR != "" { | ||
| foundCIDREgress = true | ||
| // Print the network policy if it has an egress cidr | ||
| if !networkPolicyWithCIDR { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "❌") | ||
| fmt.Println("Policies affected:") | ||
| networkPolicyWithCIDR = true | ||
| } | ||
| fmt.Printf("❌ Found NetworkPolicy: \033[31m%s\033[0m with egress cidr field in namespace: \033[31m%s\033[0m\n", policy.Name, namespace) | ||
|
|
||
| // Exit egress.to.ipBlock loop | ||
| break | ||
| } | ||
| } | ||
| } | ||
| if foundCIDREgress { | ||
| // Exit egress loop | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Print no impact if no network policy has cidr | ||
| if !networkPolicyWithCIDR { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with cidr", "✅") | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func checkForEgressPolicies(policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool { | ||
| networkPolicyWithEgress := false | ||
| for namespace, policies := range policiesByNamespace { | ||
| for _, policy := range policies { | ||
|
Check failure on line 204 in tools/azure-npm-to-cilium-validator.go
|
||
| for _, egress := range policy.Spec.Egress { | ||
| // If the policy has a egress field thats not an egress allow all flag it | ||
| if len(egress.To) > 0 || len(egress.Ports) > 0 { | ||
| if !networkPolicyWithEgress { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with egress", "❌") | ||
| fmt.Printf("%-30s | %-30s \n", "(Not allow all egress)", "") | ||
| fmt.Println("Policies affected:") | ||
| networkPolicyWithEgress = true | ||
| } | ||
| 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) | ||
|
|
||
| // Exit egress loop | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if !networkPolicyWithEgress { | ||
| fmt.Printf("%-30s | %-30s \n", "NetworkPolicy with egress", "✅") | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func checkExternalTrafficPolicyServices(namespaces *corev1.NamespaceList, servicesByNamespace map[string][]corev1.Service, policiesByNamespace map[string][]networkingv1.NetworkPolicy) bool { | ||
| var servicesAtRisk, noSelectorServices, safeServices []string | ||
|
|
||
| for _, namespace := range namespaces.Items { | ||
|
Check failure on line 232 in tools/azure-npm-to-cilium-validator.go
|
||
| // Check if are there ingress policies in the namespace if not skip | ||
| if !hasIngressPolicies(policiesByNamespace[namespace.Name]) { | ||
| continue | ||
| } | ||
| serviceListAtNamespace := servicesByNamespace[namespace.Name] | ||
|
|
||
| // Check if are there services with externalTrafficPolicy=Cluster (applicable if Type=NodePort or Type=LoadBalancer) | ||
| for _, service := range serviceListAtNamespace { | ||
|
Check failure on line 240 in tools/azure-npm-to-cilium-validator.go
|
||
| if service.Spec.Type == v1.ServiceTypeLoadBalancer || service.Spec.Type == v1.ServiceTypeNodePort { | ||
| servicePorts := []string{} | ||
| // get the Port and Protocol of the service | ||
| for _, port := range service.Spec.Ports { | ||
| servicePorts = append(servicePorts, fmt.Sprintf("%d/%s", port.Port, port.Protocol)) | ||
| } | ||
| externalTrafficPolicy := service.Spec.ExternalTrafficPolicy | ||
| // If the service has externalTrafficPolicy is set to "Cluster" add it to the servicesAtRisk list (ExternalTrafficPolicy: "" defaults to Cluster) | ||
| if externalTrafficPolicy != v1.ServiceExternalTrafficPolicyTypeLocal { | ||
| // Any service with externalTrafficPolicy=Cluster is at risk so need to elimate any services that are incorrectly flagged | ||
| servicesAtRisk = append(servicesAtRisk, fmt.Sprintf("%s/%s", namespace.Name, service.Name)) | ||
| // If the service has no selector add it to the noSelectorServices list | ||
| if service.Spec.Selector == nil { | ||
| noSelectorServices = append(noSelectorServices, fmt.Sprintf("%s/%s", namespace.Name, service.Name)) | ||
| } else { | ||
| // Check if are there services with selector that match the network policy | ||
| safeServices = checkServiceRisk(service, namespace.Name, servicePorts, policiesByNamespace[namespace.Name], safeServices) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Get the services that are at risk but not in the safe services or no selector services lists | ||
| unsafeServices := difference(servicesAtRisk, safeServices, noSelectorServices) | ||
|
|
||
| // If there is no unsafe services then migration is safe for services with extranalTrafficPolicy=Cluster | ||
| if len(unsafeServices) == 0 { | ||
| fmt.Printf("%-30s | %-30s \n", "Disruption for some", "✅") | ||
| fmt.Printf("%-30s | %-30s \n", "Services with", "") | ||
| fmt.Printf("%-30s | %-30s \n", "externalTrafficPolicy=Cluster", "") | ||
| return false | ||
| } else { | ||
| fmt.Printf("%-30s | %-30s \n", "Disruption for some", "❌") | ||
| fmt.Printf("%-30s | %-30s \n", "Services with", "") | ||
| fmt.Printf("%-30s | %-30s \n", "externalTrafficPolicy=Cluster", "") | ||
| fmt.Println("Services affected:") | ||
| // If there are any no selector services or unsafe services then print them as they could be impacted by migration | ||
| if len(noSelectorServices) > 0 { | ||
| for _, service := range noSelectorServices { | ||
| serviceName := strings.Split(service, "/")[1] | ||
| serviceNamespace := strings.Split(service, "/")[0] | ||
| fmt.Printf("❌ Found Service: \033[31m%s\033[0m without selectors in namespace: \033[31m%s\033[0m\n", serviceName, serviceNamespace) | ||
| } | ||
| } | ||
| if len(unsafeServices) > 0 { | ||
| for _, service := range unsafeServices { | ||
| serviceName := strings.Split(service, "/")[1] | ||
| serviceNamespace := strings.Split(service, "/")[0] | ||
| fmt.Printf("❌ Found Service: \033[31m%s\033[0m with selectors in namespace: \033[31m%s\033[0m\n", serviceName, serviceNamespace) | ||
| } | ||
| } | ||
| fmt.Println("Manual investigation is required to evaluate if ingress is allowed to the service's backend Pods.") | ||
| fmt.Println("Please evaluate if these services would be impacted by migration.") | ||
| return true | ||
| } | ||
|
|
||
| } | ||
|
|
||
| func hasIngressPolicies(policies []networkingv1.NetworkPolicy) bool { | ||
| // Check if any policy is ingress | ||
| for _, policy := range policies { | ||
|
Check failure on line 302 in tools/azure-npm-to-cilium-validator.go
|
||
| for _, ingress := range policy.Spec.Ingress { | ||
| if len(ingress.From) > 0 { | ||
huntergregory marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return true | ||
| } | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func checkServiceRisk(service v1.Service, namespace string, servicePorts []string, policiesListAtNamespace []networkingv1.NetworkPolicy, safeServices []string) []string { | ||
| for _, policy := range policiesListAtNamespace { | ||
|
Check failure on line 313 in tools/azure-npm-to-cilium-validator.go
|
||
| for _, ingress := range policy.Spec.Ingress { | ||
| // Check if there is an allow all ingress policy that matches labels the service is safe | ||
| if len(ingress.From) == 0 && len(ingress.Ports) == 0 { | ||
| // Check if there is an allow all ingress policy with empty selectors return true as the policy allows all services in the namespace | ||
| if len(policy.Spec.PodSelector.MatchLabels) == 0 { | ||
huntergregory marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
| safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name)) | ||
| return safeServices | ||
| } | ||
| // Check if there is an allow all ingress policy that matches the service labels | ||
| if checkPolicyMatchServiceLabels(service.Spec.Selector, policy.Spec.PodSelector.MatchLabels) { | ||
| 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) | ||
rayaisaiah marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name)) | ||
| return safeServices | ||
| } | ||
| } | ||
| // Check if all the labels in | ||
| // // If there are no ingress from but there are ports in the policy; check if the service is safe | ||
| // if len(ingress.From) == 0 && len(ingress.Ports) > 0 { | ||
| // if matchAllServiceSelector(&metav1.LabelSelector{MatchLabels: service.Spec.Selector}, &policy.Spec.PodSelector) { | ||
| // matchingPorts := []string{} | ||
| // for _, port := range ingress.Ports { | ||
| // matchingPorts = append(matchingPorts, fmt.Sprintf("%d/%s", port.Port.IntVal, string(*port.Protocol))) | ||
| // } | ||
| // for _, sevicePort := range servicePorts { | ||
| // if contains(matchingPorts, sevicePort) { | ||
| // safeServices = append(safeServices, fmt.Sprintf("%s/%s", namespace, service.Name)) | ||
| // return | ||
| // } | ||
| // } | ||
| // } | ||
| // } | ||
| } | ||
| } | ||
| return safeServices | ||
| } | ||
|
|
||
| func checkPolicyMatchServiceLabels(serviceLabels, policyLabels map[string]string) bool { | ||
| // Return false if the policy has more labels than the service | ||
| if len(policyLabels) > len(serviceLabels) { | ||
| return false | ||
| } | ||
|
|
||
| // Check for each policy label that that label is present in the service labels | ||
| for policyKey, policyValue := range policyLabels { | ||
| matchedPolicyLabelToServiceLabel := false | ||
| for serviceKey, serviceValue := range serviceLabels { | ||
| if policyKey == serviceKey && policyValue == serviceValue { | ||
| matchedPolicyLabelToServiceLabel = true | ||
| break | ||
| } | ||
| } | ||
| if !matchedPolicyLabelToServiceLabel { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func contains(slice []string, item string) bool { | ||
| for _, s := range slice { | ||
| if s == item { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| func difference(slice1, slice2, slice3 []string) []string { | ||
| m := make(map[string]bool) | ||
rayaisaiah marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for _, s := range slice2 { | ||
| m[s] = true | ||
| } | ||
| for _, s := range slice3 { | ||
| m[s] = true | ||
| } | ||
| var diff []string | ||
| for _, s := range slice1 { | ||
| if !m[s] { | ||
| diff = append(diff, s) | ||
| } | ||
| } | ||
| return diff | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.