Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions xWorkflows/xWorkflow3/README.md
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
7 changes: 7 additions & 0 deletions xWorkflows/xWorkflow3/backend/Dockerfile
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"]
159 changes: 159 additions & 0 deletions xWorkflows/xWorkflow3/backend/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

Check warning

Code 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 warning

Code 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 warning

Code 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 warning

Code 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 warning

Code 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 warning

Code 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')
3 changes: 3 additions & 0 deletions xWorkflows/xWorkflow3/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Flask
requests
pymongo==4.2.0
8 changes: 8 additions & 0 deletions xWorkflows/xWorkflow3/kubecost_query_pod/Dockerfile
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"]
58 changes: 58 additions & 0 deletions xWorkflows/xWorkflow3/kubecost_query_pod/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)


2 changes: 2 additions & 0 deletions xWorkflows/xWorkflow3/kubecost_query_pod/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests
pymongo
Loading