From 8cb59041131785fe6e857f71844c47990fea3acd Mon Sep 17 00:00:00 2001 From: Yonah Dissen <47282577+yonahd@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:13:22 +0300 Subject: [PATCH] Add tests for unused kubernetes resources finders (#58) * add tests for some of the resources * add tests for some of the resources * adding tests for services sts deployments and ingress * clean go mod * adding tests for serviceAccounts * rebase * adding tests for pvcs * adding tests for pdbs * adding tests for secrets * adding tests for secrets * add tests for all modules * add tests for services structured * add tests for sts structured * add tests for cm structured * add tests for deploy structured * add tests for deploy structured * add test structured * add test structured * add test structured * fix lint errors --------- Co-authored-by: Yonah Dissen --- .github/workflows/merge-request.yml | 8 +- README.md | 1 + cmd/kor/statefulsets.go | 6 +- go.mod | 6 +- go.sum | 14 +- pkg/kor/all.go | 56 +++--- pkg/kor/configmaps_test.go | 203 ++++++++++++++++++++ pkg/kor/confimgmaps.go | 20 +- pkg/kor/create_test_resources.go | 250 ++++++++++++++++++++++++ pkg/kor/deployments.go | 17 +- pkg/kor/deployments_test.go | 94 +++++++++ pkg/kor/hpas.go | 22 +-- pkg/kor/hpas_test.go | 101 ++++++++++ pkg/kor/ingresses.go | 26 +-- pkg/kor/ingresses_test.go | 106 +++++++++++ pkg/kor/kor.go | 4 +- pkg/kor/multi.go | 40 ++-- pkg/kor/pdbs.go | 14 +- pkg/kor/pdbs_test.go | 107 +++++++++++ pkg/kor/pvc.go | 14 +- pkg/kor/pvc_test.go | 117 ++++++++++++ pkg/kor/roles.go | 22 +-- pkg/kor/roles_test.go | 127 +++++++++++++ pkg/kor/secrets.go | 22 +-- pkg/kor/secrets_test.go | 283 ++++++++++++++++++++++++++++ pkg/kor/serviceaccounts.go | 28 +-- pkg/kor/serviceaccounts_test.go | 202 ++++++++++++++++++++ pkg/kor/services.go | 17 +- pkg/kor/services_test.go | 92 +++++++++ pkg/kor/statefulsets.go | 37 ++-- pkg/kor/statefulsets_test.go | 94 +++++++++ 31 files changed, 1954 insertions(+), 196 deletions(-) create mode 100644 pkg/kor/configmaps_test.go create mode 100644 pkg/kor/create_test_resources.go create mode 100644 pkg/kor/deployments_test.go create mode 100644 pkg/kor/hpas_test.go create mode 100644 pkg/kor/ingresses_test.go create mode 100644 pkg/kor/pdbs_test.go create mode 100644 pkg/kor/pvc_test.go create mode 100644 pkg/kor/roles_test.go create mode 100644 pkg/kor/secrets_test.go create mode 100644 pkg/kor/serviceaccounts_test.go create mode 100644 pkg/kor/services_test.go create mode 100644 pkg/kor/statefulsets_test.go diff --git a/.github/workflows/merge-request.yml b/.github/workflows/merge-request.yml index 923757ca..0192cdf2 100644 --- a/.github/workflows/merge-request.yml +++ b/.github/workflows/merge-request.yml @@ -28,9 +28,15 @@ jobs: uses: golangci/golangci-lint-action@v3 with: version: latest + skip-pkg-cache: true - name: Build run: go build -v ./... - name: Test - run: go test -v ./... \ No newline at end of file + run: go test -v ./... -coverprofile=coverage.txt + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 61a47587..f5d37681 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![GitHub go.mod Go version (subdirectory of monorepo)](https://img.shields.io/github/go-mod/go-version/yonahd/kor) ![GitHub release (with filter)](https://img.shields.io/github/v/release/yonahd/kor?color=green&link=https://github.com/yonahd/kor/releases) +[![codecov](https://codecov.io/gh/yonahd/kor/branch/main/graph/badge.svg?token=tNKcOjlxLo)](https://codecov.io/gh/yonahd/kor) # Kor - Kubernetes Orphaned Resources Finder diff --git a/cmd/kor/statefulsets.go b/cmd/kor/statefulsets.go index 5a100f1c..130da02c 100644 --- a/cmd/kor/statefulsets.go +++ b/cmd/kor/statefulsets.go @@ -10,18 +10,18 @@ import ( var stsCmd = &cobra.Command{ Use: "statefulset", Aliases: []string{"sts", "statefulsets"}, - Short: "Gets unused statefulsets", + Short: "Gets unused statefulSets", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { clientset := kor.GetKubeClient(kubeconfig) if outputFormat == "json" || outputFormat == "yaml" { - if response, err := kor.GetUnusedStatefulsetsStructured(includeExcludeLists, clientset, outputFormat); err != nil { + if response, err := kor.GetUnusedStatefulSetsStructured(includeExcludeLists, clientset, outputFormat); err != nil { fmt.Println(err) } else { fmt.Println(response) } } else { - kor.GetUnusedStatefulsets(includeExcludeLists, clientset) + kor.GetUnusedStatefulSets(includeExcludeLists, clientset) } }, diff --git a/go.mod b/go.mod index de40bb6d..3a4d8732 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -34,11 +35,14 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.11.0 // indirect + github.com/onsi/gomega v1.27.10 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.13.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/sys v0.11.0 // indirect golang.org/x/term v0.10.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/go.sum b/go.sum index a1684853..423cae20 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -64,8 +66,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -107,8 +113,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/kor/all.go b/pkg/kor/all.go index 6f6778de..5952b20c 100644 --- a/pkg/kor/all.go +++ b/pkg/kor/all.go @@ -19,8 +19,8 @@ type ResourceDiff struct { diff []string } -func getUnusedCMs(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - cmDiff, err := processNamespaceCM(kubeClient, namespace) +func getUnusedCMs(clientset kubernetes.Interface, namespace string) ResourceDiff { + cmDiff, err := processNamespaceCM(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "configmaps", namespace, err) } @@ -28,8 +28,8 @@ func getUnusedCMs(kubeClient *kubernetes.Clientset, namespace string) ResourceDi return namespaceCMDiff } -func getUnusedSVCs(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - svcDiff, err := ProcessNamespaceServices(kubeClient, namespace) +func getUnusedSVCs(clientset kubernetes.Interface, namespace string) ResourceDiff { + svcDiff, err := ProcessNamespaceServices(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "services", namespace, err) } @@ -37,8 +37,8 @@ func getUnusedSVCs(kubeClient *kubernetes.Clientset, namespace string) ResourceD return namespaceSVCDiff } -func getUnusedSecrets(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - secretDiff, err := processNamespaceSecret(kubeClient, namespace) +func getUnusedSecrets(clientset kubernetes.Interface, namespace string) ResourceDiff { + secretDiff, err := processNamespaceSecret(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "secrets", namespace, err) } @@ -46,8 +46,8 @@ func getUnusedSecrets(kubeClient *kubernetes.Clientset, namespace string) Resour return namespaceSecretDiff } -func getUnusedServiceAccounts(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - saDiff, err := processNamespaceSA(kubeClient, namespace) +func getUnusedServiceAccounts(clientset kubernetes.Interface, namespace string) ResourceDiff { + saDiff, err := processNamespaceSA(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "serviceaccounts", namespace, err) } @@ -55,8 +55,8 @@ func getUnusedServiceAccounts(kubeClient *kubernetes.Clientset, namespace string return namespaceSADiff } -func getUnusedDeployments(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - deployDiff, err := ProcessNamespaceDeployments(kubeClient, namespace) +func getUnusedDeployments(clientset kubernetes.Interface, namespace string) ResourceDiff { + deployDiff, err := ProcessNamespaceDeployments(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "deployments", namespace, err) } @@ -64,17 +64,17 @@ func getUnusedDeployments(kubeClient *kubernetes.Clientset, namespace string) Re return namespaceSADiff } -func getUnusedStatefulsets(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - stsDiff, err := ProcessNamespaceStatefulsets(kubeClient, namespace) +func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string) ResourceDiff { + stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "statefulsets", namespace, err) + fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "statefulSets", namespace, err) } - namespaceSADiff := ResourceDiff{"Statefulset", stsDiff} + namespaceSADiff := ResourceDiff{"StatefulSet", stsDiff} return namespaceSADiff } -func getUnusedRoles(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - roleDiff, err := processNamespaceRoles(kubeClient, namespace) +func getUnusedRoles(clientset kubernetes.Interface, namespace string) ResourceDiff { + roleDiff, err := processNamespaceRoles(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "roles", namespace, err) } @@ -82,8 +82,8 @@ func getUnusedRoles(kubeClient *kubernetes.Clientset, namespace string) Resource return namespaceSADiff } -func getUnusedHpas(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - hpaDiff, err := processNamespaceHpas(kubeClient, namespace) +func getUnusedHpas(clientset kubernetes.Interface, namespace string) ResourceDiff { + hpaDiff, err := processNamespaceHpas(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "hpas", namespace, err) } @@ -91,8 +91,8 @@ func getUnusedHpas(kubeClient *kubernetes.Clientset, namespace string) ResourceD return namespaceHpaDiff } -func getUnusedPvcs(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - pvcDiff, err := processNamespacePvcs(kubeClient, namespace) +func getUnusedPvcs(clientset kubernetes.Interface, namespace string) ResourceDiff { + pvcDiff, err := processNamespacePvcs(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err) } @@ -100,8 +100,8 @@ func getUnusedPvcs(kubeClient *kubernetes.Clientset, namespace string) ResourceD return namespacePvcDiff } -func getUnusedIngresses(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - ingressDiff, err := processNamespaceIngresses(kubeClient, namespace) +func getUnusedIngresses(clientset kubernetes.Interface, namespace string) ResourceDiff { + ingressDiff, err := processNamespaceIngresses(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "ingresses", namespace, err) } @@ -109,8 +109,8 @@ func getUnusedIngresses(kubeClient *kubernetes.Clientset, namespace string) Reso return namespaceIngressDiff } -func getUnusedPdbs(kubeClient *kubernetes.Clientset, namespace string) ResourceDiff { - pdbDiff, err := processNamespacePdbs(kubeClient, namespace) +func getUnusedPdbs(clientset kubernetes.Interface, namespace string) ResourceDiff { + pdbDiff, err := processNamespacePdbs(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pdbs", namespace, err) } @@ -118,7 +118,7 @@ func getUnusedPdbs(kubeClient *kubernetes.Clientset, namespace string) ResourceD return namespacePdbDiff } -func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { var allDiffs []ResourceDiff @@ -132,7 +132,7 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset *kubernetes allDiffs = append(allDiffs, namespaceSADiff) namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace) allDiffs = append(allDiffs, namespaceDeploymentDiff) - namespaceStatefulsetDiff := getUnusedStatefulsets(clientset, namespace) + namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace) allDiffs = append(allDiffs, namespaceStatefulsetDiff) namespaceRoleDiff := getUnusedRoles(clientset, namespace) allDiffs = append(allDiffs, namespaceRoleDiff) @@ -150,7 +150,7 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset *kubernetes } } -func GetUnusedAllStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedAllStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) // Create the JSON response object @@ -174,7 +174,7 @@ func GetUnusedAllStructured(includeExcludeLists IncludeExcludeLists, clientset * namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace) allDiffs = append(allDiffs, namespaceDeploymentDiff) - namespaceStatefulsetDiff := getUnusedStatefulsets(clientset, namespace) + namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace) allDiffs = append(allDiffs, namespaceStatefulsetDiff) namespaceRoleDiff := getUnusedRoles(clientset, namespace) diff --git a/pkg/kor/configmaps_test.go b/pkg/kor/configmaps_test.go new file mode 100644 index 00000000..3141c429 --- /dev/null +++ b/pkg/kor/configmaps_test.go @@ -0,0 +1,203 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestConfigmaps(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: testNamespace}, + }, metav1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + configmap1 := CreateTestConfigmap(testNamespace, "configmap-1") + configmap2 := CreateTestConfigmap(testNamespace, "configmap-2") + configmap3 := CreateTestConfigmap(testNamespace, "configmap-3") + + pod1 := CreateTestPod(testNamespace, "pod-1", "", []corev1.Volume{ + {Name: "vol-1", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: configmap1.ObjectMeta.Name}}}}, + }) + + pod2 := CreateTestPod(testNamespace, "pod-2", "", nil) + pod2.Spec.Containers = []corev1.Container{ + { + Env: []corev1.EnvVar{ + {Name: "ENV_VAR_1", ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: configmap1.ObjectMeta.Name}}}}, + }, + }, + } + + pod3 := CreateTestPod(testNamespace, "pod-3", "", nil) + pod3.Spec.Containers = []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: configmap2.ObjectMeta.Name}}}, + }, + }, + } + + pod4 := CreateTestPod(testNamespace, "pod-4", "", nil) + pod4.Spec.InitContainers = []corev1.Container{ + { + Env: []corev1.EnvVar{ + {Name: "INIT_ENV_VAR_1", ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: configmap2.ObjectMeta.Name}}}}, + }, + }, + } + + _, err = clientset.CoreV1().ConfigMaps(testNamespace).Create(context.TODO(), configmap1, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake configmap: %v", err) + } + + _, err = clientset.CoreV1().ConfigMaps(testNamespace).Create(context.TODO(), configmap2, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake configmap: %v", err) + } + + _, err = clientset.CoreV1().ConfigMaps(testNamespace).Create(context.TODO(), configmap3, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake configmap: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod1, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod2, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod3, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod4, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + return clientset +} + +func TestRetrieveConfigMapNames(t *testing.T) { + clientset := createTestConfigmaps(t) + + configMapNames, err := retrieveConfigMapNames(clientset, testNamespace) + + if err != nil { + t.Fatalf("Error retrieving configmap names: %v", err) + } + + expectedConfigMapNames := []string{"configmap-1", "configmap-2", "configmap-3"} + if !equalSlices(configMapNames, expectedConfigMapNames) { + t.Errorf("Expected configmap names %v, got %v", expectedConfigMapNames, configMapNames) + } +} + +func TestProcessNamespaceCM(t *testing.T) { + clientset := createTestConfigmaps(t) + + diff, err := processNamespaceCM(clientset, testNamespace) + if err != nil { + t.Fatalf("Error processing namespace CM: %v", err) + } + + unusedConfigmaps := []string{"configmap-3"} + if !equalSlices(diff, unusedConfigmaps) { + t.Errorf("Expected diff %v, got %v", unusedConfigmaps, diff) + } +} + +func TestRetrieveUsedCM(t *testing.T) { + clientset := createTestConfigmaps(t) + + volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(clientset, testNamespace) + + if err != nil { + t.Fatalf("Error retrieving used ConfigMaps: %v", err) + } + + expectedVolumesCM := []string{"configmap-1", "kube-root-ca.crt"} + if !equalSlices(volumesCM, expectedVolumesCM) { + t.Errorf("Expected volume configmaps %v, got %v", expectedVolumesCM, volumesCM) + } + + var expectedVolumesProjectedCM []string + if !equalSlices(volumesProjectedCM, expectedVolumesProjectedCM) { + t.Errorf("Expected volume configmaps %v, got %v", expectedVolumesProjectedCM, volumesProjectedCM) + } + + expectedEnvCM := []string{"configmap-1"} + if !equalSlices(envCM, expectedEnvCM) { + t.Errorf("Expected env configmaps %v, got %v", expectedEnvCM, envCM) + } + + expectedEnvFromCM := []string{"configmap-2"} + if !equalSlices(envFromCM, expectedEnvFromCM) { + t.Errorf("Expected envFrom configmaps %v, got %v", expectedEnvFromCM, envFromCM) + } + + expectedEnvFromContainerCM := []string{"configmap-2"} + if !equalSlices(envFromContainerCM, expectedEnvFromContainerCM) { + t.Errorf("Expected envFrom configmaps %v, got %v", expectedEnvFromContainerCM, envFromContainerCM) + } + + expectedEnvFromInitContainerCM := []string{"configmap-2"} + if !equalSlices(envFromInitContainerCM, expectedEnvFromInitContainerCM) { + t.Errorf("Expected initContainer env configmaps %v, got %v", expectedEnvFromInitContainerCM, envFromInitContainerCM) + } + +} + +func TestGetUnusedConfigmapsStructured(t *testing.T) { + clientset := createTestConfigmaps(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedConfigmapsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedConfigmapsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "ConfigMap": {"configmap-3"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/confimgmaps.go b/pkg/kor/confimgmaps.go index e5438f7f..64ca796c 100644 --- a/pkg/kor/confimgmaps.go +++ b/pkg/kor/confimgmaps.go @@ -17,7 +17,7 @@ var exceptionconfigmaps = []ExceptionResource{ {ResourceName: "kube-root-ca.crt", Namespace: "*"}, } -func retrieveUsedCM(kubeClient *kubernetes.Clientset, namespace string) ([]string, []string, []string, []string, []string, []string, error) { +func retrieveUsedCM(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, []string, error) { var volumesCM []string var volumesProjectedCM []string var envCM []string @@ -25,13 +25,11 @@ func retrieveUsedCM(kubeClient *kubernetes.Clientset, namespace string) ([]strin var envFromContainerCM []string var envFromInitContainerCM []string - // Retrieve pods in the specified namespace - pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, nil, nil, nil, nil, nil, err } - // Extract volume and environment information from pods for _, pod := range pods.Items { for _, volume := range pod.Spec.Volumes { if volume.ConfigMap != nil { @@ -85,8 +83,8 @@ func retrieveUsedCM(kubeClient *kubernetes.Clientset, namespace string) ([]strin return volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, nil } -func retrieveConfigMapNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - configmaps, err := kubeClient.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveConfigMapNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + configmaps, err := clientset.CoreV1().ConfigMaps(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -101,8 +99,8 @@ func retrieveConfigMapNames(kubeClient *kubernetes.Clientset, namespace string) return names, nil } -func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(kubeClient, namespace) +func processNamespaceCM(clientset kubernetes.Interface, namespace string) ([]string, error) { + volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM, envFromInitContainerCM, err := retrieveUsedCM(clientset, namespace) if err != nil { return nil, err } @@ -114,7 +112,7 @@ func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]s envFromContainerCM = RemoveDuplicatesAndSort(envFromContainerCM) envFromInitContainerCM = RemoveDuplicatesAndSort(envFromInitContainerCM) - configMapNames, err := retrieveConfigMapNames(kubeClient, namespace) + configMapNames, err := retrieveConfigMapNames(clientset, namespace) if err != nil { return nil, err } @@ -130,7 +128,7 @@ func processNamespaceCM(kubeClient *kubernetes.Clientset, namespace string) ([]s } -func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -145,7 +143,7 @@ func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset *kub } } -func GetUnusedConfigmapsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedConfigmapsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/create_test_resources.go b/pkg/kor/create_test_resources.go new file mode 100644 index 00000000..2f2e5ad5 --- /dev/null +++ b/pkg/kor/create_test_resources.go @@ -0,0 +1,250 @@ +package kor + +import ( + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + 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" +) + +var testNamespace = "test-namespace" + +func CreateTestDeployment(namespace, name string, replicas int32, labels map[string]string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + }, + } +} + +func CreateTestStatefulSet(namespace, name string, replicas int32, labels map[string]string) *appsv1.StatefulSet { + return &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + } +} + +func CreateTestService(namespace, name string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} + +func CreateTestPod(namespace, name, serviceAccountName string, volumes []corev1.Volume) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: corev1.PodSpec{ + Volumes: volumes, + InitContainers: nil, + Containers: nil, + ServiceAccountName: serviceAccountName, + }, + } +} + +func CreatePersistentVolumeClaimVolumeSource(name string) *corev1.PersistentVolumeClaimVolumeSource { + return &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: name, + } +} + +func CreateTestVolume(name, pvcName string) *corev1.Volume { + pvc := CreatePersistentVolumeClaimVolumeSource(pvcName) + return &corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: pvc, + }, + } + +} + +func CreateTestServiceAccount(namespace, name string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} + +func CreateTestRbacSubject(namespace, serviceAccountName string) *rbacv1.Subject { + return &rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: namespace, + } +} + +func CreateTestRoleRef(roleName string) *rbacv1.RoleRef { + return &rbacv1.RoleRef{ + Kind: "Role", + Name: roleName, + } +} + +func CreateTestRoleBinding(namespace, name, serviceAccountName string, roleRefName *rbacv1.RoleRef) *rbacv1.RoleBinding { + rbacSubject := CreateTestRbacSubject(namespace, serviceAccountName) + return &rbacv1.RoleBinding{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Subjects: []rbacv1.Subject{ + *rbacSubject, + }, + RoleRef: *roleRefName, + } +} + +func CreateTestClusterRoleBinding(namespace, name, serviceAccountName string) *rbacv1.ClusterRoleBinding { + rbacSubject := CreateTestRbacSubject(namespace, serviceAccountName) + return &rbacv1.ClusterRoleBinding{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + }, + Subjects: []rbacv1.Subject{ + *rbacSubject, + }, + } +} + +func createPolicyRule() *rbacv1.PolicyRule { + return &rbacv1.PolicyRule{ + Verbs: []string{"get"}, + Resources: []string{"pods"}, + } +} + +func CreateTestRole(namespace, name string) *rbacv1.Role { + policyRule := createPolicyRule() + return &rbacv1.Role{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Rules: []rbacv1.PolicyRule{*policyRule}, + } +} + +func CreateTestEndpoint(namespace, name string, endpointSubsetCount int) *corev1.Endpoints { + return &corev1.Endpoints{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Subsets: make([]corev1.EndpointSubset, endpointSubsetCount), + } +} +func CreateTestHpa(namespace, name, deploymentName string, minReplicas, maxReplicas int32) *autoscalingv2.HorizontalPodAutoscaler { + return &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ + MinReplicas: &minReplicas, + MaxReplicas: maxReplicas, + ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ + Kind: "Deployment", + Name: deploymentName, + }, + }, + } +} + +func CreateTestIngress(namespace, name, ServiceName, secretName string) *networkingv1.Ingress { + ingressRule := networkingv1.IngressRule{ + Host: "test.com", + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/path", + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: ServiceName, + }, + }, + }, + }, + }, + }, + } + ingressTls := networkingv1.IngressTLS{ + SecretName: secretName, + } + + return &networkingv1.Ingress{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ingressRule}, + TLS: []networkingv1.IngressTLS{ingressTls}, + }, + } +} + +func CreateTestPvc(namespace, name string) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} + +func CreateTestPdb(namespace, name string, matchLabels map[string]string) *policyv1.PodDisruptionBudget { + return &policyv1.PodDisruptionBudget{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: nil, + Selector: &v1.LabelSelector{ + MatchLabels: matchLabels, + }, + }, + } +} + +func CreateTestSecret(namespace, name string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} + +func CreateTestConfigmap(namespace, name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + } +} diff --git a/pkg/kor/deployments.go b/pkg/kor/deployments.go index 8601eaa5..3285ea7b 100644 --- a/pkg/kor/deployments.go +++ b/pkg/kor/deployments.go @@ -11,8 +11,8 @@ import ( "sigs.k8s.io/yaml" ) -func getDeploymentsWithoutReplicas(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - deploymentsList, err := kubeClient.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) +func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace string) ([]string, error) { + deploymentsList, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -32,16 +32,7 @@ func getDeploymentsWithoutReplicas(kubeClient *kubernetes.Clientset, namespace s return deploymentsWithoutReplicas, nil } -func ProcessNamespaceDeployments(clientset *kubernetes.Clientset, namespace string) ([]string, error) { - usedServices, err := getDeploymentsWithoutReplicas(clientset, namespace) - if err != nil { - return nil, err - } - - return usedServices, nil -} - -func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -56,7 +47,7 @@ func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset *ku } } -func GetUnusedDeploymentsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedDeploymentsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/deployments_test.go b/pkg/kor/deployments_test.go new file mode 100644 index 00000000..bed7ec90 --- /dev/null +++ b/pkg/kor/deployments_test.go @@ -0,0 +1,94 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestDeployments(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + appLabels := map[string]string{} + + deployment1 := CreateTestDeployment(testNamespace, "test-deployment1", 0, appLabels) + deployment2 := CreateTestDeployment(testNamespace, "test-deployment2", 1, appLabels) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + + return clientset +} + +func TestProcessNamespaceDeployments(t *testing.T) { + clientset := createTestDeployments(t) + + deploymentsWithoutReplicas, err := ProcessNamespaceDeployments(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(deploymentsWithoutReplicas) != 1 { + t.Errorf("Expected 1 deployment without replicas, got %d", len(deploymentsWithoutReplicas)) + } + + if deploymentsWithoutReplicas[0] != "test-deployment1" { + t.Errorf("Expected 'test-deployment1', got %s", deploymentsWithoutReplicas[0]) + } +} + +func TestGetUnusedDeploymentsStructured(t *testing.T) { + clientset := createTestDeployments(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedDeploymentsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedDeploymentsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Deployments": {"test-deployment1"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/hpas.go b/pkg/kor/hpas.go index e47903d7..635fb7ef 100644 --- a/pkg/kor/hpas.go +++ b/pkg/kor/hpas.go @@ -13,7 +13,7 @@ import ( "sigs.k8s.io/yaml" ) -func getDeploymentNames(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func getDeploymentNames(clientset kubernetes.Interface, namespace string) ([]string, error) { deployments, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err @@ -25,19 +25,19 @@ func getDeploymentNames(clientset *kubernetes.Clientset, namespace string) ([]st return names, nil } -func getStatefulSetNames(clientset *kubernetes.Clientset, namespace string) ([]string, error) { - statefulsets, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) +func getStatefulSetNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + statefulSets, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } - names := make([]string, 0, len(statefulsets.Items)) - for _, statefulset := range statefulsets.Items { - names = append(names, statefulset.Name) + names := make([]string, 0, len(statefulSets.Items)) + for _, statefulSet := range statefulSets.Items { + names = append(names, statefulSet.Name) } return names, nil } -func extractUnusedHpas(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func extractUnusedHpas(clientset kubernetes.Interface, namespace string) ([]string, error) { deploymentNames, err := getDeploymentNames(clientset, namespace) if err != nil { return nil, err @@ -46,7 +46,7 @@ func extractUnusedHpas(clientset *kubernetes.Clientset, namespace string) ([]str if err != nil { return nil, err } - hpas, err := clientset.AutoscalingV1().HorizontalPodAutoscalers(namespace).List(context.TODO(), metav1.ListOptions{}) + hpas, err := clientset.AutoscalingV2().HorizontalPodAutoscalers(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func extractUnusedHpas(clientset *kubernetes.Clientset, namespace string) ([]str return diff, nil } -func processNamespaceHpas(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func processNamespaceHpas(clientset kubernetes.Interface, namespace string) ([]string, error) { unusedHpas, err := extractUnusedHpas(clientset, namespace) if err != nil { return nil, err @@ -79,7 +79,7 @@ func processNamespaceHpas(clientset *kubernetes.Clientset, namespace string) ([] return unusedHpas, nil } -func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -95,7 +95,7 @@ func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset *kubernete } -func GetUnusedHpasStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedHpasStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/hpas_test.go b/pkg/kor/hpas_test.go new file mode 100644 index 00000000..ab8a8bc7 --- /dev/null +++ b/pkg/kor/hpas_test.go @@ -0,0 +1,101 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestHpas(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + deploymentName := "test-deployment" + appLabels := map[string]string{} + + deployment1 := CreateTestDeployment(testNamespace, deploymentName, 1, appLabels) + hpa1 := CreateTestHpa(testNamespace, "test-hpa1", deploymentName, 1, 1) + + hpa2 := CreateTestHpa(testNamespace, "test-hpa2", "non-existing-deployment", 1, 1) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + + _, err = clientset.AutoscalingV2().HorizontalPodAutoscalers(testNamespace).Create(context.TODO(), hpa1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake Hpa: %v", err) + } + + _, err = clientset.AutoscalingV2().HorizontalPodAutoscalers(testNamespace).Create(context.TODO(), hpa2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake Hpa: %v", err) + } + + return clientset +} + +func TestExtractUnusedHpas(t *testing.T) { + clientset := createTestHpas(t) + + unusedHpas, err := extractUnusedHpas(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(unusedHpas) != 1 { + t.Errorf("Expected 1 unused HPA, got %d", len(unusedHpas)) + } + + if unusedHpas[0] != "test-hpa2" { + t.Errorf("Expected 'test-hpa2', got %s", unusedHpas[0]) + } +} + +func TestGetUnusedHpasStructured(t *testing.T) { + clientset := createTestHpas(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedHpasStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedHpasStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Hpa": {"test-hpa2"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/ingresses.go b/pkg/kor/ingresses.go index f6a4eaec..b657dc46 100644 --- a/pkg/kor/ingresses.go +++ b/pkg/kor/ingresses.go @@ -13,11 +13,11 @@ import ( "sigs.k8s.io/yaml" ) -func validateServiceBackend(kubeClient *kubernetes.Clientset, namespace string, backend *v1.IngressBackend) bool { +func validateServiceBackend(clientset kubernetes.Interface, namespace string, backend *v1.IngressBackend) bool { if backend.Service != nil { serviceName := backend.Service.Name - _, err := kubeClient.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) + _, err := clientset.CoreV1().Services(namespace).Get(context.TODO(), serviceName, metav1.GetOptions{}) if err != nil { return false } @@ -25,8 +25,8 @@ func validateServiceBackend(kubeClient *kubernetes.Clientset, namespace string, return true } -func retrieveUsedIngress(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - ingresses, err := kubeClient.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveUsedIngress(clientset kubernetes.Interface, namespace string) ([]string, error) { + ingresses, err := clientset.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -41,11 +41,11 @@ func retrieveUsedIngress(kubeClient *kubernetes.Clientset, namespace string) ([] used := true if ingress.Spec.DefaultBackend != nil { - used = validateServiceBackend(kubeClient, namespace, ingress.Spec.DefaultBackend) + used = validateServiceBackend(clientset, namespace, ingress.Spec.DefaultBackend) } for _, rule := range ingress.Spec.Rules { for _, path := range rule.HTTP.Paths { - used = validateServiceBackend(kubeClient, namespace, &path.Backend) + used = validateServiceBackend(clientset, namespace, &path.Backend) if used { break } @@ -61,8 +61,8 @@ func retrieveUsedIngress(kubeClient *kubernetes.Clientset, namespace string) ([] return usedIngresses, nil } -func retrieveIngressNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - ingresses, err := kubeClient.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveIngressNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + ingresses, err := clientset.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -73,12 +73,12 @@ func retrieveIngressNames(kubeClient *kubernetes.Clientset, namespace string) ([ return names, nil } -func processNamespaceIngresses(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - usedIngresses, err := retrieveUsedIngress(kubeClient, namespace) +func processNamespaceIngresses(clientset kubernetes.Interface, namespace string) ([]string, error) { + usedIngresses, err := retrieveUsedIngress(clientset, namespace) if err != nil { return nil, err } - ingressNames, err := retrieveIngressNames(kubeClient, namespace) + ingressNames, err := retrieveIngressNames(clientset, namespace) if err != nil { return nil, err } @@ -88,7 +88,7 @@ func processNamespaceIngresses(kubeClient *kubernetes.Clientset, namespace strin } -func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -103,7 +103,7 @@ func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset *kube } } -func GetUnusedIngressesStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedIngressesStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/ingresses_test.go b/pkg/kor/ingresses_test.go new file mode 100644 index 00000000..7812098c --- /dev/null +++ b/pkg/kor/ingresses_test.go @@ -0,0 +1,106 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestIngresses(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + service1 := CreateTestService(testNamespace, "my-service-1") + ingress1 := CreateTestIngress(testNamespace, "test-ingress-1", "my-service-1", "test-secret") + ingress2 := CreateTestIngress(testNamespace, "test-ingress-2", "my-service-2", "test-secret") + + _, err = clientset.CoreV1().Services(testNamespace).Create(context.TODO(), service1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Service", err) + } + _, err = clientset.NetworkingV1().Ingresses(testNamespace).Create(context.TODO(), ingress1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Ingress", err) + } + _, err = clientset.NetworkingV1().Ingresses(testNamespace).Create(context.TODO(), ingress2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Ingress", err) + } + + return clientset +} + +func TestRetrieveUsedIngress(t *testing.T) { + clientset := createTestIngresses(t) + + usedIngresses, err := retrieveUsedIngress(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(usedIngresses) != 1 { + t.Errorf("Expected 1 used Ingress objects, got %d", len(usedIngresses)) + } + + if !contains(usedIngresses, "test-ingress-1") { + t.Error("Expected specific Ingress objects in the list") + } +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func TestGetUnusedIngressesStructured(t *testing.T) { + clientset := createTestIngresses(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedIngressesStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedIngressesStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Ingresses": {"test-ingress-2"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go index f7c0f9b0..d2ebe91f 100644 --- a/pkg/kor/kor.go +++ b/pkg/kor/kor.go @@ -80,7 +80,7 @@ func GetKubeClient(kubeconfig string) *kubernetes.Clientset { return clientset } -func SetNamespaceList(namespaceLists IncludeExcludeLists, kubeClient *kubernetes.Clientset) []string { +func SetNamespaceList(namespaceLists IncludeExcludeLists, clientset kubernetes.Interface) []string { namespaces := make([]string, 0) namespacesMap := make(map[string]bool) if namespaceLists.IncludeListStr != "" && namespaceLists.ExcludeListStr != "" { @@ -89,7 +89,7 @@ func SetNamespaceList(namespaceLists IncludeExcludeLists, kubeClient *kubernetes } includeNamespaces := strings.Split(namespaceLists.IncludeListStr, ",") excludeNamespaces := strings.Split(namespaceLists.ExcludeListStr, ",") - namespaceList, err := kubeClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + namespaceList, err := clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) if err != nil { fmt.Fprintf(os.Stderr, "Failed to retrieve namespaces: %v\n", err) os.Exit(1) diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go index a45d1444..9caee0ee 100644 --- a/pkg/kor/multi.go +++ b/pkg/kor/multi.go @@ -9,42 +9,42 @@ import ( "sigs.k8s.io/yaml" ) -func retrieveNamespaceDiffs(kubeClient *kubernetes.Clientset, namespace string, resourceList []string) []ResourceDiff { +func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, resourceList []string) []ResourceDiff { var allDiffs []ResourceDiff for _, resource := range resourceList { switch resource { case "cm", "configmap", "configmaps": - namespaceCMDiff := getUnusedCMs(kubeClient, namespace) + namespaceCMDiff := getUnusedCMs(clientset, namespace) allDiffs = append(allDiffs, namespaceCMDiff) case "svc", "service", "services": - namespaceSVCDiff := getUnusedSVCs(kubeClient, namespace) + namespaceSVCDiff := getUnusedSVCs(clientset, namespace) allDiffs = append(allDiffs, namespaceSVCDiff) case "scrt", "secret", "secrets": - namespaceSecretDiff := getUnusedSecrets(kubeClient, namespace) + namespaceSecretDiff := getUnusedSecrets(clientset, namespace) allDiffs = append(allDiffs, namespaceSecretDiff) case "sa", "serviceaccount", "serviceaccounts": - namespaceSADiff := getUnusedServiceAccounts(kubeClient, namespace) + namespaceSADiff := getUnusedServiceAccounts(clientset, namespace) allDiffs = append(allDiffs, namespaceSADiff) case "deploy", "deployment", "deployments": - namespaceDeploymentDiff := getUnusedDeployments(kubeClient, namespace) + namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace) allDiffs = append(allDiffs, namespaceDeploymentDiff) case "sts", "statefulset", "statefulsets": - namespaceStatefulsetDiff := getUnusedStatefulsets(kubeClient, namespace) + namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace) allDiffs = append(allDiffs, namespaceStatefulsetDiff) case "role", "roles": - namespaceRoleDiff := getUnusedRoles(kubeClient, namespace) + namespaceRoleDiff := getUnusedRoles(clientset, namespace) allDiffs = append(allDiffs, namespaceRoleDiff) case "hpa", "horizontalpodautoscaler", "horizontalpodautoscalers": - namespaceHpaDiff := getUnusedHpas(kubeClient, namespace) + namespaceHpaDiff := getUnusedHpas(clientset, namespace) allDiffs = append(allDiffs, namespaceHpaDiff) case "pvc", "persistentvolumeclaim", "persistentvolumeclaims": - namespacePvcDiff := getUnusedPvcs(kubeClient, namespace) + namespacePvcDiff := getUnusedPvcs(clientset, namespace) allDiffs = append(allDiffs, namespacePvcDiff) case "ing", "ingress", "ingresses": - namespaceIngressDiff := getUnusedIngresses(kubeClient, namespace) + namespaceIngressDiff := getUnusedIngresses(clientset, namespace) allDiffs = append(allDiffs, namespaceIngressDiff) case "pdb", "poddisruptionbudget", "poddisruptionbudgets": - namespacePdbDiff := getUnusedPdbs(kubeClient, namespace) + namespacePdbDiff := getUnusedPdbs(clientset, namespace) allDiffs = append(allDiffs, namespacePdbDiff) default: fmt.Printf("resource type %q is not supported\n", resource) @@ -54,16 +54,16 @@ func retrieveNamespaceDiffs(kubeClient *kubernetes.Clientset, namespace string, } func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string) { - var kubeClient *kubernetes.Clientset + var clientset kubernetes.Interface var namespaces []string - kubeClient = GetKubeClient(kubeconfig) + clientset = GetKubeClient(kubeconfig) resourceList := strings.Split(resourceNames, ",") - namespaces = SetNamespaceList(includeExcludeLists, kubeClient) + namespaces = SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { - allDiffs := retrieveNamespaceDiffs(kubeClient, namespace, resourceList) + allDiffs := retrieveNamespaceDiffs(clientset, namespace, resourceList) output := FormatOutputAll(namespace, allDiffs) fmt.Println(output) fmt.Println() @@ -71,19 +71,19 @@ func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourc } func GetUnusedMultiStructured(includeExcludeLists IncludeExcludeLists, kubeconfig, outputFormat, resourceNames string) (string, error) { - var kubeClient *kubernetes.Clientset + var clientset kubernetes.Interface var namespaces []string - kubeClient = GetKubeClient(kubeconfig) + clientset = GetKubeClient(kubeconfig) resourceList := strings.Split(resourceNames, ",") - namespaces = SetNamespaceList(includeExcludeLists, kubeClient) + namespaces = SetNamespaceList(includeExcludeLists, clientset) // Create the JSON response object response := make(map[string]map[string][]string) for _, namespace := range namespaces { - allDiffs := retrieveNamespaceDiffs(kubeClient, namespace, resourceList) + allDiffs := retrieveNamespaceDiffs(clientset, namespace, resourceList) // Store the unused resources for each resource type in the JSON response resourceMap := make(map[string][]string) for _, diff := range allDiffs { diff --git a/pkg/kor/pdbs.go b/pkg/kor/pdbs.go index 893dae83..b0458a0b 100644 --- a/pkg/kor/pdbs.go +++ b/pkg/kor/pdbs.go @@ -12,7 +12,7 @@ import ( "sigs.k8s.io/yaml" ) -func processNamespacePdbs(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func processNamespacePdbs(clientset kubernetes.Interface, namespace string) ([]string, error) { var unusedPdbs []string pdbs, err := clientset.PolicyV1().PodDisruptionBudgets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -25,26 +25,30 @@ func processNamespacePdbs(clientset *kubernetes.Clientset, namespace string) ([] } selector := pdb.Spec.Selector + if len(selector.MatchLabels) == 0 { + unusedPdbs = append(unusedPdbs, pdb.Name) + continue + } deployments, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(selector), }) if err != nil { return nil, err } - statefulsets, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{ + statefulSets, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{ LabelSelector: metav1.FormatLabelSelector(selector), }) if err != nil { return nil, err } - if len(deployments.Items) == 0 && len(statefulsets.Items) == 0 { + if len(deployments.Items) == 0 && len(statefulSets.Items) == 0 { unusedPdbs = append(unusedPdbs, pdb.Name) } } return unusedPdbs, nil } -func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -59,7 +63,7 @@ func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset *kubernete } } -func GetUnusedPdbsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedPdbsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/pdbs_test.go b/pkg/kor/pdbs_test.go new file mode 100644 index 00000000..cad29c2c --- /dev/null +++ b/pkg/kor/pdbs_test.go @@ -0,0 +1,107 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func createTestPdbs(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + appLabels1 := map[string]string{ + "app": "my-app", + } + appLabels2 := map[string]string{} + + pdb1 := CreateTestPdb(testNamespace, "test-pdb1", appLabels1) + pdb2 := CreateTestPdb(testNamespace, "test-pdb2", appLabels1) + pdb3 := CreateTestPdb(testNamespace, "test-pdb3", appLabels2) + _, err = clientset.PolicyV1().PodDisruptionBudgets(testNamespace).Create(context.TODO(), pdb1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pdb", err) + } + + _, err = clientset.PolicyV1().PodDisruptionBudgets(testNamespace).Create(context.TODO(), pdb2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pdb", err) + } + + _, err = clientset.PolicyV1().PodDisruptionBudgets(testNamespace).Create(context.TODO(), pdb3, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pdb", err) + } + + deployment1 := CreateTestDeployment(testNamespace, "test-deployment2", 1, appLabels1) + _, err = clientset.AppsV1().Deployments(testNamespace).Create(context.TODO(), deployment1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake deployment: %v", err) + } + + sts1 := CreateTestStatefulSet(testNamespace, "test-sts2", 1, appLabels1) + _, err = clientset.AppsV1().StatefulSets(testNamespace).Create(context.TODO(), sts1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "StatefulSet", err) + } + + return clientset +} + +func TestProcessNamespacePdbs(t *testing.T) { + clientset := createTestPdbs(t) + + unusedPdbs, err := processNamespacePdbs(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(unusedPdbs) != 1 { + t.Errorf("Expected 1 unused pdb, got %d", len(unusedPdbs)) + } + + if unusedPdbs[0] != "test-pdb3" { + t.Errorf("Expected 'test-pdb3', got %s", unusedPdbs[0]) + } +} + +func TestGetUnusedPdbsStructured(t *testing.T) { + clientset := createTestPdbs(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedPdbsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedPdbsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Pdb": {"test-pdb3"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go index 8ac4cbc0..43035664 100644 --- a/pkg/kor/pvc.go +++ b/pkg/kor/pvc.go @@ -12,8 +12,8 @@ import ( "sigs.k8s.io/yaml" ) -func retreiveUsedPvcs(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) +func retreiveUsedPvcs(clientset kubernetes.Interface, namespace string) ([]string, error) { + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { fmt.Printf("Failed to list Pods: %v\n", err) os.Exit(1) @@ -30,8 +30,8 @@ func retreiveUsedPvcs(kubeClient *kubernetes.Clientset, namespace string) ([]str return usedPvcs, err } -func processNamespacePvcs(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - pvcs, err := kubeClient.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{}) +func processNamespacePvcs(clientset kubernetes.Interface, namespace string) ([]string, error) { + pvcs, err := clientset.CoreV1().PersistentVolumeClaims(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -44,7 +44,7 @@ func processNamespacePvcs(kubeClient *kubernetes.Clientset, namespace string) ([ pvcNames = append(pvcNames, pvc.Name) } - usedPvcs, err := retreiveUsedPvcs(kubeClient, namespace) + usedPvcs, err := retreiveUsedPvcs(clientset, namespace) if err != nil { return nil, err } @@ -53,7 +53,7 @@ func processNamespacePvcs(kubeClient *kubernetes.Clientset, namespace string) ([ return diff, nil } -func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -69,7 +69,7 @@ func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset *kubernete } -func GetUnusedPvcsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedPvcsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/pvc_test.go b/pkg/kor/pvc_test.go new file mode 100644 index 00000000..a0fd5cb5 --- /dev/null +++ b/pkg/kor/pvc_test.go @@ -0,0 +1,117 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestPvcs(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + var volumeList []corev1.Volume + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + pvc1 := CreateTestPvc(testNamespace, "test-pvc1") + pvc2 := CreateTestPvc(testNamespace, "test-pvc2") + _, err = clientset.CoreV1().PersistentVolumeClaims(testNamespace).Create(context.TODO(), pvc1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pvc", err) + } + + _, err = clientset.CoreV1().PersistentVolumeClaims(testNamespace).Create(context.TODO(), pvc2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pvc", err) + } + + testVolume := CreateTestVolume("test-volume", "test-pvc1") + volumeList = append(volumeList, *testVolume) + testPod := CreateTestPod(testNamespace, "test-pod", "test-sa", volumeList) + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), testPod, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pvc", err) + } + + return clientset +} + +func TestRetreiveUsedPvcs(t *testing.T) { + clientset := createTestPvcs(t) + usedPvcs, err := retreiveUsedPvcs(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(usedPvcs) != 1 { + t.Errorf("Expected 1 used pvc, got %d", len(usedPvcs)) + } + + if usedPvcs[0] != "test-pvc1" { + t.Errorf("Expected 'test-pvc1', got %s", usedPvcs[0]) + } +} + +func TestProcessNamespacePvcs(t *testing.T) { + clientset := createTestPvcs(t) + usedPvcs, err := processNamespacePvcs(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(usedPvcs) != 1 { + t.Errorf("Expected 1 unused pvc, got %d", len(usedPvcs)) + } + + if usedPvcs[0] != "test-pvc2" { + t.Errorf("Expected 'test-pvc2', got %s", usedPvcs[0]) + } +} + +func TestGetUnusedPvcsStructured(t *testing.T) { + clientset := createTestPvcs(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedPvcsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedPvcsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Pvc": {"test-pvc2"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go index a417fffd..93ddc555 100644 --- a/pkg/kor/roles.go +++ b/pkg/kor/roles.go @@ -12,25 +12,19 @@ import ( "sigs.k8s.io/yaml" ) -func retrieveUsedRoles(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func retrieveUsedRoles(clientset kubernetes.Interface, namespace string) ([]string, error) { // Get a list of all role bindings in the specified namespace roleBindings, err := clientset.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("failed to list role bindings in namespace %s: %v", namespace, err) } - // Create a map to store role binding names usedRoles := make(map[string]bool) - - // Populate the map with role binding names for _, rb := range roleBindings.Items { usedRoles[rb.RoleRef.Name] = true } - // Create a slice to store used role names var usedRoleNames []string - - // Extract used role names from the map for role := range usedRoles { usedRoleNames = append(usedRoleNames, role) } @@ -38,8 +32,8 @@ func retrieveUsedRoles(clientset *kubernetes.Clientset, namespace string) ([]str return usedRoleNames, nil } -func retrieveRoleNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - roles, err := kubeClient.RbacV1().Roles(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveRoleNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + roles, err := clientset.RbacV1().Roles(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -54,15 +48,15 @@ func retrieveRoleNames(kubeClient *kubernetes.Clientset, namespace string) ([]st return names, nil } -func processNamespaceRoles(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - usedRoles, err := retrieveUsedRoles(kubeClient, namespace) +func processNamespaceRoles(clientset kubernetes.Interface, namespace string) ([]string, error) { + usedRoles, err := retrieveUsedRoles(clientset, namespace) if err != nil { return nil, err } usedRoles = RemoveDuplicatesAndSort(usedRoles) - roleNames, err := retrieveRoleNames(kubeClient, namespace) + roleNames, err := retrieveRoleNames(clientset, namespace) if err != nil { return nil, err } @@ -72,7 +66,7 @@ func processNamespaceRoles(kubeClient *kubernetes.Clientset, namespace string) ( } -func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -87,7 +81,7 @@ func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset *kubernet } } -func GetUnusedRolesStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedRolesStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/roles_test.go b/pkg/kor/roles_test.go new file mode 100644 index 00000000..e4e61ae8 --- /dev/null +++ b/pkg/kor/roles_test.go @@ -0,0 +1,127 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestRoles(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + role1 := CreateTestRole(testNamespace, "test-role1") + role2 := CreateTestRole(testNamespace, "test-role2") + _, err = clientset.RbacV1().Roles(testNamespace).Create(context.TODO(), role1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Role", err) + } + + _, err = clientset.RbacV1().Roles(testNamespace).Create(context.TODO(), role2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Role", err) + } + + testRoleRef := CreateTestRoleRef("test-role1") + testRoleBinding := CreateTestRoleBinding(testNamespace, "test-rb", "test-sa", testRoleRef) + _, err = clientset.RbacV1().RoleBindings(testNamespace).Create(context.TODO(), testRoleBinding, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Role", err) + } + + return clientset +} +func TestRetrieveUsedRoles(t *testing.T) { + clientset := createTestRoles(t) + + usedRoles, err := retrieveUsedRoles(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(usedRoles) != 1 { + t.Errorf("Expected 1 used role, got %d", len(usedRoles)) + } + + if usedRoles[0] != "test-role1" { + t.Errorf("Expected 'test-role1', got %s", usedRoles[0]) + } +} + +func TestRetrieveRoleNames(t *testing.T) { + clientset := createTestRoles(t) + allRoles, err := retrieveRoleNames(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(allRoles) != 2 { + t.Errorf("Expected 2 roles, got %d", len(allRoles)) + } +} + +func TestProcessNamespaceRoles(t *testing.T) { + clientset := createTestRoles(t) + + unusedRoles, err := processNamespaceRoles(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(unusedRoles) != 1 { + t.Errorf("Expected 1 unused role, got %d", len(unusedRoles)) + } + + if unusedRoles[0] != "test-role2" { + t.Errorf("Expected 'test-role2', got %s", unusedRoles[0]) + } +} + +func TestGetUnusedRolesStructured(t *testing.T) { + clientset := createTestRoles(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedRolesStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedRolesStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Roles": {"test-role2"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go index 9c1b3e48..9a421336 100644 --- a/pkg/kor/secrets.go +++ b/pkg/kor/secrets.go @@ -19,7 +19,7 @@ var exceptionSecretTypes = []string{ `kubernetes.io/service-account-token`, } -func retrieveIngressTLS(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func retrieveIngressTLS(clientset kubernetes.Interface, namespace string) ([]string, error) { secretNames := make([]string, 0) ingressList, err := clientset.NetworkingV1().Ingresses(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -37,7 +37,7 @@ func retrieveIngressTLS(clientset *kubernetes.Clientset, namespace string) ([]st } -func retrieveUsedSecret(kubeClient *kubernetes.Clientset, namespace string) ([]string, []string, []string, []string, []string, []string, error) { +func retrieveUsedSecret(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, []string, []string, []string, error) { var envSecrets []string var envSecrets2 []string var volumeSecrets []string @@ -45,7 +45,7 @@ func retrieveUsedSecret(kubeClient *kubernetes.Clientset, namespace string) ([]s var initContainerEnvSecrets []string // Retrieve pods in the specified namespace - pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, nil, nil, nil, nil, nil, err } @@ -85,7 +85,7 @@ func retrieveUsedSecret(kubeClient *kubernetes.Clientset, namespace string) ([]s } } - tlsSecrets, err := retrieveIngressTLS(kubeClient, namespace) + tlsSecrets, err := retrieveIngressTLS(clientset, namespace) if err != nil { return nil, nil, nil, nil, nil, nil, err } @@ -93,8 +93,8 @@ func retrieveUsedSecret(kubeClient *kubernetes.Clientset, namespace string) ([]s return envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, nil } -func retrieveSecretNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - secrets, err := kubeClient.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveSecretNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + secrets, err := clientset.CoreV1().Secrets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -111,8 +111,8 @@ func retrieveSecretNames(kubeClient *kubernetes.Clientset, namespace string) ([] return names, nil } -func processNamespaceSecret(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(kubeClient, namespace) +func processNamespaceSecret(clientset kubernetes.Interface, namespace string) ([]string, error) { + envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, tlsSecrets, err := retrieveUsedSecret(clientset, namespace) if err != nil { return nil, err } @@ -124,7 +124,7 @@ func processNamespaceSecret(kubeClient *kubernetes.Clientset, namespace string) pullSecrets = RemoveDuplicatesAndSort(pullSecrets) tlsSecrets = RemoveDuplicatesAndSort(tlsSecrets) - secretNames, err := retrieveSecretNames(kubeClient, namespace) + secretNames, err := retrieveSecretNames(clientset, namespace) if err != nil { return nil, err } @@ -140,7 +140,7 @@ func processNamespaceSecret(kubeClient *kubernetes.Clientset, namespace string) } -func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -155,7 +155,7 @@ func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset *kubern } } -func GetUnusedSecretsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedSecretsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/secrets_test.go b/pkg/kor/secrets_test.go new file mode 100644 index 00000000..689218eb --- /dev/null +++ b/pkg/kor/secrets_test.go @@ -0,0 +1,283 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestSecrets(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + secret1 := CreateTestSecret(testNamespace, "test-secret1") + secret2 := CreateTestSecret(testNamespace, "test-secret2") + secret3 := CreateTestSecret(testNamespace, "test-secret3") + + pod1 := CreateTestPod(testNamespace, "pod-1", "", []corev1.Volume{ + {Name: "vol-1", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret1"}}}, + }) + + pod2 := CreateTestPod(testNamespace, "pod-2", "", []corev1.Volume{ + {Name: "vol-2", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret2"}}}, + }) + + pod3 := CreateTestPod(testNamespace, "pod-3", "", nil) + pod3.Spec.Containers = []corev1.Container{ + { + Env: []corev1.EnvVar{ + {Name: "ENV_VAR_1", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: secret1.ObjectMeta.Name}}}}, + }, + }, + } + + pod4 := CreateTestPod(testNamespace, "pod-4", "", nil) + pod4.Spec.Containers = []corev1.Container{ + { + EnvFrom: []corev1.EnvFromSource{ + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: secret1.ObjectMeta.Name}}}, + }, + }, + } + + pod5 := CreateTestPod(testNamespace, "pod-5", "", nil) + pod5.Spec.InitContainers = []corev1.Container{ + { + Env: []corev1.EnvVar{ + {Name: "INIT_ENV_VAR_1", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: secret1.ObjectMeta.Name}}}}, + }, + }, + } + + pod6 := CreateTestPod(testNamespace, "pod-6", "", nil) + pod6.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ + {Name: secret1.ObjectMeta.Name}, + {Name: secret2.ObjectMeta.Name}, + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod3, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod4, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod5, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), pod6, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake pod: %v", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Secret", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Secret", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret3, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Secret", err) + } + + return clientset +} + +func TestRetrieveIngressTLS(t *testing.T) { + clientset := fake.NewSimpleClientset() + + ingress1 := CreateTestIngress(testNamespace, "test-ingress-1", "my-service-1", "test-secret1") + secret1 := CreateTestSecret(testNamespace, "test-secret1") + secret2 := CreateTestSecret(testNamespace, "test-secret2") + + _, err := clientset.NetworkingV1().Ingresses(testNamespace).Create(context.TODO(), ingress1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Ingress", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Secret", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Secret", err) + } + + tlsSecrets, err := retrieveIngressTLS(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(tlsSecrets) != 1 { + t.Errorf("Expected 1 used Secret object, got %d", len(tlsSecrets)) + } + + if tlsSecrets[0] != "test-secret1" { + t.Errorf("Expected 'test-secret1', got %s", tlsSecrets[0]) + } + +} + +func TestRetrieveUsedSecret(t *testing.T) { + clientset := createTestSecrets(t) + + envSecrets, envSecrets2, volumeSecrets, initContainerEnvSecrets, pullSecrets, _, err := retrieveUsedSecret(clientset, testNamespace) + if err != nil { + t.Fatalf("Error retrieving used secrets: %v", err) + } + + expectedVolumeSecrets := []string{"test-secret1", "test-secret2"} + if !equalSlices(volumeSecrets, expectedVolumeSecrets) { + t.Errorf("Expected volume secrets %v, got %v", expectedVolumeSecrets, volumeSecrets) + } + + expectedEnvSecrets := []string{"test-secret1"} + if !equalSlices(envSecrets, expectedEnvSecrets) { + t.Errorf("Expected env secrets %v, got %v", expectedEnvSecrets, envSecrets) + } + + expectedEnvSecrets2 := []string{"test-secret1"} + if !equalSlices(envSecrets2, expectedEnvSecrets2) { + t.Errorf("Expected envFrom secrets %v, got %v", expectedEnvSecrets2, envSecrets2) + } + + expectedInitContainerEnvSecrets := []string{"test-secret1"} + if !equalSlices(initContainerEnvSecrets, expectedInitContainerEnvSecrets) { + t.Errorf("Expected initContainer env secrets %v, got %v", expectedInitContainerEnvSecrets, initContainerEnvSecrets) + } + + expectedPullSecrets := []string{"test-secret1", "test-secret2"} + if !equalSlices(pullSecrets, expectedPullSecrets) { + t.Errorf("Expected pull secrets %v, got %v", expectedPullSecrets, pullSecrets) + } + +} + +func TestRetrieveSecretNames(t *testing.T) { + clientset := fake.NewSimpleClientset() + + secret1 := CreateTestSecret(testNamespace, "secret-1") + secret2 := CreateTestSecret(testNamespace, "secret-2") + + _, err := clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake secret: %v", err) + } + + _, err = clientset.CoreV1().Secrets(testNamespace).Create(context.TODO(), secret2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake secret: %v", err) + } + + secretNames, err := retrieveSecretNames(clientset, testNamespace) + + if err != nil { + t.Fatalf("Error retrieving secret names: %v", err) + } + + expectedSecretNames := []string{"secret-1", "secret-2"} + if !equalSlices(secretNames, expectedSecretNames) { + t.Errorf("Expected secret names %v, got %v", expectedSecretNames, secretNames) + } +} + +func TestProcessNamespaceSecret(t *testing.T) { + clientset := createTestSecrets(t) + + unusedSecrets, err := processNamespaceSecret(clientset, testNamespace) + if err != nil { + t.Fatalf("Error retrieving unused secrets: %v", err) + } + + if len(unusedSecrets) != 1 { + t.Errorf("Expected 1 used Secret objects, got %d", len(unusedSecrets)) + } + + if !contains(unusedSecrets, "test-secret3") { + t.Error("Expected specific Secret in the list") + } + +} + +func TestGetUnusedSecretsStructured(t *testing.T) { + clientset := createTestSecrets(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedSecretsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedSecretsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Secrets": {"test-secret3"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func equalSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go index f835dc05..b7e90a61 100644 --- a/pkg/kor/serviceaccounts.go +++ b/pkg/kor/serviceaccounts.go @@ -15,7 +15,7 @@ var exceptionServiceAccounts = []ExceptionResource{ {ResourceName: "default", Namespace: "*"}, } -func getServiceAccountsFromClusterRoleBindings(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func getServiceAccountsFromClusterRoleBindings(clientset kubernetes.Interface, namespace string) ([]string, error) { // Get a list of all role bindings in the specified namespace roleBindings, err := clientset.RbacV1().ClusterRoleBindings().List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -42,7 +42,7 @@ func getServiceAccountsFromClusterRoleBindings(clientset *kubernetes.Clientset, return serviceAccounts, nil } -func getServiceAccountsFromRoleBindings(clientset *kubernetes.Clientset, namespace string) ([]string, error) { +func getServiceAccountsFromRoleBindings(clientset kubernetes.Interface, namespace string) ([]string, error) { // Get a list of all role bindings in the specified namespace roleBindings, err := clientset.RbacV1().RoleBindings(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { @@ -68,11 +68,11 @@ func getServiceAccountsFromRoleBindings(clientset *kubernetes.Clientset, namespa return serviceAccounts, nil } -func retrieveUsedSA(kubeClient *kubernetes.Clientset, namespace string) ([]string, []string, []string, error) { +func retrieveUsedSA(clientset kubernetes.Interface, namespace string) ([]string, []string, []string, error) { + var podServiceAccounts []string - // Retrieve pods in the specified namespace - pods, err := kubeClient.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) + pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, nil, nil, err } @@ -90,19 +90,19 @@ func retrieveUsedSA(kubeClient *kubernetes.Clientset, namespace string) ([]strin } } - roleServiceAccounts, err := getServiceAccountsFromRoleBindings(kubeClient, namespace) + roleServiceAccounts, err := getServiceAccountsFromRoleBindings(clientset, namespace) if err != nil { return nil, nil, nil, err } - clusterRoleServiceAccounts, err := getServiceAccountsFromClusterRoleBindings(kubeClient, namespace) + clusterRoleServiceAccounts, err := getServiceAccountsFromClusterRoleBindings(clientset, namespace) if err != nil { return nil, nil, nil, err } return podServiceAccounts, roleServiceAccounts, clusterRoleServiceAccounts, nil } -func retrieveServiceAccountNames(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - serviceaccounts, err := kubeClient.CoreV1().ServiceAccounts(namespace).List(context.TODO(), metav1.ListOptions{}) +func retrieveServiceAccountNames(clientset kubernetes.Interface, namespace string) ([]string, error) { + serviceaccounts, err := clientset.CoreV1().ServiceAccounts(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -117,8 +117,8 @@ func retrieveServiceAccountNames(kubeClient *kubernetes.Clientset, namespace str return names, nil } -func processNamespaceSA(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - usedServiceAccounts, roleServiceAccounts, clusterRoleServiceAccounts, err := retrieveUsedSA(kubeClient, namespace) +func processNamespaceSA(clientset kubernetes.Interface, namespace string) ([]string, error) { + usedServiceAccounts, roleServiceAccounts, clusterRoleServiceAccounts, err := retrieveUsedSA(clientset, namespace) if err != nil { return nil, err } @@ -129,7 +129,7 @@ func processNamespaceSA(kubeClient *kubernetes.Clientset, namespace string) ([]s usedServiceAccounts = append(append(usedServiceAccounts, roleServiceAccounts...), clusterRoleServiceAccounts...) - serviceAccountNames, err := retrieveServiceAccountNames(kubeClient, namespace) + serviceAccountNames, err := retrieveServiceAccountNames(clientset, namespace) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func processNamespaceSA(kubeClient *kubernetes.Clientset, namespace string) ([]s } -func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -154,7 +154,7 @@ func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset } } -func GetUnusedServiceAccountsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedServiceAccountsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/serviceaccounts_test.go b/pkg/kor/serviceaccounts_test.go new file mode 100644 index 00000000..a7e00887 --- /dev/null +++ b/pkg/kor/serviceaccounts_test.go @@ -0,0 +1,202 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestServiceAccounts(t *testing.T) *fake.Clientset { + + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + sa1 := CreateTestServiceAccount(testNamespace, "test-sa1") + sa2 := CreateTestServiceAccount(testNamespace, "test-sa2") + _, err = clientset.CoreV1().ServiceAccounts(testNamespace).Create(context.TODO(), sa1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "ServiceAccount", err) + } + + _, err = clientset.CoreV1().ServiceAccounts(testNamespace).Create(context.TODO(), sa2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "ServiceAccount", err) + } + + return clientset +} +func TestGetServiceAccountsFromClusterRoleBindings(t *testing.T) { + clientset := createTestServiceAccounts(t) + + clusterRoleBinding1 := CreateTestClusterRoleBinding(testNamespace, "test-crb1", "test-sa1") + _, err := clientset.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "clusterRoleBinding", err) + } + + serviceAccountWithCRB, err := getServiceAccountsFromClusterRoleBindings(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(serviceAccountWithCRB) != 1 { + t.Errorf("Expected 1 serviceAccount without CRB, got %d", len(serviceAccountWithCRB)) + } + + if serviceAccountWithCRB[0] != "test-sa1" { + t.Errorf("Expected 'test-sa1', got %s", serviceAccountWithCRB[0]) + } + +} + +func TestGetServiceAccountsFromRoleBindings(t *testing.T) { + clientset := createTestServiceAccounts(t) + + testRoleRef := CreateTestRoleRef("test-role") + roleBinding1 := CreateTestRoleBinding(testNamespace, "test-crb1", "test-sa1", testRoleRef) + _, err := clientset.RbacV1().RoleBindings(testNamespace).Create(context.TODO(), roleBinding1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "roleBinding", err) + } + + serviceAccountWithRB, err := getServiceAccountsFromRoleBindings(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(serviceAccountWithRB) != 1 { + t.Errorf("Expected 1 serviceAccount without CRB, got %d", len(serviceAccountWithRB)) + } + + if serviceAccountWithRB[0] != "test-sa1" { + t.Errorf("Expected 'test-sa1', got %s", serviceAccountWithRB[0]) + } +} + +func TestRetrieveUsedSA(t *testing.T) { + var volumeList []corev1.Volume + clientset := createTestServiceAccounts(t) + + testVolume := CreateTestVolume("test-volume1", "test-pvc") + volumeList = append(volumeList, *testVolume) + + podWithSA := CreateTestPod(testNamespace, "test-pod1", "test-sa1", volumeList) + _, err := clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), podWithSA, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pod", err) + } + serviceAccountUsedByPod, _, _, err := retrieveUsedSA(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(serviceAccountUsedByPod) != 2 { + t.Errorf("Expected 2 serviceAccount Used by pod, got %d", len(serviceAccountUsedByPod)) + } + + if serviceAccountUsedByPod[0] != "test-sa1" || serviceAccountUsedByPod[1] != "default" { + t.Errorf("Expected 'test-sa1' and 'default', got %s", serviceAccountUsedByPod[0]) + } + +} + +func TestRetrieveServiceAccountNames(t *testing.T) { + clientset := createTestServiceAccounts(t) + serviceAccountNames, err := retrieveServiceAccountNames(clientset, testNamespace) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if len(serviceAccountNames) != 2 { + t.Errorf("Expected 2 serviceAccounts , got %d", len(serviceAccountNames)) + } +} + +func TestProcessNamespaceSA(t *testing.T) { + clientset := createTestServiceAccounts(t) + var volumeList []corev1.Volume + + testVolume := CreateTestVolume("test-volume1", "test-pvc") + volumeList = append(volumeList, *testVolume) + + testRoleRef := CreateTestRoleRef("test-role") + + roleBinding1 := CreateTestRoleBinding(testNamespace, "test-crb1", "test-sa1", testRoleRef) + _, err := clientset.RbacV1().RoleBindings(testNamespace).Create(context.TODO(), roleBinding1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "roleBinding", err) + } + + podWithSA := CreateTestPod(testNamespace, "test-pod1", "test-sa1", volumeList) + _, err = clientset.CoreV1().Pods(testNamespace).Create(context.TODO(), podWithSA, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "Pod", err) + } + + unusedServiceAccounts, err := processNamespaceSA(clientset, testNamespace) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if len(unusedServiceAccounts) != 1 { + t.Errorf("Expected 2 serviceAccount Used by pod, got %d", len(unusedServiceAccounts)) + } + + if unusedServiceAccounts[0] != "test-sa2" { + t.Errorf("Expected 'test-sa2', got %s", unusedServiceAccounts[0]) + } +} + +func TestGetUnusedServiceAccountsStructured(t *testing.T) { + clientset := createTestServiceAccounts(t) + + clusterRoleBinding1 := CreateTestClusterRoleBinding(testNamespace, "test-crb1", "test-sa1") + _, err := clientset.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "clusterRoleBinding", err) + } + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedServiceAccountsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedServiceAccountsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "ServiceAccounts": {"test-sa2"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/services.go b/pkg/kor/services.go index 1f5008b4..14a2cb86 100644 --- a/pkg/kor/services.go +++ b/pkg/kor/services.go @@ -11,8 +11,8 @@ import ( "sigs.k8s.io/yaml" ) -func getEndpointsWithoutSubsets(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - endpointsList, err := kubeClient.CoreV1().Endpoints(namespace).List(context.TODO(), metav1.ListOptions{}) +func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string) ([]string, error) { + endpointsList, err := clientset.CoreV1().Endpoints(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } @@ -32,16 +32,7 @@ func getEndpointsWithoutSubsets(kubeClient *kubernetes.Clientset, namespace stri return endpointsWithoutSubsets, nil } -func ProcessNamespaceServices(clientset *kubernetes.Clientset, namespace string) ([]string, error) { - usedServices, err := getEndpointsWithoutSubsets(clientset, namespace) - if err != nil { - return nil, err - } - - return usedServices, nil -} - -func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { @@ -56,7 +47,7 @@ func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset *kuber } } -func GetUnusedServicesStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedServicesStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) diff --git a/pkg/kor/services_test.go b/pkg/kor/services_test.go new file mode 100644 index 00000000..4db5901e --- /dev/null +++ b/pkg/kor/services_test.go @@ -0,0 +1,92 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestServices(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + endpoint1 := CreateTestEndpoint(testNamespace, "test-endpoint1", 0) + endpoint2 := CreateTestEndpoint(testNamespace, "test-endpoint2", 1) + _, err = clientset.CoreV1().Endpoints(testNamespace).Create(context.TODO(), endpoint1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake endpoint: %v", err) + } + + _, err = clientset.CoreV1().Endpoints(testNamespace).Create(context.TODO(), endpoint2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake endpoint: %v", err) + } + + return clientset +} + +func TestGetEndpointsWithoutSubsets(t *testing.T) { + clientset := createTestServices(t) + + servicesWithoutEndpoints, err := ProcessNamespaceServices(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(servicesWithoutEndpoints) != 1 { + t.Errorf("Expected 1 service without endpoint, got %d", len(servicesWithoutEndpoints)) + } + + if servicesWithoutEndpoints[0] != "test-endpoint1" { + t.Errorf("Expected 'test-endpoint1', got %s", servicesWithoutEndpoints[0]) + } +} + +func TestGetUnusedServicesStructured(t *testing.T) { + clientset := createTestServices(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: testNamespace, + ExcludeListStr: "", + } + + output, err := GetUnusedServicesStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedServicesStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Services": {"test-endpoint1"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +} diff --git a/pkg/kor/statefulsets.go b/pkg/kor/statefulsets.go index 5713f6fd..6c7102e6 100644 --- a/pkg/kor/statefulsets.go +++ b/pkg/kor/statefulsets.go @@ -11,57 +11,44 @@ import ( "sigs.k8s.io/yaml" ) -func getStatefulsetsWithoutReplicas(kubeClient *kubernetes.Clientset, namespace string) ([]string, error) { - statefulsetsList, err := kubeClient.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) +func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace string) ([]string, error) { + statefulSetsList, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } - var statefulsetsWithoutReplicas []string + var statefulSetsWithoutReplicas []string - for _, statefulset := range statefulsetsList.Items { - if statefulset.Labels["kor/used"] == "true" { - continue - } - - if *statefulset.Spec.Replicas == 0 { - statefulsetsWithoutReplicas = append(statefulsetsWithoutReplicas, statefulset.Name) + for _, statefulSet := range statefulSetsList.Items { + if *statefulSet.Spec.Replicas == 0 { + statefulSetsWithoutReplicas = append(statefulSetsWithoutReplicas, statefulSet.Name) } } - return statefulsetsWithoutReplicas, nil -} - -func ProcessNamespaceStatefulsets(clientset *kubernetes.Clientset, namespace string) ([]string, error) { - usedServices, err := getStatefulsetsWithoutReplicas(clientset, namespace) - if err != nil { - return nil, err - } - - return usedServices, nil + return statefulSetsWithoutReplicas, nil } -func GetUnusedStatefulsets(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset) { +func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) { namespaces := SetNamespaceList(includeExcludeLists, clientset) for _, namespace := range namespaces { - diff, err := ProcessNamespaceStatefulsets(clientset, namespace) + diff, err := ProcessNamespaceStatefulSets(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue } - output := FormatOutput(namespace, diff, "Statefulsets") + output := FormatOutput(namespace, diff, "StatefulSets") fmt.Println(output) fmt.Println() } } -func GetUnusedStatefulsetsStructured(includeExcludeLists IncludeExcludeLists, clientset *kubernetes.Clientset, outputFormat string) (string, error) { +func GetUnusedStatefulSetsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) { namespaces := SetNamespaceList(includeExcludeLists, clientset) response := make(map[string]map[string][]string) for _, namespace := range namespaces { - diff, err := ProcessNamespaceStatefulsets(clientset, namespace) + diff, err := ProcessNamespaceStatefulSets(clientset, namespace) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err) continue diff --git a/pkg/kor/statefulsets_test.go b/pkg/kor/statefulsets_test.go new file mode 100644 index 00000000..5fc3c915 --- /dev/null +++ b/pkg/kor/statefulsets_test.go @@ -0,0 +1,94 @@ +package kor + +import ( + "context" + "encoding/json" + "reflect" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func createTestStatefulSets(t *testing.T) *fake.Clientset { + clientset := fake.NewSimpleClientset() + + _, err := clientset.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{Name: testNamespace}, + }, v1.CreateOptions{}) + + if err != nil { + t.Fatalf("Error creating namespace %s: %v", testNamespace, err) + } + + appLabels := map[string]string{} + + sts1 := CreateTestStatefulSet(testNamespace, "test-sts1", 0, appLabels) + sts2 := CreateTestStatefulSet(testNamespace, "test-sts2", 1, appLabels) + _, err = clientset.AppsV1().StatefulSets(testNamespace).Create(context.TODO(), sts1, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "statefulSet", err) + } + + _, err = clientset.AppsV1().StatefulSets(testNamespace).Create(context.TODO(), sts2, v1.CreateOptions{}) + if err != nil { + t.Fatalf("Error creating fake %s: %v", "statefulSet", err) + } + + return clientset +} + +func TestProcessNamespaceStatefulSets(t *testing.T) { + clientset := createTestStatefulSets(t) + + statefulSetsWithoutReplicas, err := ProcessNamespaceStatefulSets(clientset, testNamespace) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if len(statefulSetsWithoutReplicas) != 1 { + t.Errorf("Expected 1 deployment without replicas, got %d", len(statefulSetsWithoutReplicas)) + } + + if statefulSetsWithoutReplicas[0] != "test-sts1" { + t.Errorf("Expected 'test-sts1', got %s", statefulSetsWithoutReplicas[0]) + } +} + +func TestGetUnusedStatefulSetsStructured(t *testing.T) { + clientset := createTestStatefulSets(t) + + includeExcludeLists := IncludeExcludeLists{ + IncludeListStr: "", + ExcludeListStr: "", + } + + output, err := GetUnusedStatefulSetsStructured(includeExcludeLists, clientset, "json") + if err != nil { + t.Fatalf("Error calling GetUnusedStatefulSetsStructured: %v", err) + } + + expectedOutput := map[string]map[string][]string{ + testNamespace: { + "Statefulsets": {"test-sts1"}, + }, + } + + var actualOutput map[string]map[string][]string + if err := json.Unmarshal([]byte(output), &actualOutput); err != nil { + t.Fatalf("Error unmarshaling actual output: %v", err) + } + + if !reflect.DeepEqual(expectedOutput, actualOutput) { + t.Errorf("Expected output does not match actual output") + } +} + +func init() { + scheme.Scheme = runtime.NewScheme() + _ = appsv1.AddToScheme(scheme.Scheme) +}