Skip to content

Commit

Permalink
feat: find unused networkpolicies (#296)
Browse files Browse the repository at this point in the history
* feat: find unused networkpolicies

Signed-off-by: Thuan Vo <[email protected]>

* build: bump chart versions

* chore: fix remaining lint issue

* docs: update chart readme

* chore: bump appVersion to 0.4.3

* chore: address reviews on naming & typos

* chore: plural name for all command

* chore: revert import fix to metav1

* chore: use singular name for doc

* feat: show unused reason

---------

Signed-off-by: Thuan Vo <[email protected]>
  • Loading branch information
tthvo authored Jun 18, 2024
1 parent 107c885 commit a2f3629
Show file tree
Hide file tree
Showing 11 changed files with 317 additions and 3 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
- ReplicaSets
- DaemonSets
- StorageClasses
- NetworkPolicies

![Kor Screenshot](/images/screenshot.png)

Expand Down Expand Up @@ -116,6 +117,7 @@ Kor provides various subcommands to identify and list unused resources. The avai
- `replicaset` - Gets unused replicaSets for the specified namespace or all namespaces.
- `daemonset`- Gets unused DaemonSets for the specified namespace or all namespaces.
- `finalizer` - Gets unused pending deletion resources for the specified namespace or all namespaces.
- `networkpolicy` - Gets unused NetworkPolicies for the specified namespace or all namespaces.
- `exporter` - Export Prometheus metrics.
- `version` - Print kor version information.

Expand Down Expand Up @@ -175,6 +177,7 @@ kor [subcommand] --help
| ReplicaSets | replicaSets that specify replicas to 0 and has already completed it's work |
| DaemonSets | DaemonSets not scheduled on any nodes |
| StorageClasses | StorageClasses not used by any PVs/PVCs |
| NetworkPolicies | NetworkPolicies with no Pods selected |

### Deleting Unused resources

Expand Down
4 changes: 2 additions & 2 deletions charts/kor/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ apiVersion: v2
name: kor
description: A Kubernetes Helm Chart to discover orphaned resources using kor
type: application
version: 0.1.7
appVersion: "0.4.0"
version: 0.1.8
appVersion: "0.4.3"
maintainers:
- name: "yonahd"
url: "https://github.com/yonahd/kor"
2 changes: 1 addition & 1 deletion charts/kor/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# kor

![Version: 0.1.7](https://img.shields.io/badge/Version-0.1.7-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.4.0](https://img.shields.io/badge/AppVersion-0.4.0-informational?style=flat-square)
![Version: 0.1.8](https://img.shields.io/badge/Version-0.1.8-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.4.3](https://img.shields.io/badge/AppVersion-0.4.3-informational?style=flat-square)

A Kubernetes Helm Chart to discover orphaned resources using kor

Expand Down
2 changes: 2 additions & 0 deletions charts/kor/templates/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ rules:
- jobs
- replicasets
- daemonsets
- networkpolicies
verbs:
- get
- list
Expand Down Expand Up @@ -56,6 +57,7 @@ rules:
- jobs
- replicasets
- daemonsets
- networkpolicies
{{/* cluster-scoped resources */}}
- namespaces
- clusterroles
Expand Down
30 changes: 30 additions & 0 deletions cmd/kor/networkpolicies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kor

import (
"fmt"

"github.com/spf13/cobra"

"github.com/yonahd/kor/pkg/kor"
"github.com/yonahd/kor/pkg/utils"
)

var netpolCmd = &cobra.Command{
Use: "networkpolicy",
Aliases: []string{"netpol", "networkpolicies"},
Short: "Gets unused networkpolicies",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)
if response, err := kor.GetUnusedNetworkPolicies(filterOptions, clientset, outputFormat, opts); err != nil {
fmt.Println(err)
} else {
utils.PrintLogo(outputFormat)
fmt.Println(response)
}
},
}

func init() {
rootCmd.AddCommand(netpolCmd)
}
14 changes: 14 additions & 0 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,18 @@ func getUnusedStorageClasses(clientset kubernetes.Interface, filterOpts *filters
return allScDiff
}

func getUnusedNetworkPolicies(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
netpolDiff, err := processNamespaceNetworkPolicies(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "NetworkPolicies", namespace, err)
}
namespaceNetpolDiff := ResourceDiff{
"NetworkPolicy",
netpolDiff,
}
return namespaceNetpolDiff
}

func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]ResourceInfo)
for _, namespace := range filterOpts.Namespaces(clientset) {
Expand All @@ -272,6 +284,7 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In
resources[namespace]["Job"] = getUnusedJobs(clientset, namespace, filterOpts).diff
resources[namespace]["ReplicaSet"] = getUnusedReplicaSets(clientset, namespace, filterOpts).diff
resources[namespace]["DaemonSet"] = getUnusedDaemonSets(clientset, namespace, filterOpts).diff
resources[namespace]["NetworkPolicy"] = getUnusedNetworkPolicies(clientset, namespace, filterOpts).diff
case "resource":
appendResources(resources, "ConfigMap", namespace, getUnusedCMs(clientset, namespace, filterOpts).diff)
appendResources(resources, "Service", namespace, getUnusedSVCs(clientset, namespace, filterOpts).diff)
Expand All @@ -288,6 +301,7 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In
appendResources(resources, "Job", namespace, getUnusedJobs(clientset, namespace, filterOpts).diff)
appendResources(resources, "ReplicaSet", namespace, getUnusedReplicaSets(clientset, namespace, filterOpts).diff)
appendResources(resources, "DaemonSet", namespace, getUnusedDaemonSets(clientset, namespace, filterOpts).diff)
appendResources(resources, "NetworkPolicy", namespace, getUnusedNetworkPolicies(clientset, namespace, filterOpts).diff)
}
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/kor/create_test_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,16 @@ func CreateTestUnstructered(kind, apiVersion, namespace, name string) *unstructu
},
}
}

