Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

isindir
Copy link
Contributor

@isindir isindir commented Apr 28, 2024

What this PR does / why we need it?

This PR finds empty and non-default kubernetes namespaces, and if instructed removes these from the cluster.

PR Checklist

  • This PR adds K8s exceptions (false positives)
  • This PR adds new code
  • This PR includes tests for new/existing code
  • This PR adds docs

GitHub Issue

Closes [#92]

Notes for your reviewers

@isindir
Copy link
Contributor Author

isindir commented Apr 28, 2024

fixes #92

This PR is still WIP, as it lacks parallel processing of the namespaces as well as unit tests, could you please review if in principle it is what you'd be happy to accept ?

@yonahd
Copy link
Owner

yonahd commented Apr 30, 2024

fixes #92

This PR is still WIP, as it lacks parallel processing of the namespaces as well as unit tests, could you please review if in principle it is what you'd be happy to accept ?

Hey @isindir
I'll take a look at the code in the next couple of days

@doronkg
Copy link
Contributor

doronkg commented May 1, 2024

Hi @isindir,
In order for the new feature to be completed, several additional files should be modified accordingly.

Take a look at https://github.com/yonahd/kor/blob/main/CONTRIBUTING.md#repository-structure.

@isindir isindir force-pushed the unused_namespaces branch from 2460fbc to b86c7df Compare May 1, 2024 08:30
@codecov-commenter
Copy link

codecov-commenter commented May 1, 2024

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 38.50932% with 99 lines in your changes missing coverage. Please review.

Project coverage is 43.54%. Comparing base (afe957b) to head (492a54e).
Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
pkg/kor/namespaces.go 43.54% 70 Missing ⚠️
cmd/kor/namespaces.go 0.00% 22 Missing ⚠️
pkg/kor/delete.go 53.33% 7 Missing ⚠️

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #249      +/-   ##
==========================================
- Coverage   43.72%   43.54%   -0.19%     
==========================================
  Files          63       65       +2     
  Lines        4064     4219     +155     
==========================================
+ Hits         1777     1837      +60     
- Misses       2039     2134      +95     
  Partials      248      248              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@yonahd
Copy link
Owner

yonahd commented May 1, 2024

fixes #92

This PR is still WIP, as it lacks parallel processing of the namespaces as well as unit tests, could you please review if in principle it is what you'd be happy to accept ?

This is definitely a good direction(this is not a simple one).
Definitely need to review this thoroughly but in principal this is good

pkg/kor/namespaces.go Outdated Show resolved Hide resolved
@isindir
Copy link
Contributor Author

isindir commented May 1, 2024

@doronkg,

Hi @isindir, In order for the new feature to be completed, several additional files should be modified accordingly.

Take a look at https://github.com/yonahd/kor/blob/main/CONTRIBUTING.md#repository-structure.

Helm chart I think needs extra changes:

  • at the moment it is not possible to configure kor in a deployment for example to automatically remove any orphaned resources - rbac lacks such permissions.
  • I think it would be more useful to split cronjobs so that several such jobs are configured with different schedules (may become handy when removing some resources and then namespaces in a separate run.

I'm looking to work on unit tests if time allows these weekends.

@yonahd yonahd requested a review from luisdavim May 4, 2024 18:45
@isindir isindir force-pushed the unused_namespaces branch 3 times, most recently from 07bc31c to 7246d82 Compare May 5, 2024 20:53
@doronkg
Copy link
Contributor

doronkg commented May 6, 2024

Helm chart I think needs extra changes:

  • at the moment it is not possible to configure kor in a deployment for example to automatically remove any orphaned resources - rbac lacks such permissions.

Correct, currently the deployment is only used to export metrics, hence read-only RBAC, but we can consider going that way.

  • I think it would be more useful to split cronjobs so that several such jobs are configured with different schedules (may become handy when removing some resources and then namespaces in a separate run.

Interesting, especially when scanning unused resources in high-scaled clusters that might take more time than the interval set.

We can continue with progress on both comments in separated issues/discussions (or in our Discord channel) as they are out-of-scope for this PR, but should be dippen into. I'd like to futher discuss them.

Referring to https://github.com/yonahd/kor/blob/main/CONTRIBUTING.md#repository-structure was made to make sure you modify all required files, as adding any new unused resource support should address them.

@isindir
Copy link
Contributor Author

isindir commented May 6, 2024

@doronkg ,

Referring to https://github.com/yonahd/kor/blob/main/CONTRIBUTING.md#repository-structure was made to make sure you modify all required files, as adding any new unused resource support should address them.

Thank you, I think I covered most of the files except helm chart and some unit tests, atm I'm trying to figure out if I can use fake clients to reliably test a feature I'm implementing. I deliberately left chart untouched as the change already looks very big.

btw, for chart - it will be better to have fine-tuned RBAC - possibility to enable only those read/write/per namespace permissions per feature (resource).

pkg/kor/namespaces.go Outdated Show resolved Hide resolved
pkg/filters/filters.go Outdated Show resolved Hide resolved
@isindir isindir force-pushed the unused_namespaces branch from 1ac441f to cc034af Compare May 8, 2024 08:03
@doronkg doronkg mentioned this pull request May 9, 2024
3 tasks
@doronkg doronkg mentioned this pull request May 23, 2024
3 tasks
@isindir isindir force-pushed the unused_namespaces branch from ac738c6 to 5b15ce6 Compare June 8, 2024 07:44
@isindir
Copy link
Contributor Author

isindir commented Jun 9, 2024

@yonahd @luisdavim @doronkg, I finally found time and adding some more testing to the namespace removal - but I was not following if json configs with resources to skip from namespace evaluation for different k8s distros is fully implemented or not. Could you please suggest and review if my testing in pkg/kor/namespacesMoreTests_test.go is acceptable ? Thanks

@yonahd
Copy link
Owner

yonahd commented Jun 21, 2024

@yonahd @luisdavim @doronkg, I finally found time and adding some more testing to the namespace removal - but I was not following if json configs with resources to skip from namespace evaluation for different k8s distros is fully implemented or not. Could you please suggest and review if my testing in pkg/kor/namespacesMoreTests_test.go is acceptable ? Thanks

The resource exceptions are merged fully.
Not sure we will use the namespaces exceptions as we worked around it

@yonahd
Copy link
Owner

yonahd commented Jun 21, 2024

Let me know when this is ready for review. This is a massive pr and will take quite a while to review

@isindir isindir force-pushed the unused_namespaces branch from b6dd857 to 02a3f7e Compare July 7, 2024 15:09
@isindir
Copy link
Contributor Author

isindir commented Jul 7, 2024

@yonahd I feel that I'll not be able to do 100% unit test coverage for my new code, please review this PR. Thanks.

@isindir
Copy link
Contributor Author

isindir commented Nov 28, 2024

@yonahd - I rebased the PR to the latest, would you have time to review and may be merge it ? We could negotiate next course of actions if needed from my side on a call, please reach me out on keybase , same uid as here.

@isindir isindir requested a review from yonahd November 28, 2024 16:20
@yonahd
Copy link
Owner

yonahd commented Dec 2, 2024

@yonahd - I rebased the PR to the latest, would you have time to review and may be merge it ? We could negotiate next course of actions if needed from my side on a call, please reach me out on keybase , same uid as here.

Thanks for the PR
I'm currently very short in time. Any chance @doronkg or @luisdavim can you review this?

@doronkg
Copy link
Contributor

doronkg commented Dec 9, 2024

@yonahd - I rebased the PR to the latest, would you have time to review and may be merge it ? We could negotiate next course of actions if needed from my side on a call, please reach me out on keybase , same uid as here.

Thanks for the PR

I'm currently very short in time. Any chance @doronkg or @luisdavim can you review this?

Hi, sorry for the late response.
I'm currently on vacation so my time is very limited. From a quick glimpse - I won't be able to review this PR thoroughly in the following weeks unfortunately.

@isindir
Copy link
Contributor Author

isindir commented Jan 23, 2025

@doronkg would you have some time to review it now ?

@doronkg
Copy link
Contributor

doronkg commented Jan 27, 2025

@doronkg would you have some time to review it now ?

Hey, I’m currently traveling, so reviewing everything at once might be a bit challenging. To make this manageable, let’s break it down into smaller chunks and review each component step by step.

So far, I’ve gone through all the additions (except for the tests—we’ll tackle those later) and reviewed the previous comments.

To help me better understand the PR, could you please clarify the following:
• Empty namespace evaluation: Could you explain the general logic here so we’re aligned?
• Filters logic: How do the new include/exclude/apply filters benefit, since similar logic is already implemented in namespaces() under filters.go?
• Line breaks in function arguments: Are these intentional or a result of your IDE settings?
• Output examples: Can you show some output examples from your CMD? Relating both flags / edge cases.

Also, please update the PR description with the format found under .github/pull_request_template.md.

@isindir
Copy link
Contributor Author

isindir commented Jan 28, 2025

Hi @doronkg , thank you for going through the review, I'll try to answer your questions, and I'm happy to do some rework if needed.

Empty namespace evaluation: Could you explain the general logic here so we’re aligned?

General logic is defines filter functions and run all of these with the specified parameters for a given namespace from here:

these functions are:

  • by default exclude any system (namespaces created in vanilla Kubernetes);
  • Exclude or Include namespaces
  • Filter by kor label / any label / age

once we know subjects to inspect - next check is - if namespace contains any non-default resources and resources which user specified to be ignored, which is done here:

and namespaced resource filters are applied here to identify which namespaces can be considered as empty:

Filters logic: How do the new include/exclude/apply filters benefit, since similar logic is already implemented in namespaces() under filters.go?

I guess this is the function mentioned above:

Namespaces() function is called for each for a given resource type to get a list where these resources exist,
for example Secrets or Configmaps. The function call stack processNamespaces() -> filters.ApplyFilters() , isErrorOrNamespaceContainsResources() is to get namespaces which are empty (no resources found, not system namespaces and may contain resources to ignore) - list for namespace removal. These 2 functions have slightly different purpose and output may differ based on resources to ignore input parameter as well.

Line breaks in function arguments: Are these intentional or a result of your IDE settings?

These are not automatic via IDE, but it feels that long strings may read better once refactored:

-                       fmt.Printf("Do you want to delete %s %s in namespace %s? (Y/N): ", resourceType, resource.Name, namespace)
+                       fmt.Printf(
+                               "Do you want to delete %s %s%s? (Y/N): ",
+                               resourceType,
+                               resource.Name,
+                               namespacedMessageSuffix(namespace),
+                       )

but I can undo these changes.

Output examples: Can you show some output examples from your CMD? Relating both flags / edge cases.

I'll try to do this tonight and will send via new comment.

@isindir
Copy link
Contributor Author

isindir commented Jan 29, 2025

I have a couple of examples now out of the box where it prints unwanted information:

% ./build/kor ns --show-reason
kor version: vdev

  _  _____  ____
 | |/ / _ \|  _ \
 | ' / | | | |_) |
 | . \ |_| |  _ <
 |_|\_\___/|_| \_\

Unused resources in namespace: ""
+---+---------------+---------------+------------------+
| # | RESOURCE TYPE | RESOURCE NAME |      REASON      |
+---+---------------+---------------+------------------+
| 1 | Namespaces    | abc           | unused namespace |
+---+---------------+---------------+------------------+

and

% ./build/kor ns --show-reason -o json
{
  "": {
    "Namespaces": [
      {
        "name": "abc",
        "reason": "unused namespace"
      }
    ]
  }
}

@isindir
Copy link
Contributor Author

isindir commented Jan 29, 2025

 ./build/kor ns --newer-than 8m
kor version: vdev

  _  _____  ____
 | |/ / _ \|  _ \
 | ' / | | | |_) |
 | . \ |_| |  _ <
 |_|\_\___/|_| \_\

Unused resources in namespace: ""
+---+---------------+---------------+
| # | RESOURCE TYPE | RESOURCE NAME |
+---+---------------+---------------+
| 1 | Namespaces    | abc           |
+---+---------------+---------------+
% ./build/kor ns --older-than 8m
kor version: vdev

  _  _____  ____
 | |/ / _ \|  _ \
 | ' / | | | |_) |
 | . \ |_| |  _ <
 |_|\_\___/|_| \_\
% ./build/kor ns --include-namespaces cde --exclude-namespaces qqq
Exclude namespaces can't be used together with include namespaces. Ignoring --exclude-namespace(-e) flag
kor version: vdev

 _  _____  ____
| |/ / _ \|  _ \
| ' / | | | |_) |
| . \ |_| |  _ <
|_|\_\___/|_| \_\
% ./build/kor ns --delete
Do you want to delete Namespace abc? (Y/N): y
Deleting Namespace abc
kor version: vdev

 _  _____  ____
| |/ / _ \|  _ \
| ' / | | | |_) |
| . \ |_| |  _ <
|_|\_\___/|_| \_\

Unused resources in namespace: ""
+---+---------------+---------------+
| # | RESOURCE TYPE | RESOURCE NAME |
+---+---------------+---------------+
| 1 | Namespaces    | abc           |
+---+---------------+---------------+

@doronkg
Copy link
Contributor

doronkg commented Feb 6, 2025

Thank you, I have forked your branch to review it more thoroughly and run some tests.
As said, this PR modifies multiple components and I'd like to complete them one by one, so for that matter, we'll break it down to 3 major reviews:

  1. Filters
  2. Unused Namespaces Evaluation
  3. General/Meta

We'll begin with Filters, that includes changes mostly in:

  • pkg/filters/filters.go
  • pkg/filters/filters_test.go
  • pkg/kor/namespaces.go

From my impression, all filter-related additions could be reverted or refactored, I'll explain.

IncludeNamespacesFilter(), ExcludeNamespacesFilter() & ApplyFilters()

As mentioned in #249 (comment), this logic is already implemented under pkg/filters/options.go by Namespaces(), where it's called once and properly sets the desired list of namespaces to evaluate, whether its for the namespaces themselves (i.e. the newly added feature) or other namespaced resources:

// Namespaces returns the namespaces, only called once
func (o *Options) Namespaces(clientset kubernetes.Interface) []string {
	o.once.Do(func() {
		namespaces := make([]string, 0)
		namespacesMap := make(map[string]bool)
		if len(o.IncludeNamespaces) > 0 && len(o.ExcludeNamespaces) > 0 {
			fmt.Fprintf(os.Stderr, "Exclude namespaces can't be used together with include namespaces. Ignoring --exclude-namespaces (-e) flag\n")
			o.ExcludeNamespaces = nil
		}
		includeNamespaces := o.IncludeNamespaces
		excludeNamespaces := o.ExcludeNamespaces
...

Therefore, both IncludeNamespacesFilter() & ExcludeNamespacesFilter() are excessive.
As for the aggregating function - ApplyFilters(), there's no need for that function as all the filter flags (i.e. namespaces, age & labels) are covered when passing filterOpts *filters.Options to the resource processing.

Let's see an example for that with DaemonSet implementation under pkg/kor/daemonsets.go:

func GetUnusedDaemonSets(filterOpts *filters.Options, clientset kubernetes.Interface, outputFormat string, opts common.Opts) (string, error) {
	resources := make(map[string]map[string][]ResourceInfo)
	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
		}
...

In order to process the cluster DaemonSets, we call range filterOpts.Namespaces(clientset) to iterate the desired list of namespaces as set by the user (defaults to all unless include/exclude flags are used).

func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
	daemonSetsList, err := clientset.AppsV1().DaemonSets(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: filterOpts.IncludeLabels})
	if err != nil {
		return nil, err
	}
	...
        for _, daemonSet := range daemonSetsList.Items {
        		if pass, _ := filter.SetObject(&daemonSet).Run(filterOpts); pass {
	        		continue
		        }
		        if daemonSet.Labels["kor/used"] == "false" {
		        	reason := "Marked with unused label"
			        daemonSetsWithoutReplicas = append(daemonSetsWithoutReplicas, ResourceInfo{Name: daemonSet.Name, Reason: reason})
			        continue
	        	}
...

Afterwards, we iterate all the found DaemonSets in each namespace, and apply resource-specific filters such as labels and age with filter.SetObject(&daemonSet).Run(filterOpts).

NOTE: The KorLabelFilter() is not applied in this method and still used in most places with a specific condition, we should consolidate it and refactor all occurences, but it's out-of-scope for this PR.

Moving forward, please refactor pkg/kor/namespaces.go to that logic and remove these filter functions & their tests.


SystemNamespaceFilter()

This filter is of course needed, but its not in the appropriate format, for the following reasons:

  1. filters.go should be used for generic resource filters, while this is a resource-specific filter.
  2. The system namespaces list is hard-coded in the filter instead of using an external resource, which also affects its testing.

The method we use to overcome these requirements is using Exceptions, let's return to DaemonSets to better understand it.
Under pkg/kor/exceptions/ we list all default resources in various K8s distributions to avoid needless scans and false-positive results, in JSON format. For example, daemonsets/daemonsets.json:

{
  "exceptionDaemonSets": [
    {
      "Namespace": "kube-system",
      "ResourceName": "aws-node"
    },
...

NOTE: Regex could be used here, see example in serviceaccounts.

And then, we easily filter these specific resources out in pkg/kor/daemonsets.go:

//go:embed exceptions/daemonsets/daemonsets.json
var daemonsetsConfig []byte

func processNamespaceDaemonSets(clientset kubernetes.Interface, namespace string, filterOpts *filters.Options) ([]ResourceInfo, error) {
...
	for _, daemonSet := range daemonSetsList.Items {
		if pass, _ := filter.SetObject(&daemonSet).Run(filterOpts); pass {
			continue
		}

		exceptionFound, err := isResourceException(daemonSet.Name, daemonSet.Namespace, config.ExceptionDaemonSets)
		if err != nil {
			return nil, err
		}

		if exceptionFound {
			continue
		}
...

NOTE: Make sure to reference your new JSON here for it to apply.

Same here, please refactor pkg/kor/namespaces.go to that logic and remove these filter functions & their tests.


In summary

  1. Namespace filters should be reverted and used via filter options instead.
  2. System namespaces filter should be refactored to use exceptions instead.
  3. Both filters.go & filters_test.go should remain untouched in this PR.

That's it for now, feel free to ask me any questions if something is unclear or if I missed something.
I'm looking forward to review the refactor and move on to reviewing Unused Namespaces Evaluation!

Lastly, please update the PR description with the format found under .github/pull_request_template.md.

@isindir
Copy link
Contributor Author

isindir commented Feb 9, 2025

@doronkg , please check now - I think my refactoring for the first batch is what you asked me to do:

  • added PR description
  • removed my filtering in favour of built-in
  • added system namespace filtering via JSON file
  • I have not added new tests, but removed all the tests which were made for my filter logic
  • I manually tested it, seems to be fine

I've also added one TODO mark, where I think refactoring can be done similarly to filtering out namespaces using exception list - but for child resources of the namespace (some hardcoded checks for child resources - https://github.com/yonahd/kor/pull/249/files#diff-ddfa783436a2acc5672cb487e7af28e8a3da84aa480ece960065740409317216R125 ), please let me know if you think this should be refactored using exception lists or similar method. The only concern from my side is that exception list data structure may need some extra information and logic added and can't be used as is.

@doronkg
Copy link
Contributor

doronkg commented Feb 10, 2025

The refactor looks good, can you add a commit to discard the additions of filters.go & filters_test.go?

I'll get started on the following review.

@isindir
Copy link
Contributor Author

isindir commented Feb 10, 2025

@doronkg , removed - somehow through git manipulations made these changes back and did not double check the final result, sorry

Copy link
Contributor

@doronkg doronkg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review covers Unused Namespaces Evaluation, I commented on the following files:

  • pkg/kor/namespaces.go
  • pkg/kor/namespaces_test.go
  • pkg/kor/exceptions/namespaces/namespaces.json

As for pkg/kor/namespacesMoreTests_test.go -
The direction with the different discovery scenarios is very nice and provides solid test coverage, but, it really differs from the method we widely use in our tests, leveraging fake client and creating custom resources for the test suites.
I left no comments here for now, @yonahd I'd like your take on this before moving forward with this test structure.

pkg/kor/exceptions/namespaces/namespaces.json Outdated Show resolved Hide resolved
pkg/kor/namespaces.go Show resolved Hide resolved
pkg/kor/namespaces.go Outdated Show resolved Hide resolved
pkg/kor/namespaces.go Outdated Show resolved Hide resolved
pkg/kor/namespaces.go Outdated Show resolved Hide resolved
pkg/kor/namespaces.go Outdated Show resolved Hide resolved
Comment on lines +181 to +162
gv := strings.Split(apiResourceList.GroupVersion, "/")
gvr, err := getGVR(apiResource.Name, gv)
Copy link
Contributor

@doronkg doronkg Feb 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the splitting action into getGVR() would have a better flow.

func getGVR(name string, groupVersion string) (*schema.GroupVersionResource, error) {
        splitGV = strings.Split(groupVersion, "/")
	switch NumberOfGVPartsFound := len(splitGV); NumberOfGVPartsFound {
...

return true, err
}

unstructuredList, err := dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both unstructuredList & unstructuredObj are not indicative parameter names.

pkg/kor/namespaces.go Outdated Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Fix function tests if needed following namespaces.go review.
  2. Remove tests for removed functions.
  3. Refactor function naming convention to match the rest of the codebase - TestFunctionName() (for example, Test_namespaces_IgnoreResourceTypes() -> TestIgnoreResourceTypes).
  4. Missing tests for main functions: processNamespaces() & GetUnusedNamespaces() (see TestGetUnusedStorageClassesStructured() for example). Create test namespaces and use *fake.ClientSet to evaluate them.

NOTE: Highly recommended to review other resources' tests before working on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants