Skip to content
This repository was archived by the owner on Oct 3, 2020. It is now read-only.

Commit d267b2d

Browse files
Exclude namespaces with regex (#118)
* Add regex match to exclude namespaces * Modifid default value of exclude namespace to ^kube-system$ * use Pattern object * mention regex in README * test CLI option regex mapping * use Python 3.8 Co-authored-by: Andrea Mercanti <a.mercanti@reply.it>
1 parent c38f566 commit d267b2d

File tree

7 files changed

+107
-9
lines changed

7 files changed

+107
-9
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ dist: bionic
22
sudo: yes
33
language: python
44
python:
5-
- "3.7"
5+
- "3.8"
66
services:
77
- docker
88
install:

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Available command line options:
160160
``--default-downtime``
161161
Default time range to scale down for (default: never), can also be configured via environment variable ``DEFAULT_DOWNTIME`` or via the annotation ``downscaler/downtime`` on each deployment
162162
``--exclude-namespaces``
163-
Exclude namespaces from downscaling (default: kube-system), can also be configured via environment variable ``EXCLUDE_NAMESPACES``
163+
Exclude namespaces from downscaling (list of regex patterns, default: kube-system), can also be configured via environment variable ``EXCLUDE_NAMESPACES``
164164
``--exclude-deployments``
165165
Exclude specific deployments/statefulsets/cronjobs from downscaling (default: kube-downscaler, downscaler), can also be configured via environment variable ``EXCLUDE_DEPLOYMENTS``.
166166
Despite its name, this option will match the name of any included resource type (Deployment, StatefulSet, CronJob, ..).

kube_downscaler/cmd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def get_parser():
6868
)
6969
parser.add_argument(
7070
"--exclude-namespaces",
71-
help="Exclude namespaces from downscaling (default: kube-system)",
71+
help="Exclude namespaces from downscaling, comma-separated list of regex patterns (default: kube-system)",
7272
default=os.getenv("EXCLUDE_NAMESPACES", "kube-system"),
7373
)
7474
parser.add_argument(

kube_downscaler/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22
import logging
3+
import re
34
import time
45

56
from kube_downscaler import __version__
@@ -69,7 +70,9 @@ def run_loop(
6970
default_uptime,
7071
default_downtime,
7172
include_resources=frozenset(include_resources.split(",")),
72-
exclude_namespaces=frozenset(exclude_namespaces.split(",")),
73+
exclude_namespaces=frozenset(
74+
re.compile(pattern) for pattern in exclude_namespaces.split(",")
75+
),
7376
exclude_deployments=frozenset(exclude_deployments.split(",")),
7477
dry_run=dry_run,
7578
grace_period=grace_period,

kube_downscaler/scaler.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging
44
from typing import FrozenSet
55
from typing import Optional
6+
from typing import Pattern
67

78
import pykube
89
from pykube import CronJob
@@ -317,7 +318,7 @@ def autoscale_resources(
317318
api,
318319
kind,
319320
namespace: str,
320-
exclude_namespaces: FrozenSet[str],
321+
exclude_namespaces: FrozenSet[Pattern],
321322
exclude_names: FrozenSet[str],
322323
upscale_period: str,
323324
downscale_period: str,
@@ -341,9 +342,11 @@ def autoscale_resources(
341342

342343
for current_namespace, resources in sorted(resources_by_namespace.items()):
343344

344-
if current_namespace in exclude_namespaces:
345+
if any(
346+
[pattern.fullmatch(current_namespace) for pattern in exclude_namespaces]
347+
):
345348
logger.debug(
346-
f"Namespace {current_namespace} was excluded (exclusion list matches)"
349+
f"Namespace {current_namespace} was excluded (exclusion list regex matches)"
347350
)
348351
continue
349352

@@ -412,7 +415,7 @@ def scale(
412415
default_uptime: str,
413416
default_downtime: str,
414417
include_resources: FrozenSet[str],
415-
exclude_namespaces: FrozenSet[str],
418+
exclude_namespaces: FrozenSet[Pattern],
416419
exclude_deployments: FrozenSet[str],
417420
dry_run: bool,
418421
grace_period: int,

tests/test_main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os.path
2+
import re
23
from unittest.mock import MagicMock
34

45
import pytest
@@ -59,3 +60,17 @@ def mock_scale(*args, **kwargs):
5960
main(["--dry-run", "--interval=0"])
6061

6162
assert len(calls) == 2
63+
64+
65+
def test_main_exclude_namespaces(kubeconfig, monkeypatch):
66+
monkeypatch.setattr(os.path, "expanduser", lambda x: str(kubeconfig))
67+
68+
mock_scale = MagicMock()
69+
monkeypatch.setattr("kube_downscaler.main.scale", mock_scale)
70+
71+
main(["--dry-run", "--once", "--exclude-namespaces=foo,.*-infra-.*"])
72+
73+
mock_scale.assert_called_once()
74+
assert mock_scale.call_args.kwargs["exclude_namespaces"] == frozenset(
75+
[re.compile("foo"), re.compile(".*-infra-.*")]
76+
)

tests/test_scaler.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import json
3+
import re
34
from unittest.mock import MagicMock
45

56
from kube_downscaler.scaler import DOWNTIME_REPLICAS_ANNOTATION
@@ -110,7 +111,83 @@ def get(url, version, **kwargs):
110111
default_uptime="never",
111112
default_downtime="always",
112113
include_resources=include_resources,
113-
exclude_namespaces=["system-ns"],
114+
exclude_namespaces=[re.compile("system-ns")],
115+
exclude_deployments=[],
116+
dry_run=False,
117+
grace_period=300,
118+
downtime_replicas=0,
119+
)
120+
121+
assert api.patch.call_count == 1
122+
123+
# make sure that deploy-2 was updated (namespace of sysdep-1 was excluded)
124+
patch_data = {
125+
"metadata": {
126+
"name": "deploy-2",
127+
"namespace": "default",
128+
"creationTimestamp": "2019-03-01T16:38:00Z",
129+
"annotations": {ORIGINAL_REPLICAS_ANNOTATION: "2"},
130+
},
131+
"spec": {"replicas": 0},
132+
}
133+
assert api.patch.call_args[1]["url"] == "/deployments/deploy-2"
134+
assert json.loads(api.patch.call_args[1]["data"]) == patch_data
135+
136+
137+
def test_scaler_namespace_excluded_regex(monkeypatch):
138+
api = MagicMock()
139+
monkeypatch.setattr(
140+
"kube_downscaler.scaler.helper.get_kube_api", MagicMock(return_value=api)
141+
)
142+
143+
def get(url, version, **kwargs):
144+
if url == "pods":
145+
data = {"items": []}
146+
elif url == "deployments":
147+
data = {
148+
"items": [
149+
{
150+
"metadata": {
151+
"name": "sysdep-1",
152+
"namespace": "system-ns",
153+
"creationTimestamp": "2019-03-01T16:38:00Z",
154+
},
155+
"spec": {"replicas": 1},
156+
},
157+
{
158+
"metadata": {
159+
"name": "deploy-2",
160+
"namespace": "default",
161+
"creationTimestamp": "2019-03-01T16:38:00Z",
162+
},
163+
"spec": {"replicas": 2},
164+
},
165+
]
166+
}
167+
elif url == "namespaces/default":
168+
data = {"metadata": {}}
169+
else:
170+
raise Exception(f"unexpected call: {url}, {version}, {kwargs}")
171+
172+
response = MagicMock()
173+
response.json.return_value = data
174+
return response
175+
176+
api.get = get
177+
178+
include_resources = frozenset(["deployments"])
179+
scale(
180+
namespace=None,
181+
upscale_period="never",
182+
downscale_period="never",
183+
default_uptime="never",
184+
default_downtime="always",
185+
include_resources=include_resources,
186+
exclude_namespaces=[
187+
re.compile("foo.*"),
188+
re.compile("syst?em-.*"),
189+
re.compile("def"),
190+
],
114191
exclude_deployments=[],
115192
dry_run=False,
116193
grace_period=300,

0 commit comments

Comments
 (0)