func CreateTestNetworkPolicy(name, namespace string, podSelector v1.LabelSelector, labels map[string]string) *networkingv1.NetworkPolicy {
return &networkingv1.NetworkPolicy{
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: labels,
},
Spec: networkingv1.NetworkPolicySpec{
PodSelector: podSelector,
},
}
}
7 changes: 7 additions & 0 deletions pkg/kor/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespa
"StorageClass": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.StorageV1().StorageClasses().Delete(context.TODO(), name, metav1.DeleteOptions{})
},
"NetworkPolicy": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.NetworkingV1().NetworkPolicies(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
},
}

return deleteResourceApiMap
Expand Down Expand Up @@ -165,6 +168,8 @@ func updateResource(clientset kubernetes.Interface, namespace, resourceType stri
return clientset.AppsV1().DaemonSets(namespace).Update(context.TODO(), resource.(*appsv1.DaemonSet), metav1.UpdateOptions{})
case "StorageClass":
return clientset.StorageV1().StorageClasses().Update(context.TODO(), resource.(*storagev1.StorageClass), metav1.UpdateOptions{})
case "NetworkPolicy":
return clientset.NetworkingV1().NetworkPolicies(namespace).Update(context.TODO(), resource.(*networkingv1.NetworkPolicy), metav1.UpdateOptions{})
}
return nil, fmt.Errorf("resource type '%s' is not supported", resourceType)
}
Expand Down Expand Up @@ -207,6 +212,8 @@ func getResource(clientset kubernetes.Interface, namespace, resourceType, resour
return clientset.AppsV1().DaemonSets(namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
case "StorageClass":
return clientset.StorageV1().StorageClasses().Get(context.TODO(), resourceName, metav1.GetOptions{})
case "NetworkPolicy":
return clientset.NetworkingV1().NetworkPolicies(namespace).Get(context.TODO(), resourceName, metav1.GetOptions{})
}
return nil, fmt.Errorf("resource type '%s' is not supported", resourceType)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/kor/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re
diffResult = getUnusedReplicaSets(clientset, namespace, filterOpts)
case "ds", "daemonset", "daemonsets":
diffResult = getUnusedDaemonSets(clientset, namespace, filterOpts)
case "netpol", "networkpolicy", "networkpolicies":
diffResult = getUnusedNetworkPolicies(clientset, namespace, filterOpts)
default:
fmt.Printf("resource type %q is not supported\n", resource)
}
Expand Down
100 changes: 100 additions & 0 deletions pkg/kor/networkpolicies.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package kor

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/yonahd/kor/pkg/filters"
)

func processNamespaceNetworkPolicies(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
netpolList, err := clientset.NetworkingV1().NetworkPolicies(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
}

var unusedNetpols []ResourceInfo

for _, netpol := range netpolList.Items {
if pass, _ := filter.SetObject(&netpol).Run(filterOpts); pass {
continue
}

if netpol.Labels["kor/used"] == "false" {
reason := "Marked with unused label"
unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: reason})
continue
}

// retrieve pods selected by the NetworkPolicy
labelSelector, err := metav1.LabelSelectorAsSelector(&netpol.Spec.PodSelector)
if err != nil {
return nil, err
}
podList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{
LabelSelector: labelSelector.String(),
})
if err != nil {
return nil, err
}

if len(podList.Items) == 0 {
reason := "NetworkPolicy selects no pods"
unusedNetpols = append(unusedNetpols, ResourceInfo{Name: netpol.Name, Reason: reason})
}
}

return unusedNetpols, nil
}

func GetUnusedNetworkPolicies(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
resources := make(map[string]map[string][]ResourceInfo)

for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceNetworkPolicies(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}

switch opts.GroupBy {
case "namespace":
resources[namespace] = make(map[string][]ResourceInfo)
resources[namespace]["NetworkPolicy"] = diff
case "resource":
appendResources(resources, "NetworkPolicy", namespace, diff)
}

if opts.DeleteFlag {
if diff, err := DeleteResource2(diff, clientset, namespace, "NetworkPolicy", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete NetworkPolicy %s in namespace %s: %v\n", diff, namespace, err)
}
}
}

var outputBuffer bytes.Buffer
var jsonResponse []byte

switch outputFormat {
case "table":
outputBuffer = FormatOutput(resources, opts)
case "json", "yaml":
var err error
if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
return "", err
}
}

unusedNetworkPolicies, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
if err != nil {
fmt.Printf("err: %v\n", err)
}

return unusedNetworkPolicies, nil
}
Loading

0 comments on commit a2f3629

Please sign in to comment.