From e3ea20f208b205b8a7f36ae9db09843101f43c3c Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:37:15 +0200 Subject: [PATCH 1/8] feat: added scaledobject support for downscaler/downtime-replicas --- kube_downscaler/resources/keda.py | 8 +- kube_downscaler/scaler.py | 17 +++- tests/test_autoscale_resource.py | 2 +- tests/test_resources.py | 2 +- tests/test_scaler.py | 161 +++++++++++++++++++++++++++++- 5 files changed, 184 insertions(+), 6 deletions(-) diff --git a/kube_downscaler/resources/keda.py b/kube_downscaler/resources/keda.py index 79095fb..9873fa8 100644 --- a/kube_downscaler/resources/keda.py +++ b/kube_downscaler/resources/keda.py @@ -12,16 +12,20 @@ class ScaledObject(NamespacedAPIObject): keda_pause_annotation = "autoscaling.keda.sh/paused-replicas" last_keda_pause_annotation_if_present = "downscaler/original-pause-replicas" + # GoLang 32-bit signed integer max value + 1. The value was choosen because 2147483647 is the max allowed + # for Deployment/StatefulSet.spec.template.replicas + KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 + @property def replicas(self): if ScaledObject.keda_pause_annotation in self.annotations: if self.annotations[ScaledObject.keda_pause_annotation] is None: - replicas = 1 + replicas = self.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 elif self.annotations[ScaledObject.keda_pause_annotation] == "0": replicas = 0 elif self.annotations[ScaledObject.keda_pause_annotation] != "0" and self.annotations[ScaledObject.keda_pause_annotation] is not None: replicas = int(self.annotations[ScaledObject.keda_pause_annotation]) else: - replicas = 1 + replicas = self.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 return replicas diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index 2ac02b1..b3779be 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -41,6 +41,11 @@ DOWNTIME_REPLICAS_ANNOTATION = "downscaler/downtime-replicas" GRACE_PERIOD_ANNOTATION="downscaler/grace-period" +# GoLang 32-bit signed integer max value + 1. The value was choosen because 2147483647 is the max allowed +# for Deployment/StatefulSet.spec.template.replicas. This value is usefult to allow +# ScaledObject to support "downscaler/downtime-replcas" annotation +KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 + RESOURCE_CLASSES = [ Deployment, StatefulSet, @@ -406,6 +411,16 @@ def get_replicas( logger.debug( f"{resource.kind} {resource.namespace}/{resource.name} is {state} (original: {original_state}, uptime: {uptime})" ) + elif resource.kind == "ScaledObject": + replicas = resource.replicas + if replicas == KUBERNETES_MAX_ALLOWED_REPLICAS + 1: + logger.debug( + f"{resource.kind} {resource.namespace}/{resource.name} is currently active (uptime: {uptime})" + ) + else: + logger.debug( + f"{resource.kind} {resource.namespace}/{resource.name} is currently paused with {replicas} replicas (uptime: {uptime})" + ) else: replicas = resource.replicas logger.debug( @@ -665,7 +680,7 @@ def scale_down( if resource.annotations[ScaledObject.keda_pause_annotation] is not None: paused_replicas = resource.annotations[ScaledObject.keda_pause_annotation] resource.annotations[ScaledObject.last_keda_pause_annotation_if_present] = paused_replicas - resource.annotations[ScaledObject.keda_pause_annotation] = "0" + resource.annotations[ScaledObject.keda_pause_annotation] = str(target_replicas) logger.info( f"Pausing {resource.kind} {resource.namespace}/{resource.name} (uptime: {uptime}, downtime: {downtime})" ) diff --git a/tests/test_autoscale_resource.py b/tests/test_autoscale_resource.py index 8c9b71c..f54b2b3 100644 --- a/tests/test_autoscale_resource.py +++ b/tests/test_autoscale_resource.py @@ -1304,4 +1304,4 @@ def test_upscale_scaledobject_without_keda_pause_annotation(): # Check if the annotations have been correctly updated for the upscale operation assert so.annotations[ScaledObject.keda_pause_annotation] is None assert so.annotations.get(ScaledObject.last_keda_pause_annotation_if_present) is None - assert so.replicas == 1 + assert so.replicas == so.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 diff --git a/tests/test_resources.py b/tests/test_resources.py index aaf90d7..048669e 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -51,7 +51,7 @@ def test_scaledobject(): scalable_mock = {"metadata": {}} api_mock.obj = MagicMock(name="APIObjMock") d = ScaledObject(api_mock, scalable_mock) - assert d.replicas == 1 + assert d.replicas == d.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 d.annotations[ScaledObject.keda_pause_annotation] = "0" assert d.replicas == 0 diff --git a/tests/test_scaler.py b/tests/test_scaler.py index f8f2e69..6b5e42f 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -2985,7 +2985,7 @@ def get(url, version, **kwargs): "creationTimestamp": "2023-08-21T10:00:00Z", "annotations": { "autoscaling.keda.sh/paused-replicas": "0", - "downscaler/original-replicas": "1" + "downscaler/original-replicas": "2147483648" } } } @@ -3069,3 +3069,162 @@ def get(url, version, **kwargs): } } assert json.loads(api.patch.call_args[1]["data"]) == patch_data + +def test_scaler_downscale_keda_with_downscale_replicas_annotation(monkeypatch): + api = MagicMock() + monkeypatch.setattr( + "kube_downscaler.scaler.helper.get_kube_api", MagicMock(return_value=api) + ) + monkeypatch.setattr( + "kube_downscaler.scaler.helper.add_event", MagicMock(return_value=None) + ) + + def get(url, version, **kwargs): + if url == "pods": + data = {"items": []} + elif url == "scaledobjects": + data = { + "items": [ + { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "downscaler/downtime-replicas": "1" + } + } + }, + ] + } + elif url == "namespaces/default": + data = { + "metadata": { + } + } + else: + raise Exception(f"unexpected call: {url}, {version}, {kwargs}") + + response = MagicMock() + response.json.return_value = data + return response + + api.get = get + + include_resources = frozenset(["scaledobjects"]) + scale( + constrained_downscaler=False, + namespaces=[], + upscale_period="never", + downscale_period="never", + default_uptime="never", + default_downtime="always", + upscale_target_only=False, + include_resources=include_resources, + exclude_namespaces=[], + exclude_deployments=[], + admission_controller="", + dry_run=False, + grace_period=300, + downtime_replicas=0, + enable_events=True, + matching_labels=frozenset([re.compile("")]), + ) + + assert api.patch.call_count == 1 + assert api.patch.call_args[1]["url"] == "/scaledobjects/scaledobject-1" + + patch_data = { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "1", + 'downscaler/downtime-replicas': '1', + 'downscaler/original-replicas': '2147483648' + } + } + } + assert json.loads(api.patch.call_args[1]["data"]) == patch_data + + +def test_scaler_upscale_keda_with_downscale_replicas_annotation(monkeypatch): + api = MagicMock() + monkeypatch.setattr( + "kube_downscaler.scaler.helper.get_kube_api", MagicMock(return_value=api) + ) + monkeypatch.setattr( + "kube_downscaler.scaler.helper.add_event", MagicMock(return_value=None) + ) + + def get(url, version, **kwargs): + if url == "pods": + data = {"items": []} + elif url == "scaledobjects": + data = { + "items": [ + { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "1", + "downscaler/downtime-replicas": "1", + "downscaler/original-replicas": "2147483648" + } + } + }, + ] + } + elif url == "namespaces/default": + data = { + "metadata": { + } + } + else: + raise Exception(f"unexpected call: {url}, {version}, {kwargs}") + + response = MagicMock() + response.json.return_value = data + return response + + api.get = get + + include_resources = frozenset(["scaledobjects"]) + scale( + constrained_downscaler=False, + namespaces=[], + upscale_period="never", + downscale_period="never", + default_uptime="always", + default_downtime="never", + upscale_target_only=False, + include_resources=include_resources, + exclude_namespaces=[], + exclude_deployments=[], + admission_controller="", + dry_run=False, + grace_period=300, + downtime_replicas=0, + enable_events=True, + matching_labels=frozenset([re.compile("")]), + ) + + assert api.patch.call_count == 1 + assert api.patch.call_args[1]["url"] == "/scaledobjects/scaledobject-1" + + patch_data = { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": None, + "downscaler/original-replicas": None, + "downscaler/downtime-replicas": "1" + } + } + } + assert json.loads(api.patch.call_args[1]["data"]) == patch_data \ No newline at end of file From e069318be6a6d4360ff868bf66b0ee8d097054fe Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:03:07 +0200 Subject: [PATCH 2/8] docs: added scaledobject paragraph --- README.md | 20 ++++++++++++++++++++ kube_downscaler/resources/keda.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35cdd60..685769c 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,26 @@ The feature to scale DaemonSets can be very useful for reducing the base occupan 1. Downtime Hours: Kube Downscaler will add to each targeted DaemonSet a Node Selector that cannot be satisfied `kube-downscaler-non-existent=true` 2. Uptime Hours: Kube Downscaler will remove the `kube-downscaler-non-existent=true` Node Selector from each targeted DaemonSet +### Scaling ScaledObjects + +The ability to downscale ScaledObjects is very useful for workloads that use Keda to support +a wider range of horizontal scaling metrics compared to the native Horizontal Pod Autoscaler (HPA). +Keda provides a built-in way to disable ScaledObjects when they are not needed. This can be achieved by using +the annotation `"autoscaling.keda.sh/paused-replicas"`. + +The KubeDownscaler algorithm will apply the annotation `"autoscaling.keda.sh/paused-replicas" ` +during downtime periods, setting its value to what the user specifies through an environment variable +or the annotation `"downscaler/downtime-replicas"`. During uptime, KubeDownscaler will remove the +`"autoscaling.keda.sh/paused-replicas"` annotation, allowing the ScaledObject to operate as originally configured. + +**Important**: KubeDownscaler has an automatic mechanism that detects if the `"autoscaling.keda.sh/paused-replicas" ` +annotation is already present on the ScaledObject. If that is the case, KubeDownscaler will overwrite it +with the target value specified for downtime and, during uptime, will restore the original value. + +**Important**: During downscaling, KubeDownscaler will set the annotation `"downscaler/original-replicas"` to the value 2147483648, +which is one unit higher than GoLang’s maximum integer value (2147483647). This value acts as a placeholder to indicate +that the ScaledObject was active during uptime, as ScaledObjects themselves don’t inherently track replica counts. + ### Matching Labels Argument Labels, in Kubernetes, are key-value pairs that can be used to identify and group resources. diff --git a/kube_downscaler/resources/keda.py b/kube_downscaler/resources/keda.py index 9873fa8..70fd7e2 100644 --- a/kube_downscaler/resources/keda.py +++ b/kube_downscaler/resources/keda.py @@ -13,7 +13,7 @@ class ScaledObject(NamespacedAPIObject): last_keda_pause_annotation_if_present = "downscaler/original-pause-replicas" # GoLang 32-bit signed integer max value + 1. The value was choosen because 2147483647 is the max allowed - # for Deployment/StatefulSet.spec.template.replicas + # for Deployment/StatefulSet.spec.replicas KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 @property From 792b28d58d8b3e9b488424313c84c215a81f03e1 Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:42:20 +0200 Subject: [PATCH 3/8] feat: scaled object docs typo, logs messages, added unit tests --- README.md | 1 + kube_downscaler/scaler.py | 4 +- tests/test_scaler.py | 163 +++++++++++++++++++++++++++++++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 685769c..9f688ae 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Scale down / "pause" Kubernetes workload (`Deployments`, `StatefulSets`, - [Scaling Jobs Natively](#scaling-jobs-natively) - [Scaling Jobs With Admission Controller](#scaling-jobs-with-admission-controller) - [Scaling DaemonSets](#scaling-daemonsets) + - [Scaling ScaledObjects](#scaling-scaledobjects) - [Matching Labels Argument](#matching-labels-argument) - [Namespace Defaults](#namespace-defaults) - [Migrate From Codeberg](#migrate-from-codeberg) diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index b3779be..0434958 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -415,11 +415,11 @@ def get_replicas( replicas = resource.replicas if replicas == KUBERNETES_MAX_ALLOWED_REPLICAS + 1: logger.debug( - f"{resource.kind} {resource.namespace}/{resource.name} is currently active (uptime: {uptime})" + f"{resource.kind} {resource.namespace}/{resource.name} controls some replicas (uptime: {uptime})" ) else: logger.debug( - f"{resource.kind} {resource.namespace}/{resource.name} is currently paused with {replicas} replicas (uptime: {uptime})" + f"{resource.kind} {resource.namespace}/{resource.name} controls {replicas} replicas (original: {original_replicas}, uptime: {uptime})" ) else: replicas = resource.replicas diff --git a/tests/test_scaler.py b/tests/test_scaler.py index 6b5e42f..93efd32 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -2777,7 +2777,6 @@ def get(url, version, **kwargs): "creationTimestamp": "2023-08-21T10:00:00Z", "annotations": { "autoscaling.keda.sh/paused-replicas": "2", - "downscaler/original-pause-replicas": "2" } } }, @@ -3227,4 +3226,166 @@ def get(url, version, **kwargs): } } } + assert json.loads(api.patch.call_args[1]["data"]) == patch_data + +def test_scaler_downscale_keda_already_with_pause_annotation_and_downtime_replicas(monkeypatch): + api = MagicMock() + monkeypatch.setattr( + "kube_downscaler.scaler.helper.get_kube_api", MagicMock(return_value=api) + ) + monkeypatch.setattr( + "kube_downscaler.scaler.helper.add_event", MagicMock(return_value=None) + ) + + def get(url, version, **kwargs): + if url == "pods": + data = {"items": []} + elif url == "scaledobjects": + data = { + "items": [ + { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "2", + "downscaler/downtime-replicas": "1" + } + } + }, + ] + } + elif url == "namespaces/default": + data = { + "metadata": { + } + } + else: + raise Exception(f"unexpected call: {url}, {version}, {kwargs}") + + response = MagicMock() + response.json.return_value = data + return response + + api.get = get + + include_resources = frozenset(["scaledobjects"]) + scale( + constrained_downscaler=False, + namespaces=[], + upscale_period="never", + downscale_period="never", + default_uptime="never", + default_downtime="always", + upscale_target_only=False, + include_resources=include_resources, + exclude_namespaces=[], + exclude_deployments=[], + admission_controller="", + dry_run=False, + grace_period=300, + downtime_replicas=0, + enable_events=True, + matching_labels=frozenset([re.compile("")]), + ) + + assert api.patch.call_count == 1 + assert api.patch.call_args[1]["url"] == "/scaledobjects/scaledobject-1" + + patch_data = { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "1", + "downscaler/original-pause-replicas": "2", + "downscaler/downtime-replicas": "1", + "downscaler/original-replicas": "2", + } + } + } + assert json.loads(api.patch.call_args[1]["data"]) == patch_data + +def test_scaler_upscale_keda_already_with_pause_annotation_and_downtime_replicas(monkeypatch): + api = MagicMock() + monkeypatch.setattr( + "kube_downscaler.scaler.helper.get_kube_api", MagicMock(return_value=api) + ) + monkeypatch.setattr( + "kube_downscaler.scaler.helper.add_event", MagicMock(return_value=None) + ) + + def get(url, version, **kwargs): + if url == "pods": + data = {"items": []} + elif url == "scaledobjects": + data = { + "items": [ + { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "1", + "downscaler/original-pause-replicas": "2", + "downscaler/downtime-replicas": "1", + "downscaler/original-replicas": "2", + } + } + }, + ] + } + elif url == "namespaces/default": + data = { + "metadata": { + } + } + else: + raise Exception(f"unexpected call: {url}, {version}, {kwargs}") + + response = MagicMock() + response.json.return_value = data + return response + + api.get = get + + include_resources = frozenset(["scaledobjects"]) + scale( + constrained_downscaler=False, + namespaces=[], + upscale_period="never", + downscale_period="never", + default_uptime="always", + default_downtime="never", + upscale_target_only=False, + include_resources=include_resources, + exclude_namespaces=[], + exclude_deployments=[], + admission_controller="", + dry_run=False, + grace_period=300, + downtime_replicas=0, + enable_events=True, + matching_labels=frozenset([re.compile("")]), + ) + + assert api.patch.call_count == 1 + assert api.patch.call_args[1]["url"] == "/scaledobjects/scaledobject-1" + + patch_data = { + "metadata": { + "name": "scaledobject-1", + "namespace": "default", + "creationTimestamp": "2023-08-21T10:00:00Z", + "annotations": { + "autoscaling.keda.sh/paused-replicas": "2", + "downscaler/original-pause-replicas": None, + "downscaler/original-replicas": None, + "downscaler/downtime-replicas": "1" + } + } + } assert json.loads(api.patch.call_args[1]["data"]) == patch_data \ No newline at end of file From 76f1e415edad344bff580d80aef526dcfb74378e Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:34:10 +0200 Subject: [PATCH 4/8] fix: docs and log messages for scaledobject --- README.md | 8 ++++++-- kube_downscaler/scaler.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9f688ae..b0db291 100644 --- a/README.md +++ b/README.md @@ -607,13 +607,17 @@ during downtime periods, setting its value to what the user specifies through an or the annotation `"downscaler/downtime-replicas"`. During uptime, KubeDownscaler will remove the `"autoscaling.keda.sh/paused-replicas"` annotation, allowing the ScaledObject to operate as originally configured. +**Important**: When using the `"downscaler/downtime-replicas"` annotation at the workload level, it is crucial that +this annotation is included in both the ScaledObject and the corresponding Deployment or StatefulSet that it controls +and the values of the annotation must match in both locations. + **Important**: KubeDownscaler has an automatic mechanism that detects if the `"autoscaling.keda.sh/paused-replicas" ` annotation is already present on the ScaledObject. If that is the case, KubeDownscaler will overwrite it with the target value specified for downtime and, during uptime, will restore the original value. -**Important**: During downscaling, KubeDownscaler will set the annotation `"downscaler/original-replicas"` to the value 2147483648, +**Technical Detail**: During downscaling, KubeDownscaler will set the annotation `"downscaler/original-replicas"` to the value 2147483648, which is one unit higher than GoLang’s maximum integer value (2147483647). This value acts as a placeholder to indicate -that the ScaledObject was active during uptime, as ScaledObjects themselves don’t inherently track replica counts. +that the ScaledObject was active during uptime. ### Matching Labels Argument diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index 0434958..d6d3ee6 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -415,11 +415,11 @@ def get_replicas( replicas = resource.replicas if replicas == KUBERNETES_MAX_ALLOWED_REPLICAS + 1: logger.debug( - f"{resource.kind} {resource.namespace}/{resource.name} controls some replicas (uptime: {uptime})" + f"{resource.kind} {resource.namespace}/{resource.name} is not suspended (uptime: {uptime})" ) else: logger.debug( - f"{resource.kind} {resource.namespace}/{resource.name} controls {replicas} replicas (original: {original_replicas}, uptime: {uptime})" + f"{resource.kind} {resource.namespace}/{resource.name} is suspended (uptime: {uptime})" ) else: replicas = resource.replicas From cb4e3613af0cecaeda4d251ea6a1ca2218506cf6 Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:45:12 +0200 Subject: [PATCH 5/8] fix: added comments on keda.py, fixed docs --- README.md | 4 ++-- kube_downscaler/resources/keda.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b0db291..2e9ac6f 100644 --- a/README.md +++ b/README.md @@ -603,8 +603,8 @@ Keda provides a built-in way to disable ScaledObjects when they are not needed. the annotation `"autoscaling.keda.sh/paused-replicas"`. The KubeDownscaler algorithm will apply the annotation `"autoscaling.keda.sh/paused-replicas" ` -during downtime periods, setting its value to what the user specifies through an environment variable -or the annotation `"downscaler/downtime-replicas"`. During uptime, KubeDownscaler will remove the +during downtime periods, setting its value to what the user specifies through the KubeDownscaler argument `--downtime-replicas` +or the workload annotation `"downscaler/downtime-replicas"`. During uptime, KubeDownscaler will remove the `"autoscaling.keda.sh/paused-replicas"` annotation, allowing the ScaledObject to operate as originally configured. **Important**: When using the `"downscaler/downtime-replicas"` annotation at the workload level, it is crucial that diff --git a/kube_downscaler/resources/keda.py b/kube_downscaler/resources/keda.py index 70fd7e2..9adf072 100644 --- a/kube_downscaler/resources/keda.py +++ b/kube_downscaler/resources/keda.py @@ -16,6 +16,8 @@ class ScaledObject(NamespacedAPIObject): # for Deployment/StatefulSet.spec.replicas KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 + # If keda_pause_annotation is not present return KUBERNETES_MAX_ALLOWED_REPLICAS + 1, which means + # the ScaledObject is active. Otherwise returns the amount of replicas specified inside keda_pause_annotation @property def replicas(self): if ScaledObject.keda_pause_annotation in self.annotations: From 5d86654351fb988cea3901dd73afb20ea0818b15 Mon Sep 17 00:00:00 2001 From: Samuel Esposito <40698384+samuel-esp@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:51:06 +0200 Subject: [PATCH 6/8] fixed typo Co-authored-by: Jonathan Mayer --- kube_downscaler/scaler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index d6d3ee6..0e00ddc 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -42,7 +42,7 @@ GRACE_PERIOD_ANNOTATION="downscaler/grace-period" # GoLang 32-bit signed integer max value + 1. The value was choosen because 2147483647 is the max allowed -# for Deployment/StatefulSet.spec.template.replicas. This value is usefult to allow +# for Deployment/StatefulSet.spec.template.replicas. This value is used to allow # ScaledObject to support "downscaler/downtime-replcas" annotation KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 From f924974f4d92696e74f5044b2cd1806f52edb516 Mon Sep 17 00:00:00 2001 From: Samuel <40698384+samuel-esp@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:38:24 +0200 Subject: [PATCH 7/8] feat: replicas=-1 to downscale ScaledObject, added info to docs --- README.md | 7 ++++--- kube_downscaler/resources/keda.py | 12 ++++-------- kube_downscaler/scaler.py | 6 +++--- tests/test_autoscale_resource.py | 2 +- tests/test_resources.py | 2 +- tests/test_scaler.py | 6 +++--- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2e9ac6f..f00e453 100644 --- a/README.md +++ b/README.md @@ -609,14 +609,15 @@ or the workload annotation `"downscaler/downtime-replicas"`. During uptime, Kube **Important**: When using the `"downscaler/downtime-replicas"` annotation at the workload level, it is crucial that this annotation is included in both the ScaledObject and the corresponding Deployment or StatefulSet that it controls -and the values of the annotation must match in both locations. +and the values of the annotation must match in both locations. Alternatively, it is possible to exclude the Deployment +or StatefulSet from scaling by using the annotation `"downscaler/exclude"`, while keeping downscaling active only +on the ScaledObject. **Important**: KubeDownscaler has an automatic mechanism that detects if the `"autoscaling.keda.sh/paused-replicas" ` annotation is already present on the ScaledObject. If that is the case, KubeDownscaler will overwrite it with the target value specified for downtime and, during uptime, will restore the original value. -**Technical Detail**: During downscaling, KubeDownscaler will set the annotation `"downscaler/original-replicas"` to the value 2147483648, -which is one unit higher than GoLang’s maximum integer value (2147483647). This value acts as a placeholder to indicate +**Technical Detail**: During downscaling, KubeDownscaler will set the annotation `"downscaler/original-replicas"` to -1, this value acts as a placeholder to indicate that the ScaledObject was active during uptime. ### Matching Labels Argument diff --git a/kube_downscaler/resources/keda.py b/kube_downscaler/resources/keda.py index 9adf072..e06e0ad 100644 --- a/kube_downscaler/resources/keda.py +++ b/kube_downscaler/resources/keda.py @@ -12,22 +12,18 @@ class ScaledObject(NamespacedAPIObject): keda_pause_annotation = "autoscaling.keda.sh/paused-replicas" last_keda_pause_annotation_if_present = "downscaler/original-pause-replicas" - # GoLang 32-bit signed integer max value + 1. The value was choosen because 2147483647 is the max allowed - # for Deployment/StatefulSet.spec.replicas - KUBERNETES_MAX_ALLOWED_REPLICAS = 2147483647 - - # If keda_pause_annotation is not present return KUBERNETES_MAX_ALLOWED_REPLICAS + 1, which means - # the ScaledObject is active. Otherwise returns the amount of replicas specified inside keda_pause_annotation + # If keda_pause_annotation is not present return -1 which means the ScaledObject is active + # Otherwise returns the amount of replicas specified inside keda_pause_annotation @property def replicas(self): if ScaledObject.keda_pause_annotation in self.annotations: if self.annotations[ScaledObject.keda_pause_annotation] is None: - replicas = self.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 + replicas = -1 elif self.annotations[ScaledObject.keda_pause_annotation] == "0": replicas = 0 elif self.annotations[ScaledObject.keda_pause_annotation] != "0" and self.annotations[ScaledObject.keda_pause_annotation] is not None: replicas = int(self.annotations[ScaledObject.keda_pause_annotation]) else: - replicas = self.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 + replicas = -1 return replicas diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index d6d3ee6..fd536ed 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -944,7 +944,7 @@ def autoscale_resource( and is_uptime and replicas == downtime_replicas and original_replicas - and original_replicas > 0 + and (original_replicas > 0 or original_replicas == -1) ): scale_up( resource, @@ -959,8 +959,8 @@ def autoscale_resource( elif ( not ignore and not is_uptime - and replicas > 0 - and replicas > downtime_replicas + and (replicas > 0 or replicas == -1) + and (replicas > downtime_replicas or replicas == -1) ): if within_grace_period( resource, grace_period, now, deployment_time_annotation diff --git a/tests/test_autoscale_resource.py b/tests/test_autoscale_resource.py index f54b2b3..c77974c 100644 --- a/tests/test_autoscale_resource.py +++ b/tests/test_autoscale_resource.py @@ -1304,4 +1304,4 @@ def test_upscale_scaledobject_without_keda_pause_annotation(): # Check if the annotations have been correctly updated for the upscale operation assert so.annotations[ScaledObject.keda_pause_annotation] is None assert so.annotations.get(ScaledObject.last_keda_pause_annotation_if_present) is None - assert so.replicas == so.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 + assert so.replicas == -1 diff --git a/tests/test_resources.py b/tests/test_resources.py index 048669e..3394d78 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -51,7 +51,7 @@ def test_scaledobject(): scalable_mock = {"metadata": {}} api_mock.obj = MagicMock(name="APIObjMock") d = ScaledObject(api_mock, scalable_mock) - assert d.replicas == d.KUBERNETES_MAX_ALLOWED_REPLICAS + 1 + assert d.replicas == -1 d.annotations[ScaledObject.keda_pause_annotation] = "0" assert d.replicas == 0 diff --git a/tests/test_scaler.py b/tests/test_scaler.py index 93efd32..909df19 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -2984,7 +2984,7 @@ def get(url, version, **kwargs): "creationTimestamp": "2023-08-21T10:00:00Z", "annotations": { "autoscaling.keda.sh/paused-replicas": "0", - "downscaler/original-replicas": "2147483648" + "downscaler/original-replicas": "-1" } } } @@ -3141,7 +3141,7 @@ def get(url, version, **kwargs): "annotations": { "autoscaling.keda.sh/paused-replicas": "1", 'downscaler/downtime-replicas': '1', - 'downscaler/original-replicas': '2147483648' + 'downscaler/original-replicas': '-1' } } } @@ -3171,7 +3171,7 @@ def get(url, version, **kwargs): "annotations": { "autoscaling.keda.sh/paused-replicas": "1", "downscaler/downtime-replicas": "1", - "downscaler/original-replicas": "2147483648" + "downscaler/original-replicas": "-1" } } }, From 4c1a9302262484b918301fe331273ff6d7ebd0fd Mon Sep 17 00:00:00 2001 From: Samuel Esposito <40698384+samuel-esp@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:00:34 +0200 Subject: [PATCH 8/8] fix: refactored boolean expression Co-authored-by: Jonathan Mayer --- kube_downscaler/scaler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/kube_downscaler/scaler.py b/kube_downscaler/scaler.py index 1efb0c1..569727d 100644 --- a/kube_downscaler/scaler.py +++ b/kube_downscaler/scaler.py @@ -959,8 +959,9 @@ def autoscale_resource( elif ( not ignore and not is_uptime - and (replicas > 0 or replicas == -1) - and (replicas > downtime_replicas or replicas == -1) + and (replicas > 0 + and replicas > downtime_replicas + or replicas == -1) ): if within_grace_period( resource, grace_period, now, deployment_time_annotation