diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index cfe09a0b..7e848c45 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,4 +1,4 @@
-Please read [CONTRIBUTING.md](CONTRIBUTING.md) for additional information on contributing to this repository!
+
-## What this PR does / why we need it
+## What this PR does / why we need it?
## PR Checklist
- [ ] This PR adds K8s exceptions (false positives)
- [ ] This PR adds new code
-- [ ] This PR includes test for any new code
+- [ ] This PR includes tests for new/existing code
+- [ ] This PR adds docs
+
-## Github Issue
+## GitHub Issue
[XX-XX]
diff --git a/README.md b/README.md
index 06e531d0..57010692 100644
--- a/README.md
+++ b/README.md
@@ -123,10 +123,11 @@ Kor provides various subcommands to identify and list unused resources. The avai
```
--delete Delete unused resources
- -l, --exclude-labels string Selector to filter out, Example: --exclude-labels key1=value1,key2=value2. If --include-labels is set, --exclude-labels will be ignored.
- --exclude-namespaces strings Namespaces to be excluded, split by commas. Example: --exclude-namespaces ns1,ns2,ns3. If --include-namespaces is set, --exclude-namespaces will be ignored.
+ -l, --exclude-labels strings Selector to filter out, Example: --exclude-labels key1=value1,key2=value2. If --include-labels is set, --exclude-labels will be ignored.
+ -e, --exclude-namespaces strings Namespaces to be excluded, split by commas. Example: --exclude-namespaces ns1,ns2,ns3. If --include-namespaces is set, --exclude-namespaces will be ignored.
+ --group-by string Group output by (namespace, resource) (default "namespace")
-h, --help help for kor
- --include-labels string Selector to filter in, Example: --include-labels key1=value1,key2=value2.
+ --include-labels string Selector to filter in, Example: --include-labels key1=value1.(currently supports one label)
-n, --include-namespaces strings Namespaces to run on, split by commas. Example: --include-namespaces ns1,ns2,ns3. If set, non-namespaced resources will be ignored.
-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
@@ -151,7 +152,7 @@ For more information about each subcommand and its available flags, you can use
kor [subcommand] --help
```
-## Supported resources and limitations
+### Supported resources and limitations
| Resource | What it looks for | Known False Positives ⚠️ |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -169,12 +170,12 @@ kor [subcommand] --help
| CRDs | CRDs not used the cluster | |
| Pvs | PVs not bound to a PVC | |
| Pdbs | PDBs not used in Deployments
PDBs not used in StatefulSets | |
-| Jobs | Jobs status is completed | |
+| Jobs | Jobs status is completed
Jobs failed with no retries left | |
| ReplicaSets | replicaSets that specify replicas to 0 and has already completed it's work |
| DaemonSets | DaemonSets not scheduled on any nodes |
| StorageClasses | StorageClasses not used by any PVs/PVCs |
-## Deleting Unused resources
+### Deleting Unused resources
If you want to delete resources in an interactive way using Kor you can run:
@@ -194,7 +195,7 @@ To delete with no prompt ( ⚠️ use with caution):
kor configmap --include-namespaces my-namespace --delete --no-interactive
```
-## Ignore Resources
+### Ignore Resources
The resources labeled with:
@@ -204,7 +205,7 @@ kor/used=true
Will be ignored by kor even if they are unused. You can add this label to resources you want to ignore.
-## Force clean Resources
+### Force clean Resources
The resources labeled with:
@@ -214,6 +215,66 @@ kor/used=false
Will be cleaned always. This is a good way to mark resources for later cleanup.
+### Output Formats
+
+Kor supports three output formats: `table`, `json`, and `yaml`. The default output format is `table`.
+Additionally, you can use the `--group-by` flag to group the output by `namespace` or `resource`.
+
+#### Group by resource
+
+```sh
+kor all --group-by=resource --output=table
+```
+```
+Unused ConfigMaps:
++---+-----------+---------------+
+| # | NAMESPACE | RESOURCE NAME |
++---+-----------+---------------+
+| 1 | ns1 | cm1 |
+| 2 | ns1 | cm2 |
+| 3 | ns2 | cm3 |
++---+-----------+---------------+
+Unused Deployments:
++---+-----------+---------------+
+| # | NAMESPACE | RESOURCE NAME |
++---+-----------+---------------+
+| 1 | ns1 | deploy1 |
+| 2 | ns2 | deploy2 |
++---+-----------+---------------+
+Unused ReplicaSets:
++---+-----------+--------------------+
+| # | NAMESPACE | RESOURCE NAME |
++---+-----------+--------------------+
+| 1 | ns1 | deploy1-654d48b75f |
+| 2 | ns2 | deploy2-79f48888c6 |
++---+-----------+--------------------+
+```
+
+#### Group by namespace
+
+```sh
+kor all --group-by=namespace --output=table
+```
+```
+Unused resources in namespace: "ns1"
++---+---------------+--------------------+
+| # | RESOURCE TYPE | RESOURCE NAME |
++---+---------------+--------------------+
+| 1 | ConfigMap | cm1 |
+| 2 | ConfigMap | cm2 |
+| 3 | ReplicaSet | deploy1-654d48b75f |
+| 4 | Deployment | deploy1 |
++---+---------------+--------------------+
+Unused resources in namespace: "ns2"
++---+---------------+--------------------+
+| # | RESOURCE TYPE | RESOURCE NAME |
++---+---------------+--------------------+
+| 1 | ReplicaSet | deploy2-79f48888c6 |
+| 2 | ConfigMap | cm3 |
+| 3 | Deployment | deploy2 |
++---+---------------+--------------------+
+```
+
## In Cluster Usage
To use this tool inside the cluster running as a CronJob and sending the results to a Slack Webhook as raw text(has characters limits of 4000) or to a Slack channel by uploading a file(recommended), you can use the following commands:
diff --git a/charts/kor/Chart.yaml b/charts/kor/Chart.yaml
index 0b28be06..0e91b296 100644
--- a/charts/kor/Chart.yaml
+++ b/charts/kor/Chart.yaml
@@ -1,24 +1,6 @@
apiVersion: v2
name: kor
-description: A Helm chart for Kubernetes
-
-# A chart can be either an 'application' or a 'library' chart.
-#
-# Application charts are a collection of templates that can be packaged into versioned archives
-# to be deployed.
-#
-# Library charts provide useful utilities or functions for the chart developer. They're included as
-# a dependency of application charts to inject those utilities and functions into the rendering
-# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+description: A Kubernetes Helm Chart to discover orphaned resources using kor
type: application
-
-# This is the chart version. This version number should be incremented each time you make changes
-# to the chart and its templates, including the app version.
-# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.1.4
-
-# This is the version number of the application being deployed. This version number should be
-# incremented each time you make changes to the application. Versions are not expected to
-# follow Semantic Versioning. They should reflect the version the application is using.
-# It is recommended to use it with quotes.
-appVersion: "0.3.4"
+version: 0.1.6
+appVersion: "0.4.0"
diff --git a/charts/kor/README.md b/charts/kor/README.md
index a13219e5..8808b803 100644
--- a/charts/kor/README.md
+++ b/charts/kor/README.md
@@ -1,13 +1,14 @@
# kor
-  
+  
-A Helm chart for Kubernetes
+A Kubernetes Helm Chart to discover orphaned resources using kor
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
+| additionalLabels | object | `{}` | Custom labels to add into metadata |
| cronJob.args[0] | string | `"all"` | |
| cronJob.command[0] | string | `"kor"` | |
| cronJob.enabled | bool | `false` | |
@@ -41,17 +42,17 @@ A Helm chart for Kubernetes
| prometheusExporter.service.port | int | `8080` | |
| prometheusExporter.service.type | string | `"ClusterIP"` | |
| prometheusExporter.serviceMonitor.enabled | bool | `true` | |
-| prometheusExporter.serviceMonitor.interval | string | `"30s"` | |
-| prometheusExporter.serviceMonitor.labels | object | `{}` | |
+| prometheusExporter.serviceMonitor.interval | string | `"30s"` | Set how frequently Prometheus should scrape |
+| prometheusExporter.serviceMonitor.labels | object | `{}` | Service monitor labels |
| prometheusExporter.serviceMonitor.metricRelabelings | list | `[]` | |
-| prometheusExporter.serviceMonitor.namespace | string | `""` | |
+| prometheusExporter.serviceMonitor.namespace | string | `""` | Set the namespace the ServiceMonitor should be deployed, if empty namespace will be `.Release.Namespace` |
| prometheusExporter.serviceMonitor.relabelings | list | `[]` | |
-| prometheusExporter.serviceMonitor.targetLabels | list | `[]` | |
+| prometheusExporter.serviceMonitor.targetLabels | list | `[]` | Set of labels to transfer on the Kubernetes Service onto the target. |
| prometheusExporter.serviceMonitor.telemetryPath | string | `"/metrics"` | |
-| prometheusExporter.serviceMonitor.timeout | string | `"10s"` | |
-| serviceAccount.annotations | object | `{}` | |
-| serviceAccount.create | bool | `true` | |
-| serviceAccount.name | string | `""` | |
+| prometheusExporter.serviceMonitor.timeout | string | `"10s"` | Set timeout for scrape |
+| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
+| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
+| serviceAccount.name | string | `""` | If not set and create is true, a name is generated using the fullname template |
----------------------------------------------
-Autogenerated from chart metadata using [helm-docs v1.12.0](https://github.com/norwoodj/helm-docs/releases/v1.12.0)
+Autogenerated from chart metadata using [helm-docs v1.13.1](https://github.com/norwoodj/helm-docs/releases/v1.13.1)
diff --git a/charts/kor/templates/_helpers.tpl b/charts/kor/templates/_helpers.tpl
index 5106eec9..d33123b0 100644
--- a/charts/kor/templates/_helpers.tpl
+++ b/charts/kor/templates/_helpers.tpl
@@ -51,6 +51,9 @@ helm.sh/chart: {{ include "kor.chart" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- with .Values.additionalLabels }}
+{{ toYaml . }}
+{{- end }}
{{- end }}
{{/*
diff --git a/charts/kor/templates/cronjob.yaml b/charts/kor/templates/cronjob.yaml
index 596ef272..273f191e 100644
--- a/charts/kor/templates/cronjob.yaml
+++ b/charts/kor/templates/cronjob.yaml
@@ -12,6 +12,7 @@ spec:
spec:
template:
metadata:
+ {{- include "kor.labels" . | nindent 8 }}
name: {{ .Release.Name }}
spec:
serviceAccountName: {{ include "kor.serviceAccountName" . }}
diff --git a/charts/kor/templates/deployment.yaml b/charts/kor/templates/deployment.yaml
index 9e140525..b1566bfa 100644
--- a/charts/kor/templates/deployment.yaml
+++ b/charts/kor/templates/deployment.yaml
@@ -18,6 +18,7 @@ spec:
template:
metadata:
labels:
+ {{- include "kor.labels" . | nindent 8 }}
app: {{ .Values.prometheusExporter.name }}
spec:
serviceAccountName: {{ include "kor.serviceAccountName" . }}
diff --git a/charts/kor/values.yaml b/charts/kor/values.yaml
index 09091f24..4a251c0b 100644
--- a/charts/kor/values.yaml
+++ b/charts/kor/values.yaml
@@ -61,25 +61,28 @@ prometheusExporter:
serviceMonitor:
enabled: true
- # Set the namespace the ServiceMonitor should be deployed, if empty namespace will be .Release.Namespace
+ # -- Set the namespace the ServiceMonitor should be deployed, if empty namespace will be `.Release.Namespace`
namespace: ""
- # Service monitor labels
+ # -- Service monitor labels
labels: {}
- # Set how frequently Prometheus should scrape
+ # -- Set how frequently Prometheus should scrape
interval: 30s
telemetryPath: /metrics
- # Set timeout for scrape
+ # -- Set timeout for scrape
timeout: 10s
relabelings: []
- # Set of labels to transfer on the Kubernetes Service onto the target.
+ # -- Set of labels to transfer on the Kubernetes Service onto the target.
targetLabels: []
metricRelabelings: []
+# -- Custom labels to add into metadata
+additionalLabels: {}
+
serviceAccount:
- # Specifies whether a service account should be created
+ # -- Specifies whether a service account should be created
create: true
- # Annotations to add to the service account
+ # -- Annotations to add to the service account
annotations: {}
- # The name of the service account to use.
- # If not set and create is true, a name is generated using the fullname template
+ # -- The name of the service account to use.
+ # -- If not set and create is true, a name is generated using the fullname template
name: ""
diff --git a/cmd/kor/root.go b/cmd/kor/root.go
index 44285fcd..bcc2c98d 100644
--- a/cmd/kor/root.go
+++ b/cmd/kor/root.go
@@ -59,6 +59,7 @@ func init() {
rootCmd.PersistentFlags().BoolVar(&opts.DeleteFlag, "delete", false, "Delete unused resources")
rootCmd.PersistentFlags().BoolVar(&opts.NoInteractive, "no-interactive", false, "Do not prompt for confirmation when deleting resources. Be careful using this flag!")
rootCmd.PersistentFlags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Verbose output (print empty namespaces)")
+ rootCmd.PersistentFlags().StringVar(&opts.GroupBy, "group-by", "namespace", "Group output by (namespace, resource)")
addFilterOptionsFlag(rootCmd, filterOptions)
}
@@ -80,6 +81,6 @@ func addFilterOptionsFlag(cmd *cobra.Command, opts *filters.Options) {
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")
cmd.PersistentFlags().StringVar(&opts.IncludeLabels, "include-labels", opts.IncludeLabels, "Selector to filter in, Example: --include-labels key1=value1.(currently supports one label)")
- cmd.PersistentFlags().StringSliceVar(&opts.ExcludeNamespaces, "exclude-namespaces", opts.ExcludeNamespaces, "Namespaces to be excluded, split by commas. Example: --exclude-namespaces ns1,ns2,ns3. If --include-namespaces is set, --exclude-namespaces will be ignored.")
+ cmd.PersistentFlags().StringSliceVarP(&opts.ExcludeNamespaces, "exclude-namespaces", "e", opts.ExcludeNamespaces, "Namespaces to be excluded, split by commas. Example: --exclude-namespaces ns1,ns2,ns3. If --include-namespaces is set, --exclude-namespaces will be ignored.")
cmd.PersistentFlags().StringSliceVarP(&opts.IncludeNamespaces, "include-namespaces", "n", opts.IncludeNamespaces, "Namespaces to run on, split by commas. Example: --include-namespaces ns1,ns2,ns3. If set, non-namespaced resources will be ignored.")
}
diff --git a/go.mod b/go.mod
index 7f90491a..46798db5 100644
--- a/go.mod
+++ b/go.mod
@@ -5,14 +5,14 @@ go 1.22.0
toolchain go1.22.2
require (
- github.com/fatih/color v1.16.0
+ github.com/fatih/color v1.17.0
github.com/olekukonko/tablewriter v0.0.5
- github.com/prometheus/client_golang v1.19.0
+ github.com/prometheus/client_golang v1.19.1
github.com/spf13/cobra v1.8.0
- k8s.io/api v0.30.0
- k8s.io/apiextensions-apiserver v0.30.0
- k8s.io/apimachinery v0.30.0
- k8s.io/client-go v0.30.0
+ k8s.io/api v0.30.1
+ k8s.io/apiextensions-apiserver v0.30.1
+ k8s.io/apimachinery v0.30.1
+ k8s.io/client-go v0.30.1
k8s.io/utils v0.0.0-20230726121419-3b25d923346b
sigs.k8s.io/yaml v1.4.0
)
diff --git a/go.sum b/go.sum
index 67802709..1625cc8a 100644
--- a/go.sum
+++ b/go.sum
@@ -11,8 +11,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@@ -86,8 +86,8 @@ 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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
@@ -173,14 +173,14 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA=
-k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE=
-k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs=
-k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
-k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA=
-k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
-k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ=
-k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY=
+k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY=
+k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM=
+k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws=
+k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4=
+k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U=
+k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
+k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
+k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
diff --git a/kor b/kor
new file mode 100755
index 00000000..5db9950d
Binary files /dev/null and b/kor differ
diff --git a/pkg/kor/all.go b/pkg/kor/all.go
index 7f6ab15d..4dfea912 100644
--- a/pkg/kor/all.go
+++ b/pkg/kor/all.go
@@ -28,16 +28,22 @@ func getUnusedCMs(clientset kubernetes.Interface, namespace string, filterOpts *
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "configmaps", namespace, err)
}
- namespaceCMDiff := ResourceDiff{"ConfigMap", cmDiff}
+ namespaceCMDiff := ResourceDiff{
+ "ConfigMap",
+ cmDiff,
+ }
return namespaceCMDiff
}
func getUnusedSVCs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- svcDiff, err := ProcessNamespaceServices(clientset, namespace, filterOpts)
+ svcDiff, err := processNamespaceServices(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "services", namespace, err)
}
- namespaceSVCDiff := ResourceDiff{"Service", svcDiff}
+ namespaceSVCDiff := ResourceDiff{
+ "Service",
+ svcDiff,
+ }
return namespaceSVCDiff
}
@@ -46,7 +52,10 @@ func getUnusedSecrets(clientset kubernetes.Interface, namespace string, filterOp
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "secrets", namespace, err)
}
- namespaceSecretDiff := ResourceDiff{"Secret", secretDiff}
+ namespaceSecretDiff := ResourceDiff{
+ "Secret",
+ secretDiff,
+ }
return namespaceSecretDiff
}
@@ -55,25 +64,34 @@ func getUnusedServiceAccounts(clientset kubernetes.Interface, namespace string,
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "serviceaccounts", namespace, err)
}
- namespaceSADiff := ResourceDiff{"ServiceAccount", saDiff}
+ namespaceSADiff := ResourceDiff{
+ "ServiceAccount",
+ saDiff,
+ }
return namespaceSADiff
}
func getUnusedDeployments(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- deployDiff, err := ProcessNamespaceDeployments(clientset, namespace, filterOpts)
+ deployDiff, err := processNamespaceDeployments(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "deployments", namespace, err)
}
- namespaceSADiff := ResourceDiff{"Deployment", deployDiff}
+ namespaceSADiff := ResourceDiff{
+ "Deployment",
+ deployDiff,
+ }
return namespaceSADiff
}
func getUnusedStatefulSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- stsDiff, err := ProcessNamespaceStatefulSets(clientset, namespace, filterOpts)
+ stsDiff, err := processNamespaceStatefulSets(clientset, namespace, filterOpts)
if err != nil {
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
}
@@ -82,7 +100,10 @@ func getUnusedRoles(clientset kubernetes.Interface, namespace string, filterOpts
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "roles", namespace, err)
}
- namespaceSADiff := ResourceDiff{"Role", roleDiff}
+ namespaceSADiff := ResourceDiff{
+ "Role",
+ roleDiff,
+ }
return namespaceSADiff
}
@@ -91,7 +112,10 @@ func getUnusedClusterRoles(clientset kubernetes.Interface, filterOpts *filters.O
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s: %v\n", "clusterRoles", err)
}
- aDiff := ResourceDiff{"ClusterRole", clusterRoleDiff}
+ aDiff := ResourceDiff{
+ "ClusterRole",
+ clusterRoleDiff,
+ }
return aDiff
}
@@ -100,7 +124,10 @@ func getUnusedHpas(clientset kubernetes.Interface, namespace string, filterOpts
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "hpas", namespace, err)
}
- namespaceHpaDiff := ResourceDiff{"Hpa", hpaDiff}
+ namespaceHpaDiff := ResourceDiff{
+ "Hpa",
+ hpaDiff,
+ }
return namespaceHpaDiff
}
@@ -109,7 +136,10 @@ func getUnusedPvcs(clientset kubernetes.Interface, namespace string, filterOpts
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pvcs", namespace, err)
}
- namespacePvcDiff := ResourceDiff{"Pvc", pvcDiff}
+ namespacePvcDiff := ResourceDiff{
+ "Pvc",
+ pvcDiff,
+ }
return namespacePvcDiff
}
@@ -118,7 +148,10 @@ func getUnusedIngresses(clientset kubernetes.Interface, namespace string, filter
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "ingresses", namespace, err)
}
- namespaceIngressDiff := ResourceDiff{"Ingress", ingressDiff}
+ namespaceIngressDiff := ResourceDiff{
+ "Ingress",
+ ingressDiff,
+ }
return namespaceIngressDiff
}
@@ -127,7 +160,10 @@ func getUnusedPdbs(clientset kubernetes.Interface, namespace string, filterOpts
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pdbs", namespace, err)
}
- namespacePdbDiff := ResourceDiff{"Pdb", pdbDiff}
+ namespacePdbDiff := ResourceDiff{
+ "Pdb",
+ pdbDiff,
+ }
return namespacePdbDiff
}
@@ -136,7 +172,10 @@ func getUnusedCrds(apiExtClient apiextensionsclientset.Interface, dynamicClient
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s: %v\n", "Crds", err)
}
- allCrdDiff := ResourceDiff{"Crd", crdDiff}
+ allCrdDiff := ResourceDiff{
+ "Crd",
+ crdDiff,
+ }
return allCrdDiff
}
@@ -145,43 +184,58 @@ func getUnusedPvs(clientset kubernetes.Interface, filterOpts *filters.Options) R
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s: %v\n", "Pvs", err)
}
- allPvDiff := ResourceDiff{"Pv", pvDiff}
+ allPvDiff := ResourceDiff{
+ "Pv",
+ pvDiff,
+ }
return allPvDiff
}
func getUnusedPods(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- podDiff, err := ProcessNamespacePods(clientset, namespace, filterOpts)
+ podDiff, err := processNamespacePods(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "pods", namespace, err)
}
- namespacePodDiff := ResourceDiff{"Pod", podDiff}
+ namespacePodDiff := ResourceDiff{
+ "Pod",
+ podDiff,
+ }
return namespacePodDiff
}
func getUnusedJobs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- jobDiff, err := ProcessNamespaceJobs(clientset, namespace, filterOpts)
+ jobDiff, err := processNamespaceJobs(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "jobs", namespace, err)
}
- namespaceJobDiff := ResourceDiff{"Job", jobDiff}
+ namespaceJobDiff := ResourceDiff{
+ "Job",
+ jobDiff,
+ }
return namespaceJobDiff
}
func getUnusedReplicaSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- replicaSetDiff, err := ProcessNamespaceReplicaSets(clientset, namespace, filterOpts)
+ replicaSetDiff, err := processNamespaceReplicaSets(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "jobs", namespace, err)
}
- namespaceRSDiff := ResourceDiff{"ReplicaSet", replicaSetDiff}
+ namespaceRSDiff := ResourceDiff{
+ "ReplicaSet",
+ replicaSetDiff,
+ }
return namespaceRSDiff
}
func getUnusedDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ResourceDiff {
- dsDiff, err := ProcessNamespaceDaemonSets(clientset, namespace, filterOpts)
+ dsDiff, err := processNamespaceDaemonSets(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s namespace %s: %v\n", "DaemonSets", namespace, err)
}
- namespaceSADiff := ResourceDiff{"DaemonSet", dsDiff}
+ namespaceSADiff := ResourceDiff{
+ "DaemonSet",
+ dsDiff,
+ }
return namespaceSADiff
}
@@ -190,64 +244,63 @@ func getUnusedStorageClasses(clientset kubernetes.Interface, filterOpts *filters
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get %s: %v\n", "StorageClasses", err)
}
- allScDiff := ResourceDiff{"StorageClass", scDiff}
+ allScDiff := ResourceDiff{
+ "StorageClass",
+ scDiff,
+ }
return allScDiff
}
func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
-
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- var allDiffs []ResourceDiff
- namespaceCMDiff := getUnusedCMs(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceCMDiff)
- namespaceSVCDiff := getUnusedSVCs(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceSVCDiff)
- namespaceSecretDiff := getUnusedSecrets(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceSecretDiff)
- namespaceSADiff := getUnusedServiceAccounts(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceSADiff)
- namespaceDeploymentDiff := getUnusedDeployments(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceDeploymentDiff)
- namespaceStatefulsetDiff := getUnusedStatefulSets(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceStatefulsetDiff)
- namespaceRoleDiff := getUnusedRoles(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceRoleDiff)
- namespaceHpaDiff := getUnusedHpas(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceHpaDiff)
- namespacePvcDiff := getUnusedPvcs(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespacePvcDiff)
- namespacePodDiff := getUnusedPods(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespacePodDiff)
- namespaceIngressDiff := getUnusedIngresses(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceIngressDiff)
- namespacePdbDiff := getUnusedPdbs(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespacePdbDiff)
- namespaceJobDiff := getUnusedJobs(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceJobDiff)
- namespaceRSDiff := getUnusedReplicaSets(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceRSDiff)
- namespaceDaemonsetDiff := getUnusedDaemonSets(clientset, namespace, filterOpts)
- allDiffs = append(allDiffs, namespaceDaemonsetDiff)
-
- output := FormatOutputAll(namespace, allDiffs, opts)
-
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- for _, diff := range allDiffs {
- resourceMap[diff.resourceType] = diff.diff
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["ConfigMap"] = getUnusedCMs(clientset, namespace, filterOpts).diff
+ resources[namespace]["Service"] = getUnusedSVCs(clientset, namespace, filterOpts).diff
+ resources[namespace]["Secret"] = getUnusedSecrets(clientset, namespace, filterOpts).diff
+ resources[namespace]["ServiceAccount"] = getUnusedServiceAccounts(clientset, namespace, filterOpts).diff
+ resources[namespace]["Deployment"] = getUnusedDeployments(clientset, namespace, filterOpts).diff
+ resources[namespace]["StatefulSet"] = getUnusedStatefulSets(clientset, namespace, filterOpts).diff
+ resources[namespace]["Role"] = getUnusedRoles(clientset, namespace, filterOpts).diff
+ resources[namespace]["Hpa"] = getUnusedHpas(clientset, namespace, filterOpts).diff
+ resources[namespace]["Pvc"] = getUnusedPvcs(clientset, namespace, filterOpts).diff
+ resources[namespace]["Pod"] = getUnusedPods(clientset, namespace, filterOpts).diff
+ resources[namespace]["Ingress"] = getUnusedIngresses(clientset, namespace, filterOpts).diff
+ resources[namespace]["Pdb"] = getUnusedPdbs(clientset, namespace, filterOpts).diff
+ resources[namespace]["Job"] = getUnusedJobs(clientset, namespace, filterOpts).diff
+ resources[namespace]["ReplicaSet"] = getUnusedReplicaSets(clientset, namespace, filterOpts).diff
+ resources[namespace]["DaemonSet"] = getUnusedDaemonSets(clientset, namespace, filterOpts).diff
+ case "resource":
+ appendResources(resources, "ConfigMap", namespace, getUnusedCMs(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Service", namespace, getUnusedSVCs(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Secret", namespace, getUnusedSecrets(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "ServiceAccount", namespace, getUnusedServiceAccounts(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Deployment", namespace, getUnusedDeployments(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "StatefulSet", namespace, getUnusedStatefulSets(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Role", namespace, getUnusedRoles(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Hpa", namespace, getUnusedHpas(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Pvc", namespace, getUnusedPvcs(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Pod", namespace, getUnusedPods(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Ingress", namespace, getUnusedIngresses(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Pdb", namespace, getUnusedPdbs(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "Job", namespace, getUnusedJobs(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "ReplicaSet", namespace, getUnusedReplicaSets(clientset, namespace, filterOpts).diff)
+ appendResources(resources, "DaemonSet", namespace, getUnusedDaemonSets(clientset, namespace, filterOpts).diff)
}
- response[namespace] = resourceMap
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedAllNamespaced, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
@@ -259,44 +312,31 @@ func GetUnusedAllNamespaced(filterOpts *filters.Options, clientset kubernetes.In
}
func GetUnusedAllNonNamespaced(filterOpts *filters.Options, clientset kubernetes.Interface, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts Opts) (string, error) {
+ resources := make(map[string]map[string][]string)
+ switch opts.GroupBy {
+ case "namespace":
+ resources[""] = make(map[string][]string)
+ resources[""]["Crd"] = getUnusedCrds(apiExtClient, dynamicClient, filterOpts).diff
+ resources[""]["Pv"] = getUnusedPvs(clientset, filterOpts).diff
+ resources[""]["ClusterRole"] = getUnusedClusterRoles(clientset, filterOpts).diff
+ resources[""]["StorageClass"] = getUnusedStorageClasses(clientset, filterOpts).diff
+ case "resource":
+ appendResources(resources, "Crd", "", getUnusedCrds(apiExtClient, dynamicClient, filterOpts).diff)
+ appendResources(resources, "Pv", "", getUnusedPvs(clientset, filterOpts).diff)
+ appendResources(resources, "ClusterRole", "", getUnusedClusterRoles(clientset, filterOpts).diff)
+ appendResources(resources, "StorageClass", "", getUnusedStorageClasses(clientset, filterOpts).diff)
+ }
+
var outputBuffer bytes.Buffer
- response := make(map[string]map[string][]string)
-
- var allDiffs []ResourceDiff
- noNamespaceResourceMap := make(map[string][]string)
- crdDiff := getUnusedCrds(apiExtClient, dynamicClient, filterOpts)
- crdOutput := FormatOutputAll("", []ResourceDiff{crdDiff}, opts)
- outputBuffer.WriteString(crdOutput)
- outputBuffer.WriteString("\n")
- noNamespaceResourceMap[crdDiff.resourceType] = crdDiff.diff
-
- pvDiff := getUnusedPvs(clientset, filterOpts)
- pvOutput := FormatOutputAll("", []ResourceDiff{pvDiff}, opts)
- outputBuffer.WriteString(pvOutput)
- outputBuffer.WriteString("\n")
- noNamespaceResourceMap[pvDiff.resourceType] = pvDiff.diff
-
- clusterRoleDiff := getUnusedClusterRoles(clientset, filterOpts)
- clusterRoleOutput := FormatOutputAll("", []ResourceDiff{clusterRoleDiff}, opts)
- outputBuffer.WriteString(clusterRoleOutput)
- outputBuffer.WriteString("\n")
- noNamespaceResourceMap[clusterRoleDiff.resourceType] = clusterRoleDiff.diff
-
- storageClassDiff := getUnusedStorageClasses(clientset, filterOpts)
- storageClassOutput := FormatOutputAll("", []ResourceDiff{storageClassDiff}, opts)
- outputBuffer.WriteString(storageClassOutput)
- outputBuffer.WriteString("\n")
- noNamespaceResourceMap[storageClassDiff.resourceType] = storageClassDiff.diff
-
- output := FormatOutputAll("", allDiffs, opts)
-
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
- response[""] = noNamespaceResourceMap
-
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedAllNonNamespaced, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/clusterroles.go b/pkg/kor/clusterroles.go
index 139bf810..557c2a29 100644
--- a/pkg/kor/clusterroles.go
+++ b/pkg/kor/clusterroles.go
@@ -172,38 +172,34 @@ func processClusterRoles(clientset kubernetes.Interface, filterOpts *filters.Opt
}
func GetUnusedClusterRoles(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
-
- response := make(map[string]map[string][]string)
-
+ resources := make(map[string]map[string][]string)
diff, err := processClusterRoles(clientset, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process cluster role : %v\n", err)
}
-
- if len(diff) > 0 {
- // We consider cluster scope resources in "" (empty string) namespace, as it is common in k8s
- if response[""] == nil {
- response[""] = make(map[string][]string)
- }
- response[""]["ClusterRoles"] = diff
+ switch opts.GroupBy {
+ case "namespace":
+ resources[""] = make(map[string][]string)
+ resources[""]["ClusterRole"] = diff
+ case "resource":
+ appendResources(resources, "ClusterRole", "", diff)
}
-
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, "", "ClusterRole", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete clusterRole %s : %v\n", diff, err)
}
}
- output := FormatOutput("", diff, "ClusterRoles", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
- response[""]["ClusterRoles"] = diff
- }
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedClusterRoles, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/clusterroles_test.go b/pkg/kor/clusterroles_test.go
index 2b23a971..919f471d 100644
--- a/pkg/kor/clusterroles_test.go
+++ b/pkg/kor/clusterroles_test.go
@@ -95,7 +95,11 @@ func TestRetrieveUsedClusterRoles(t *testing.T) {
t.Errorf("Expected 3 used cluster role, got %d", len(usedClusterRoles))
}
- expectedRoles := []string{"test-clusterRole2", "test-clusterRole3", "test-clusterRole6"}
+ expectedRoles := []string{
+ "test-clusterRole2",
+ "test-clusterRole3",
+ "test-clusterRole6",
+ }
sort.Strings(usedClusterRoles)
t.Log(usedClusterRoles)
if !reflect.DeepEqual(usedClusterRoles, expectedRoles) {
@@ -140,6 +144,7 @@ func TestGetUnusedClusterRolesStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedClusterRoles(&filters.Options{}, clientset, "json", opts)
@@ -149,7 +154,10 @@ func TestGetUnusedClusterRolesStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
"": {
- "ClusterRoles": {"test-clusterRole1", "test-clusterRole5"},
+ "ClusterRole": {
+ "test-clusterRole1",
+ "test-clusterRole5",
+ },
},
}
diff --git a/pkg/kor/configmaps.go b/pkg/kor/configmaps.go
index 6a733cca..5b6acac7 100644
--- a/pkg/kor/configmaps.go
+++ b/pkg/kor/configmaps.go
@@ -150,35 +150,37 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string, filter
}
func GetUnusedConfigmaps(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- response := make(map[string]map[string][]string)
-
+ resources := make(map[string]map[string][]string)
for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceCM(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["ConfigMap"] = diff
+ case "resource":
+ appendResources(resources, "ConfigMap", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "ConfigMap", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete ConfigMap %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Configmaps", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["ConfigMap"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedCMs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/configmaps_test.go b/pkg/kor/configmaps_test.go
index 845df2e3..065c4f01 100644
--- a/pkg/kor/configmaps_test.go
+++ b/pkg/kor/configmaps_test.go
@@ -58,14 +58,20 @@ func createTestConfigmaps(t *testing.T) *fake.Clientset {
}
pod1 := CreateTestPod(testNamespace, "pod-1", "", []corev1.Volume{
- {Name: "vol-1", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: configmap1.ObjectMeta.Name}}}},
+ {
+ Name: "vol-1",
+ VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: configmap1.ObjectMeta.Name}}},
+ },
}, AppLabels)
pod2 := CreateTestPod(testNamespace, "pod-2", "", nil, AppLabels)
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}}}},
+ {
+ Name: "ENV_VAR_1",
+ ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: configmap1.ObjectMeta.Name}}},
+ },
},
},
}
@@ -83,7 +89,10 @@ func createTestConfigmaps(t *testing.T) *fake.Clientset {
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}}}},
+ {
+ Name: "INIT_ENV_VAR_1",
+ ValueFrom: &corev1.EnvVarSource{ConfigMapKeyRef: &corev1.ConfigMapKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: configmap2.ObjectMeta.Name}}},
+ },
},
},
}
@@ -120,7 +129,11 @@ func TestRetrieveConfigMapNames(t *testing.T) {
t.Fatalf("Error retrieving configmap names: %v", err)
}
- expectedConfigMapNames := []string{"configmap-1", "configmap-2", "configmap-3"}
+ expectedConfigMapNames := []string{
+ "configmap-1",
+ "configmap-2",
+ "configmap-3",
+ }
if !equalSlices(configMapNames, expectedConfigMapNames) {
t.Errorf("Expected configmap names %v, got %v", expectedConfigMapNames, configMapNames)
}
@@ -134,7 +147,10 @@ func TestProcessNamespaceCM(t *testing.T) {
t.Fatalf("Error processing namespace CM: %v", err)
}
- unusedConfigmaps := []string{"configmap-3", "configmap-5"}
+ unusedConfigmaps := []string{
+ "configmap-3",
+ "configmap-5",
+ }
if !equalSlices(diff, unusedConfigmaps) {
t.Errorf("Expected diff %v, got %v", unusedConfigmaps, diff)
}
@@ -149,7 +165,12 @@ func TestRetrieveUsedCM(t *testing.T) {
t.Fatalf("Error retrieving used ConfigMaps: %v", err)
}
- expectedVolumesCM := []string{"configmap-1", "kube-root-ca.crt"}
+ expectedVolumesCM := []string{
+ "configmap-1",
+ "kube-root-ca.crt",
+ "openshift-service-ca.crt",
+ }
+
if !equalSlices(volumesCM, expectedVolumesCM) {
t.Errorf("Expected volume configmaps %v, got %v", expectedVolumesCM, volumesCM)
}
@@ -185,6 +206,7 @@ func TestGetUnusedConfigmapsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedConfigmaps(&filters.Options{}, clientset, "json", opts)
@@ -194,7 +216,10 @@ func TestGetUnusedConfigmapsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "ConfigMap": {"configmap-3", "configmap-5"},
+ "ConfigMap": {
+ "configmap-3",
+ "configmap-5",
+ },
},
}
diff --git a/pkg/kor/crds.go b/pkg/kor/crds.go
index 1383395d..86f13c01 100644
--- a/pkg/kor/crds.go
+++ b/pkg/kor/crds.go
@@ -59,32 +59,30 @@ func processCrds(apiExtClient apiextensionsclientset.Interface, dynamicClient dy
return unusedCRDs, nil
}
-func GetUnusedCrds(filterOpts *filters.Options, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts Opts) (string, error) {
-
- var outputBuffer bytes.Buffer
+func GetUnusedCrds(_ *filters.Options, apiExtClient apiextensionsclientset.Interface, dynamicClient dynamic.Interface, outputFormat string, opts Opts) (string, error) {
+ resources := make(map[string]map[string][]string)
diff, err := processCrds(apiExtClient, dynamicClient, &filters.Options{})
-
- response := make(map[string]map[string][]string)
-
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process crds: %v\n", err)
}
- if len(diff) > 0 {
- // We consider cluster scope resources in "" (empty string) namespace, as it is common in k8s
- if response[""] == nil {
- response[""] = make(map[string][]string)
- }
- response[""]["Crd"] = diff
- }
- output := FormatOutput("", diff, "Crds", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
+ switch opts.GroupBy {
+ case "namespace":
+ resources[""] = make(map[string][]string)
+ resources[""]["Crd"] = diff
+ case "resource":
+ appendResources(resources, "Crd", "", diff)
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedCRDs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/daemonsets.go b/pkg/kor/daemonsets.go
index 7ef5e3b5..e597b1c8 100644
--- a/pkg/kor/daemonsets.go
+++ b/pkg/kor/daemonsets.go
@@ -17,7 +17,7 @@ import (
//go:embed exceptions/daemonsets/daemonsets.json
var daemonsetsConfig []byte
-func ProcessNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
daemonSetsList, err := clientset.AppsV1().DaemonSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -53,35 +53,37 @@ func ProcessNamespaceDaemonSets(clientset kubernetes.Interface, namespace string
}
func GetUnusedDaemonSets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespaceDaemonSets(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespaceDaemonSets(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["DaemonSet"] = diff
+ case "resource":
+ appendResources(resources, "DaemonSet", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "DaemonSet", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete DaemonSet %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "DaemonSets", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["DaemonSets"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedDaemonSets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/daemonsets_test.go b/pkg/kor/daemonsets_test.go
index 30468d34..c09c4f4a 100644
--- a/pkg/kor/daemonsets_test.go
+++ b/pkg/kor/daemonsets_test.go
@@ -65,7 +65,7 @@ func createTestDaemonSets(t *testing.T) *fake.Clientset {
func TestProcessNamespaceDaemonSets(t *testing.T) {
clientset := createTestDaemonSets(t)
- daemonSetsWithoutReplicas, err := ProcessNamespaceDaemonSets(clientset, testNamespace, &filters.Options{})
+ daemonSetsWithoutReplicas, err := processNamespaceDaemonSets(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
@@ -88,6 +88,7 @@ func TestGetUnusedDaemonSetsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedDaemonSets(&filters.Options{}, clientset, "json", opts)
@@ -97,7 +98,10 @@ func TestGetUnusedDaemonSetsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "DaemonSets": {"test-ds1", "test-ds4"},
+ "DaemonSet": {
+ "test-ds1",
+ "test-ds4",
+ },
},
}
diff --git a/pkg/kor/deployments.go b/pkg/kor/deployments.go
index a2686171..81356197 100644
--- a/pkg/kor/deployments.go
+++ b/pkg/kor/deployments.go
@@ -13,7 +13,7 @@ import (
"github.com/yonahd/kor/pkg/filters"
)
-func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespaceDeployments(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
deploymentsList, err := clientset.AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -40,35 +40,37 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin
}
func GetUnusedDeployments(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- response := make(map[string]map[string][]string)
-
+ resources := make(map[string]map[string][]string)
for _, namespace := range filterOpts.Namespaces(clientset) {
- diff, err := ProcessNamespaceDeployments(clientset, namespace, filterOpts)
+ diff, err := processNamespaceDeployments(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Deployment"] = diff
+ case "resource":
+ appendResources(resources, "Deployment", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Deployment", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Deployment %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Deployments", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Deployments"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedDeployments, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/deployments_test.go b/pkg/kor/deployments_test.go
index 5db9f7ae..9ea5b218 100644
--- a/pkg/kor/deployments_test.go
+++ b/pkg/kor/deployments_test.go
@@ -57,7 +57,7 @@ func createTestDeployments(t *testing.T) *fake.Clientset {
func TestProcessNamespaceDeployments(t *testing.T) {
clientset := createTestDeployments(t)
- deploymentsWithoutReplicas, err := ProcessNamespaceDeployments(clientset, testNamespace, &filters.Options{})
+ deploymentsWithoutReplicas, err := processNamespaceDeployments(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
@@ -80,6 +80,7 @@ func TestGetUnusedDeploymentsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedDeployments(&filters.Options{}, clientset, "json", opts)
@@ -89,7 +90,10 @@ func TestGetUnusedDeploymentsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Deployments": {"test-deployment1", "test-deployment4"},
+ "Deployment": {
+ "test-deployment1",
+ "test-deployment4",
+ },
},
}
diff --git a/pkg/kor/exceptions/clusterroles/clusterroles.json b/pkg/kor/exceptions/clusterroles/clusterroles.json
index e01578bc..bcf7fd59 100644
--- a/pkg/kor/exceptions/clusterroles/clusterroles.json
+++ b/pkg/kor/exceptions/clusterroles/clusterroles.json
@@ -83,6 +83,186 @@
{
"Namespace": "",
"ResourceName": "view"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:node-bootstrapper"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:aggregated-metrics-reader"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "alert-routing-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "cluster-debugger"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "global-operators-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "global-operators-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "global-operators-view"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "monitoring-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "monitoring-rules-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "monitoring-rules-view"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "olm-operators-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "olm-operators-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "olm-operators-view"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-cluster-monitoring-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-cluster-monitoring-edit"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-cluster-monitoring-view"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-main-attacher-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-main-provisioner-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-main-resizer-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-main-snapshotter-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-provisioner-configmap-and-secret-reader-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-provisioner-volumeattachment-reader-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-provisioner-volumesnapshot-reader-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-resizer-infrastructure-reader-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "openshift-csi-resizer-storageclass-reader-role"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "resource-metrics-server-resources"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "storage-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "sudoer"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:build-strategy-custom"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:image-auditor"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:image-pusher"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:image-signer"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:node-reader"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:aggregate-snapshots-to-storage-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:aggregate-to-storage-admin"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:hostaccess"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:hostmount"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:hostnetwork"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:nonroot"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:nonroot-v2"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:privileged"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:scc:restricted"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:openshift:templateservicebroker-client"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:router"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "system:sdn-manager"
}
]
}
diff --git a/pkg/kor/exceptions/configmaps/configmaps.json b/pkg/kor/exceptions/configmaps/configmaps.json
index 81d9cd5f..f75a95c3 100644
--- a/pkg/kor/exceptions/configmaps/configmaps.json
+++ b/pkg/kor/exceptions/configmaps/configmaps.json
@@ -4,6 +4,10 @@
"Namespace": "*",
"ResourceName": "kube-root-ca.crt"
},
+ {
+ "Namespace": "*",
+ "ResourceName": "openshift-service-ca.crt"
+ },
{
"Namespace": "gmp-system",
"ResourceName": "config-images"
@@ -76,6 +80,22 @@
"Namespace": "kube-system",
"ResourceName": "overlay-upgrade-data"
},
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "bootstrap"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "cluster-config-v1"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "root-ca"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "cluster-dns"
+ },
{
"Namespace": "kubernetes-dashboard",
"ResourceName": "kubernetes-dashboard-settings"
diff --git a/pkg/kor/exceptions/crds/crds.json b/pkg/kor/exceptions/crds/crds.json
index 6bac7012..9f176ab5 100644
--- a/pkg/kor/exceptions/crds/crds.json
+++ b/pkg/kor/exceptions/crds/crds.json
@@ -83,6 +83,286 @@
{
"Namespace": "",
"ResourceName": "volumesnapshots.snapshot.storage.k8s.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "etcdsnapshotfiles.k3s.cattle.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "helmchartconfigs.helm.cattle.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ingressrouteudps.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ingressrouteudps.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "middlewaretcps.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ingressroutetcps.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "serverstransports.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "middlewares.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "serverstransports.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "tlsstores.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "traefikservices.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "tlsstores.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "tlsoptions.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "traefikservices.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "serverstransporttcps.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "middlewaretcps.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ingressroutes.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "tlsoptions.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ingressroutetcps.traefik.containo.us"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "middlewares.traefik.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "adminpolicybasedexternalroutes.k8s.ovn.org"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "alertingrules.monitoring.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "alertmanagerconfigs.monitoring.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "alertrelabelconfigs.monitoring.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "baremetalhosts.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "bmceventsubscriptions.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "clusterautoscalers.autoscaling.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "clustercsidrivers.operator.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "clusterresourcequotas.quota.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "consoleexternalloglinks.console.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "consolelinks.console.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "consolenotifications.console.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "consolesamples.console.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "consoleyamlsamples.console.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "containerruntimeconfigs.machineconfiguration.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "controlplanemachinesets.machine.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "dnsrecords.ingress.operator.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "egressfirewalls.k8s.ovn.org"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "egressips.k8s.ovn.org"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "egressqoses.k8s.ovn.org"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "egressrouters.network.operator.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "egressservices.k8s.ovn.org"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "firmwareschemas.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "hardwaredata.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "hostfirmwaresettings.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "imagecontentpolicies.config.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "imagecontentsourcepolicies.operator.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "imagedigestmirrorsets.config.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "imagetagmirrorsets.config.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "installplans.operators.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "ippools.whereabouts.cni.cncf.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "kubeletconfigs.machineconfiguration.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "machineautoscalers.autoscaling.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "machines.machine.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "machinesets.machine.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "metal3remediations.infrastructure.cluster.x-k8s.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "metal3remediationtemplates.infrastructure.cluster.x-k8s.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "network-attachment-definitions.k8s.cni.cncf.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "operators.operators.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "overlappingrangeipreservations.whereabouts.cni.cncf.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "performanceprofiles.performance.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "podmonitors.monitoring.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "preprovisioningimages.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "probes.monitoring.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "projecthelmchartrepositories.helm.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "provisionings.metal3.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "rolebindingrestrictions.authorization.openshift.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "storagestates.migration.k8s.io"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "subscriptions.operators.coreos.com"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "thanosrulers.monitoring.coreos.com"
}
]
}
diff --git a/pkg/kor/exceptions/jobs/jobs.json b/pkg/kor/exceptions/jobs/jobs.json
new file mode 100644
index 00000000..af4155e6
--- /dev/null
+++ b/pkg/kor/exceptions/jobs/jobs.json
@@ -0,0 +1,16 @@
+{
+ "exceptionJobs": [
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "helm-install-traefik-crd"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "helm-install-traefik"
+ },
+ {
+ "Namespace": "assisted-installer",
+ "ResourceName": "assisted-installer-controller"
+ }
+ ]
+ }
diff --git a/pkg/kor/exceptions/secrets/secrets.json b/pkg/kor/exceptions/secrets/secrets.json
index a91be3fb..57232f28 100644
--- a/pkg/kor/exceptions/secrets/secrets.json
+++ b/pkg/kor/exceptions/secrets/secrets.json
@@ -4,6 +4,22 @@
"Namespace": "kube-system",
"ResourceName": "bootstrap-token-*"
},
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "k3s-serving"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "*.node-password.k3s"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "kube-cloud-cfg"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "kubeadmin"
+ },
{
"Namespace": "kubernetes-dashboard",
"ResourceName": "kubernetes-dashboard-certs"
@@ -15,6 +31,10 @@
{
"Namespace": "kubernetes-dashboard",
"ResourceName": "kubernetes-dashboard-key-holder"
+ },
+ {
+ "Namespace": "assisted-installer",
+ "ResourceName": "assisted-installer-controller-dockercfg-*"
}
]
}
diff --git a/pkg/kor/exceptions/services/services.json b/pkg/kor/exceptions/services/services.json
index bb326184..ba48335d 100644
--- a/pkg/kor/exceptions/services/services.json
+++ b/pkg/kor/exceptions/services/services.json
@@ -7,6 +7,10 @@
{
"Namespace": "kube-system",
"ResourceName": "vpa-recommender"
+ },
+ {
+ "Namespace": "kube-system",
+ "ResourceName": "docker.io-hostpath"
}
]
}
diff --git a/pkg/kor/exceptions/storageclasses/storageclasses.json b/pkg/kor/exceptions/storageclasses/storageclasses.json
index 6c1d97a8..7e2dd028 100644
--- a/pkg/kor/exceptions/storageclasses/storageclasses.json
+++ b/pkg/kor/exceptions/storageclasses/storageclasses.json
@@ -43,6 +43,10 @@
{
"Namespace": "",
"ResourceName": "standard-rwo"
+ },
+ {
+ "Namespace": "",
+ "ResourceName": "local-path"
}
]
}
diff --git a/pkg/kor/finalizers.go b/pkg/kor/finalizers.go
index 09154897..a01a4bf7 100644
--- a/pkg/kor/finalizers.go
+++ b/pkg/kor/finalizers.go
@@ -97,9 +97,8 @@ func GetUnusedfinalizers(filterOpts *filters.Options, clientset kubernetes.Inter
allDiffs[gvr.Resource] = resourceDiff
}
- output := FormatOutputFromMap(namespace, allDiffs, opts)
+ output := formatOutputForNamespace(namespace, allDiffs, opts)
outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
response[namespace] = allDiffs
}
diff --git a/pkg/kor/hpas.go b/pkg/kor/hpas.go
index 62de0166..b4a8047a 100644
--- a/pkg/kor/hpas.go
+++ b/pkg/kor/hpas.go
@@ -81,36 +81,37 @@ func processNamespaceHpas(clientset kubernetes.Interface, namespace string, filt
}
func GetUnusedHpas(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceHpas(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Hpa"] = diff
+ case "resource":
+ appendResources(resources, "Hpa", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "HPA", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete HPA %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "HPAs", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Hpa"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedHpas, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/hpas_test.go b/pkg/kor/hpas_test.go
index 961ce100..7f0fc950 100644
--- a/pkg/kor/hpas_test.go
+++ b/pkg/kor/hpas_test.go
@@ -89,6 +89,7 @@ func TestGetUnusedHpasStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedHpas(&filters.Options{}, clientset, "json", opts)
@@ -98,7 +99,10 @@ func TestGetUnusedHpasStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Hpa": {"test-hpa2", "test-hpa4"},
+ "Hpa": {
+ "test-hpa2",
+ "test-hpa4",
+ },
},
}
diff --git a/pkg/kor/ingresses.go b/pkg/kor/ingresses.go
index 431a22f9..f4ad87b0 100644
--- a/pkg/kor/ingresses.go
+++ b/pkg/kor/ingresses.go
@@ -107,36 +107,37 @@ func processNamespaceIngresses(clientset kubernetes.Interface, namespace string,
}
func GetUnusedIngresses(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceIngresses(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Ingress"] = diff
+ case "resource":
+ appendResources(resources, "Ingress", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Ingress", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Ingress %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Ingresses", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Ingresses"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedIngresses, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/ingresses_test.go b/pkg/kor/ingresses_test.go
index 751d2ef0..f1c1ce43 100644
--- a/pkg/kor/ingresses_test.go
+++ b/pkg/kor/ingresses_test.go
@@ -96,6 +96,7 @@ func TestGetUnusedIngressesStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedIngresses(&filters.Options{}, clientset, "json", opts)
@@ -105,7 +106,10 @@ func TestGetUnusedIngressesStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Ingresses": {"test-ingress-2", "test-ingress-4"},
+ "Ingress": {
+ "test-ingress-2",
+ "test-ingress-4",
+ },
},
}
diff --git a/pkg/kor/jobs.go b/pkg/kor/jobs.go
index 6dc131dc..3fb4cb4c 100644
--- a/pkg/kor/jobs.go
+++ b/pkg/kor/jobs.go
@@ -3,22 +3,32 @@ package kor
import (
"bytes"
"context"
+ _ "embed"
"encoding/json"
"fmt"
"os"
+ batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"github.com/yonahd/kor/pkg/filters"
)
-func ProcessNamespaceJobs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+//go:embed exceptions/jobs/jobs.json
+var jobsConfig []byte
+
+func processNamespaceJobs(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
jobsList, err := clientset.BatchV1().Jobs(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
}
+ config, err := unmarshalConfig(jobsConfig)
+ if err != nil {
+ return nil, err
+ }
+
var unusedJobNames []string
for _, job := range jobsList.Items {
@@ -26,9 +36,22 @@ func ProcessNamespaceJobs(clientset kubernetes.Interface, namespace string, filt
continue
}
+ if isResourceException(job.Name, job.Namespace, config.ExceptionJobs) {
+ continue
+ }
+
// if the job has completionTime and succeeded count greater than zero, think the job is completed
if job.Status.CompletionTime != nil && job.Status.Succeeded > 0 {
unusedJobNames = append(unusedJobNames, job.Name)
+ continue
+ } else {
+ // Check if the job has a condition indicating it has exceeded the backoff limit
+ for _, condition := range job.Status.Conditions {
+ if condition.Type == batchv1.JobFailed && condition.Reason == "BackoffLimitExceeded" {
+ unusedJobNames = append(unusedJobNames, job.Name)
+ break
+ }
+ }
}
}
@@ -36,36 +59,37 @@ func ProcessNamespaceJobs(clientset kubernetes.Interface, namespace string, filt
}
func GetUnusedJobs(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespaceJobs(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespaceJobs(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Job"] = diff
+ case "resource":
+ appendResources(resources, "Job", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Job", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Job %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Job", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Jobs"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedJobs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/jobs_test.go b/pkg/kor/jobs_test.go
index e0e750a9..6fe0235b 100644
--- a/pkg/kor/jobs_test.go
+++ b/pkg/kor/jobs_test.go
@@ -71,23 +71,44 @@ func createTestJobs(t *testing.T) *fake.Clientset {
t.Fatalf("Error creating fake job: %v", err)
}
+ job5 := CreateTestJob(testNamespace, "test-job5", &batchv1.JobStatus{
+ Succeeded: 0,
+ Failed: 1,
+ Conditions: []batchv1.JobCondition{
+ {
+ Type: batchv1.JobFailed,
+ Status: corev1.ConditionTrue,
+ Reason: "BackoffLimitExceeded",
+ Message: "Job has reached the specified backoff limit",
+ },
+ },
+ }, AppLabels)
+
+ _, err = clientset.BatchV1().Jobs(testNamespace).Create(context.TODO(), job5, v1.CreateOptions{})
+ if err != nil {
+ t.Fatalf("Error creating fake job: %v", err)
+ }
+
return clientset
}
func TestProcessNamespaceJobs(t *testing.T) {
clientset := createTestJobs(t)
- completedJobs, err := ProcessNamespaceJobs(clientset, testNamespace, &filters.Options{})
+ unusedJobs, err := processNamespaceJobs(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
- if len(completedJobs) != 2 {
- t.Errorf("Expected 2 job been completed, got %d", len(completedJobs))
+ if len(unusedJobs) != 3 {
+ t.Errorf("Expected 3 jobs unused got %d", len(unusedJobs))
}
- if completedJobs[0] != "test-job2" && completedJobs[1] != "test-job4" {
- t.Errorf("job2', got %s", completedJobs[0])
+ expectedJobs := []string{"test-job2", "test-job4", "test-job5"}
+ for _, jobName := range expectedJobs {
+ if !contains(unusedJobs, jobName) {
+ t.Errorf("Expected job %s to be considered unused, but it was not found", jobName)
+ }
}
}
@@ -100,6 +121,7 @@ func TestGetUnusedJobsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedJobs(&filters.Options{}, clientset, "json", opts)
@@ -109,7 +131,11 @@ func TestGetUnusedJobsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Jobs": {"test-job2", "test-job4"},
+ "Job": {
+ "test-job2",
+ "test-job4",
+ "test-job5",
+ },
},
}
diff --git a/pkg/kor/kor.go b/pkg/kor/kor.go
index 6df7a425..650d9c83 100644
--- a/pkg/kor/kor.go
+++ b/pkg/kor/kor.go
@@ -38,6 +38,7 @@ type Config struct {
ExceptionServiceAccounts []ExceptionResource `json:"exceptionServiceAccounts"`
ExceptionServices []ExceptionResource `json:"exceptionServices"`
ExceptionStorageClasses []ExceptionResource `json:"exceptionStorageClasses"`
+ ExceptionJobs []ExceptionResource `json:"exceptionJobs"`
// Add other configurations if needed
}
@@ -48,6 +49,7 @@ type Opts struct {
WebhookURL string
Channel string
Token string
+ GroupBy string
}
func RemoveDuplicatesAndSort(slice []string) []string {
@@ -129,116 +131,125 @@ func GetDynamicClient(kubeconfig string) *dynamic.DynamicClient {
return clientset
}
-func FormatOutput(namespace string, resources []string, resourceType string, opts Opts) string {
- if opts.Verbose && len(resources) == 0 {
- return fmt.Sprintf("No unused %s found in the namespace: %s \n", resourceType, namespace)
- } else if len(resources) == 0 {
- return ""
+func appendResources(resources map[string]map[string][]string, resourceType, namespace string, diff []string) {
+ for _, d := range diff {
+ if _, ok := resources[resourceType]; !ok {
+ resources[resourceType] = make(map[string][]string)
+ }
+ resources[resourceType][namespace] = append(resources[resourceType][namespace], d)
}
+}
- var buf bytes.Buffer
- table := tablewriter.NewWriter(&buf)
- table.SetHeader([]string{
- "#",
- "Resource Name",
- })
-
- for i, name := range resources {
- table.Append([]string{
- fmt.Sprintf("%d", i+1),
- name,
- })
+func getTableHeader(groupBy string) []string {
+ switch groupBy {
+ case "namespace":
+ return []string{
+ "#",
+ "RESOURCE TYPE",
+ "RESOURCE NAME",
+ }
+ case "resource":
+ return []string{
+ "#",
+ "NAMESPACE",
+ "RESOURCE NAME",
+ }
+ default:
+ return nil
}
+}
- table.Render()
+func getTableRow(index int, columns ...string) []string {
+ row := make([]string, 0, len(columns)+1)
+ row = append(row, fmt.Sprintf("%d", index+1))
+ row = append(row, columns...)
+ return row
+}
- return fmt.Sprintf("Unused %s in Namespace: %s\n%s", resourceType, namespace, buf.String())
+// FormatOutput formats the output based on the group by option
+func FormatOutput(resources map[string]map[string][]string, opts Opts) bytes.Buffer {
+ var output bytes.Buffer
+ switch opts.GroupBy {
+ case "namespace":
+ for namespace, diffs := range resources {
+ output.WriteString(formatOutputForNamespace(namespace, diffs, opts))
+ }
+ case "resource":
+ for resource, diffs := range resources {
+ output.WriteString(formatOutputForResource(resource, diffs, opts))
+ }
+ }
+ return output
}
-func FormatOutputFromMap(namespace string, allDiffs map[string][]string, opts Opts) string {
- i := 0
+func formatOutputForResource(resource string, resources map[string][]string, opts Opts) string {
+ if len(resources) == 0 {
+ if opts.Verbose {
+ return fmt.Sprintf("No unused %ss found\n", resource)
+ }
+ return ""
+ }
var buf bytes.Buffer
table := tablewriter.NewWriter(&buf)
- table.SetHeader([]string{
- "#",
- "Resource Type",
- "Resource Name",
- })
-
- // TODO parse resourceType, diff
-
- allEmpty := true
- for resourceType, diff := range allDiffs {
- if len(diff) == 0 {
- continue
+ table.SetHeader(getTableHeader(opts.GroupBy))
+ var index int
+ for ns, diffs := range resources {
+ for _, d := range diffs {
+ row := getTableRow(index, ns, d)
+ table.Append(row)
+ index++
}
+ }
+ table.Render()
+ return fmt.Sprintf("Unused %ss:\n%s\n", resource, buf.String())
+}
- allEmpty = false
+func formatOutputForNamespace(namespace string, resources map[string][]string, opts Opts) string {
+ var buf bytes.Buffer
+ table := tablewriter.NewWriter(&buf)
+ table.SetHeader(getTableHeader(opts.GroupBy))
+ allEmpty := true
+ var index int
+ for resourceType, diff := range resources {
for _, val := range diff {
- row := []string{
- fmt.Sprintf("%d", i+1),
- resourceType,
- val,
- }
+ row := getTableRow(index, resourceType, val)
table.Append(row)
- i += 1
+ allEmpty = false
+ index++
}
}
-
- if opts.Verbose && allEmpty {
- return fmt.Sprintf("No unused resources found in the namespace: %s", namespace)
- } else if allEmpty {
+ if allEmpty {
+ if opts.Verbose {
+ return fmt.Sprintf("No unused resources found in the namespace: %q\n", namespace)
+ }
return ""
}
-
table.Render()
- if namespace == "" {
- return fmt.Sprintf("Unused CRDs: \n%s", buf.String())
- }
- return fmt.Sprintf("Unused Resources in Namespace: %s\n%s", namespace, buf.String())
+ return fmt.Sprintf("Unused resources in namespace: %q\n%s\n", namespace, buf.String())
}
func FormatOutputAll(namespace string, allDiffs []ResourceDiff, opts Opts) string {
- i := 0
var buf bytes.Buffer
table := tablewriter.NewWriter(&buf)
- table.SetHeader([]string{
- "#",
- "Resource Type",
- "Resource Name",
- })
-
- // TODO parse resourceType, diff
-
+ table.SetHeader(getTableHeader(opts.GroupBy))
allEmpty := true
+ var index int
for _, data := range allDiffs {
- if len(data.diff) == 0 {
- continue
- }
-
- allEmpty = false
for _, val := range data.diff {
- row := []string{
- fmt.Sprintf("%d", i+1),
- data.resourceType,
- val,
- }
+ row := getTableRow(index, data.resourceType, val)
table.Append(row)
- i += 1
+ allEmpty = false
+ index++
}
}
-
- if opts.Verbose && allEmpty {
- return fmt.Sprintf("No unused resources found in the namespace: %s", namespace)
- } else if allEmpty {
+ if allEmpty {
+ if opts.Verbose {
+ return fmt.Sprintf("No unused resources found in the namespace: %q\n", namespace)
+ }
return ""
}
-
table.Render()
- if namespace == "" {
- return fmt.Sprintf("Unused %ss: \n%s", allDiffs[0].resourceType, buf.String())
- }
- return fmt.Sprintf("Unused Resources in Namespace: %s\n%s", namespace, buf.String())
+ return fmt.Sprintf("Unused resources in namespace: %q\n%s\n", namespace, buf.String())
}
// TODO create formatter by resource "#", "Resource Name", "Namespace"
@@ -261,24 +272,20 @@ func CalculateResourceDifference(usedResourceNames []string, allResourceNames []
}
func unusedResourceFormatter(outputFormat string, outputBuffer bytes.Buffer, opts Opts, jsonResponse []byte) (string, error) {
- if outputFormat == "table" {
-
- if opts.WebhookURL != "" || opts.Channel != "" && opts.Token != "" {
- if err := SendToSlack(SlackMessage{}, opts, outputBuffer.String()); err != nil {
- fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
- os.Exit(1)
- }
- } else {
+ switch outputFormat {
+ case "table":
+ if opts.WebhookURL == "" || opts.Channel == "" || opts.Token != "" {
return outputBuffer.String(), nil
}
- } else {
- if outputFormat == "yaml" {
- yamlResponse, err := yaml.JSONToYAML(jsonResponse)
- if err != nil {
- fmt.Printf("err: %v\n", err)
- }
- return string(yamlResponse), nil
+ if err := SendToSlack(SlackMessage{}, opts, outputBuffer.String()); err != nil {
+ return "", fmt.Errorf("failed to send message to slack: %w", err)
+ }
+ case "yaml":
+ yamlResponse, err := yaml.JSONToYAML(jsonResponse)
+ if err != nil {
+ return "", fmt.Errorf("failed to convert json to yaml: %w", err)
}
+ return string(yamlResponse), nil
}
return string(jsonResponse), nil
}
diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go
index 17ced677..09086537 100644
--- a/pkg/kor/multi.go
+++ b/pkg/kor/multi.go
@@ -109,7 +109,6 @@ func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset
if len(diff.diff) != 0 {
output := FormatOutputAll("", []ResourceDiff{diff}, opts)
outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
resourceMap := make(map[string][]string)
resourceMap[diff.resourceType] = diff.diff
@@ -139,7 +138,6 @@ func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset
output := FormatOutputAll(namespace, allDiffs, opts)
if output != "" {
outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
resourceMap := make(map[string][]string)
for _, diff := range allDiffs {
@@ -147,7 +145,6 @@ func GetUnusedMulti(resourceNames string, filterOpts *filters.Options, clientset
}
response[namespace] = resourceMap
}
-
}
jsonResponse, err := json.MarshalIndent(response, "", " ")
diff --git a/pkg/kor/pdbs.go b/pkg/kor/pdbs.go
index ce9e1828..690e5567 100644
--- a/pkg/kor/pdbs.go
+++ b/pkg/kor/pdbs.go
@@ -56,36 +56,37 @@ func processNamespacePdbs(clientset kubernetes.Interface, namespace string, filt
}
func GetUnusedPdbs(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespacePdbs(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Pdb"] = diff
+ case "resource":
+ appendResources(resources, "Pdb", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "PDB", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete PDB %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "PDBs", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Pdb"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedPdbs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/pdbs_test.go b/pkg/kor/pdbs_test.go
index a3d816c1..b357e5ce 100644
--- a/pkg/kor/pdbs_test.go
+++ b/pkg/kor/pdbs_test.go
@@ -99,6 +99,7 @@ func TestGetUnusedPdbsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedPdbs(&filters.Options{}, clientset, "json", opts)
@@ -108,7 +109,10 @@ func TestGetUnusedPdbsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Pdb": {"test-pdb3", "test-pdb5"},
+ "Pdb": {
+ "test-pdb3",
+ "test-pdb5",
+ },
},
}
diff --git a/pkg/kor/pods.go b/pkg/kor/pods.go
index 9aa1b1b9..46079448 100644
--- a/pkg/kor/pods.go
+++ b/pkg/kor/pods.go
@@ -14,7 +14,7 @@ import (
"github.com/yonahd/kor/pkg/filters"
)
-func ProcessNamespacePods(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespacePods(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
podsList, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -42,36 +42,37 @@ func ProcessNamespacePods(clientset kubernetes.Interface, namespace string, filt
}
func GetUnusedPods(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespacePods(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespacePods(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Pod"] = diff
+ case "resource":
+ appendResources(resources, "Pod", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Pod", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Pod %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Pods", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Pods"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedPods, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/pods_test.go b/pkg/kor/pods_test.go
index ab63a6e0..15167c8a 100644
--- a/pkg/kor/pods_test.go
+++ b/pkg/kor/pods_test.go
@@ -69,7 +69,14 @@ func createTestPods(t *testing.T) *fake.Clientset {
Message: "",
}
- pods := []*corev1.Pod{pod1, pod2, pod3, pod4, pod5, pod6}
+ pods := []*corev1.Pod{
+ pod1,
+ pod2,
+ pod3,
+ pod4,
+ pod5,
+ pod6,
+ }
// Add test pods to the clientset
for _, pod := range pods {
@@ -84,12 +91,15 @@ func createTestPods(t *testing.T) *fake.Clientset {
func TestProcessNamespacePods(t *testing.T) {
clientset := createTestPods(t)
- evictedPods, err := ProcessNamespacePods(clientset, testNamespace, &filters.Options{})
+ evictedPods, err := processNamespacePods(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
- expectedEvictedPods := []string{"pod-2", "pod-6"}
+ expectedEvictedPods := []string{
+ "pod-2",
+ "pod-6",
+ }
if len(evictedPods) != len(expectedEvictedPods) {
t.Errorf("Expected %d evicted pods, got %d", len(expectedEvictedPods), len(evictedPods))
@@ -111,6 +121,7 @@ func TestGetUnusedPodsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedPods(&filters.Options{}, clientset, "json", opts)
@@ -120,7 +131,10 @@ func TestGetUnusedPodsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Pods": {"pod-2", "pod-6"},
+ "Pod": {
+ "pod-2",
+ "pod-6",
+ },
},
}
diff --git a/pkg/kor/pv.go b/pkg/kor/pv.go
index 3aad62cf..7789378d 100644
--- a/pkg/kor/pv.go
+++ b/pkg/kor/pv.go
@@ -43,40 +43,34 @@ func processPvs(clientset kubernetes.Interface, filterOpts *filters.Options) ([]
}
func GetUnusedPvs(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- response := make(map[string]map[string][]string)
-
+ resources := make(map[string]map[string][]string)
diff, err := processPvs(clientset, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process pvs: %v\n", err)
}
-
- if len(diff) > 0 {
- // We consider cluster scope resources in "" (empty string) namespace, as it is common in k8s
- if response[""] == nil {
- response[""] = make(map[string][]string)
- }
- response[""]["Pv"] = diff
+ switch opts.GroupBy {
+ case "namespace":
+ resources[""] = make(map[string][]string)
+ resources[""]["Pv"] = diff
+ case "resource":
+ appendResources(resources, "Pv", "", diff)
}
-
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, "", "PV", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete PV %s: %v\n", diff, err)
}
}
- output := FormatOutput("", diff, "PVs", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- response[""]["Pv"] = diff
-
- }
-
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedPvs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/pv_test.go b/pkg/kor/pv_test.go
index 70e16ade..3f8e5503 100644
--- a/pkg/kor/pv_test.go
+++ b/pkg/kor/pv_test.go
@@ -67,6 +67,7 @@ func TestGetUnusedPvs(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedPvs(&filters.Options{}, clientset, "json", opts)
@@ -76,7 +77,10 @@ func TestGetUnusedPvs(t *testing.T) {
expectedOutput := map[string]map[string][]string{
"": {
- "Pv": {"test-pv2", "test-pv4"},
+ "Pv": {
+ "test-pv2",
+ "test-pv4",
+ },
},
}
diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go
index 2e19496e..ecd1c91d 100644
--- a/pkg/kor/pvc.go
+++ b/pkg/kor/pvc.go
@@ -64,36 +64,37 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string, filt
}
func GetUnusedPvcs(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespacePvcs(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Pvc"] = diff
+ case "resource":
+ appendResources(resources, "Pvc", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "PVC", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete PVC %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "PVCs", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Pvc"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedPvcs, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/pvc_test.go b/pkg/kor/pvc_test.go
index 5f96b8e7..e838c271 100644
--- a/pkg/kor/pvc_test.go
+++ b/pkg/kor/pvc_test.go
@@ -105,6 +105,7 @@ func TestGetUnusedPvcsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedPvcs(&filters.Options{}, clientset, "json", opts)
@@ -114,7 +115,10 @@ func TestGetUnusedPvcsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Pvc": {"test-pvc2", "test-pvc4"},
+ "Pvc": {
+ "test-pvc2",
+ "test-pvc4",
+ },
},
}
diff --git a/pkg/kor/replicaset.go b/pkg/kor/replicaset.go
index 63798d49..c707e4da 100644
--- a/pkg/kor/replicaset.go
+++ b/pkg/kor/replicaset.go
@@ -13,7 +13,7 @@ import (
"github.com/yonahd/kor/pkg/filters"
)
-func ProcessNamespaceReplicaSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespaceReplicaSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
replicaSetList, err := clientset.AppsV1().ReplicaSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -36,36 +36,37 @@ func ProcessNamespaceReplicaSets(clientset kubernetes.Interface, namespace strin
}
func GetUnusedReplicaSets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespaceReplicaSets(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespaceReplicaSets(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["ReplicaSet"] = diff
+ case "resource":
+ appendResources(resources, "ReplicaSet", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "ReplicaSet", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete ReplicaSet %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "ReplicaSets", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["ReplicaSets"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedReplicaSets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/replicaset_test.go b/pkg/kor/replicaset_test.go
index 57e6a2fa..1f7c2910 100644
--- a/pkg/kor/replicaset_test.go
+++ b/pkg/kor/replicaset_test.go
@@ -63,6 +63,7 @@ func TestProcessNamespaceReplicaSets(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedReplicaSets(&filters.Options{}, clientset, "json", opts)
@@ -72,7 +73,7 @@ func TestProcessNamespaceReplicaSets(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "ReplicaSets": {"test-replicaSet2"},
+ "ReplicaSet": {"test-replicaSet2"},
},
}
diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go
index 38d14773..4c347e46 100644
--- a/pkg/kor/roles.go
+++ b/pkg/kor/roles.go
@@ -92,36 +92,37 @@ func processNamespaceRoles(clientset kubernetes.Interface, namespace string, fil
}
func GetUnusedRoles(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceRoles(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Role"] = diff
+ case "resource":
+ appendResources(resources, "Role", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Role", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Role %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Roles", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Roles"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedRoles, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/roles_test.go b/pkg/kor/roles_test.go
index 1ff5a4c0..c5681084 100644
--- a/pkg/kor/roles_test.go
+++ b/pkg/kor/roles_test.go
@@ -115,6 +115,7 @@ func TestGetUnusedRolesStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedRoles(&filters.Options{}, clientset, "json", opts)
@@ -124,7 +125,10 @@ func TestGetUnusedRolesStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Roles": {"test-role2", "test-role4"},
+ "Role": {
+ "test-role2",
+ "test-role4",
+ },
},
}
diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go
index 75a98633..7c824ecf 100644
--- a/pkg/kor/secrets.go
+++ b/pkg/kor/secrets.go
@@ -19,6 +19,7 @@ import (
var exceptionSecretTypes = []string{
`helm.sh/release.v1`,
`kubernetes.io/dockerconfigjson`,
+ `kubernetes.io/dockercfg`,
`kubernetes.io/service-account-token`,
}
@@ -175,36 +176,37 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string, fi
}
func GetUnusedSecrets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceSecret(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Secret"] = diff
+ case "resource":
+ appendResources(resources, "Secret", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Secret", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Secret %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Secrets", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Secrets"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedSecrets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/secrets_test.go b/pkg/kor/secrets_test.go
index 5c5b5bf2..d8b64f42 100644
--- a/pkg/kor/secrets_test.go
+++ b/pkg/kor/secrets_test.go
@@ -34,18 +34,27 @@ func createTestSecrets(t *testing.T) *fake.Clientset {
secret5 := CreateTestSecret(testNamespace, "test-secret5", UnusedLabels)
pod1 := CreateTestPod(testNamespace, "pod-1", "", []corev1.Volume{
- {Name: "vol-1", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret1"}}},
+ {
+ Name: "vol-1",
+ VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret1"}},
+ },
}, AppLabels)
pod2 := CreateTestPod(testNamespace, "pod-2", "", []corev1.Volume{
- {Name: "vol-2", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret2"}}},
+ {
+ Name: "vol-2",
+ VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "test-secret2"}},
+ },
}, AppLabels)
pod3 := CreateTestPod(testNamespace, "pod-3", "", nil, AppLabels)
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}}}},
+ {
+ Name: "ENV_VAR_1",
+ ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: secret1.ObjectMeta.Name}}},
+ },
},
},
}
@@ -63,7 +72,10 @@ func createTestSecrets(t *testing.T) *fake.Clientset {
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}}}},
+ {
+ Name: "INIT_ENV_VAR_1",
+ ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{LocalObjectReference: corev1.LocalObjectReference{Name: secret1.ObjectMeta.Name}}},
+ },
},
},
}
@@ -178,7 +190,10 @@ func TestRetrieveUsedSecret(t *testing.T) {
t.Fatalf("Error retrieving used secrets: %v", err)
}
- expectedVolumeSecrets := []string{"test-secret1", "test-secret2"}
+ expectedVolumeSecrets := []string{
+ "test-secret1",
+ "test-secret2",
+ }
if !equalSlices(volumeSecrets, expectedVolumeSecrets) {
t.Errorf("Expected volume secrets %v, got %v", expectedVolumeSecrets, volumeSecrets)
}
@@ -198,7 +213,10 @@ func TestRetrieveUsedSecret(t *testing.T) {
t.Errorf("Expected initContainer env secrets %v, got %v", expectedInitContainerEnvSecrets, initContainerEnvSecrets)
}
- expectedPullSecrets := []string{"test-secret1", "test-secret2"}
+ expectedPullSecrets := []string{
+ "test-secret1",
+ "test-secret2",
+ }
if !equalSlices(pullSecrets, expectedPullSecrets) {
t.Errorf("Expected pull secrets %v, got %v", expectedPullSecrets, pullSecrets)
}
@@ -228,7 +246,10 @@ func TestRetrieveSecretNames(t *testing.T) {
t.Fatalf("Error retrieving secret names: %v", err)
}
- expectedSecretNames := []string{"secret-1", "secret-2"}
+ expectedSecretNames := []string{
+ "secret-1",
+ "secret-2",
+ }
if !equalSlices(secretNames, expectedSecretNames) {
t.Errorf("Expected secret names %v, got %v", expectedSecretNames, secretNames)
}
@@ -261,6 +282,7 @@ func TestGetUnusedSecretsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedSecrets(&filters.Options{}, clientset, "json", opts)
@@ -270,7 +292,10 @@ func TestGetUnusedSecretsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Secrets": {"test-secret3", "test-secret5"},
+ "Secret": {
+ "test-secret3",
+ "test-secret5",
+ },
},
}
diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go
index 99222a56..7ca37ce4 100644
--- a/pkg/kor/serviceaccounts.go
+++ b/pkg/kor/serviceaccounts.go
@@ -155,37 +155,37 @@ func processNamespaceSA(clientset kubernetes.Interface, namespace string, filter
}
func GetUnusedServiceAccounts(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
-
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
diff, err := processNamespaceSA(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["ServiceAccount"] = diff
+ case "resource":
+ appendResources(resources, "ServiceAccount", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "ServiceAccount", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Serviceaccount %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Serviceaccounts", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["ServiceAccounts"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedServiceAccounts, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/serviceaccounts_test.go b/pkg/kor/serviceaccounts_test.go
index eda4616f..83ad1a52 100644
--- a/pkg/kor/serviceaccounts_test.go
+++ b/pkg/kor/serviceaccounts_test.go
@@ -190,6 +190,7 @@ func TestGetUnusedServiceAccountsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedServiceAccounts(&filters.Options{}, clientset, "json", opts)
@@ -199,7 +200,10 @@ func TestGetUnusedServiceAccountsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "ServiceAccounts": {"test-sa2", "test-sa4"},
+ "ServiceAccount": {
+ "test-sa2",
+ "test-sa4",
+ },
},
}
diff --git a/pkg/kor/services.go b/pkg/kor/services.go
index 92f8acbd..e0735248 100644
--- a/pkg/kor/services.go
+++ b/pkg/kor/services.go
@@ -17,7 +17,7 @@ import (
//go:embed exceptions/services/services.json
var servicesConfig []byte
-func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespaceServices(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
endpointsList, err := clientset.CoreV1().Endpoints(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -52,37 +52,37 @@ func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string,
}
func GetUnusedServices(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
-
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespaceServices(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespaceServices(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
-
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["Service"] = diff
+ case "resource":
+ appendResources(resources, "Service", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "Service", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Service %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Services", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Services"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedServices, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/services_test.go b/pkg/kor/services_test.go
index 892d9c59..cd159d68 100644
--- a/pkg/kor/services_test.go
+++ b/pkg/kor/services_test.go
@@ -57,7 +57,7 @@ func createTestServices(t *testing.T) *fake.Clientset {
func TestGetEndpointsWithoutSubsets(t *testing.T) {
clientset := createTestServices(t)
- servicesWithoutEndpoints, err := ProcessNamespaceServices(clientset, testNamespace, &filters.Options{})
+ servicesWithoutEndpoints, err := processNamespaceServices(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
@@ -80,6 +80,7 @@ func TestGetUnusedServicesStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedServices(&filters.Options{}, clientset, "json", opts)
@@ -89,7 +90,10 @@ func TestGetUnusedServicesStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Services": {"test-endpoint1", "test-endpoint4"},
+ "Service": {
+ "test-endpoint1",
+ "test-endpoint4",
+ },
},
}
diff --git a/pkg/kor/statefulsets.go b/pkg/kor/statefulsets.go
index b7f85589..bd04eb4b 100644
--- a/pkg/kor/statefulsets.go
+++ b/pkg/kor/statefulsets.go
@@ -13,7 +13,7 @@ import (
"github.com/yonahd/kor/pkg/filters"
)
-func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
+func processNamespaceStatefulSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]string, error) {
statefulSetsList, err := clientset.AppsV1().StatefulSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
if err != nil {
return nil, err
@@ -40,35 +40,37 @@ func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace stri
}
func GetUnusedStatefulSets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- namespaces := filterOpts.Namespaces(clientset)
- response := make(map[string]map[string][]string)
-
- for _, namespace := range namespaces {
- diff, err := ProcessNamespaceStatefulSets(clientset, namespace, filterOpts)
+ resources := make(map[string]map[string][]string)
+ for _, namespace := range filterOpts.Namespaces(clientset) {
+ diff, err := processNamespaceStatefulSets(clientset, namespace, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
continue
}
+ switch opts.GroupBy {
+ case "namespace":
+ resources[namespace] = make(map[string][]string)
+ resources[namespace]["StatefulSet"] = diff
+ case "resource":
+ appendResources(resources, "StatefulSet", namespace, diff)
+ }
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, namespace, "StatefulSet", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete Statefulset %s in namespace %s: %v\n", diff, namespace, err)
}
}
- output := FormatOutput(namespace, diff, "Statefulsets", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- resourceMap := make(map[string][]string)
- resourceMap["Statefulsets"] = diff
- response[namespace] = resourceMap
- }
}
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedStatefulsets, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/statefulsets_test.go b/pkg/kor/statefulsets_test.go
index 8fad0712..513f7307 100644
--- a/pkg/kor/statefulsets_test.go
+++ b/pkg/kor/statefulsets_test.go
@@ -57,7 +57,7 @@ func createTestStatefulSets(t *testing.T) *fake.Clientset {
func TestProcessNamespaceStatefulSets(t *testing.T) {
clientset := createTestStatefulSets(t)
- statefulSetsWithoutReplicas, err := ProcessNamespaceStatefulSets(clientset, testNamespace, &filters.Options{})
+ statefulSetsWithoutReplicas, err := processNamespaceStatefulSets(clientset, testNamespace, &filters.Options{})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
@@ -80,6 +80,7 @@ func TestGetUnusedStatefulSetsStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedStatefulSets(&filters.Options{}, clientset, "json", opts)
@@ -89,7 +90,10 @@ func TestGetUnusedStatefulSetsStructured(t *testing.T) {
expectedOutput := map[string]map[string][]string{
testNamespace: {
- "Statefulsets": {"test-sts1", "test-sts4"},
+ "StatefulSet": {
+ "test-sts1",
+ "test-sts4",
+ },
},
}
diff --git a/pkg/kor/storageclasses.go b/pkg/kor/storageclasses.go
index 16858efc..df728e7d 100644
--- a/pkg/kor/storageclasses.go
+++ b/pkg/kor/storageclasses.go
@@ -93,40 +93,34 @@ func processStorageClasses(clientset kubernetes.Interface, filterOpts *filters.O
}
func GetUnusedStorageClasses(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts Opts) (string, error) {
- var outputBuffer bytes.Buffer
- response := make(map[string]map[string][]string)
-
+ resources := make(map[string]map[string][]string)
diff, err := processStorageClasses(clientset, filterOpts)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to process storageClasses: %v\n", err)
}
-
- if len(diff) > 0 {
- // We consider cluster scope resources in "" (empty string) namespace, as it is common in k8s
- if response[""] == nil {
- response[""] = make(map[string][]string)
- }
- response[""]["StorageClass"] = diff
+ switch opts.GroupBy {
+ case "namespace":
+ resources[""] = make(map[string][]string)
+ resources[""]["StorageClass"] = diff
+ case "resource":
+ appendResources(resources, "StorageClass", "", diff)
}
-
if opts.DeleteFlag {
if diff, err = DeleteResource(diff, clientset, "", "StorageClass", opts.NoInteractive); err != nil {
fmt.Fprintf(os.Stderr, "Failed to delete StorageClass %s: %v\n", diff, err)
}
}
- output := FormatOutput("", diff, "StorageClasses", opts)
- if output != "" {
- outputBuffer.WriteString(output)
- outputBuffer.WriteString("\n")
-
- response[""]["StorageClass"] = diff
-
- }
-
- jsonResponse, err := json.MarshalIndent(response, "", " ")
- if err != nil {
- return "", err
+ var outputBuffer bytes.Buffer
+ var jsonResponse []byte
+ switch outputFormat {
+ case "table":
+ outputBuffer = FormatOutput(resources, opts)
+ case "json", "yaml":
+ var err error
+ if jsonResponse, err = json.MarshalIndent(resources, "", " "); err != nil {
+ return "", err
+ }
}
unusedStorageClasses, err := unusedResourceFormatter(outputFormat, outputBuffer, opts, jsonResponse)
diff --git a/pkg/kor/storageclasses_test.go b/pkg/kor/storageclasses_test.go
index 0c198ea2..ab856dd4 100644
--- a/pkg/kor/storageclasses_test.go
+++ b/pkg/kor/storageclasses_test.go
@@ -75,6 +75,7 @@ func TestGetUnusedStorageClassesStructured(t *testing.T) {
Token: "",
DeleteFlag: false,
NoInteractive: true,
+ GroupBy: "namespace",
}
output, err := GetUnusedStorageClasses(&filters.Options{}, clientset, "json", opts)