Skip to content
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

feat(namespaces): add empty namespace detection and removal #249

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
.vscode
*.iml
.idea/
.vscode/
dist/**
main.exe
coverage.txt
coverage.*
build/
kor
!kor/
*.swp
hack/exceptions
.envrc
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ EXCEPTIONS_FILE_PATTERN := *.json
build:
go build -o build/kor main.go

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

lint:
golangci-lint run

Expand All @@ -15,6 +18,10 @@ lint-fix:
test:
go test -race -coverprofile=coverage.txt -shuffle on ./...

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

sort-exception-files:
@echo "Sorting exception files..."
@find $(EXCEPTIONS_DIR) -name '$(EXCEPTIONS_FILE_PATTERN)' | xargs -I{} -P 4 sh -c ' \
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
- StorageClasses
- NetworkPolicies
- RoleBindings
- Namespaces

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

Expand Down Expand Up @@ -133,6 +134,7 @@ Kor provides various subcommands to identify and list unused resources. The avai
- `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.
- `namespace` - Gets unused Namespaces for the specified namespace or all namespaces.
- `exporter` - Export Prometheus metrics.
- `version` - Print kor version information.

Expand Down Expand Up @@ -194,6 +196,7 @@ kor [subcommand] --help
| DaemonSets | DaemonSets not scheduled on any nodes |
| StorageClasses | StorageClasses not used by any PVs / PVCs |
| NetworkPolicies | NetworkPolicies with no Pods selected by podSelector or Ingress / Egress rules |
| Namespaces | Only empty namespaces |

### Deleting Unused resources

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
43 changes: 43 additions & 0 deletions cmd/kor/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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)
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: 2 additions & 0 deletions pkg/filters/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Options struct {
ExcludeNamespaces []string
// IncludeNamespaces is a namespace selector to include resources in matching namespaces
IncludeNamespaces []string
// IgnoreResourceTypes is a namespace selector to exclude specified resource type evaluation, only applicable to namespaces
IgnoreResourceTypes []string

namespace []string
once sync.Once
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 @@ -84,6 +84,9 @@ func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespa
"RoleBinding": func(clientset kubernetes.Interface, namespace, name string) error {
return clientset.RbacV1().RoleBindings(namespace).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 @@ -277,6 +280,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 @@ -288,7 +298,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 @@ -299,7 +309,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 @@ -309,17 +319,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": "kuberenetes-dashboard",
"ResourceName": ""
},
{
"Namespace": "gmp-system",
"ResourceName": ""
},
{
"Namespace": "local-path-storage",
"ResourceName": ""
},
{
"Namespace": "assisted-installer",
"ResourceName": ""
},
{
"Namespace": "openshift-.*",
"ResourceName": "",
"MatchRegex": true
}
]
}
1 change: 1 addition & 0 deletions pkg/kor/kor.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Config struct {
ExceptionJobs []ExceptionResource `json:"exceptionJobs"`
ExceptionPdbs []ExceptionResource `json:"exceptionPdbs"`
ExceptionRoleBindings []ExceptionResource `json:"exceptionRoleBindings"`
ExceptionNamespaces []ExceptionResource `json:"exceptionNamespaces"`
// Add other configurations if needed
}

Expand Down
37 changes: 37 additions & 0 deletions pkg/kor/kor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,40 @@ func TestResourceExceptionWithRegexPrefixInNamespace(t *testing.T) {
t.Error("Expected to find exception")
}
}

func TestNamespacedMessageSuffix(t *testing.T) {
type args struct {
namespace string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty string passed",
args: args{
namespace: "",
},
want: "",
},
{
name: "namespace name passed",
args: args{
namespace: "test-ns1",
},
want: " in namespace test-ns1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := namespacedMessageSuffix(tt.args.namespace); got != tt.want {
t.Errorf(
"namespacedMessageSuffix() = '%v', want '%v'",
got,
tt.want,
)
}
})
}
}
Loading