Skip to content

Commit

Permalink
Add finalizer subcommand (#172)
Browse files Browse the repository at this point in the history
* add finalizer subcommand

* add tests for deleting resources with finalizers

* add tests for finding terminating resources

* fix missed pv lines

* remove unnecessary print

* add command to readme
  • Loading branch information
Yuni-sa committed Jan 10, 2024
1 parent 15c865e commit a911793
Show file tree
Hide file tree
Showing 8 changed files with 520 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -95,6 +95,7 @@ Kor provides various subcommands to identify and list unused resources. The avai
- `crd` - Gets unused CRDs in the cluster(non namespaced resource).
- `jobs` - Gets unused jobs for the specified namespace or all namespaces.
- `replicasets` - Gets unused replicaSets for the specified namespace or all namespaces.
- `finalizers` - Gets unused pending deletion resources for the specified namespace or all namespaces.
- `exporter` - Export Prometheus metrics.

### Supported Flags
Expand Down
28 changes: 28 additions & 0 deletions cmd/kor/finalizers.go
@@ -0,0 +1,28 @@
package kor

import (
"fmt"

"github.com/spf13/cobra"
"github.com/yonahd/kor/pkg/kor"
)

var finalizerCmd = &cobra.Command{
Use: "finalizer",
Aliases: []string{"fin", "finalizers"},
Short: "Gets resources waiting for finalizers to delete",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)
dynamicClient := kor.GetDynamicClient(kubeconfig)
if response, err := kor.GetUnusedfinalizers(includeExcludeLists, filterOptions, clientset, dynamicClient, outputFormat, opts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
}
},
}

func init() {
rootCmd.AddCommand(finalizerCmd)
}
19 changes: 17 additions & 2 deletions pkg/kor/create_test_resources.go
Expand Up @@ -8,7 +8,8 @@ import (
networkingv1 "k8s.io/api/networking/v1"
policyv1 "k8s.io/api/policy/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

var testNamespace = "test-namespace"
Expand Down Expand Up @@ -261,6 +262,20 @@ func CreateTestConfigmap(namespace, name string) *corev1.ConfigMap {
}
}

func CreateTestUnstuctered(kind, apiVersion, namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": kind,
"apiVersion": apiVersion,
"metadata": map[string]interface{}{
"name": name,
"namespace": namespace,
},
"spec": map[string]interface{}{},
},
}
}

func CreateTestJob(namespace, name string, status *batchv1.JobStatus) *batchv1.Job {
return &batchv1.Job{
ObjectMeta: v1.ObjectMeta{
Expand Down Expand Up @@ -306,4 +321,4 @@ func CreateTestReplicaSet(namespace, name string, specReplicas *int32, status *a
},
Status: *status,
}
}
}
76 changes: 76 additions & 0 deletions pkg/kor/delete.go
Expand Up @@ -16,6 +16,9 @@ import (
policyv1beta1 "k8s.io/api/policy/v1beta1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

Expand Down Expand Up @@ -71,6 +74,28 @@ func DeleteResourceCmd() map[string]func(clientset kubernetes.Interface, namespa
return deleteResourceApiMap
}

func FlagDynamicResource(dynamicClient dynamic.Interface, namespace string, gvr schema.GroupVersionResource, resourceName string) error {
resource, err := dynamicClient.
Resource(gvr).
Namespace(namespace).
Get(context.TODO(), resourceName, metav1.GetOptions{})
if err != nil {
return err
}

labels := resource.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels["kor/used"] = "true"
resource.SetLabels(labels)
_, err = dynamicClient.
Resource(gvr).
Namespace(namespace).
Update(context.TODO(), resource, metav1.UpdateOptions{})
return err
}

func FlagResource(clientset kubernetes.Interface, namespace, resourceType, resourceName string) error {
resource, err := getResource(clientset, namespace, resourceType, resourceName)
if err != nil {
Expand Down Expand Up @@ -165,6 +190,57 @@ func getResource(clientset kubernetes.Interface, namespace, resourceType, resour
return nil, fmt.Errorf("resource type '%s' is not supported", resourceType)
}

func DeleteResourceWithFinalizer(diff []string, dynamicClient dynamic.Interface, namespace string, gvr schema.GroupVersionResource, noInteractive bool) ([]string, error) {
deletedDiff := []string{}

for _, resourceName := range diff {

if !noInteractive {
fmt.Printf("Do you want to delete %s %s in namespace %s? (Y/N): ", gvr.Resource, resourceName, namespace)
var confirmation string
_, err := fmt.Scanf("%s", &confirmation)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
continue
}

if strings.ToLower(confirmation) != "y" && strings.ToLower(confirmation) != "yes" {
deletedDiff = append(deletedDiff, resourceName)

fmt.Printf("Do you want flag the resource %s %s in namespace %s as In Use? (Y/N): ", gvr.Resource, resourceName, namespace)
var inUse string
_, err := fmt.Scanf("%s", &inUse)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
continue
}

if strings.ToLower(inUse) == "y" || strings.ToLower(inUse) == "yes" {
if err := FlagDynamicResource(dynamicClient, namespace, gvr, resourceName); err != nil {
fmt.Fprintf(os.Stderr, "Failed to flag resource %s %s in namespace %s as In Use: %v\n", gvr.Resource, resourceName, namespace, err)
}
continue
}
continue
}
}

fmt.Printf("Deleting %s %s in namespace %s\n", gvr.Resource, resourceName, namespace)
if _, err := dynamicClient.
Resource(gvr).
Namespace(namespace).
Patch(context.TODO(), resourceName, types.MergePatchType,
[]byte(`{"metadata":{"finalizers":null}}`),
metav1.PatchOptions{}); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete %s %s in namespace %s: %v\n", gvr.Resource, resourceName, namespace, err)
continue
}
deletedDiff = append(deletedDiff, resourceName+"-DELETED")
}

return deletedDiff, nil
}

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

