From 22d502bbe5ad66b4950cf663d312bdfa52ae4cd4 Mon Sep 17 00:00:00 2001 From: Diego Najar Date: Thu, 16 Sep 2021 14:52:04 +0200 Subject: [PATCH] Catch exceptions in k8s class, remove from annotation the .io, update Python 3.9.7, update Alpine --- Dockerfile | 2 +- README.md | 30 ++++++++--------- files/aautoscaler.py | 20 ++++------- files/k8s.py | 79 +++++++++++++++++++++++++++++++++----------- 4 files changed, 83 insertions(+), 48 deletions(-) diff --git a/Dockerfile b/Dockerfile index 467e314..ce5e21e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.5-alpine3.13 +FROM python:3.9.7-alpine3.14 ENV PYTHONUNBUFFERED=0 diff --git a/README.md b/README.md index a37db24..f1c0c29 100644 --- a/README.md +++ b/README.md @@ -31,38 +31,38 @@ kubectl get pods -n another ## Configuration The following annotations for the deployments are valid (`metadata.annotations`). -- `another-autoscaler.io/stop-time`: Define the date and time when the replica of the deployment will be set to 0. -- `another-autoscaler.io/start-time` Define the date and time when the replica of the deployment will be set to 1. -- `another-autoscaler.io/restart-time:`: Define the date and time when the rollout restart will be peformerd to a deployment. -- `another-autoscaler.io/stop-replicas`: This is the number of replicas to set when Another Autoscaler scale down the deployment, by default is 0. -- `another-autoscaler.io/start-replicas`: This is the number of replicas to set when Another Autoscaler scale up the deployment, by default is 1. +- `another-autoscaler/stop-time`: Define the date and time when the replica of the deployment will be set to 0. +- `another-autoscaler/start-time` Define the date and time when the replica of the deployment will be set to 1. +- `another-autoscaler/restart-time:`: Define the date and time when the rollout restart will be peformerd to a deployment. +- `another-autoscaler/stop-replicas`: This is the number of replicas to set when Another Autoscaler scale down the deployment, by default is 0. +- `another-autoscaler/start-replicas`: This is the number of replicas to set when Another Autoscaler scale up the deployment, by default is 1. ## Examples ### Stop pods at 6pm every day: ``` -another-autoscaler.io/stop-time: "00 18 * * *" +another-autoscaler/stop-time: "00 18 * * *" ``` ### Start pods at 1pm every day: ``` -another-autoscaler.io/start-time: "00 13 * * *" +another-autoscaler/start-time: "00 13 * * *" ``` ### Start 3 pods at 2:30pm every day: ``` -another-autoscaler.io/start-time: "30 14 * * *" -another-autoscaler.io/start-replicas: "3" +another-autoscaler/start-time: "30 14 * * *" +another-autoscaler/start-replicas: "3" ``` ### Restart pods at 9:15am every day: ``` -another-autoscaler.io/restart-time: "15 09 * * *" +another-autoscaler/restart-time: "15 09 * * *" ``` ### Restart pods at 2:30am, only on Saturday and Sunday: ``` -another-autoscaler.io/restart-time: "00 02 * * 0,6" +another-autoscaler/restart-time: "00 02 * * 0,6" ``` ### Full example, how to start pods at 2pm and stop them at 3pm every day @@ -80,10 +80,10 @@ metadata: labels: app: nginx annotations: - another-autoscaler.io/start-time: "00 14 * * *" - another-autoscaler.io/start-replicas: "5" - another-autoscaler.io/stop-time: "00 15 * * *" - another-autoscaler.io/stop-replicas: "1" + another-autoscaler/start-time: "00 14 * * *" + another-autoscaler/start-replicas: "5" + another-autoscaler/stop-time: "00 15 * * *" + another-autoscaler/stop-replicas: "1" spec: replicas: 0 selector: diff --git a/files/aautoscaler.py b/files/aautoscaler.py index 60570d9..1bfe482 100644 --- a/files/aautoscaler.py +++ b/files/aautoscaler.py @@ -33,7 +33,7 @@ def __start__(self, namespace:str, deploy:dict, currentTime:datetime): deployAnnotations = deploy.metadata.annotations deployReplicas = deploy.spec.replicas - startAnnotation = 'another-autoscaler.io/start-time' + startAnnotation = 'another-autoscaler/start-time' if startAnnotation in deployAnnotations: self.logs.debug({'message': 'Start time detected.', 'namespace': namespace, 'deployment': deployName}) startTime = deployAnnotations[startAnnotation] @@ -43,17 +43,14 @@ def __start__(self, namespace:str, deploy:dict, currentTime:datetime): # start-replicas startReplicas = 1 - startReplicasAnnotation = 'another-autoscaler.io/start-replicas' + startReplicasAnnotation = 'another-autoscaler/start-replicas' if startReplicasAnnotation in deployAnnotations: self.logs.debug({'message': 'Replicas defined by the user for start.', 'namespace': namespace, 'deployment': deployName, 'startReplicas': deployAnnotations[startReplicasAnnotation]}) startReplicas = int(deployAnnotations[startReplicasAnnotation]) if deployReplicas != startReplicas: self.logs.info({'message': 'Deployment set to start.', 'namespace': namespace, 'deployment': deployName, 'startTime': str(startTime), 'availableReplicas': deploy.status.available_replicas, 'startReplicas': str(startReplicas)}) - try: - self.k8s.setReplicas(namespace, deployName, startReplicas) - except: - self.logs.error({'message': 'There was an error increasing the replicas, don\'t worry, we\'ll try again.'}) + self.k8s.setReplicas(namespace, deployName, startReplicas) def __stop__(self, namespace:str, deploy:dict, currentTime:datetime): ''' @@ -63,7 +60,7 @@ def __stop__(self, namespace:str, deploy:dict, currentTime:datetime): deployAnnotations = deploy.metadata.annotations deployReplicas = deploy.spec.replicas - stopAnnotation = 'another-autoscaler.io/stop-time' + stopAnnotation = 'another-autoscaler/stop-time' if stopAnnotation in deployAnnotations: self.logs.debug({'message': 'Stop time detected.', 'namespace': namespace, 'deployment': deployName}) stopTime = deployAnnotations[stopAnnotation] @@ -73,17 +70,14 @@ def __stop__(self, namespace:str, deploy:dict, currentTime:datetime): # stop-replicas stopReplicas = 0 - stopReplicasAnnotation = 'another-autoscaler.io/stop-replicas' + stopReplicasAnnotation = 'another-autoscaler/stop-replicas' if stopReplicasAnnotation in deployAnnotations: self.logs.debug({'message': 'Replicas defined by the user for stop.', 'namespace': namespace, 'deployment': deployName, 'stopReplicas': deployAnnotations[stopReplicasAnnotation]}) stopReplicas = int(deployAnnotations[stopReplicasAnnotation]) if deployReplicas != stopReplicas: self.logs.info({'message': 'Deployment set to stop.', 'namespace': namespace, 'deployment': deployName, 'stopTime': str(stopTime), 'availableReplicas': deploy.status.available_replicas, 'stopReplicas': str(stopReplicas)}) - try: - self.k8s.setReplicas(namespace, deployName, stopReplicas) - except: - self.logs.error({'message': 'There was an error decreasing the replicas, don\'t worry, we\'ll try again.'}) + self.k8s.setReplicas(namespace, deployName, stopReplicas) def __restart__(self, namespace:str, deploy:dict, currentTime:datetime): ''' @@ -92,7 +86,7 @@ def __restart__(self, namespace:str, deploy:dict, currentTime:datetime): deployName = deploy.metadata.name deployAnnotations = deploy.metadata.annotations - restartAnnotation = 'another-autoscaler.io/restart-time' + restartAnnotation = 'another-autoscaler/restart-time' if restartAnnotation in deployAnnotations: self.logs.debug({'message': 'Restart time detected.', 'namespace': namespace, 'deployment': deployName}) restartTime = deployAnnotations[restartAnnotation] diff --git a/files/k8s.py b/files/k8s.py index 2a2835a..b3348b9 100644 --- a/files/k8s.py +++ b/files/k8s.py @@ -2,6 +2,8 @@ import pytz import urllib3 from kubernetes import client, config +from kubernetes.client.rest import ApiException +from logs import Logs class K8s: @@ -28,40 +30,59 @@ def getNamespaces(self) -> list: ''' Returns a list of namespaces. ''' - response = self.CoreV1Api.list_namespace() - return response.items + try: + response = self.CoreV1Api.list_namespace() + return response.items + except ApiException as e: + self.logs.error({'message': 'Exception when calling CoreV1Api.list_namespace', 'exception': e}) + return [] def getDeployments(self, namespace:str, labelSelector:str=False) -> list: ''' Returns all deployments from a namespace. Label selector should be an string "app=kube-web-view". ''' - if labelSelector: - response = self.AppsV1Api.list_namespaced_deployment(namespace=namespace, label_selector=labelSelector) - else: - response = self.AppsV1Api.list_namespaced_deployment(namespace=namespace) - return response.items + try: + if labelSelector: + response = self.AppsV1Api.list_namespaced_deployment(namespace=namespace, label_selector=labelSelector) + else: + response = self.AppsV1Api.list_namespaced_deployment(namespace=namespace) + return response.items + except ApiException as e: + self.logs.error({'message': 'Exception when calling AppsV1Api.list_namespaced_deployment', 'exception': e}) + return [] def getDeployment(self, namespace:str, deploymentName:str): ''' Returns a particular deployment. ''' - return self.AppsV1Api.read_namespaced_deployment(namespace=namespace, name=deploymentName) + try: + return self.AppsV1Api.read_namespaced_deployment(namespace=namespace, name=deploymentName) + except ApiException as e: + self.logs.error({'message': 'Exception when calling AppsV1Api.read_namespaced_deployment', 'exception': e}) + return [] def getPods(self, namespace:str, labelSelector:str, limit:int=1) -> list: ''' Returns a list of pods for the label selector. Label selector should be an string "app=kube-web-view". ''' - response = self.CoreV1Api.list_namespaced_pod(namespace=namespace, label_selector=labelSelector, limit=limit) - return response.items + try: + response = self.CoreV1Api.list_namespaced_pod(namespace=namespace, label_selector=labelSelector, limit=limit) + return response.items + except ApiException as e: + self.logs.error({'message': 'Exception when calling CoreV1Api.list_namespaced_pod', 'exception': e}) + return [] def getPodsByDeployment(self, namespace:str, deploymentName:str, limit:int=1) -> list: ''' Returns a list of pods related to a deployment. ''' - deploy = self.getDeployment(namespace, deploymentName) - matchLabels = deploy.spec.selector.match_labels + deployment = self.getDeployment(namespace, deploymentName) + if not deployment: + return [] + + matchLabels = deployment.spec.selector.match_labels labelSelector = '' for key, value in matchLabels.items(): labelSelector += key+'='+value+',' @@ -73,6 +94,9 @@ def deleteAllPods(self, namespace:str, labelSelector:str): Delete all pods from a namespace filter by label selector. ''' deployments = self.getDeployments(namespace=namespace, labelSelector=labelSelector) + if not deployments: + return False + deployment = deployments[0] for labelKey, labelValue in deployment.spec.selector.match_labels.items(): pods = self.getPods(namespace, labelKey+'='+labelValue) @@ -80,27 +104,44 @@ def deleteAllPods(self, namespace:str, labelSelector:str): self.deletePod(namespace=namespace, podName=pod.metadata.name) return True - def setReplicas(self, namespace:str, deploymentName:str, replicas:int): + def setReplicas(self, namespace:str, deploymentName:str, replicas:int) -> bool: ''' Set the number of replicas of a deployment. ''' - currentScale = self.AppsV1Api.read_namespaced_deployment_scale(namespace=namespace, name=deploymentName) - currentScale.spec.replicas = replicas - self.AppsV1Api.replace_namespaced_deployment_scale(namespace=namespace, name=deploymentName, body=currentScale) + try: + currentScale = self.AppsV1Api.read_namespaced_deployment_scale(namespace=namespace, name=deploymentName) + currentScale.spec.replicas = replicas + self.AppsV1Api.replace_namespaced_deployment_scale(namespace=namespace, name=deploymentName, body=currentScale) + return True + except ApiException as e: + self.logs.error({'message': 'Exception when calling AppsV1Api.read_namespaced_deployment_scale', 'exception': e}) + return False def getReplicas(self, namespace:str, deploymentName:str): ''' Returns the number of replicas of a deployment. ''' - return self.AppsV1Api.read_namespaced_deployment_scale(namespace=namespace, name=deploymentName) + try: + return self.AppsV1Api.read_namespaced_deployment_scale(namespace=namespace, name=deploymentName) + except ApiException as e: + self.logs.error({'message': 'Exception when calling AppsV1Api.read_namespaced_deployment_scale', 'exception': e}) + return False - def rolloutDeployment(self, namespace:str, deploymentName:str): + def rolloutDeployment(self, namespace:str, deploymentName:str) -> bool: ''' Execute a rollout restart deployment. ''' deploymentManifest = self.getDeployment(namespace, deploymentName) + if not deploymentManifest: + return False + deploymentManifest.spec.template.metadata.annotations = {"kubectl.kubernetes.io/restartedAt": datetime.datetime.utcnow().replace(tzinfo=pytz.UTC).isoformat()} - self.AppsV1Api.replace_namespaced_deployment(namespace=namespace, name=deploymentName, body=deploymentManifest) + try: + self.AppsV1Api.replace_namespaced_deployment(namespace=namespace, name=deploymentName, body=deploymentManifest) + return True + except ApiException as e: + self.logs.error({'message': 'Exception when calling AppsV1Api.replace_namespaced_deployment', 'exception': e}) + return False def getIngress(self, namespace, ingressName): response = self.ExtensionsV1beta1Api.read_namespaced_ingress(namespace=namespace, name=ingressName)