Skip to content

Commit

Permalink
feat: add customization and filtering options for finding unused reso…
Browse files Browse the repository at this point in the history
…urces (#116)

* feat: add testify package

* feat: add the new FilterOptions struct

* chore: add optional FilterOptions to existing APIs

* feat: add the new persistent flag to root command

* fixup: replace wrong flag

* refactor: use newer-than and older-than flags for age filter

This commit changes the implementation of the HasIncludedAge function to use the newer-than and older-than flags instead of the MinAge and MaxAge flags. This makes the function more user-friendly and expressive, as users can specify queries like older-than 1h or newer-than 5m. The commit also updates the FilterOptions struct, the Validate function, and the flag descriptions accordingly.

* refactor: remove max-size and min-size flags

This commit removes max-size and min-size flags from filterOptions
flagset as well as all the size calculation functions defined in
resource_size.go. we may add them in the future.

* chore: update README file
  • Loading branch information
Azhovan authored Nov 1, 2023
1 parent 88aae2f commit d55b4a9
Show file tree
Hide file tree
Showing 37 changed files with 435 additions and 126 deletions.
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,17 @@ Kor provides various subcommands to identify and list unused resources. The avai

### Supported Flags
```
-e, --exclude-namespaces string Namespaces to be excluded, split by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
-h, --help help for kor
-n, --include-namespaces string Namespaces to run on, split by comma. Example: --include-namespace ns1,ns2,ns3.
-k, --kubeconfig string Path to kubeconfig file (optional)
--output string Output format (table or json) (default "table")
--slack-auth-token string Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.
--slack-channel string Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.
--slack-webhook-url string Slack webhook URL to send notifications to
-l, --exclude-labels string Selector to filter out, Example: --exclude-labels key1=value1,key2=value2.
-e, --exclude-namespaces string Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
-h, --help help for kor
-n, --include-namespaces string Namespaces to run on, splited by comma. Example: --include-namespace ns1,ns2,ns3.
-k, --kubeconfig string Path to kubeconfig file (optional)
--newer-than string The maximum age of the resources to be considered unused. This flag cannot be used together with older-than flag. Example: --newer-than=1h2m
--older-than string The minimum age of the resources to be considered unused. This flag cannot be used together with newer-than flag. Example: --older-than=1h2m
--output string Output format (table, json or yaml) (default "table")
--slack-auth-token string Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.
--slack-channel string Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.
--slack-webhook-url string Slack webhook URL to send notifications to
```

To use a specific subcommand, run `kor [subcommand] [flags]`.
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var allCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedAll(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedAll(includeExcludeLists,filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var configmapCmd = &cobra.Command{
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)
if response, err := kor.GetUnusedConfigmaps(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedConfigmaps(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var deployCmd = &cobra.Command{
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)
if response, err := kor.GetUnusedDeployments(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedDeployments(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/hpas.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var hpaCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedHpas(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedHpas(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/ingresses.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var ingressCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedIngresses(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedIngresses(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pdbs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var pdbCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedPdbs(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedPdbs(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var pvcCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedPvcs(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedPvcs(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var roleCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedRoles(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedRoles(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
13 changes: 13 additions & 0 deletions cmd/kor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var (
kubeconfig string
includeExcludeLists kor.IncludeExcludeLists
slackOpts kor.SlackOpts
filterOptions = kor.NewFilterOptions()
)

func Execute() {
Expand All @@ -52,8 +53,20 @@ func Execute() {
rootCmd.PersistentFlags().StringVar(&slackOpts.WebhookURL, "slack-webhook-url", "", "Slack webhook URL to send notifications to")
rootCmd.PersistentFlags().StringVar(&slackOpts.Channel, "slack-channel", "", "Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.")
rootCmd.PersistentFlags().StringVar(&slackOpts.Token, "slack-auth-token", "", "Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.")
addFilterOptionsFlag(rootCmd, filterOptions)

if err := filterOptions.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "Error while validating filter options '%s'", err)
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error while executing your CLI '%s'", err)
os.Exit(1)
}
}

func addFilterOptionsFlag(cmd *cobra.Command, opts *kor.FilterOptions) {
cmd.PersistentFlags().StringVarP(&opts.ExcludeLabels, "exclude-labels", "l", opts.ExcludeLabels, "Selector to filter out, Example: --exclude-labels key1=value1,key2=value2.")
cmd.PersistentFlags().StringVar(&opts.NewerThan, "newer-than", opts.NewerThan, "The maximum age of the resources to be considered unused. This flag cannot be used together with older-than flag. Example: --newer-than=1h2m")
cmd.PersistentFlags().StringVar(&opts.OlderThan, "older-than", opts.OlderThan, "The minimum age of the resources to be considered unused. This flag cannot be used together with newer-than flag. Example: --older-than=1h2m")
}
2 changes: 1 addition & 1 deletion cmd/kor/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var secretCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedSecrets(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedSecrets(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 1 addition & 1 deletion cmd/kor/statefulsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var stsCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
clientset := kor.GetKubeClient(kubeconfig)

if response, err := kor.GetUnusedStatefulSets(includeExcludeLists, clientset, outputFormat, slackOpts); err != nil {
if response, err := kor.GetUnusedStatefulSets(includeExcludeLists, filterOptions, clientset, outputFormat, slackOpts); err != nil {
fmt.Println(err)
} else {
fmt.Println(response)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5
github.com/prometheus/client_golang v1.17.0
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
Expand Down Expand Up @@ -45,6 +46,7 @@ require (
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/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
Expand Down
56 changes: 28 additions & 28 deletions pkg/kor/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type ResourceDiff struct {
diff []string
}

func getUnusedCMs(clientset kubernetes.Interface, namespace string) ResourceDiff {
cmDiff, err := processNamespaceCM(clientset, namespace)
func getUnusedCMs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
cmDiff, err := processNamespaceCM(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "configmaps", namespace, err)
}
Expand All @@ -37,8 +37,8 @@ func getUnusedSVCs(clientset kubernetes.Interface, namespace string) ResourceDif
return namespaceSVCDiff
}

func getUnusedSecrets(clientset kubernetes.Interface, namespace string) ResourceDiff {
secretDiff, err := processNamespaceSecret(clientset, namespace)
func getUnusedSecrets(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
secretDiff, err := processNamespaceSecret(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "secrets", namespace, err)
}
Expand All @@ -55,98 +55,98 @@ func getUnusedServiceAccounts(clientset kubernetes.Interface, namespace string)
return namespaceSADiff
}

func getUnusedDeployments(clientset kubernetes.Interface, namespace string) ResourceDiff {
deployDiff, err := ProcessNamespaceDeployments(clientset, namespace)
func getUnusedDeployments(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
deployDiff, err := ProcessNamespaceDeployments(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "deployments", namespace, err)
}
namespaceSADiff := ResourceDiff{"Deployment", deployDiff}
return namespaceSADiff
}

func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string) ResourceDiff {
stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace)
func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "statefulSets", namespace, err)
}
namespaceSADiff := ResourceDiff{"StatefulSet", stsDiff}
return namespaceSADiff
}

func getUnusedRoles(clientset kubernetes.Interface, namespace string) ResourceDiff {
roleDiff, err := processNamespaceRoles(clientset, namespace)
func getUnusedRoles(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
roleDiff, err := processNamespaceRoles(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "roles", namespace, err)
}
namespaceSADiff := ResourceDiff{"Role", roleDiff}
return namespaceSADiff
}

func getUnusedHpas(clientset kubernetes.Interface, namespace string) ResourceDiff {
hpaDiff, err := processNamespaceHpas(clientset, namespace)
func getUnusedHpas(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
hpaDiff, err := processNamespaceHpas(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "hpas", namespace, err)
}
namespaceHpaDiff := ResourceDiff{"Hpa", hpaDiff}
return namespaceHpaDiff
}

func getUnusedPvcs(clientset kubernetes.Interface, namespace string) ResourceDiff {
pvcDiff, err := processNamespacePvcs(clientset, namespace)
func getUnusedPvcs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
pvcDiff, err := processNamespacePvcs(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err)
}
namespacePvcDiff := ResourceDiff{"Pvc", pvcDiff}
return namespacePvcDiff
}

func getUnusedIngresses(clientset kubernetes.Interface, namespace string) ResourceDiff {
ingressDiff, err := processNamespaceIngresses(clientset, namespace)
func getUnusedIngresses(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
ingressDiff, err := processNamespaceIngresses(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "ingresses", namespace, err)
}
namespaceIngressDiff := ResourceDiff{"Ingress", ingressDiff}
return namespaceIngressDiff
}

func getUnusedPdbs(clientset kubernetes.Interface, namespace string) ResourceDiff {
pdbDiff, err := processNamespacePdbs(clientset, namespace)
func getUnusedPdbs(clientset kubernetes.Interface, namespace string, opts *FilterOptions) ResourceDiff {
pdbDiff, err := processNamespacePdbs(clientset, namespace, opts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pdbs", namespace, err)
}
namespacePdbDiff := ResourceDiff{"Pdb", pdbDiff}
return namespacePdbDiff
}

func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
func GetUnusedAll(includeExcludeLists IncludeExcludeLists, opts *FilterOptions, clientset kubernetes.Interface, outputFormat string, slackOpts SlackOpts) (string, error) {
var outputBuffer bytes.Buffer

namespaces := SetNamespaceList(includeExcludeLists, clientset)
response := make(map[string]map[string][]string)

for _, namespace := range namespaces {
var allDiffs []ResourceDiff
namespaceCMDiff := getUnusedCMs(clientset, namespace)
namespaceCMDiff := getUnusedCMs(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceCMDiff)
namespaceSVCDiff := getUnusedSVCs(clientset, namespace)
allDiffs = append(allDiffs, namespaceSVCDiff)
namespaceSecretDiff := getUnusedSecrets(clientset, namespace)
namespaceSecretDiff := getUnusedSecrets(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceSecretDiff)
namespaceSADiff := getUnusedServiceAccounts(clientset, namespace)
allDiffs = append(allDiffs, namespaceSADiff)
namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace)
namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceDeploymentDiff)
namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace)
namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceStatefulsetDiff)
namespaceRoleDiff := getUnusedRoles(clientset, namespace)
namespaceRoleDiff := getUnusedRoles(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceRoleDiff)
namespaceHpaDiff := getUnusedHpas(clientset, namespace)
namespaceHpaDiff := getUnusedHpas(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceHpaDiff)
namespacePvcDiff := getUnusedPvcs(clientset, namespace)
namespacePvcDiff := getUnusedPvcs(clientset, namespace, opts)
allDiffs = append(allDiffs, namespacePvcDiff)
namespaceIngressDiff := getUnusedIngresses(clientset, namespace)
namespaceIngressDiff := getUnusedIngresses(clientset, namespace, opts)
allDiffs = append(allDiffs, namespaceIngressDiff)
namespacePdbDiff := getUnusedPdbs(clientset, namespace)
namespacePdbDiff := getUnusedPdbs(clientset, namespace, opts)
allDiffs = append(allDiffs, namespacePdbDiff)

output := FormatOutputAll(namespace, allDiffs)
Expand Down
6 changes: 3 additions & 3 deletions pkg/kor/configmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func createTestConfigmaps(t *testing.T) *fake.Clientset {
func TestRetrieveConfigMapNames(t *testing.T) {
clientset := createTestConfigmaps(t)

configMapNames, err := retrieveConfigMapNames(clientset, testNamespace)
configMapNames, err := retrieveConfigMapNames(clientset, testNamespace, &FilterOptions{})

if err != nil {
t.Fatalf("Error retrieving configmap names: %v", err)
Expand All @@ -116,7 +116,7 @@ func TestRetrieveConfigMapNames(t *testing.T) {
func TestProcessNamespaceCM(t *testing.T) {
clientset := createTestConfigmaps(t)

diff, err := processNamespaceCM(clientset, testNamespace)
diff, err := processNamespaceCM(clientset, testNamespace, &FilterOptions{})
if err != nil {
t.Fatalf("Error processing namespace CM: %v", err)
}
Expand Down Expand Up @@ -182,7 +182,7 @@ func TestGetUnusedConfigmapsStructured(t *testing.T) {
Token: "",
}

output, err := GetUnusedConfigmaps(includeExcludeLists, clientset, "json", slackopts)
output, err := GetUnusedConfigmaps(includeExcludeLists, &FilterOptions{}, clientset, "json", slackopts)
if err != nil {
t.Fatalf("Error calling GetUnusedConfigmapsStructured: %v", err)
}
Expand Down
Loading

0 comments on commit d55b4a9

Please sign in to comment.