Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
.vscode
*.iml
.idea/
.vscode/
dist/**
main.exe
coverage.txt
coverage.*
build/
kor
!kor/
*.swp
main
hack/exceptions
.envrc
.tool-versions
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,21 @@ EXCEPTIONS_FILE_PATTERN := *.json
build:
go build -o build/kor main.go

clean:
rm -fr build coverage.txt coverage.html

lint:
golangci-lint run

lint-fix:
golangci-lint run --fix

test:
go test -race -coverprofile=coverage.txt -shuffle on ./...
go test -race -coverprofile=coverage.txt -shuffle on ./pkg/...

cover: test
go tool cover -func=coverage.txt
go tool cover -o coverage.html -html=coverage.txt

sort-exception-files:
@echo "Sorting exception files..."
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
- RoleBindings
- VolumeAttachments
- PriorityClasses
- Namespaces

> **Looking for cost analysis and multi-cluster management?** Check out [KorPro](#korpro), our cloud-based platform built on top of Kor.

Expand Down Expand Up @@ -144,6 +145,7 @@ Kor provides various subcommands to identify and list unused resources. The avai
- `priorityclass` - Gets unused PriorityClasses in the cluster (non-namespaced resource).
- `finalizer` - Gets unused pending deletion resources for the specified namespace or all namespaces.
- `networkpolicy` - Gets unused NetworkPolicies for the specified namespace or all namespaces.
- `namespace` - Gets unused Namespaces for the specified namespace or all namespaces.
- `exporter` - Export Prometheus metrics.
- `version` - Print kor version information.

Expand Down Expand Up @@ -195,6 +197,7 @@ kor [subcommand] --help
| HPAs | HPAs not used in Deployments<br/> HPAs not used in StatefulSets | |
| Ingresses | Ingresses not pointing at any Service | |
| Jobs | Jobs status is completed<br/> Jobs status is suspended<br/> Jobs failed with backoff limit exceeded (including indexed jobs) <br/> Jobs failed with dedaline exceeded | |
| Namespaces | Only empty namespaces |
| NetworkPolicies | NetworkPolicies with no Pods selected by podSelector or Ingress / Egress rules |
| PDBs | PDBs not used in Deployments / StatefulSets (templates) or in arbitrary Pods<br/>PDBs with empty selectors (match every pod) but no running pods in namespace | |
| Pods | Pods in `Failed` phase with reason `Evicted` (i.e., evicted pods)<br/> Pods in Crashloopbackoff | |
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

var allCmd = &cobra.Command{
Use: "all",
Short: "Gets unused resources",
Short: "Gets unused namespaced resources",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)
Expand Down
42 changes: 42 additions & 0 deletions cmd/kor/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package kor

import (
"context"
"fmt"

"github.com/spf13/cobra"

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

var namespaceCmd = &cobra.Command{
Use: "namespace",
Aliases: []string{"ns", "namespaces"},
Short: "Gets unused namespaces",
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
clientset := kor.GetKubeClient(kubeconfig)
dynamicClient := kor.GetDynamicClient(kubeconfig)
if response, err := kor.GetUnusedNamespaces(ctx, filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil {
fmt.Println(err)
} else {
utils.PrintLogo(outputFormat, opts.ClusterName)
fmt.Println(response)
}
},
}

func init() {
namespaceCmd.PersistentFlags().StringSliceVarP(
&filterOptions.IgnoreResourceTypes,
"ignore-resource-types",
"i",
filterOptions.IgnoreResourceTypes,
"Child resource type selector to filter out from namespace emptiness evaluation,"+
" example: --ignore-resource-types secrets,configmaps."+
" Types should be specified in a format printed out in NAME column by 'kubectl api-resources --namespaced=true'.",
)
rootCmd.AddCommand(namespaceCmd)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
k8s.io/apiextensions-apiserver v0.36.2
k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
sigs.k8s.io/yaml v1.6.0
)

Expand Down Expand Up @@ -66,7 +67,6 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions pkg/filters/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type Options struct {
IncludeNamespaces []string
// IgnoreOwnerReferences skips any resource that has ownerReferences set (for all resource types)
IgnoreOwnerReferences bool
// IgnoreResourceTypes is a namespace selector to exclude specified resource type evaluation, only applicable to namespaces
IgnoreResourceTypes []string

namespace []string
once sync.Once
Expand Down
11 changes: 6 additions & 5 deletions pkg/kor/create_test_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

var testNamespace = "test-namespace"
var AppLabels = map[string]string{}
var UsedLabels = map[string]string{"kor/used": "true"}
var UnusedLabels = map[string]string{"kor/used": "false"}
var (
testNamespace = "test-namespace"
AppLabels = map[string]string{}
UsedLabels = map[string]string{"kor/used": "true"}
UnusedLabels = map[string]string{"kor/used": "false"}
)

func CreateTestDeployment(namespace, name string, replicas int32, labels map[string]string) *appsv1.Deployment {
return &appsv1.Deployment{
Expand Down Expand Up @@ -98,7 +100,6 @@ func CreateTestVolume(name, pvcName string) *corev1.Volume {
PersistentVolumeClaim: pvc,
},
}

}

func CreateEphemeralVolumeDefinition(name, size string) *corev1.Volume {
Expand Down
23 changes: 17 additions & 6 deletions pkg/kor/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespace, name string) error {
var deleteResourceApiMap = map[string]func(clientset kubernetes.Interface, namespace, name string) error{
deleteResourceApiMap := map[string]func(clientset kubernetes.Interface, namespace, name string) error{
"ConfigMap": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.CoreV1().ConfigMaps(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{})
},
Expand Down Expand Up @@ -93,6 +93,9 @@ func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespa
"PriorityClass": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.SchedulingV1().PriorityClasses().Delete(context.TODO(), name, metav1.DeleteOptions{})
},
"Namespace": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.CoreV1().Namespaces().Delete(context.TODO(), name, metav1.DeleteOptions{})
},
}

return deleteResourceApiMap
Expand Down Expand Up @@ -294,6 +297,13 @@ func DeleteResourceWithFinalizer(resources []ResourceInfo, dynamicClient dynamic
return remainingResources, nil
}

func namespacedMessageSuffix(namespace string) string {
if namespace != "" {
return " in namespace " + namespace
}
return ""
}

func DeleteResource(diff []ResourceInfo, clientset kubernetes.Interface, namespace, resourceType string, noInteractive bool) ([]ResourceInfo, error) {
deletedDiff := []ResourceInfo{}

Expand All @@ -305,7 +315,7 @@ func DeleteResource(diff []ResourceInfo, clientset kubernetes.Interface, namespa
}

if !noInteractive {
fmt.Printf("Do you want to delete %s %s in namespace %s? (Y/N): ", resourceType, resource.Name, namespace)
fmt.Printf("Do you want to delete %s %s%s? (Y/N): ", resourceType, resource.Name, namespacedMessageSuffix(namespace))
var confirmation string
_, err := fmt.Scanf("%s\n", &confirmation)
if err != nil {
Expand All @@ -316,7 +326,7 @@ func DeleteResource(diff []ResourceInfo, clientset kubernetes.Interface, namespa
if strings.ToLower(confirmation) != "y" && strings.ToLower(confirmation) != "yes" {
deletedDiff = append(deletedDiff, resource)

fmt.Printf("Do you want flag the resource %s %s in namespace %s as In Use? (Y/N): ", resourceType, resource.Name, namespace)
fmt.Printf("Do you want flag the resource %s %s%s as In Use? (Y/N): ", resourceType, resource.Name, namespacedMessageSuffix(namespace))
var inUse string
_, err := fmt.Scanf("%s\n", &inUse)
if err != nil {
Expand All @@ -326,17 +336,18 @@ func DeleteResource(diff []ResourceInfo, clientset kubernetes.Interface, namespa

if strings.ToLower(inUse) == "y" || strings.ToLower(inUse) == "yes" {
if err := FlagResource(clientset, namespace, resourceType, resource.Name); err != nil {
fmt.Fprintf(os.Stderr, "Failed to flag resource %s %s in namespace %s as In Use: %v\n", resourceType, resource.Name, namespace, err)
fmt.Fprintf(os.Stderr, "Failed to flag resource %s %s%s as In Use: %v\n", resourceType, resource.Name, namespacedMessageSuffix(namespace), err)
}
continue
}
continue
}
}

fmt.Printf("Deleting %s %s in namespace %s\n", resourceType, resource.Name, namespace)
fmt.Printf("Deleting %s %s%s\n", resourceType, resource.Name, namespacedMessageSuffix(namespace))

if err := deleteFunc(clientset, namespace, resource.Name); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete %s %s in namespace %s: %v\n", resourceType, resource.Name, namespace, err)
fmt.Fprintf(os.Stderr, "Failed to delete %s %s%s: %v\n", resourceType, resource.Name, namespacedMessageSuffix(namespace), err)
continue
}
deletedResource := resource
Expand Down
41 changes: 41 additions & 0 deletions pkg/kor/exceptions/namespaces/namespaces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"exceptionNamespaces": [
{
"Namespace": "default",
"ResourceName": ""
},
{
"Namespace": "kube-system",
"ResourceName": ""
},
{
"Namespace": "kube-public",
"ResourceName": ""
},
{
"Namespace": "kube-node-lease",
"ResourceName": ""
},
{
"Namespace": "kubernetes-dashboard",
"ResourceName": ""
},
{
"Namespace": "gmp-system",
"ResourceName": ""
},
{
"Namespace": "local-path-storage",
"ResourceName": ""
},
{
"Namespace": "assisted-installer",
"ResourceName": ""
},
{
"Namespace": "openshift-.*",
"ResourceName": "",
"MatchRegex": true
}
]
}
74 changes: 60 additions & 14 deletions pkg/kor/kor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sort"

apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand All @@ -23,6 +24,15 @@ type ExceptionResource struct {
ResourceName string
MatchRegex bool
}

// All resources in this struct must be defined as regex
type ExceptionNamespacedResource struct {
Namespace string
ResourceName string
MatchRegex bool
ResourceType string
}

type IncludeExcludeLists struct {
IncludeListStr string
ExcludeListStr string
Expand All @@ -33,20 +43,22 @@ type ResourceKind struct {
}

type Config struct {
ExceptionClusterRoles []ExceptionResource `json:"exceptionClusterRoles"`
ExceptionClusterRoleBindings []ExceptionResource `json:"exceptionClusterRoleBindings"`
ExceptionConfigMaps []ExceptionResource `json:"exceptionConfigMaps"`
ExceptionCrds []ExceptionResource `json:"exceptionCrds"`
ExceptionDaemonSets []ExceptionResource `json:"exceptionDaemonSets"`
ExceptionRoles []ExceptionResource `json:"exceptionRoles"`
ExceptionSecrets []ExceptionResource `json:"exceptionSecrets"`
ExceptionServiceAccounts []ExceptionResource `json:"exceptionServiceAccounts"`
ExceptionServices []ExceptionResource `json:"exceptionServices"`
ExceptionStorageClasses []ExceptionResource `json:"exceptionStorageClasses"`
ExceptionJobs []ExceptionResource `json:"exceptionJobs"`
ExceptionPdbs []ExceptionResource `json:"exceptionPdbs"`
ExceptionRoleBindings []ExceptionResource `json:"exceptionRoleBindings"`
ExceptionPriorityClasses []ExceptionResource `json:"exceptionPriorityClasses"`
ExceptionClusterRoles []ExceptionResource `json:"exceptionClusterRoles"`
ExceptionClusterRoleBindings []ExceptionResource `json:"exceptionClusterRoleBindings"`
ExceptionConfigMaps []ExceptionResource `json:"exceptionConfigMaps"`
ExceptionCrds []ExceptionResource `json:"exceptionCrds"`
ExceptionDaemonSets []ExceptionResource `json:"exceptionDaemonSets"`
ExceptionRoles []ExceptionResource `json:"exceptionRoles"`
ExceptionSecrets []ExceptionResource `json:"exceptionSecrets"`
ExceptionServiceAccounts []ExceptionResource `json:"exceptionServiceAccounts"`
ExceptionServices []ExceptionResource `json:"exceptionServices"`
ExceptionStorageClasses []ExceptionResource `json:"exceptionStorageClasses"`
ExceptionJobs []ExceptionResource `json:"exceptionJobs"`
ExceptionPdbs []ExceptionResource `json:"exceptionPdbs"`
ExceptionRoleBindings []ExceptionResource `json:"exceptionRoleBindings"`
ExceptionPriorityClasses []ExceptionResource `json:"exceptionPriorityClasses"`
ExceptionNamespaces []ExceptionResource `json:"exceptionNamespaces"`
ExceptionNamespacedResources []ExceptionNamespacedResource `json:"exceptionNamespacedResources"`
// Add other configurations if needed
}

Expand Down Expand Up @@ -157,6 +169,21 @@ func GetDynamicClient(kubeconfig string) *dynamic.DynamicClient {
return clientset
}

func GetDiscoveryClient(kubeconfig string) *discovery.DiscoveryClient {
config, err := GetConfig(kubeconfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load kubeconfig: %v\n", err)
os.Exit(1)
}

discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create Kubernetes client: %v\n", err)
os.Exit(1)
}
return discoveryClient
}

// TODO create formatter by resource "#", "Resource Name", "Namespace"
// TODO Functions that use this object are accompanied by repeated data acquisition operations and can be optimized.
func CalculateResourceDifference(usedResourceNames []string, allResourceNames []string) []string {
Expand Down Expand Up @@ -202,6 +229,25 @@ func isResourceException(resourceName, namespace string, exceptions []ExceptionR
return match, nil
}

func isNamespacedResourceException(resourceName, namespace, resourceType string, exceptions []ExceptionNamespacedResource) (bool, error) {
var match bool
for _, e := range exceptions {
namespaceRegexp, err := regexp.Compile(e.Namespace)
if err != nil {
return false, err
}
nameRegexp, err := regexp.Compile(e.ResourceName)
if err != nil {
return false, err
}
if nameRegexp.MatchString(resourceName) && namespaceRegexp.MatchString(namespace) && e.ResourceType == resourceType {
match = true
break
}
}
return match, nil
}

func unmarshalConfig(data []byte) (*Config, error) {
var config Config
if err := json.Unmarshal(data, &config); err != nil {
Expand Down
Loading
Loading