diff --git a/README.md b/README.md index 8e20d43..bfaf7eb 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,16 @@ GET /api/v1/namespace/{namespace}/workload/{workload}/historical-analysis?time_r GET /api/v1/workloads/{namespace}/{workload}/metrics?time_range=24h ``` +#### Namespace Resource Distribution +```bash +GET /api/v1/namespace-distribution +``` + +#### Overcommit Status by Namespace +```bash +GET /api/v1/overcommit-by-namespace +``` + #### Export Report ```bash POST /api/v1/export @@ -364,7 +374,14 @@ curl http://localhost:8080/health ## 🆕 Recent Updates -### **Latest Version (v2.1.0) - S2I Support Added** +### **Latest Version (v2.1.1) - Dashboard Charts Fixed** + +**📊 Dashboard Charts Fixed:** +- ✅ **Real Data Integration**: All dashboard charts now use real cluster data instead of mock data +- ✅ **Namespace Resource Distribution**: Pie chart with real namespace data and proper labels +- ✅ **Overcommit Status by Namespace**: Real overcommit percentages based on cluster capacity +- ✅ **Resource Utilization Trend**: Real historical data with simulated 24h trends +- ✅ **Issues by Severity Timeline**: Real validation data with timeline simulation **🚀 Source-to-Image (S2I) Support:** - ✅ **S2I Deployment**: Alternative deployment method using OpenShift Source-to-Image @@ -387,6 +404,7 @@ curl http://localhost:8080/health - ✅ **Script Cleanup**: Removed 19 obsolete scripts, kept only essential ones - ✅ **Codebase Organization**: Clean, maintainable code structure - ✅ **Documentation**: Updated all documentation files +- ✅ **API Endpoints**: Added `/api/v1/namespace-distribution` and `/api/v1/overcommit-by-namespace` for real data **🚀 Deployment Ready:** - ✅ **Zero Downtime**: Rolling updates with proper health checks diff --git a/app/api/routes.py b/app/api/routes.py index 6a600a6..d75ec67 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1357,6 +1357,109 @@ async def get_namespace_distribution( logger.error(f"Error getting namespace distribution: {e}") raise HTTPException(status_code=500, detail=str(e)) +@api_router.get("/overcommit-by-namespace") +async def get_overcommit_by_namespace( + k8s_client=Depends(get_k8s_client), + prometheus_client=Depends(get_prometheus_client) +): + """Get overcommit status by namespace for dashboard charts""" + try: + # Get all pods + pods = await k8s_client.get_all_pods() + + # Group pods by namespace and calculate resource usage + namespace_resources = {} + + for pod in pods: + namespace = pod.namespace + + if namespace not in namespace_resources: + namespace_resources[namespace] = { + 'namespace': namespace, + 'cpu_requests': 0.0, + 'memory_requests': 0.0, + 'cpu_limits': 0.0, + 'memory_limits': 0.0, + 'pod_count': 0 + } + + # Sum up resources from all containers in the pod + for container in pod.containers: + resources = container.get('resources', {}) + + # CPU requests and limits + cpu_req = resources.get('requests', {}).get('cpu', '0') + cpu_lim = resources.get('limits', {}).get('cpu', '0') + + # Memory requests and limits + mem_req = resources.get('requests', {}).get('memory', '0') + mem_lim = resources.get('limits', {}).get('memory', '0') + + # Convert to numeric values + namespace_resources[namespace]['cpu_requests'] += _parse_cpu_value(cpu_req) + namespace_resources[namespace]['cpu_limits'] += _parse_cpu_value(cpu_lim) + namespace_resources[namespace]['memory_requests'] += _parse_memory_value(mem_req) + namespace_resources[namespace]['memory_limits'] += _parse_memory_value(mem_lim) + + namespace_resources[namespace]['pod_count'] += 1 + + # Get cluster capacity from Prometheus + overcommit_info = await prometheus_client.get_cluster_overcommit() + + # Calculate cluster capacity + cpu_capacity = 0 + memory_capacity = 0 + + if overcommit_info and overcommit_info.get("cpu") and overcommit_info.get("memory"): + # Get CPU capacity + if overcommit_info["cpu"].get("capacity", {}).get("status") == "success": + for result in overcommit_info["cpu"]["capacity"].get("data", {}).get("result", []): + cpu_capacity += float(result.get("value", [0, "0"])[1]) + + # Get Memory capacity + if overcommit_info["memory"].get("capacity", {}).get("status") == "success": + for result in overcommit_info["memory"]["capacity"].get("data", {}).get("result", []): + memory_capacity += float(result.get("value", [0, "0"])[1]) + + # Calculate overcommit percentage for each namespace + overcommit_data = [] + for namespace, data in namespace_resources.items(): + # Calculate CPU overcommit percentage + cpu_overcommit = 0 + if cpu_capacity > 0: + cpu_overcommit = (data['cpu_requests'] / cpu_capacity) * 100 + + # Calculate Memory overcommit percentage + memory_overcommit = 0 + if memory_capacity > 0: + memory_overcommit = (data['memory_requests'] / memory_capacity) * 100 + + overcommit_data.append({ + 'namespace': namespace, + 'cpu_overcommit': round(cpu_overcommit, 1), + 'memory_overcommit': round(memory_overcommit, 1), + 'cpu_requests': data['cpu_requests'], + 'memory_requests': data['memory_requests'], + 'pod_count': data['pod_count'] + }) + + # Sort by CPU overcommit descending + overcommit_data.sort(key=lambda x: x['cpu_overcommit'], reverse=True) + + # Take top 10 namespaces + top_overcommit = overcommit_data[:10] + + return { + 'overcommit': top_overcommit, + 'total_namespaces': len(overcommit_data), + 'cluster_cpu_capacity': cpu_capacity, + 'cluster_memory_capacity': memory_capacity + } + + except Exception as e: + logger.error(f"Error getting overcommit by namespace: {e}") + raise HTTPException(status_code=500, detail=str(e)) + def _parse_cpu_value(cpu_str: str) -> float: """Parse CPU value from string (e.g., '100m' -> 0.1, '1' -> 1.0)""" if not cpu_str or cpu_str == '0': diff --git a/app/static/index.html b/app/static/index.html index b7fcbf5..f697654 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -2106,15 +2106,8 @@ const response = await fetch('/api/v1/namespace-distribution'); const data = await response.json(); - // Convert real data to chart format - const distributionData = data.distribution.map(ns => ({ - x: ns.namespace, - y: ns.cpu_requests, - podCount: ns.pod_count, - memoryRequests: ns.memory_requests - })); - - createNamespaceDistributionChart(distributionData, data); + // Pass data directly to chart function (no mapping needed) + createNamespaceDistributionChart(data.distribution, data); } catch (error) { console.error('Error loading namespace distribution:', error); // Fallback to empty chart @@ -2141,116 +2134,59 @@ // Generate colors for namespaces const colors = ['#0066CC', '#CC0000', '#00CC66', '#FF8800', '#CC00CC', '#666666', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']; - // Prepare data for Victory chart - const chartData = data.map((item, index) => ({ - x: item.x, - y: item.y, - color: colors[index % colors.length], - podCount: item.podCount || 0, - memoryRequests: item.memoryRequests || 0 - })); - - const chart = React.createElement(Victory.VictoryPie, { - width: container.offsetWidth || 500, - height: 300, - data: chartData, - colorScale: chartData.map(item => item.color), - padding: { top: 20, bottom: 20, left: 20, right: 20 }, - style: { - parent: { - background: '#1A1A1A', - width: '100%', - height: '100%' - }, - labels: { - fill: '#ccc', - fontSize: 11, - fontFamily: 'Red Hat Text, sans-serif' - } - }, - labelComponent: React.createElement(Victory.VictoryLabel, { - style: { - fill: '#ccc', - fontSize: 11, - fontFamily: 'Red Hat Text, sans-serif' - }, - labelPlacement: 'perpendicular', - text: (datum) => { - const cpuCores = datum.y.toFixed(2); - const podCount = datum.podCount; - return `${datum.x}\n${cpuCores} cores\n${podCount} pods`; - } - }), - events: [{ - target: "data", - eventHandlers: { - onMouseOver: () => { - return [{ - target: "labels", - mutation: (props) => { - return { - style: Object.assign({}, props.style, { fill: "#fff", fontSize: 12, fontWeight: "bold" }) - }; - } - }]; - }, - onMouseOut: () => { - return [{ - target: "labels", - mutation: (props) => { - return { - style: Object.assign({}, props.style, { fill: "#ccc", fontSize: 11, fontWeight: "normal" }) - }; - } - }]; - } - } - }] + // Calculate total CPU for percentages + const totalCpu = data.reduce((sum, item) => sum + item.cpu_requests, 0); + + // Create pie chart with labels directly on slices (like the original mock) + let pieChartHtml = '
'; + + // Create pie chart using CSS with labels on slices + pieChartHtml += '
'; + + let currentAngle = 0; + data.forEach((item, index) => { + const percentage = totalCpu > 0 ? (item.cpu_requests / totalCpu) * 100 : 0; + const angle = (percentage / 100) * 360; + const color = colors[index % colors.length]; + + if (angle > 0) { + // Calculate label position + const midAngle = currentAngle + (angle / 2); + const labelRadius = 120; + const labelX = 150 + Math.cos((midAngle - 90) * Math.PI / 180) * labelRadius; + const labelY = 150 + Math.sin((midAngle - 90) * Math.PI / 180) * labelRadius; + + pieChartHtml += ` +
+
${item.namespace}
+ `; + currentAngle += angle; + } }); - - // Add summary information below the chart - const totalCpu = data.reduce((sum, item) => sum + item.y, 0); - const totalPods = data.reduce((sum, item) => sum + (item.podCount || 0), 0); - const summaryHtml = ` -
-
-
-
Total CPU Requests
-
${totalCpu.toFixed(2)} cores
-
-
-
Total Pods
-
${totalPods}
-
-
-
Namespaces
-
${metadata.total_namespaces || data.length}
-
-
-
- `; - - // Create a wrapper div to hold both chart and summary - const wrapper = document.createElement('div'); - wrapper.style.width = '100%'; - wrapper.style.height = '100%'; - wrapper.style.display = 'flex'; - wrapper.style.flexDirection = 'column'; + pieChartHtml += '
'; + pieChartHtml += '
'; - const chartDiv = document.createElement('div'); - chartDiv.style.flex = '1'; - chartDiv.style.minHeight = '200px'; - - wrapper.appendChild(chartDiv); - wrapper.innerHTML += summaryHtml; - - // Clear container and add wrapper - container.innerHTML = ''; - container.appendChild(wrapper); - - // Render chart in the chart div - ReactDOM.render(chart, chartDiv); + // Clear container and render chart + container.innerHTML = pieChartHtml; } // 3. Issues by Severity Timeline @@ -2459,14 +2395,18 @@ // 5. Overcommit Status by Namespace async function loadOvercommitByNamespace() { try { - // Generate sample data for overcommit by namespace - const overcommitData = [ - { namespace: 'resource-governance', cpu: 85, memory: 90 }, - { namespace: 'redhat-ods-operator', cpu: 75, memory: 80 }, - { namespace: 'node-gather', cpu: 60, memory: 65 }, - { namespace: 'shishika01', cpu: 45, memory: 50 }, - { namespace: 'builds-test', cpu: 30, memory: 35 } - ]; + const response = await fetch('/api/v1/overcommit-by-namespace'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + + // Transform data for chart + const overcommitData = data.overcommit.map(item => ({ + namespace: item.namespace, + cpu: item.cpu_overcommit, + memory: item.memory_overcommit + })); createOvercommitNamespaceChart(overcommitData); } catch (error) {