diff --git a/xWorkflows/xWorkflow3/README.md b/xWorkflows/xWorkflow3/README.md new file mode 100644 index 0000000..64c3a9f --- /dev/null +++ b/xWorkflows/xWorkflow3/README.md @@ -0,0 +1,123 @@ + +# X-Workflow 3: Detecting and deleting Abandoned Workloads in a Kubernetes Cluster + +## Overview + +X-Workflow 3 aims to identify unutilized workloads in a Kubernetes cluster, and display them on a dashboard using API for user information. The workflow involves querying Kubecost for cost-based workloads(ingress/egress) utilization data, storing this information in MongoDB, and providing a user interface for node monitoring. + + + +## How it Works: + +1. **Kubecost Query Pod**: + - The query pod continuously queries Kubecost to retrieve pods data based on ingress and egress. + - The query pod dumps this data into a MongoDB database deployed as a StatefulSet in the cluster. +2. **Flask Backend**: + - The backend pod queries the MongoDB database to display results to users about abandonend workloads pods. + +## Steps for Installation and Testing + + + +### 1. Install Kubecost + +Follow the instructions to install Kubecost on your Kubernetes cluster: +- [Kubecost Installation Guide](https://docs.kubecost.com/install-and-configure/install) + +### 2. Deploy MongoDB StatefulSet + +Deploy MongoDB using the StatefulSet configuration (`manifests/mongodb-statefuleset.yaml`). + +```bash +kubectl apply -f k8manifests/mongodb-statefuleset.yaml +``` + +### 3. Deploy Kubecost Query Pod + +1. **Build Docker Image for Kubecost Query Pod**: + + Navigate to the `kubecost_query_pod` directory and build the Docker image. + + ```bash + cd kubecost_query_pod_go + docker build -t /kubecost-query-pod -f Dockerfile . + docker push /kubecost-query-pod + ``` + +2. **Update Image Tag**: + + Update the image tag in `manifests/kubecost-query-pod.yaml` to the one you just pushed. + +3. **Deploy the Kubecost Query Pod**: + + ```bash + kubectl apply -f manifests/kubecost-query-pod.yaml + ``` + +### 4. Deploy Flask Backend + +1. **Build Docker Image for Flask Backend**: + + Navigate to the `backend` directory and build the Docker image. + + ```bash + cd backend + docker build -t /backend -f Dockerfile . + docker push /backend + ``` + +2. **Update Image Tag**: + + Update the image tag in `manifests/flask-backend.yaml` to the one you just pushed. + +3. **Deploy the Flask Backend**: + + ```bash + kubectl apply -f manifests/flask-backend.yaml + ``` + +### 5. Create necessary Roles and Rolebindings + +We need to create the roles and rolebindings in order to give permisstion to robusta runner to update the workloads, so that a user decrease the replicas of abandonend workloads. + ```bash + kubectl apply -f manifests/roles-rolebindings.yaml + ``` +The Flask backend service is set up as a ClusterIP service. You need to forward the port to access this on localhost. + +### 6. Push custom actions to robusta playbooks + +We need to add our custom actions to robusta playbooks. + ```bash + robusta playbooks push rb-actions + ``` + +### 7. Access the API + +The Flask backend service is set up as a ClusterIP service. You need to forward the port to access this on localhost. + +1. **Find the Pod**: + + ```bash + kubectl get pods -n + ``` + +2. **Port-forwarding**: + + ```bash + kubectl port-forward -n 5000 + ``` + +3. **Access the API**: + + Open your browser and navigate to `http://localhost:5000` to see the API results. + +### Additional Notes + +- The Docker image registry needs to be publicly accessible so that the pod can pull the container image. If the registry is private, you need to set up registry authentication accordingly. + +By following these steps, you should be able to set up and test the Kubecost query pod, MongoDB StatefulSet, and Flask backend successfully. + + +## 🧾 License + +XkOps is licensed under Apache License, Version 2.0. See [LICENSE.md](https://github.com/XgridInc/xkops/blob/master/LICENSE "LICENSE.md") for more information \ No newline at end of file diff --git a/xWorkflows/xWorkflow3/backend/Dockerfile b/xWorkflows/xWorkflow3/backend/Dockerfile new file mode 100644 index 0000000..1f93283 --- /dev/null +++ b/xWorkflows/xWorkflow3/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12 +WORKDIR /apps +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +RUN pip show pymongo +COPY . . +CMD ["python", "app.py"] \ No newline at end of file diff --git a/xWorkflows/xWorkflow3/backend/app.py b/xWorkflows/xWorkflow3/backend/app.py new file mode 100644 index 0000000..7ca11f2 --- /dev/null +++ b/xWorkflows/xWorkflow3/backend/app.py @@ -0,0 +1,159 @@ +from flask import Flask, jsonify, request +from pymongo import MongoClient +import requests +import pymongo +import os + +app = Flask(__name__) + +# Environment Variables +MONGO_USERNAME = os.getenv("MONGO_USERNAME") +MONGO_PASSWORD = os.getenv("MONGO_PASSWORD") +MONGO_HOST = os.getenv("MONGO_HOST") +MONGO_PORT = os.getenv("MONGO_PORT") +MONGO_DB_NAME = os.getenv("MONGO_DB_NAME") +MONGO_COLLECTION_NAME = os.getenv("MONGO_COLLECTION_NAME") +ROBUSTA_URL = os.getenv("ROBUSTA_URL") + +# Connect to MongoDB +client = MongoClient(f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}") + +# Accessing the db +def get_db(): + try: + db = client[MONGO_DB_NAME] + return db + except pymongo.errors.PyMongoError: + return jsonify({"error": "Error accessing database"}), 500 + +# Accessing the collection +def get_collection(): + try: + db = get_db() # Reuse the database access logic + collection = db[MONGO_COLLECTION_NAME] + return collection + except pymongo.errors.PyMongoError: + return jsonify({"error": "Error accessing collection"}), 500 + +# List the Deployments with Abandoned Workloads +@app.route('/deployments', methods=['GET']) +def get_deployments(): + try: + collection = get_collection() # Use the function to access collection and client + deployments = list(collection.find({}, {'_id': 0, 'owners': 1})) + + # Filter out entries with empty owner name or kind + filteredDeployments = [deployment for deployment in deployments if all(owner.get('name') and owner.get('kind') for owner in deployment.get('owners', []))] + + return jsonify(filteredDeployments) + except pymongo.errors.PyMongoError: + return jsonify({"error": "Error fetching deployments"}), 500 + +# Delete the Deployment with Abandoned Workload +@app.route('/deployments/delete/', methods=['POST']) +def delete_deployments(deployment_name): + print("The name of the Deployment is:", deployment_name) + data = request.get_json() + deployment_namespace = data.get("namespace") + try: + payload = { + "action_name": "deleteDeployment", + "action_params": {"name": deployment_name, "namespace": deployment_namespace}, + } + headers = {"Content-Type": "application/json"} + response = requests.post(ROBUSTA_URL, json=payload, headers=headers) + response.raise_for_status() # Raise exception for non-2xx status codes + + # Update PV status in MongoDB (optional for informational purposes) + with MongoClient(f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}") as client: + db = client[MONGO_DB_NAME] + col = db[MONGO_COLLECTION_NAME] + col.update_one({"name": deployment_name}, {"$set": {"status": "deleted"}}) # Update only if necessary + + return jsonify({"message": "Deployment deletion initiated"}) + except requests.exceptions.RequestException as e: + return jsonify({"error": f"Error from Robusta API: {e}"}), e.response.status_code + except pymongo.errors.PyMongoError as e: + return jsonify({"error": f"MongoDB error: {e}"}), 500 + except Exception as e: # Catch other unexpected errors + return jsonify({"error": "Internal server error"}), 500 + +# Resize the Deployment with Abandoned Workloads by changing the replicas +@app.route('/deployments/replicasresize/', methods=['POST']) +def resize_deployments(deploymentName): + print("The name of the Deployment is:", deploymentName) + data = request.get_json() + deploymentNamespace = data.get("namespace") + updatedReplicas = data.get("replicas") + try: + payload = { + "action_name": "resizeDeploymentReplicaCount", + "action_params": {"name": deploymentName, "namespace": deploymentNamespace, "replicas": updatedReplicas}, + } + headers = {"Content-Type": "application/json"} + response = requests.post(ROBUSTA_URL, json=payload, headers=headers) + response.raise_for_status() # Raise exception for non-2xx status codes + + # Update PV status in MongoDB (optional for informational purposes) + with MongoClient(f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}") as client: + db = client[MONGO_DB_NAME] + col = db[MONGO_COLLECTION_NAME] + col.update_one({"name": deploymentName}, {"$set": {"status": "Resized"}}) # Update only if necessary + + return jsonify({"message": "Deployment resizing initiated"}) + except requests.exceptions.RequestException as e: + return jsonify({"error": f"Error from Robusta API: {e}"}), e.response.status_code + except pymongo.errors.PyMongoError as e: + return jsonify({"error": f"MongoDB error: {e}"}), 500 + except Exception as e: # Catch other unexpected errors + return jsonify({"error": "Internal server error"}), 500 +# Get the pods +@app.route('/pods', methods=['GET']) +def get_pods(): + try: + collection = get_collection() # Use the function to access collection + pods = list(collection.find({}, {'_id': 0})) + return jsonify(pods) + except pymongo.errors.PyMongoError: + return jsonify({"error": "Error fetching pods"}), 500 + +# Delete the pod +@app.route('/pods/delete/', methods=['POST']) +def delete_pods(pod_name): + print("The name of the Pod is:", pod_name) + data = request.get_json() + pod_namespace = data.get("namespace") + try: + payload = { + "action_name": "deletePod", + "action_params": {"name": pod_name, "namespace": pod_namespace}, + } + headers = {"Content-Type": "application/json"} + response = requests.post(ROBUSTA_URL, json=payload, headers=headers) + response.raise_for_status() # Raise exception for non-2xx status codes + + # Update PV status in MongoDB (optional for informational purposes) + with MongoClient(f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}") as client: + db = client[MONGO_DB_NAME] + col = db[MONGO_COLLECTION_NAME] + col.update_one({"name": pod_name}, {"$set": {"status": "deleted"}}) # Update only if necessary + + return jsonify({"message": "Pod deletion initiated"}) + except requests.exceptions.RequestException as e: + return jsonify({"error": f"Error from Robusta API: {e}"}), e.response.status_code + except pymongo.errors.PyMongoError as e: + return jsonify({"error": f"MongoDB error: {e}"}), 500 + except Exception as e: # Catch other unexpected errors + return jsonify({"error": "Internal server error"}), 500 + +# Health check endpoint +@app.route('/health') +def health(): + return jsonify({"status": "OK"}), 200 # Simple JSON response with 200 status code + +@app.route('/') +def home(): + return "Welcome to the Flask API!" + +if __name__ == '__main__': + app.run(debug=False, host='0.0.0.0') diff --git a/xWorkflows/xWorkflow3/backend/requirements.txt b/xWorkflows/xWorkflow3/backend/requirements.txt new file mode 100644 index 0000000..6c560ce --- /dev/null +++ b/xWorkflows/xWorkflow3/backend/requirements.txt @@ -0,0 +1,3 @@ +Flask +requests +pymongo==4.2.0 \ No newline at end of file diff --git a/xWorkflows/xWorkflow3/kubecost_query_pod/Dockerfile b/xWorkflows/xWorkflow3/kubecost_query_pod/Dockerfile new file mode 100644 index 0000000..f11f356 --- /dev/null +++ b/xWorkflows/xWorkflow3/kubecost_query_pod/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/xWorkflows/xWorkflow3/kubecost_query_pod/main.py b/xWorkflows/xWorkflow3/kubecost_query_pod/main.py new file mode 100644 index 0000000..a1ddb1d --- /dev/null +++ b/xWorkflows/xWorkflow3/kubecost_query_pod/main.py @@ -0,0 +1,58 @@ +import requests +from pymongo import MongoClient +import os + +# Environment Variables +MONGO_USERNAME = os.getenv("MONGO_USERNAME") +MONGO_PASSWORD = os.getenv("MONGO_PASSWORD") +MONGO_HOST = os.getenv("MONGO_HOST") +MONGO_PORT = os.getenv("MONGO_PORT") +MONGO_DB_NAME = os.getenv("MONGO_DB_NAME") +MONGO_COLLECTION_NAME = os.getenv("MONGO_COLLECTION_NAME") +ROBUSTA_URL = os.getenv("ROBUSTA_URL") + +# Get the list of Abandoned Workloads from Kubecost API +def getAbandonendWorkloads(): + api_endpoint = ROBUSTA_URL + params = { + "days": 2, + "threshold": 500, + "filter": "" # Replace with actual filter if needed + } + + try: + response = requests.get(api_endpoint, params=params) + response.raise_for_status() # Raises HTTPError for bad responses + response_json = response.json() + abandonendWorkloads = [ + { + "pod": item['pod'], + "namespace": item['namespace'], + "owners": item['owners'] + } + for item in response_json + ] + return abandonendWorkloads + except requests.exceptions.RequestException as e: + print(f"Request failed: {e}") + return [] + +def insert_pods_to_mongodb(abandonendWorkloads): + try: + client = MongoClient(f"mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}") + db = client[MONGO_DB_NAME] # Replace with your database name + collection = db[MONGO_COLLECTION_NAME] # Replace with your collection name + print("Searching for Abandoned Workloads") + for abandonendWorkload in abandonendWorkloads: + # Check if the pod is part of a deployment + collection.insert_one(abandonendWorkload) + except Exception as e: + print(f"Failed to insert pods into MongoDB: {e}") + +if __name__ == "__main__": + abandonendWorkloads = getAbandonendWorkloads() + if abandonendWorkloads: + print(abandonendWorkloads) + insert_pods_to_mongodb(abandonendWorkloads) + + diff --git a/xWorkflows/xWorkflow3/kubecost_query_pod/requirements.txt b/xWorkflows/xWorkflow3/kubecost_query_pod/requirements.txt new file mode 100644 index 0000000..eae0f08 --- /dev/null +++ b/xWorkflows/xWorkflow3/kubecost_query_pod/requirements.txt @@ -0,0 +1,2 @@ +requests +pymongo diff --git a/xWorkflows/xWorkflow3/manifests/flask-backend.yaml b/xWorkflows/xWorkflow3/manifests/flask-backend.yaml new file mode 100644 index 0000000..14b5704 --- /dev/null +++ b/xWorkflows/xWorkflow3/manifests/flask-backend.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: flask-backend + namespace: xworkflow3 +spec: + replicas: 1 # Adjust as per your scaling needs + selector: + matchLabels: + app: flask-backend + template: + metadata: + labels: + app: flask-backend + spec: + containers: + - name: flask-backend + image: hamzaarshad10/workflow3backend:v11.8 # Replace with your Docker image name and tag + ports: + - containerPort: 5000 # Port on which Flask app listens + # We need to move password and username in the secrets and mongo host, port, db name and collection name to config + env: + - name: MONGO_HOST + value: "mongo-db-0.mongo-db.xworkflow3.svc.cluster.local" + - name: MONGO_PORT + value: "27017" # MongoDB port + - name: MONGO_DB_NAME + value: "k8sData" # MongoDB database name + - name: MONGO_USERNAME + value: "admin" # MongoDB username + - name: MONGO_PASSWORD + value: "password" # MongoDB password + - name: MONGO_COLLECTION_NAME + value: "abandonendworkloads" + - name: ROBUSTA_URL + value: "http://robusta-runner.default.svc.cluster.local/api/trigger" +--- +apiVersion: v1 +kind: Service +metadata: + name: flask-backend-service + namespace: xworkflow3 +spec: + selector: + app: flask-backend # Selects the Pods with label 'app: flask-backend' + ports: + - protocol: TCP + port: 5000 # Port on the Service + targetPort: 5000 # Port on the Pod (Flask app) + diff --git a/xWorkflows/xWorkflow3/manifests/kubecost-query-pod.yaml b/xWorkflows/xWorkflow3/manifests/kubecost-query-pod.yaml new file mode 100644 index 0000000..36d1df2 --- /dev/null +++ b/xWorkflows/xWorkflow3/manifests/kubecost-query-pod.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Pod +metadata: + name: kubecost-query-pod1 + namespace: xworkflow3 +spec: + containers: + - name: kubecost-query-container + image: hamzaarshad10/my-multistage-image:latest + env: + - name: MONGO_HOST + value: "mongo-db-0.mongo-db.xworkflow3.svc.cluster.local" + - name: MONGO_PORT + value: "27017" # MongoDB port + - name: MONGO_DB_NAME + value: "k8sData" # MongoDB database name + - name: MONGO_USERNAME + value: "admin" # MongoDB username + - name: MONGO_PASSWORD + value: "password" # MongoDB password + - name: MONGO_COLLECTION_NAME + value: "abandonendworkloads" + - name: ROBUSTA_URL + value: "http://kubecost-cost-analyzer.kubecost.svc.cluster.local:9090/model/savings/abandonedWorkloads" diff --git a/xWorkflows/xWorkflow3/manifests/mongodb-statefuleset.yaml b/xWorkflows/xWorkflow3/manifests/mongodb-statefuleset.yaml new file mode 100644 index 0000000..129ed2a --- /dev/null +++ b/xWorkflows/xWorkflow3/manifests/mongodb-statefuleset.yaml @@ -0,0 +1,69 @@ +apiVersion: "v1" +kind: "PersistentVolumeClaim" +metadata: + name: "mongodb-pvc" + namespace: xworkflow3 + labels: + app: "mongo-db" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: standard + +--- +apiVersion: "apps/v1" +kind: "StatefulSet" +metadata: + name: "mongo-db" + namespace: xworkflow3 +spec: + serviceName: "mongo-db" + replicas: 1 + selector: + matchLabels: + app: "mongo-db" + template: + metadata: + labels: + app: "mongo-db" + spec: + containers: + - name: "mongo-db" + image: "mongo" + imagePullPolicy: "Always" + env: + - name: "MONGO_INITDB_ROOT_USERNAME" + value: "admin" + - name: "MONGO_INITDB_ROOT_PASSWORD" + value: "password" + ports: + - containerPort: 27017 + name: "mongodb" + volumeMounts: + - name: "mongodb-persistent-storage" + mountPath: "/data/db" + + volumes: + - name: "mongodb-persistent-storage" + persistentVolumeClaim: + claimName: "mongodb-pvc" + +--- +apiVersion: "v1" +kind: "Service" +metadata: + name: "mongo-db" + namespace: xworkflow3 + labels: + app: "mongo-db" +spec: + ports: + - name: "mongodb" + port: 27017 + targetPort: 27017 + # clusterIP: "None" + selector: + app: "mongo-db" diff --git a/xWorkflows/xWorkflow3/manifests/roles-rolebindings.yaml b/xWorkflows/xWorkflow3/manifests/roles-rolebindings.yaml new file mode 100644 index 0000000..b21b2a8 --- /dev/null +++ b/xWorkflows/xWorkflow3/manifests/roles-rolebindings.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: deployment-editor + namespace: default +rules: +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "list", "watch", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: robusta-runner-binding + namespace: default +subjects: +- kind: ServiceAccount + name: robusta-runner-service-account + namespace: default +roleRef: + kind: Role + name: deployment-editor + apiGroup: rbac.authorization.k8s.io diff --git a/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deleteDeployment.py b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deleteDeployment.py new file mode 100644 index 0000000..112d67e --- /dev/null +++ b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deleteDeployment.py @@ -0,0 +1,55 @@ +# Copyright (c) 2023, Xgrid Inc, https://xgrid.co + +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for deleting a deployment +""" +import logging + +from robusta.api import ( + Finding, + FindingSource, + FindingType, + MarkdownBlock, + DeploymentEvent, + action, +) + + +@action +def deleteDeployment(event: DeploymentEvent): + """ + Deletes a persistent volume + """ + if not event.get_deployment(): + logging.error("Failed to get the Deployment for deletion") + return + deployment = event.get_deployment() + deploymentName= deployment.metadata.name + event.get_deployment().delete() + + functionName = "deleteDeployment" + finding = Finding( + title="Deployment deletion", + source=FindingSource.MANUAL, + aggregation_key=functionName, + finding_type=FindingType.REPORT, + failure=False, + ) + finding.add_enrichment( + [ + MarkdownBlock(f"{deploymentName} is deleted."), + ] + ) + event.add_finding(finding) diff --git a/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deletePod.py b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deletePod.py new file mode 100644 index 0000000..3d2e2a4 --- /dev/null +++ b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/deletePod.py @@ -0,0 +1,59 @@ +# Copyright (c) 2023, Xgrid Inc, https://xgrid.co + +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Module for deleting a pod +""" +import logging + +from robusta.api import ( + Finding, + FindingSource, + FindingType, + MarkdownBlock, + PodEvent, + action, +) + + +@action +def deletePod(event: PodEvent): + """ + Deletes a persistent volume + """ + # Check if the persistent volume is present + if not event.get_pod(): + # Log an error message if the volume is not found + logging.error("Failed to get the pod for deletion") + return + pod = event.get_pod() + podName= pod.metadata.name + event.get_pod().delete() + + # Create a Finding object to store and send the details of the deleted volume + functionName = "deletePod" + finding = Finding( + title="Pod deleted", + source=FindingSource.MANUAL, + aggregation_key=functionName, + finding_type=FindingType.REPORT, + failure=False, + ) + # Add a MarkdownBlock to the finding object to display the deleted volume name + finding.add_enrichment( + [ + MarkdownBlock(f"{podName} is deleted."), + ] + ) + event.add_finding(finding) diff --git a/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/resizeDeploymentReplicaCount.py b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/resizeDeploymentReplicaCount.py new file mode 100644 index 0000000..fbd28f9 --- /dev/null +++ b/xWorkflows/xWorkflow3/rb-actions/my_playbook_repo/resizeDeploymentReplicaCount.py @@ -0,0 +1,63 @@ +import logging +from robusta.api import ( + Finding, + FindingSource, + FindingType, + MarkdownBlock, + DeploymentEvent, + ActionParams, + action, +) +from kubernetes import client, config + +class resizeParams(ActionParams): + replicas: int + +@action +def resizeDeploymentReplicaCount(event: DeploymentEvent, params: resizeParams): + """ + Resizes the replica count of a deployment to the specified number of replicas + """ + if not event.get_deployment(): + logging.error("Failed to get the Deployment for Resizing") + return + + deployment = event.get_deployment() + deploymentName = deployment.metadata.name + namespace = deployment.metadata.namespace + + # Load the kube config + config.load_incluster_config() + + # Create a Kubernetes API client + appsV1 = client.AppsV1Api() + + # Get the current deployment + currentDeployment = appsV1.read_namespaced_deployment(deploymentName, namespace) + + # Modify the replica count using the parameter + currentDeployment.spec.replicas = params.replicas + + # Update the deployment + updatedDeployment = appsV1.replace_namespaced_deployment( + name=deploymentName, + namespace=namespace, + body=currentDeployment, + ) + + logging.info(f"Resized deployment {deploymentName} to {params.replicas} replicas") + + functionName = "resizeDeploymentReplicaCount" + finding = Finding( + title="Deployment Resized", + source=FindingSource.MANUAL, + aggregation_key=functionName, + finding_type=FindingType.REPORT, + failure=False, + ) + finding.add_enrichment( + [ + MarkdownBlock(f"{deploymentName} has been resized to {params.replicas} replicas."), + ] + ) + event.add_finding(finding) diff --git a/xWorkflows/xWorkflow3/rb-actions/pyproject.toml b/xWorkflows/xWorkflow3/rb-actions/pyproject.toml new file mode 100644 index 0000000..55e7efe --- /dev/null +++ b/xWorkflows/xWorkflow3/rb-actions/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "my_playbook_repo" +version = "0.0.1" +description = "" +authors = ["xkops "] + +# if playbook requires additional dependencies +[tool.poetry.dependencies] + +[tool.poetry.dev-dependencies] +robusta-cli = "^0.8.9" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"