-
Notifications
You must be signed in to change notification settings - Fork 5
Workflow3 Abandonend Workloads #85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <image_registry_name>/kubecost-query-pod -f Dockerfile . | ||
| docker push <image_registry_name>/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 <image_registry_name>/backend -f Dockerfile . | ||
| docker push <image_registry_name>/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 <namespace> | ||
| ``` | ||
|
|
||
| 2. **Port-forwarding**: | ||
|
|
||
| ```bash | ||
| kubectl port-forward <flask-backend-pod-name> -n <namespace> 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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/<deployment_name>', 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 | ||
Check warningCode scanning / CodeQL Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
|
||
| 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/<deploymentName>', 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 | ||
Check warningCode scanning / CodeQL Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
|
||
| except pymongo.errors.PyMongoError as e: | ||
| return jsonify({"error": f"MongoDB error: {e}"}), 500 | ||
Check warningCode scanning / CodeQL Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
|
||
| 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/<pod_name>', 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 | ||
Check warningCode scanning / CodeQL Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
|
||
| except pymongo.errors.PyMongoError as e: | ||
| return jsonify({"error": f"MongoDB error: {e}"}), 500 | ||
Check warningCode scanning / CodeQL Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
|
||
| 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') | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Flask | ||
| requests | ||
| pymongo==4.2.0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
|
||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| requests | ||
| pymongo |
Check warning
Code scanning / CodeQL
Information exposure through an exception