Skip to content

Commit f171a4d

Browse files
authored
feat(namespace): regex match (#336)
This MR will add ability in selecting some namespaces that will be scanned by using regex pattern
1 parent 04cadbb commit f171a4d

File tree

3 files changed

+92
-5
lines changed

3 files changed

+92
-5
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ List as many namespaces as you want with `-n` (in this case, `default` and `ingr
307307
krr simple -n default -n ingress-nginx
308308
```
309309

310+
The -n flag also supports regex matches like -n kube-.*. To use regexes, you must have permissions to list namespaces in the target cluster.
311+
312+
```sh
313+
krr simple -n default -n 'ingress-.*'
314+
```
315+
310316
See [example ServiceAccount and RBAC permissions](./tests/single_namespace_permissions.yaml)
311317
</details>
312318

robusta_krr/core/integrations/kubernetes/__init__.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import asyncio
22
import logging
3+
import re
34
from collections import defaultdict
45
from concurrent.futures import ThreadPoolExecutor
5-
from typing import Any, Awaitable, Callable, Iterable, Optional, Union
6+
from typing import Any, Awaitable, Callable, Iterable, Optional, Union, Literal
67

78
from kubernetes import client, config # type: ignore
89
from kubernetes.client import ApiException
@@ -47,6 +48,43 @@ def __init__(self, cluster: Optional[str]=None):
4748

4849
self.__jobs_for_cronjobs: dict[str, list[V1Job]] = {}
4950
self.__jobs_loading_locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
51+
self.__namespaces: Union[list[str, None]] = None
52+
53+
@property
54+
def namespaces(self) -> Union[list[str], Literal["*"]]:
55+
"""wrapper for settings.namespaces, which will do expand namespace list if some regex pattern included
56+
57+
Returns:
58+
A list of namespace that will be scanned
59+
"""
60+
# just return the list if it's already initialized
61+
if self.__namespaces != None:
62+
return self.__namespaces
63+
64+
setting_ns = settings.namespaces
65+
if setting_ns == "*":
66+
self.__namespaces = setting_ns
67+
return self.__namespaces
68+
69+
self.__namespaces = []
70+
expand_list: list[re.Pattern] = []
71+
ns_regex_chars = re.compile(r"[\\*|\(.*?\)|\[.*?\]|\^|\$]")
72+
for ns in setting_ns:
73+
if ns_regex_chars.search(ns):
74+
logger.debug(f"{ns} is detected as regex pattern in expanding namespace list")
75+
expand_list.append(re.compile(ns))
76+
else:
77+
self.__namespaces.append(ns)
78+
79+
if expand_list:
80+
logger.info("found regex pattern in provided namespace argument, expanding namespace list")
81+
all_ns = [ ns.metadata.name for ns in self.core.list_namespace().items ]
82+
for expand_ns in expand_list:
83+
for ns in all_ns:
84+
if expand_ns.fullmatch(ns) and ns not in self.__namespaces:
85+
self.__namespaces.append(ns)
86+
87+
return self.__namespaces
5088

5189
async def list_scannable_objects(self) -> list[K8sObjectData]:
5290
"""List all scannable objects.
@@ -56,7 +94,7 @@ async def list_scannable_objects(self) -> list[K8sObjectData]:
5694
"""
5795

5896
logger.info(f"Listing scannable objects in {self.cluster}")
59-
logger.debug(f"Namespaces: {settings.namespaces}")
97+
logger.debug(f"Namespaces: {self.namespaces}")
6098
logger.debug(f"Resources: {settings.resources}")
6199

62100
self.__hpa_list = await self._try_list_hpa()
@@ -75,7 +113,7 @@ async def list_scannable_objects(self) -> list[K8sObjectData]:
75113
for workload_objects in workload_object_lists
76114
for object in workload_objects
77115
# NOTE: By default we will filter out kube-system namespace
78-
if not (settings.namespaces == "*" and object.namespace == "kube-system")
116+
if not (self.namespaces == "*" and object.namespace == "kube-system")
79117
]
80118

81119
async def _list_jobs_for_cronjobs(self, namespace: str) -> list[V1Job]:
@@ -189,7 +227,7 @@ async def _list_namespaced_or_global_objects(
189227
logger.debug(f"Listing {kind}s in {self.cluster}")
190228
loop = asyncio.get_running_loop()
191229

192-
if settings.namespaces == "*":
230+
if self.namespaces == "*":
193231
requests = [
194232
loop.run_in_executor(
195233
self.executor,
@@ -209,7 +247,7 @@ async def _list_namespaced_or_global_objects(
209247
label_selector=settings.selector,
210248
),
211249
)
212-
for namespace in settings.namespaces
250+
for namespace in self.namespaces
213251
]
214252

215253
result = [

tests/test_krr.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import pytest
2+
from typing import Literal, Union
3+
from unittest.mock import patch, Mock, MagicMock
24
from typer.testing import CliRunner
35

46
from robusta_krr.main import app, load_commands
7+
from robusta_krr.core.integrations.kubernetes import ClusterLoader
8+
from robusta_krr.core.models.config import settings
59

610
runner = CliRunner(mix_stderr=False)
711
load_commands()
@@ -34,3 +38,42 @@ def test_output_formats(format: str, output: str):
3438
assert result.exit_code == 0, result.exc_info
3539
except AssertionError as e:
3640
raise e from result.exception
41+
42+
@pytest.mark.parametrize(
43+
"setting_namespaces,cluster_all_ns,expected",[
44+
(
45+
# default settings
46+
"*",
47+
["kube-system", "robusta-frontend", "robusta-backend", "infra-grafana"],
48+
"*"
49+
),
50+
(
51+
# list of namespace provided from arguments without regex pattern
52+
["robusta-krr", "kube-system"],
53+
["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"],
54+
["robusta-krr", "kube-system"]
55+
),
56+
(
57+
# list of namespace provided from arguments with regex pattern and will not duplicating in final result
58+
["robusta-.*", "robusta-frontend"],
59+
["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"],
60+
["robusta-frontend", "robusta-backend", "robusta-krr"]
61+
),
62+
(
63+
# namespace provided with regex pattern and will match for some namespaces
64+
[".*end$"],
65+
["kube-system", "robusta-frontend", "robusta-backend", "robusta-krr"],
66+
["robusta-frontend", "robusta-backend"]
67+
)
68+
]
69+
)
70+
def test_cluster_namespace_list(
71+
setting_namespaces: Union[Literal["*"], list[str]],
72+
cluster_all_ns: list[str],
73+
expected: Union[Literal["*"], list[str]],
74+
):
75+
cluster = ClusterLoader()
76+
with patch("robusta_krr.core.models.config.settings.namespaces", setting_namespaces):
77+
with patch.object(cluster.core, "list_namespace", return_value=MagicMock(
78+
items=[MagicMock(**{"metadata.name": m}) for m in cluster_all_ns])):
79+
assert sorted(cluster.namespaces) == sorted(expected)

0 commit comments

Comments
 (0)