Expand Down
137 changes: 136 additions & 1 deletion pkg/kor/delete_test.go
@@ -1,9 +1,15 @@
package kor

import (
"context"
"testing"

"k8s.io/client-go/kubernetes/fake"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
fakedynamic "k8s.io/client-go/dynamic/fake"
fake "k8s.io/client-go/kubernetes/fake"
)

func TestDeleteResource(t *testing.T) {
Expand Down Expand Up @@ -37,3 +43,132 @@ func TestDeleteResource(t *testing.T) {
})
}
}

func TestDeleteDeleteResourceWithFinalizer(t *testing.T) {
scheme := runtime.NewScheme()
gvr := schema.GroupVersionResource{Group: "testgroup", Version: "v1", Resource: "TestResource"}
testResource := CreateTestUnstuctered(gvr.Resource, gvr.GroupVersion().String(), testNamespace, "test-resource")
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, testResource)

_, err := dynamicClient.Resource(gvr).
Namespace(testNamespace).
Create(context.TODO(), testResource, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Error creating test resource: %v", err)
}

_, err = dynamicClient.
Resource(gvr).
Namespace(testNamespace).
Patch(context.TODO(), "test-resource", types.MergePatchType,
[]byte(`{"metadata":{"finalizers":["finalizer1", "finalizer2", "finalizer3"]}}`),
metav1.PatchOptions{})

if err != nil {
t.Fatalf("Error patching test resource: %v", err)
}

tests := []struct {
name string
diff []string
resourceType string
expectedDiff []string
expectedError bool
}{
{
name: "Test deletion confirmation",
diff: []string{testResource.GetName()},
expectedDiff: []string{testResource.GetName() + "-DELETED"},
expectedError: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
deletedDiff, _ := DeleteResourceWithFinalizer(test.diff, dynamicClient, testNamespace, gvr, true)

for i, deleted := range deletedDiff {
if deleted != test.expectedDiff[i] {
t.Errorf("Expected: %s, Got: %s", test.expectedDiff[i], deleted)
resource, err := dynamicClient.Resource(gvr).
Namespace(testNamespace).
Get(context.TODO(), deleted, metav1.GetOptions{})
if err != nil {
t.Error(err)
}
if resource.GetFinalizers() != nil {
t.Error("Finalizers not patched")
}
}
}

})
}
}

func TestFlagDynamicResource(t *testing.T) {
scheme := runtime.NewScheme()
gvr := schema.GroupVersionResource{Group: "testgroup", Version: "v1", Resource: "TestResource"}
testResource := CreateTestUnstuctered(gvr.Resource, gvr.GroupVersion().String(), testNamespace, "test-resource")
testResourceWithLabel := CreateTestUnstuctered(gvr.Resource, gvr.GroupVersion().String(), testNamespace, "test-resource-with-label")
dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme, testResource, testResourceWithLabel)
testResourceWithLabel.SetLabels(map[string]string{
"test": "true",
})

_, err := dynamicClient.Resource(gvr).
Namespace(testNamespace).
Create(context.TODO(), testResource, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Error creating test resource: %v", err)
}
_, err = dynamicClient.Resource(gvr).
Namespace(testNamespace).
Create(context.TODO(), testResourceWithLabel, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Error creating test resource with finalizers: %v", err)
}

tests := []struct {
name string
gvr schema.GroupVersionResource
resourceName string
labels bool
expectedError bool
}{
{
name: "Test flagging dynamic resource",
resourceName: "test-resource",
labels: false,
expectedError: false,
},
{
name: "Test flagging dynamic resource with labels",
resourceName: "test-resource-with-label",
labels: true,
expectedError: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := FlagDynamicResource(dynamicClient, testNamespace, gvr, test.resourceName)

if (err != nil) != test.expectedError {
t.Errorf("Expected error: %v, Got: %v", test.expectedError, err)
}
resource, err := dynamicClient.Resource(gvr).
Namespace(testNamespace).
Get(context.TODO(), test.resourceName, metav1.GetOptions{})
if err != nil {
t.Error(err)
}
if resource.GetLabels()["kor/used"] != "true" {
t.Errorf("Expected resource flagged as used, Got: %v", resource.GetLabels()["kor/used"])
}
if test.labels == true && resource.GetLabels()["test"] != "true" {
t.Errorf("Resource Lost his labels")
}
})
}
}

0 comments on commit a911793

Please sign in to comment.