Initial commit: OpenShift Resource Governance Tool
- Implementa ferramenta completa de governança de recursos - Backend Python com FastAPI para coleta de dados - Validações seguindo best practices Red Hat - Integração com Prometheus e VPA - UI web interativa para visualização - Relatórios em JSON, CSV e PDF - Deploy como DaemonSet com RBAC - Scripts de automação para build e deploy
This commit is contained in:
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# OpenShift Resource Governance Tool
|
||||
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API routes
|
||||
292
app/api/routes.py
Normal file
292
app/api/routes.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Rotas da API
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.models.resource_models import (
|
||||
ClusterReport, NamespaceReport, ExportRequest,
|
||||
ApplyRecommendationRequest
|
||||
)
|
||||
from app.services.validation_service import ValidationService
|
||||
from app.services.report_service import ReportService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Criar router
|
||||
api_router = APIRouter()
|
||||
|
||||
# Inicializar serviços
|
||||
validation_service = ValidationService()
|
||||
report_service = ReportService()
|
||||
|
||||
def get_k8s_client(request: Request):
|
||||
"""Dependency para obter cliente Kubernetes"""
|
||||
return request.app.state.k8s_client
|
||||
|
||||
def get_prometheus_client(request: Request):
|
||||
"""Dependency para obter cliente Prometheus"""
|
||||
return request.app.state.prometheus_client
|
||||
|
||||
@api_router.get("/cluster/status")
|
||||
async def get_cluster_status(
|
||||
k8s_client=Depends(get_k8s_client),
|
||||
prometheus_client=Depends(get_prometheus_client)
|
||||
):
|
||||
"""Obter status geral do cluster"""
|
||||
try:
|
||||
# Coletar dados básicos
|
||||
pods = await k8s_client.get_all_pods()
|
||||
nodes_info = await k8s_client.get_nodes_info()
|
||||
|
||||
# Validar recursos
|
||||
all_validations = []
|
||||
for pod in pods:
|
||||
pod_validations = validation_service.validate_pod_resources(pod)
|
||||
all_validations.extend(pod_validations)
|
||||
|
||||
# Obter informações de overcommit
|
||||
overcommit_info = await prometheus_client.get_cluster_overcommit()
|
||||
|
||||
# Obter recomendações VPA
|
||||
vpa_recommendations = await k8s_client.get_vpa_recommendations()
|
||||
|
||||
# Gerar relatório
|
||||
report = report_service.generate_cluster_report(
|
||||
pods=pods,
|
||||
validations=all_validations,
|
||||
vpa_recommendations=vpa_recommendations,
|
||||
overcommit_info=overcommit_info,
|
||||
nodes_info=nodes_info
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter status do cluster: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/namespace/{namespace}/status")
|
||||
async def get_namespace_status(
|
||||
namespace: str,
|
||||
k8s_client=Depends(get_k8s_client),
|
||||
prometheus_client=Depends(get_prometheus_client)
|
||||
):
|
||||
"""Obter status de um namespace específico"""
|
||||
try:
|
||||
# Coletar dados do namespace
|
||||
namespace_resources = await k8s_client.get_namespace_resources(namespace)
|
||||
|
||||
# Validar recursos
|
||||
all_validations = []
|
||||
for pod in namespace_resources.pods:
|
||||
pod_validations = validation_service.validate_pod_resources(pod)
|
||||
all_validations.extend(pod_validations)
|
||||
|
||||
# Obter uso de recursos do Prometheus
|
||||
resource_usage = await prometheus_client.get_namespace_resource_usage(namespace)
|
||||
|
||||
# Gerar relatório do namespace
|
||||
report = report_service.generate_namespace_report(
|
||||
namespace=namespace,
|
||||
pods=namespace_resources.pods,
|
||||
validations=all_validations,
|
||||
resource_usage=resource_usage
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter status do namespace {namespace}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/pods")
|
||||
async def get_pods(
|
||||
namespace: Optional[str] = None,
|
||||
k8s_client=Depends(get_k8s_client)
|
||||
):
|
||||
"""Listar pods com informações de recursos"""
|
||||
try:
|
||||
if namespace:
|
||||
namespace_resources = await k8s_client.get_namespace_resources(namespace)
|
||||
return namespace_resources.pods
|
||||
else:
|
||||
return await k8s_client.get_all_pods()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao listar pods: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/validations")
|
||||
async def get_validations(
|
||||
namespace: Optional[str] = None,
|
||||
severity: Optional[str] = None,
|
||||
k8s_client=Depends(get_k8s_client)
|
||||
):
|
||||
"""Listar validações de recursos"""
|
||||
try:
|
||||
# Coletar pods
|
||||
if namespace:
|
||||
namespace_resources = await k8s_client.get_namespace_resources(namespace)
|
||||
pods = namespace_resources.pods
|
||||
else:
|
||||
pods = await k8s_client.get_all_pods()
|
||||
|
||||
# Validar recursos
|
||||
all_validations = []
|
||||
for pod in pods:
|
||||
pod_validations = validation_service.validate_pod_resources(pod)
|
||||
all_validations.extend(pod_validations)
|
||||
|
||||
# Filtrar por severidade se especificado
|
||||
if severity:
|
||||
all_validations = [
|
||||
v for v in all_validations if v.severity == severity
|
||||
]
|
||||
|
||||
return all_validations
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter validações: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/vpa/recommendations")
|
||||
async def get_vpa_recommendations(
|
||||
namespace: Optional[str] = None,
|
||||
k8s_client=Depends(get_k8s_client)
|
||||
):
|
||||
"""Obter recomendações do VPA"""
|
||||
try:
|
||||
recommendations = await k8s_client.get_vpa_recommendations()
|
||||
|
||||
if namespace:
|
||||
recommendations = [
|
||||
r for r in recommendations if r.namespace == namespace
|
||||
]
|
||||
|
||||
return recommendations
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter recomendações VPA: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.post("/export")
|
||||
async def export_report(
|
||||
export_request: ExportRequest,
|
||||
k8s_client=Depends(get_k8s_client),
|
||||
prometheus_client=Depends(get_prometheus_client)
|
||||
):
|
||||
"""Exportar relatório em diferentes formatos"""
|
||||
try:
|
||||
# Gerar relatório
|
||||
pods = await k8s_client.get_all_pods()
|
||||
nodes_info = await k8s_client.get_nodes_info()
|
||||
|
||||
# Filtrar por namespaces se especificado
|
||||
if export_request.namespaces:
|
||||
pods = [p for p in pods if p.namespace in export_request.namespaces]
|
||||
|
||||
# Validar recursos
|
||||
all_validations = []
|
||||
for pod in pods:
|
||||
pod_validations = validation_service.validate_pod_resources(pod)
|
||||
all_validations.extend(pod_validations)
|
||||
|
||||
# Obter informações adicionais
|
||||
overcommit_info = {}
|
||||
vpa_recommendations = []
|
||||
|
||||
if export_request.include_vpa:
|
||||
vpa_recommendations = await k8s_client.get_vpa_recommendations()
|
||||
|
||||
if export_request.include_validations:
|
||||
overcommit_info = await prometheus_client.get_cluster_overcommit()
|
||||
|
||||
# Gerar relatório
|
||||
report = report_service.generate_cluster_report(
|
||||
pods=pods,
|
||||
validations=all_validations,
|
||||
vpa_recommendations=vpa_recommendations,
|
||||
overcommit_info=overcommit_info,
|
||||
nodes_info=nodes_info
|
||||
)
|
||||
|
||||
# Exportar
|
||||
filepath = await report_service.export_report(report, export_request)
|
||||
|
||||
return {
|
||||
"message": "Relatório exportado com sucesso",
|
||||
"filepath": filepath,
|
||||
"format": export_request.format
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao exportar relatório: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/export/files")
|
||||
async def list_exported_files():
|
||||
"""Listar arquivos exportados"""
|
||||
try:
|
||||
files = report_service.get_exported_reports()
|
||||
return files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao listar arquivos exportados: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/export/files/{filename}")
|
||||
async def download_exported_file(filename: str):
|
||||
"""Download de arquivo exportado"""
|
||||
try:
|
||||
files = report_service.get_exported_reports()
|
||||
file_info = next((f for f in files if f["filename"] == filename), None)
|
||||
|
||||
if not file_info:
|
||||
raise HTTPException(status_code=404, detail="Arquivo não encontrado")
|
||||
|
||||
return FileResponse(
|
||||
path=file_info["filepath"],
|
||||
filename=filename,
|
||||
media_type='application/octet-stream'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao baixar arquivo {filename}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.post("/apply/recommendation")
|
||||
async def apply_recommendation(
|
||||
recommendation: ApplyRecommendationRequest,
|
||||
k8s_client=Depends(get_k8s_client)
|
||||
):
|
||||
"""Aplicar recomendação de recursos"""
|
||||
try:
|
||||
# TODO: Implementar aplicação de recomendações
|
||||
# Por enquanto, apenas simular
|
||||
if recommendation.dry_run:
|
||||
return {
|
||||
"message": "Dry run - recomendação seria aplicada",
|
||||
"pod": recommendation.pod_name,
|
||||
"namespace": recommendation.namespace,
|
||||
"container": recommendation.container_name,
|
||||
"action": f"{recommendation.action} {recommendation.resource_type} = {recommendation.value}"
|
||||
}
|
||||
else:
|
||||
# Implementar aplicação real da recomendação
|
||||
raise HTTPException(status_code=501, detail="Aplicação de recomendações não implementada ainda")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao aplicar recomendação: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@api_router.get("/health")
|
||||
async def health_check():
|
||||
"""Health check da API"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "resource-governance-api",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core modules
|
||||
45
app/core/config.py
Normal file
45
app/core/config.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Configurações da aplicação
|
||||
"""
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configurações da aplicação"""
|
||||
|
||||
# Configurações do OpenShift/Kubernetes
|
||||
kubeconfig_path: Optional[str] = None
|
||||
cluster_url: Optional[str] = None
|
||||
token: Optional[str] = None
|
||||
|
||||
# Configurações do Prometheus
|
||||
prometheus_url: str = "http://prometheus.openshift-monitoring.svc.cluster.local:9090"
|
||||
|
||||
# Configurações de validação
|
||||
cpu_limit_ratio: float = 3.0 # Ratio padrão limit:request para CPU
|
||||
memory_limit_ratio: float = 3.0 # Ratio padrão limit:request para memória
|
||||
min_cpu_request: str = "10m" # Mínimo de CPU request
|
||||
min_memory_request: str = "32Mi" # Mínimo de memória request
|
||||
|
||||
# Namespaces críticos para VPA
|
||||
critical_namespaces: List[str] = [
|
||||
"openshift-monitoring",
|
||||
"openshift-ingress",
|
||||
"openshift-apiserver",
|
||||
"openshift-controller-manager",
|
||||
"openshift-sdn"
|
||||
]
|
||||
|
||||
# Configurações de relatório
|
||||
report_export_path: str = "/tmp/reports"
|
||||
|
||||
# Configurações de segurança
|
||||
enable_rbac: bool = True
|
||||
service_account_name: str = "resource-governance-sa"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
settings = Settings()
|
||||
234
app/core/kubernetes_client.py
Normal file
234
app/core/kubernetes_client.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Cliente Kubernetes/OpenShift para coleta de dados
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from kubernetes import client, config
|
||||
from kubernetes.client.rest import ApiException
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.resource_models import PodResource, NamespaceResources, VPARecommendation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class K8sClient:
|
||||
"""Cliente para interação com Kubernetes/OpenShift"""
|
||||
|
||||
def __init__(self):
|
||||
self.v1 = None
|
||||
self.autoscaling_v1 = None
|
||||
self.apps_v1 = None
|
||||
self.initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""Inicializar cliente Kubernetes"""
|
||||
try:
|
||||
# Tentar carregar configuração do cluster
|
||||
if settings.kubeconfig_path:
|
||||
config.load_kube_config(config_file=settings.kubeconfig_path)
|
||||
else:
|
||||
# Usar configuração in-cluster
|
||||
config.load_incluster_config()
|
||||
|
||||
# Inicializar clientes da API
|
||||
self.v1 = client.CoreV1Api()
|
||||
self.autoscaling_v1 = client.AutoscalingV1Api()
|
||||
self.apps_v1 = client.AppsV1Api()
|
||||
|
||||
self.initialized = True
|
||||
logger.info("Cliente Kubernetes inicializado com sucesso")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao inicializar cliente Kubernetes: {e}")
|
||||
raise
|
||||
|
||||
async def get_all_pods(self) -> List[PodResource]:
|
||||
"""Coletar informações de todos os pods do cluster"""
|
||||
if not self.initialized:
|
||||
raise RuntimeError("Cliente Kubernetes não inicializado")
|
||||
|
||||
pods_data = []
|
||||
|
||||
try:
|
||||
# Listar todos os pods em todos os namespaces
|
||||
pods = self.v1.list_pod_for_all_namespaces(watch=False)
|
||||
|
||||
for pod in pods.items:
|
||||
pod_resource = PodResource(
|
||||
name=pod.metadata.name,
|
||||
namespace=pod.metadata.namespace,
|
||||
node_name=pod.spec.node_name,
|
||||
phase=pod.status.phase,
|
||||
containers=[]
|
||||
)
|
||||
|
||||
# Processar containers do pod
|
||||
for container in pod.spec.containers:
|
||||
container_resource = {
|
||||
"name": container.name,
|
||||
"image": container.image,
|
||||
"resources": {
|
||||
"requests": {},
|
||||
"limits": {}
|
||||
}
|
||||
}
|
||||
|
||||
# Extrair requests e limits
|
||||
if container.resources:
|
||||
if container.resources.requests:
|
||||
container_resource["resources"]["requests"] = {
|
||||
k: v for k, v in container.resources.requests.items()
|
||||
}
|
||||
if container.resources.limits:
|
||||
container_resource["resources"]["limits"] = {
|
||||
k: v for k, v in container.resources.limits.items()
|
||||
}
|
||||
|
||||
pod_resource.containers.append(container_resource)
|
||||
|
||||
pods_data.append(pod_resource)
|
||||
|
||||
logger.info(f"Coletados {len(pods_data)} pods")
|
||||
return pods_data
|
||||
|
||||
except ApiException as e:
|
||||
logger.error(f"Erro ao listar pods: {e}")
|
||||
raise
|
||||
|
||||
async def get_namespace_resources(self, namespace: str) -> NamespaceResources:
|
||||
"""Coletar recursos de um namespace específico"""
|
||||
if not self.initialized:
|
||||
raise RuntimeError("Cliente Kubernetes não inicializado")
|
||||
|
||||
try:
|
||||
# Listar pods do namespace
|
||||
pods = self.v1.list_namespaced_pod(namespace=namespace)
|
||||
|
||||
namespace_resource = NamespaceResources(
|
||||
name=namespace,
|
||||
pods=[],
|
||||
total_cpu_requests="0",
|
||||
total_cpu_limits="0",
|
||||
total_memory_requests="0",
|
||||
total_memory_limits="0"
|
||||
)
|
||||
|
||||
for pod in pods.items:
|
||||
pod_resource = PodResource(
|
||||
name=pod.metadata.name,
|
||||
namespace=pod.metadata.namespace,
|
||||
node_name=pod.spec.node_name,
|
||||
phase=pod.status.phase,
|
||||
containers=[]
|
||||
)
|
||||
|
||||
for container in pod.spec.containers:
|
||||
container_resource = {
|
||||
"name": container.name,
|
||||
"image": container.image,
|
||||
"resources": {
|
||||
"requests": {},
|
||||
"limits": {}
|
||||
}
|
||||
}
|
||||
|
||||
if container.resources:
|
||||
if container.resources.requests:
|
||||
container_resource["resources"]["requests"] = {
|
||||
k: v for k, v in container.resources.requests.items()
|
||||
}
|
||||
if container.resources.limits:
|
||||
container_resource["resources"]["limits"] = {
|
||||
k: v for k, v in container.resources.limits.items()
|
||||
}
|
||||
|
||||
pod_resource.containers.append(container_resource)
|
||||
|
||||
namespace_resource.pods.append(pod_resource)
|
||||
|
||||
return namespace_resource
|
||||
|
||||
except ApiException as e:
|
||||
logger.error(f"Erro ao coletar recursos do namespace {namespace}: {e}")
|
||||
raise
|
||||
|
||||
async def get_vpa_recommendations(self) -> List[VPARecommendation]:
|
||||
"""Coletar recomendações do VPA"""
|
||||
if not self.initialized:
|
||||
raise RuntimeError("Cliente Kubernetes não inicializado")
|
||||
|
||||
recommendations = []
|
||||
|
||||
try:
|
||||
# Listar VPA objects em todos os namespaces
|
||||
vpa_list = self.autoscaling_v1.list_vertical_pod_autoscaler_for_all_namespaces()
|
||||
|
||||
for vpa in vpa_list.items:
|
||||
if vpa.status and vpa.status.recommendation:
|
||||
recommendation = VPARecommendation(
|
||||
name=vpa.metadata.name,
|
||||
namespace=vpa.metadata.namespace,
|
||||
target_ref=vpa.spec.target_ref,
|
||||
recommendations=vpa.status.recommendation
|
||||
)
|
||||
recommendations.append(recommendation)
|
||||
|
||||
logger.info(f"Coletadas {len(recommendations)} recomendações VPA")
|
||||
return recommendations
|
||||
|
||||
except ApiException as e:
|
||||
logger.error(f"Erro ao coletar recomendações VPA: {e}")
|
||||
# VPA pode não estar instalado, retornar lista vazia
|
||||
return []
|
||||
|
||||
async def get_nodes_info(self) -> List[Dict[str, Any]]:
|
||||
"""Coletar informações dos nós do cluster"""
|
||||
if not self.initialized:
|
||||
raise RuntimeError("Cliente Kubernetes não inicializado")
|
||||
|
||||
try:
|
||||
nodes = self.v1.list_node()
|
||||
nodes_info = []
|
||||
|
||||
for node in nodes.items:
|
||||
node_info = {
|
||||
"name": node.metadata.name,
|
||||
"labels": node.metadata.labels or {},
|
||||
"capacity": {},
|
||||
"allocatable": {},
|
||||
"conditions": []
|
||||
}
|
||||
|
||||
# Capacidade do nó
|
||||
if node.status.capacity:
|
||||
node_info["capacity"] = {
|
||||
k: v for k, v in node.status.capacity.items()
|
||||
}
|
||||
|
||||
# Recursos alocáveis
|
||||
if node.status.allocatable:
|
||||
node_info["allocatable"] = {
|
||||
k: v for k, v in node.status.allocatable.items()
|
||||
}
|
||||
|
||||
# Condições do nó
|
||||
if node.status.conditions:
|
||||
node_info["conditions"] = [
|
||||
{
|
||||
"type": condition.type,
|
||||
"status": condition.status,
|
||||
"reason": condition.reason,
|
||||
"message": condition.message
|
||||
}
|
||||
for condition in node.status.conditions
|
||||
]
|
||||
|
||||
nodes_info.append(node_info)
|
||||
|
||||
return nodes_info
|
||||
|
||||
except ApiException as e:
|
||||
logger.error(f"Erro ao coletar informações dos nós: {e}")
|
||||
raise
|
||||
131
app/core/prometheus_client.py
Normal file
131
app/core/prometheus_client.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Cliente Prometheus para coleta de métricas
|
||||
"""
|
||||
import logging
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PrometheusClient:
|
||||
"""Cliente para interação com Prometheus"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.prometheus_url
|
||||
self.session = None
|
||||
self.initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""Inicializar cliente Prometheus"""
|
||||
try:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
# Testar conexão
|
||||
async with self.session.get(f"{self.base_url}/api/v1/query?query=up") as response:
|
||||
if response.status == 200:
|
||||
self.initialized = True
|
||||
logger.info("Cliente Prometheus inicializado com sucesso")
|
||||
else:
|
||||
logger.warning(f"Prometheus retornou status {response.status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao inicializar cliente Prometheus: {e}")
|
||||
# Prometheus pode não estar disponível, continuar sem ele
|
||||
self.initialized = False
|
||||
|
||||
async def query(self, query: str, time: Optional[datetime] = None) -> Dict[str, Any]:
|
||||
"""Executar query no Prometheus"""
|
||||
if not self.initialized or not self.session:
|
||||
return {"status": "error", "message": "Prometheus não disponível"}
|
||||
|
||||
try:
|
||||
params = {"query": query}
|
||||
if time:
|
||||
params["time"] = int(time.timestamp())
|
||||
|
||||
async with self.session.get(
|
||||
f"{self.base_url}/api/v1/query",
|
||||
params=params
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return data
|
||||
else:
|
||||
logger.error(f"Erro na query Prometheus: {response.status}")
|
||||
return {"status": "error", "message": f"HTTP {response.status}"}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao executar query Prometheus: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
async def get_pod_cpu_usage(self, namespace: str, pod_name: str) -> Dict[str, Any]:
|
||||
"""Obter uso de CPU de um pod específico"""
|
||||
query = f'rate(container_cpu_usage_seconds_total{{namespace="{namespace}", pod="{pod_name}"}}[5m])'
|
||||
return await self.query(query)
|
||||
|
||||
async def get_pod_memory_usage(self, namespace: str, pod_name: str) -> Dict[str, Any]:
|
||||
"""Obter uso de memória de um pod específico"""
|
||||
query = f'container_memory_working_set_bytes{{namespace="{namespace}", pod="{pod_name}"}}'
|
||||
return await self.query(query)
|
||||
|
||||
async def get_namespace_resource_usage(self, namespace: str) -> Dict[str, Any]:
|
||||
"""Obter uso de recursos de um namespace"""
|
||||
cpu_query = f'sum(rate(container_cpu_usage_seconds_total{{namespace="{namespace}"}}[5m]))'
|
||||
memory_query = f'sum(container_memory_working_set_bytes{{namespace="{namespace}"}})'
|
||||
|
||||
cpu_result = await self.query(cpu_query)
|
||||
memory_result = await self.query(memory_query)
|
||||
|
||||
return {
|
||||
"cpu": cpu_result,
|
||||
"memory": memory_result
|
||||
}
|
||||
|
||||
async def get_cluster_overcommit(self) -> Dict[str, Any]:
|
||||
"""Verificar overcommit no cluster"""
|
||||
# CPU overcommit
|
||||
cpu_capacity_query = 'sum(kube_node_status_capacity{resource="cpu"})'
|
||||
cpu_requests_query = 'sum(kube_pod_container_resource_requests{resource="cpu"})'
|
||||
|
||||
# Memory overcommit
|
||||
memory_capacity_query = 'sum(kube_node_status_capacity{resource="memory"})'
|
||||
memory_requests_query = 'sum(kube_pod_container_resource_requests{resource="memory"})'
|
||||
|
||||
cpu_capacity = await self.query(cpu_capacity_query)
|
||||
cpu_requests = await self.query(cpu_requests_query)
|
||||
memory_capacity = await self.query(memory_capacity_query)
|
||||
memory_requests = await self.query(memory_requests_query)
|
||||
|
||||
return {
|
||||
"cpu": {
|
||||
"capacity": cpu_capacity,
|
||||
"requests": cpu_requests
|
||||
},
|
||||
"memory": {
|
||||
"capacity": memory_capacity,
|
||||
"requests": memory_requests
|
||||
}
|
||||
}
|
||||
|
||||
async def get_node_resource_usage(self) -> List[Dict[str, Any]]:
|
||||
"""Obter uso de recursos por nó"""
|
||||
query = '''
|
||||
(
|
||||
kube_node_status_capacity{resource="cpu"} or
|
||||
kube_node_status_capacity{resource="memory"} or
|
||||
kube_pod_container_resource_requests{resource="cpu"} or
|
||||
kube_pod_container_resource_requests{resource="memory"}
|
||||
)
|
||||
'''
|
||||
|
||||
result = await self.query(query)
|
||||
return result
|
||||
|
||||
async def close(self):
|
||||
"""Fechar sessão HTTP"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
81
app/main.py
Normal file
81
app/main.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
OpenShift Resource Governance Tool
|
||||
Aplicação para governança de recursos no cluster OpenShift
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from app.core.config import settings
|
||||
from app.api.routes import api_router
|
||||
from app.core.kubernetes_client import K8sClient
|
||||
from app.core.prometheus_client import PrometheusClient
|
||||
|
||||
# Configuração de logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Inicialização e cleanup da aplicação"""
|
||||
logger.info("Iniciando OpenShift Resource Governance Tool")
|
||||
|
||||
# Inicializar clientes
|
||||
app.state.k8s_client = K8sClient()
|
||||
app.state.prometheus_client = PrometheusClient()
|
||||
|
||||
try:
|
||||
await app.state.k8s_client.initialize()
|
||||
await app.state.prometheus_client.initialize()
|
||||
logger.info("Clientes inicializados com sucesso")
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao inicializar clientes: {e}")
|
||||
raise
|
||||
|
||||
yield
|
||||
|
||||
logger.info("Finalizando aplicação")
|
||||
|
||||
# Criar aplicação FastAPI
|
||||
app = FastAPI(
|
||||
title="OpenShift Resource Governance Tool",
|
||||
description="Ferramenta de governança de recursos para clusters OpenShift",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Incluir rotas da API
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
# Servir arquivos estáticos
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
"""Página principal da aplicação"""
|
||||
with open("app/static/index.html", "r") as f:
|
||||
return HTMLResponse(content=f.read())
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "openshift-resource-governance",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8080,
|
||||
reload=True
|
||||
)
|
||||
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models
|
||||
82
app/models/resource_models.py
Normal file
82
app/models/resource_models.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Modelos de dados para recursos Kubernetes
|
||||
"""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ContainerResource(BaseModel):
|
||||
"""Recursos de um container"""
|
||||
name: str
|
||||
image: str
|
||||
resources: Dict[str, Dict[str, str]]
|
||||
|
||||
class PodResource(BaseModel):
|
||||
"""Recursos de um pod"""
|
||||
name: str
|
||||
namespace: str
|
||||
node_name: Optional[str] = None
|
||||
phase: str
|
||||
containers: List[ContainerResource]
|
||||
|
||||
class NamespaceResources(BaseModel):
|
||||
"""Recursos de um namespace"""
|
||||
name: str
|
||||
pods: List[PodResource]
|
||||
total_cpu_requests: str = "0"
|
||||
total_cpu_limits: str = "0"
|
||||
total_memory_requests: str = "0"
|
||||
total_memory_limits: str = "0"
|
||||
|
||||
class VPARecommendation(BaseModel):
|
||||
"""Recomendação do VPA"""
|
||||
name: str
|
||||
namespace: str
|
||||
target_ref: Dict[str, str]
|
||||
recommendations: Dict[str, Any]
|
||||
|
||||
class ResourceValidation(BaseModel):
|
||||
"""Resultado de validação de recursos"""
|
||||
pod_name: str
|
||||
namespace: str
|
||||
container_name: str
|
||||
validation_type: str # "missing_requests", "missing_limits", "invalid_ratio", "overcommit"
|
||||
severity: str # "warning", "error", "critical"
|
||||
message: str
|
||||
recommendation: Optional[str] = None
|
||||
|
||||
class ClusterReport(BaseModel):
|
||||
"""Relatório do cluster"""
|
||||
timestamp: str
|
||||
total_pods: int
|
||||
total_namespaces: int
|
||||
total_nodes: int
|
||||
validations: List[ResourceValidation]
|
||||
vpa_recommendations: List[VPARecommendation]
|
||||
overcommit_info: Dict[str, Any]
|
||||
summary: Dict[str, Any]
|
||||
|
||||
class NamespaceReport(BaseModel):
|
||||
"""Relatório de um namespace"""
|
||||
namespace: str
|
||||
timestamp: str
|
||||
total_pods: int
|
||||
validations: List[ResourceValidation]
|
||||
resource_usage: Dict[str, Any]
|
||||
recommendations: List[str]
|
||||
|
||||
class ExportRequest(BaseModel):
|
||||
"""Request para exportar relatório"""
|
||||
format: str # "json", "csv", "pdf"
|
||||
namespaces: Optional[List[str]] = None
|
||||
include_vpa: bool = True
|
||||
include_validations: bool = True
|
||||
|
||||
class ApplyRecommendationRequest(BaseModel):
|
||||
"""Request para aplicar recomendação"""
|
||||
pod_name: str
|
||||
namespace: str
|
||||
container_name: str
|
||||
resource_type: str # "cpu", "memory"
|
||||
action: str # "requests", "limits"
|
||||
value: str
|
||||
dry_run: bool = True
|
||||
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services
|
||||
306
app/services/report_service.py
Normal file
306
app/services/report_service.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
Serviço de geração de relatórios
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import csv
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Optional
|
||||
from io import StringIO
|
||||
|
||||
from app.models.resource_models import (
|
||||
ClusterReport, NamespaceReport, ResourceValidation,
|
||||
VPARecommendation, ExportRequest
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ReportService:
|
||||
"""Serviço para geração de relatórios"""
|
||||
|
||||
def __init__(self):
|
||||
self.export_path = settings.report_export_path
|
||||
os.makedirs(self.export_path, exist_ok=True)
|
||||
|
||||
def generate_cluster_report(
|
||||
self,
|
||||
pods: List[Any],
|
||||
validations: List[ResourceValidation],
|
||||
vpa_recommendations: List[VPARecommendation],
|
||||
overcommit_info: Dict[str, Any],
|
||||
nodes_info: List[Dict[str, Any]]
|
||||
) -> ClusterReport:
|
||||
"""Gerar relatório do cluster"""
|
||||
|
||||
# Contar namespaces únicos
|
||||
namespaces = set(pod.namespace for pod in pods)
|
||||
|
||||
# Gerar resumo
|
||||
summary = self._generate_summary(validations, vpa_recommendations, overcommit_info)
|
||||
|
||||
report = ClusterReport(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
total_pods=len(pods),
|
||||
total_namespaces=len(namespaces),
|
||||
total_nodes=len(nodes_info),
|
||||
validations=validations,
|
||||
vpa_recommendations=vpa_recommendations,
|
||||
overcommit_info=overcommit_info,
|
||||
summary=summary
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
def generate_namespace_report(
|
||||
self,
|
||||
namespace: str,
|
||||
pods: List[Any],
|
||||
validations: List[ResourceValidation],
|
||||
resource_usage: Dict[str, Any]
|
||||
) -> NamespaceReport:
|
||||
"""Gerar relatório de um namespace"""
|
||||
|
||||
# Filtrar validações do namespace
|
||||
namespace_validations = [
|
||||
v for v in validations if v.namespace == namespace
|
||||
]
|
||||
|
||||
# Gerar recomendações
|
||||
recommendations = self._generate_namespace_recommendations(namespace_validations)
|
||||
|
||||
report = NamespaceReport(
|
||||
namespace=namespace,
|
||||
timestamp=datetime.now().isoformat(),
|
||||
total_pods=len(pods),
|
||||
validations=namespace_validations,
|
||||
resource_usage=resource_usage,
|
||||
recommendations=recommendations
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
def _generate_summary(
|
||||
self,
|
||||
validations: List[ResourceValidation],
|
||||
vpa_recommendations: List[VPARecommendation],
|
||||
overcommit_info: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Gerar resumo do relatório"""
|
||||
|
||||
# Contar validações por severidade
|
||||
severity_counts = {}
|
||||
for validation in validations:
|
||||
severity = validation.severity
|
||||
if severity not in severity_counts:
|
||||
severity_counts[severity] = 0
|
||||
severity_counts[severity] += 1
|
||||
|
||||
# Contar validações por tipo
|
||||
type_counts = {}
|
||||
for validation in validations:
|
||||
validation_type = validation.validation_type
|
||||
if validation_type not in type_counts:
|
||||
type_counts[validation_type] = 0
|
||||
type_counts[validation_type] += 1
|
||||
|
||||
return {
|
||||
"total_validations": len(validations),
|
||||
"severity_breakdown": severity_counts,
|
||||
"validation_types": type_counts,
|
||||
"vpa_recommendations_count": len(vpa_recommendations),
|
||||
"overcommit_detected": overcommit_info.get("overcommit_detected", False),
|
||||
"critical_issues": severity_counts.get("critical", 0),
|
||||
"warnings": severity_counts.get("warning", 0),
|
||||
"errors": severity_counts.get("error", 0)
|
||||
}
|
||||
|
||||
def _generate_namespace_recommendations(
|
||||
self,
|
||||
validations: List[ResourceValidation]
|
||||
) -> List[str]:
|
||||
"""Gerar recomendações para um namespace"""
|
||||
recommendations = []
|
||||
|
||||
# Agrupar por tipo de problema
|
||||
problems = {}
|
||||
for validation in validations:
|
||||
problem_type = validation.validation_type
|
||||
if problem_type not in problems:
|
||||
problems[problem_type] = []
|
||||
problems[problem_type].append(validation)
|
||||
|
||||
# Gerar recomendações específicas
|
||||
if "missing_requests" in problems:
|
||||
count = len(problems["missing_requests"])
|
||||
recommendations.append(
|
||||
f"Criar LimitRange para definir requests padrão "
|
||||
f"({count} containers sem requests)"
|
||||
)
|
||||
|
||||
if "missing_limits" in problems:
|
||||
count = len(problems["missing_limits"])
|
||||
recommendations.append(
|
||||
f"Definir limits para {count} containers para evitar consumo excessivo"
|
||||
)
|
||||
|
||||
if "invalid_ratio" in problems:
|
||||
count = len(problems["invalid_ratio"])
|
||||
recommendations.append(
|
||||
f"Ajustar ratio limit:request para {count} containers"
|
||||
)
|
||||
|
||||
if "overcommit" in problems:
|
||||
recommendations.append(
|
||||
"Resolver overcommit de recursos no namespace"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
async def export_report(
|
||||
self,
|
||||
report: ClusterReport,
|
||||
export_request: ExportRequest
|
||||
) -> str:
|
||||
"""Exportar relatório em diferentes formatos"""
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
if export_request.format == "json":
|
||||
return await self._export_json(report, timestamp)
|
||||
elif export_request.format == "csv":
|
||||
return await self._export_csv(report, timestamp)
|
||||
elif export_request.format == "pdf":
|
||||
return await self._export_pdf(report, timestamp)
|
||||
else:
|
||||
raise ValueError(f"Formato não suportado: {export_request.format}")
|
||||
|
||||
async def _export_json(self, report: ClusterReport, timestamp: str) -> str:
|
||||
"""Exportar relatório em JSON"""
|
||||
filename = f"cluster_report_{timestamp}.json"
|
||||
filepath = os.path.join(self.export_path, filename)
|
||||
|
||||
# Converter para dict para serialização
|
||||
report_dict = report.dict()
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(report_dict, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Relatório JSON exportado: {filepath}")
|
||||
return filepath
|
||||
|
||||
async def _export_csv(self, report: ClusterReport, timestamp: str) -> str:
|
||||
"""Exportar relatório em CSV"""
|
||||
filename = f"cluster_report_{timestamp}.csv"
|
||||
filepath = os.path.join(self.export_path, filename)
|
||||
|
||||
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# Cabeçalho
|
||||
writer.writerow([
|
||||
"Pod Name", "Namespace", "Container Name",
|
||||
"Validation Type", "Severity", "Message", "Recommendation"
|
||||
])
|
||||
|
||||
# Dados das validações
|
||||
for validation in report.validations:
|
||||
writer.writerow([
|
||||
validation.pod_name,
|
||||
validation.namespace,
|
||||
validation.container_name,
|
||||
validation.validation_type,
|
||||
validation.severity,
|
||||
validation.message,
|
||||
validation.recommendation or ""
|
||||
])
|
||||
|
||||
logger.info(f"Relatório CSV exportado: {filepath}")
|
||||
return filepath
|
||||
|
||||
async def _export_pdf(self, report: ClusterReport, timestamp: str) -> str:
|
||||
"""Exportar relatório em PDF"""
|
||||
try:
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib import colors
|
||||
|
||||
filename = f"cluster_report_{timestamp}.pdf"
|
||||
filepath = os.path.join(self.export_path, filename)
|
||||
|
||||
doc = SimpleDocTemplate(filepath, pagesize=letter)
|
||||
styles = getSampleStyleSheet()
|
||||
story = []
|
||||
|
||||
# Título
|
||||
title = Paragraph("OpenShift Resource Governance Report", styles['Title'])
|
||||
story.append(title)
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# Resumo
|
||||
summary_text = f"""
|
||||
<b>Resumo do Cluster:</b><br/>
|
||||
Total de Pods: {report.total_pods}<br/>
|
||||
Total de Namespaces: {report.total_namespaces}<br/>
|
||||
Total de Nós: {report.total_nodes}<br/>
|
||||
Total de Validações: {report.summary['total_validations']}<br/>
|
||||
Problemas Críticos: {report.summary['critical_issues']}<br/>
|
||||
"""
|
||||
story.append(Paragraph(summary_text, styles['Normal']))
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
# Tabela de validações
|
||||
if report.validations:
|
||||
data = [["Pod", "Namespace", "Container", "Tipo", "Severidade", "Mensagem"]]
|
||||
for validation in report.validations[:50]: # Limitar a 50 para PDF
|
||||
data.append([
|
||||
validation.pod_name,
|
||||
validation.namespace,
|
||||
validation.container_name,
|
||||
validation.validation_type,
|
||||
validation.severity,
|
||||
validation.message[:50] + "..." if len(validation.message) > 50 else validation.message
|
||||
])
|
||||
|
||||
table = Table(data)
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 14),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
||||
]))
|
||||
|
||||
story.append(Paragraph("<b>Validações:</b>", styles['Heading2']))
|
||||
story.append(table)
|
||||
|
||||
doc.build(story)
|
||||
logger.info(f"Relatório PDF exportado: {filepath}")
|
||||
return filepath
|
||||
|
||||
except ImportError:
|
||||
logger.error("reportlab não instalado. Instale com: pip install reportlab")
|
||||
raise ValueError("PDF export requer reportlab")
|
||||
|
||||
def get_exported_reports(self) -> List[Dict[str, str]]:
|
||||
"""Listar relatórios exportados"""
|
||||
reports = []
|
||||
|
||||
for filename in os.listdir(self.export_path):
|
||||
if filename.endswith(('.json', '.csv', '.pdf')):
|
||||
filepath = os.path.join(self.export_path, filename)
|
||||
stat = os.stat(filepath)
|
||||
reports.append({
|
||||
"filename": filename,
|
||||
"filepath": filepath,
|
||||
"size": stat.st_size,
|
||||
"created": datetime.fromtimestamp(stat.st_ctime).isoformat(),
|
||||
"format": filename.split('.')[-1]
|
||||
})
|
||||
|
||||
return sorted(reports, key=lambda x: x["created"], reverse=True)
|
||||
345
app/services/validation_service.py
Normal file
345
app/services/validation_service.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Serviço de validação de recursos seguindo best practices Red Hat
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import re
|
||||
|
||||
from app.models.resource_models import PodResource, ResourceValidation, NamespaceResources
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ValidationService:
|
||||
"""Serviço para validação de recursos"""
|
||||
|
||||
def __init__(self):
|
||||
self.cpu_ratio = settings.cpu_limit_ratio
|
||||
self.memory_ratio = settings.memory_limit_ratio
|
||||
self.min_cpu_request = settings.min_cpu_request
|
||||
self.min_memory_request = settings.min_memory_request
|
||||
|
||||
def validate_pod_resources(self, pod: PodResource) -> List[ResourceValidation]:
|
||||
"""Validar recursos de um pod"""
|
||||
validations = []
|
||||
|
||||
for container in pod.containers:
|
||||
container_validations = self._validate_container_resources(
|
||||
pod.name, pod.namespace, container
|
||||
)
|
||||
validations.extend(container_validations)
|
||||
|
||||
return validations
|
||||
|
||||
def _validate_container_resources(
|
||||
self,
|
||||
pod_name: str,
|
||||
namespace: str,
|
||||
container: Dict[str, Any]
|
||||
) -> List[ResourceValidation]:
|
||||
"""Validar recursos de um container"""
|
||||
validations = []
|
||||
resources = container.get("resources", {})
|
||||
requests = resources.get("requests", {})
|
||||
limits = resources.get("limits", {})
|
||||
|
||||
# 1. Verificar se requests estão definidos
|
||||
if not requests:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container["name"],
|
||||
validation_type="missing_requests",
|
||||
severity="error",
|
||||
message="Container sem requests definidos",
|
||||
recommendation="Definir requests de CPU e memória para garantir QoS"
|
||||
))
|
||||
|
||||
# 2. Verificar se limits estão definidos
|
||||
if not limits:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container["name"],
|
||||
validation_type="missing_limits",
|
||||
severity="warning",
|
||||
message="Container sem limits definidos",
|
||||
recommendation="Definir limits para evitar consumo excessivo de recursos"
|
||||
))
|
||||
|
||||
# 3. Validar ratio limit:request
|
||||
if requests and limits:
|
||||
cpu_validation = self._validate_cpu_ratio(
|
||||
pod_name, namespace, container["name"], requests, limits
|
||||
)
|
||||
if cpu_validation:
|
||||
validations.append(cpu_validation)
|
||||
|
||||
memory_validation = self._validate_memory_ratio(
|
||||
pod_name, namespace, container["name"], requests, limits
|
||||
)
|
||||
if memory_validation:
|
||||
validations.append(memory_validation)
|
||||
|
||||
# 4. Validar valores mínimos
|
||||
if requests:
|
||||
min_validation = self._validate_minimum_values(
|
||||
pod_name, namespace, container["name"], requests
|
||||
)
|
||||
validations.extend(min_validation)
|
||||
|
||||
return validations
|
||||
|
||||
def _validate_cpu_ratio(
|
||||
self,
|
||||
pod_name: str,
|
||||
namespace: str,
|
||||
container_name: str,
|
||||
requests: Dict[str, str],
|
||||
limits: Dict[str, str]
|
||||
) -> ResourceValidation:
|
||||
"""Validar ratio CPU limit:request"""
|
||||
if "cpu" not in requests or "cpu" not in limits:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_value = self._parse_cpu_value(requests["cpu"])
|
||||
limit_value = self._parse_cpu_value(limits["cpu"])
|
||||
|
||||
if request_value > 0:
|
||||
ratio = limit_value / request_value
|
||||
|
||||
if ratio > self.cpu_ratio * 1.5: # 50% de tolerância
|
||||
return ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="invalid_ratio",
|
||||
severity="warning",
|
||||
message=f"Ratio CPU limit:request muito alto ({ratio:.2f}:1)",
|
||||
recommendation=f"Considerar reduzir limits ou aumentar requests (ratio recomendado: {self.cpu_ratio}:1)"
|
||||
)
|
||||
elif ratio < 1.0:
|
||||
return ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="invalid_ratio",
|
||||
severity="error",
|
||||
message=f"CPU limit menor que request ({ratio:.2f}:1)",
|
||||
recommendation="CPU limit deve ser maior ou igual ao request"
|
||||
)
|
||||
|
||||
except (ValueError, InvalidOperation) as e:
|
||||
logger.warning(f"Erro ao validar ratio CPU: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _validate_memory_ratio(
|
||||
self,
|
||||
pod_name: str,
|
||||
namespace: str,
|
||||
container_name: str,
|
||||
requests: Dict[str, str],
|
||||
limits: Dict[str, str]
|
||||
) -> ResourceValidation:
|
||||
"""Validar ratio memória limit:request"""
|
||||
if "memory" not in requests or "memory" not in limits:
|
||||
return None
|
||||
|
||||
try:
|
||||
request_value = self._parse_memory_value(requests["memory"])
|
||||
limit_value = self._parse_memory_value(limits["memory"])
|
||||
|
||||
if request_value > 0:
|
||||
ratio = limit_value / request_value
|
||||
|
||||
if ratio > self.memory_ratio * 1.5: # 50% de tolerância
|
||||
return ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="invalid_ratio",
|
||||
severity="warning",
|
||||
message=f"Ratio memória limit:request muito alto ({ratio:.2f}:1)",
|
||||
recommendation=f"Considerar reduzir limits ou aumentar requests (ratio recomendado: {self.memory_ratio}:1)"
|
||||
)
|
||||
elif ratio < 1.0:
|
||||
return ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="invalid_ratio",
|
||||
severity="error",
|
||||
message=f"Memória limit menor que request ({ratio:.2f}:1)",
|
||||
recommendation="Memória limit deve ser maior ou igual ao request"
|
||||
)
|
||||
|
||||
except (ValueError, InvalidOperation) as e:
|
||||
logger.warning(f"Erro ao validar ratio memória: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _validate_minimum_values(
|
||||
self,
|
||||
pod_name: str,
|
||||
namespace: str,
|
||||
container_name: str,
|
||||
requests: Dict[str, str]
|
||||
) -> List[ResourceValidation]:
|
||||
"""Validar valores mínimos de requests"""
|
||||
validations = []
|
||||
|
||||
# Validar CPU mínima
|
||||
if "cpu" in requests:
|
||||
try:
|
||||
request_value = self._parse_cpu_value(requests["cpu"])
|
||||
min_value = self._parse_cpu_value(self.min_cpu_request)
|
||||
|
||||
if request_value < min_value:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="minimum_value",
|
||||
severity="warning",
|
||||
message=f"CPU request muito baixo ({requests['cpu']})",
|
||||
recommendation=f"Considerar aumentar para pelo menos {self.min_cpu_request}"
|
||||
))
|
||||
except (ValueError, InvalidOperation):
|
||||
pass
|
||||
|
||||
# Validar memória mínima
|
||||
if "memory" in requests:
|
||||
try:
|
||||
request_value = self._parse_memory_value(requests["memory"])
|
||||
min_value = self._parse_memory_value(self.min_memory_request)
|
||||
|
||||
if request_value < min_value:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name=pod_name,
|
||||
namespace=namespace,
|
||||
container_name=container_name,
|
||||
validation_type="minimum_value",
|
||||
severity="warning",
|
||||
message=f"Memória request muito baixa ({requests['memory']})",
|
||||
recommendation=f"Considerar aumentar para pelo menos {self.min_memory_request}"
|
||||
))
|
||||
except (ValueError, InvalidOperation):
|
||||
pass
|
||||
|
||||
return validations
|
||||
|
||||
def _parse_cpu_value(self, value: str) -> float:
|
||||
"""Converter valor de CPU para float (cores)"""
|
||||
if value.endswith('m'):
|
||||
return float(value[:-1]) / 1000
|
||||
elif value.endswith('n'):
|
||||
return float(value[:-1]) / 1000000000
|
||||
else:
|
||||
return float(value)
|
||||
|
||||
def _parse_memory_value(self, value: str) -> int:
|
||||
"""Converter valor de memória para bytes"""
|
||||
value = value.upper()
|
||||
|
||||
if value.endswith('KI'):
|
||||
return int(float(value[:-2]) * 1024)
|
||||
elif value.endswith('MI'):
|
||||
return int(float(value[:-2]) * 1024 * 1024)
|
||||
elif value.endswith('GI'):
|
||||
return int(float(value[:-2]) * 1024 * 1024 * 1024)
|
||||
elif value.endswith('K'):
|
||||
return int(float(value[:-1]) * 1000)
|
||||
elif value.endswith('M'):
|
||||
return int(float(value[:-1]) * 1000 * 1000)
|
||||
elif value.endswith('G'):
|
||||
return int(float(value[:-1]) * 1000 * 1000 * 1000)
|
||||
else:
|
||||
return int(value)
|
||||
|
||||
def validate_namespace_overcommit(
|
||||
self,
|
||||
namespace_resources: NamespaceResources,
|
||||
node_capacity: Dict[str, str]
|
||||
) -> List[ResourceValidation]:
|
||||
"""Validar overcommit em um namespace"""
|
||||
validations = []
|
||||
|
||||
# Calcular total de requests do namespace
|
||||
total_cpu_requests = self._parse_cpu_value(namespace_resources.total_cpu_requests)
|
||||
total_memory_requests = self._parse_memory_value(namespace_resources.total_memory_requests)
|
||||
|
||||
# Calcular capacidade total dos nós
|
||||
total_cpu_capacity = self._parse_cpu_value(node_capacity.get("cpu", "0"))
|
||||
total_memory_capacity = self._parse_memory_value(node_capacity.get("memory", "0"))
|
||||
|
||||
# Verificar overcommit de CPU
|
||||
if total_cpu_capacity > 0:
|
||||
cpu_utilization = (total_cpu_requests / total_cpu_capacity) * 100
|
||||
if cpu_utilization > 100:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name="namespace",
|
||||
namespace=namespace_resources.name,
|
||||
container_name="all",
|
||||
validation_type="overcommit",
|
||||
severity="critical",
|
||||
message=f"Overcommit de CPU no namespace: {cpu_utilization:.1f}%",
|
||||
recommendation="Reduzir requests de CPU ou adicionar mais nós ao cluster"
|
||||
))
|
||||
|
||||
# Verificar overcommit de memória
|
||||
if total_memory_capacity > 0:
|
||||
memory_utilization = (total_memory_requests / total_memory_capacity) * 100
|
||||
if memory_utilization > 100:
|
||||
validations.append(ResourceValidation(
|
||||
pod_name="namespace",
|
||||
namespace=namespace_resources.name,
|
||||
container_name="all",
|
||||
validation_type="overcommit",
|
||||
severity="critical",
|
||||
message=f"Overcommit de memória no namespace: {memory_utilization:.1f}%",
|
||||
recommendation="Reduzir requests de memória ou adicionar mais nós ao cluster"
|
||||
))
|
||||
|
||||
return validations
|
||||
|
||||
def generate_recommendations(self, validations: List[ResourceValidation]) -> List[str]:
|
||||
"""Gerar recomendações baseadas nas validações"""
|
||||
recommendations = []
|
||||
|
||||
# Agrupar validações por tipo
|
||||
validation_counts = {}
|
||||
for validation in validations:
|
||||
validation_type = validation.validation_type
|
||||
if validation_type not in validation_counts:
|
||||
validation_counts[validation_type] = 0
|
||||
validation_counts[validation_type] += 1
|
||||
|
||||
# Gerar recomendações baseadas nos problemas encontrados
|
||||
if validation_counts.get("missing_requests", 0) > 0:
|
||||
recommendations.append(
|
||||
f"Implementar LimitRange no namespace para definir requests padrão "
|
||||
f"({validation_counts['missing_requests']} containers sem requests)"
|
||||
)
|
||||
|
||||
if validation_counts.get("missing_limits", 0) > 0:
|
||||
recommendations.append(
|
||||
f"Definir limits para {validation_counts['missing_limits']} containers "
|
||||
"para evitar consumo excessivo de recursos"
|
||||
)
|
||||
|
||||
if validation_counts.get("invalid_ratio", 0) > 0:
|
||||
recommendations.append(
|
||||
f"Ajustar ratio limit:request para {validation_counts['invalid_ratio']} containers "
|
||||
f"(recomendado: {self.cpu_ratio}:1)"
|
||||
)
|
||||
|
||||
if validation_counts.get("overcommit", 0) > 0:
|
||||
recommendations.append(
|
||||
f"Resolver overcommit em {validation_counts['overcommit']} namespaces "
|
||||
"para evitar problemas de performance"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
530
app/static/index.html
Normal file
530
app/static/index.html
Normal file
@@ -0,0 +1,530 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenShift Resource Governance Tool</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #cc0000, #8b0000);
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #cc0000;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #cc0000;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #cc0000;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #8b0000;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #545b62;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.validation-item {
|
||||
padding: 1rem;
|
||||
border-left: 4px solid #ccc;
|
||||
margin: 0.5rem 0;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.validation-item.error {
|
||||
border-left-color: #dc3545;
|
||||
background: #f8d7da;
|
||||
}
|
||||
|
||||
.validation-item.warning {
|
||||
border-left-color: #ffc107;
|
||||
background: #fff3cd;
|
||||
}
|
||||
|
||||
.validation-item.critical {
|
||||
border-left-color: #dc3545;
|
||||
background: #f8d7da;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.validation-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.validation-recommendation {
|
||||
font-style: italic;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.export-section select,
|
||||
.export-section input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.severity-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.severity-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.severity-warning {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.severity-critical {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.export-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>OpenShift Resource Governance Tool</h1>
|
||||
<p>Ferramenta de governança de recursos para clusters OpenShift</p>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<!-- Estatísticas do Cluster -->
|
||||
<div class="stats-grid" id="statsGrid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalPods">-</div>
|
||||
<div class="stat-label">Total de Pods</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalNamespaces">-</div>
|
||||
<div class="stat-label">Namespaces</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="totalNodes">-</div>
|
||||
<div class="stat-label">Nós</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number" id="criticalIssues">-</div>
|
||||
<div class="stat-label">Problemas Críticos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controles -->
|
||||
<div class="card">
|
||||
<h2>Controles</h2>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||
<button class="btn" onclick="loadClusterStatus()">Atualizar Status</button>
|
||||
<button class="btn btn-secondary" onclick="loadValidations()">Ver Validações</button>
|
||||
<button class="btn btn-secondary" onclick="loadVPARecommendations()">Ver VPA</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exportar Relatórios -->
|
||||
<div class="card">
|
||||
<h2>Exportar Relatórios</h2>
|
||||
<div class="export-section">
|
||||
<select id="exportFormat">
|
||||
<option value="json">JSON</option>
|
||||
<option value="csv">CSV</option>
|
||||
<option value="pdf">PDF</option>
|
||||
</select>
|
||||
<input type="text" id="namespaces" placeholder="Namespaces (opcional, separados por vírgula)">
|
||||
<label>
|
||||
<input type="checkbox" id="includeVPA" checked> Incluir VPA
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="includeValidations" checked> Incluir Validações
|
||||
</label>
|
||||
<button class="btn" onclick="exportReport()">Exportar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validações -->
|
||||
<div class="card" id="validationsCard" style="display: none;">
|
||||
<h2>Validações de Recursos</h2>
|
||||
<div id="validationsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Recomendações VPA -->
|
||||
<div class="card" id="vpaCard" style="display: none;">
|
||||
<h2>Recomendações VPA</h2>
|
||||
<div id="vpaList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="loading hidden" id="loading">
|
||||
<p>Carregando dados...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div class="error hidden" id="error"></div>
|
||||
|
||||
<!-- Success -->
|
||||
<div class="success hidden" id="success"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentData = null;
|
||||
|
||||
// Carregar status inicial
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadClusterStatus();
|
||||
});
|
||||
|
||||
async function loadClusterStatus() {
|
||||
showLoading();
|
||||
hideMessages();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/cluster/status');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
currentData = data;
|
||||
updateStats(data);
|
||||
showSuccess('Status do cluster carregado com sucesso');
|
||||
|
||||
} catch (error) {
|
||||
showError('Erro ao carregar status do cluster: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadValidations() {
|
||||
if (!currentData) {
|
||||
showError('Carregue o status do cluster primeiro');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/validations');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const validations = await response.json();
|
||||
displayValidations(validations);
|
||||
document.getElementById('validationsCard').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
showError('Erro ao carregar validações: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVPARecommendations() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/vpa/recommendations');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const recommendations = await response.json();
|
||||
displayVPARecommendations(recommendations);
|
||||
document.getElementById('vpaCard').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
showError('Erro ao carregar recomendações VPA: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function exportReport() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const format = document.getElementById('exportFormat').value;
|
||||
const namespaces = document.getElementById('namespaces').value;
|
||||
const includeVPA = document.getElementById('includeVPA').checked;
|
||||
const includeValidations = document.getElementById('includeValidations').checked;
|
||||
|
||||
const requestBody = {
|
||||
format: format,
|
||||
includeVPA: includeVPA,
|
||||
includeValidations: includeValidations
|
||||
};
|
||||
|
||||
if (namespaces.trim()) {
|
||||
requestBody.namespaces = namespaces.split(',').map(n => n.trim());
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/export', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
showSuccess(`Relatório exportado: ${result.filepath}`);
|
||||
|
||||
} catch (error) {
|
||||
showError('Erro ao exportar relatório: ' + error.message);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
document.getElementById('totalPods').textContent = data.total_pods || 0;
|
||||
document.getElementById('totalNamespaces').textContent = data.total_namespaces || 0;
|
||||
document.getElementById('totalNodes').textContent = data.total_nodes || 0;
|
||||
document.getElementById('criticalIssues').textContent = data.summary?.critical_issues || 0;
|
||||
}
|
||||
|
||||
function displayValidations(validations) {
|
||||
const container = document.getElementById('validationsList');
|
||||
|
||||
if (validations.length === 0) {
|
||||
container.innerHTML = '<p>Nenhuma validação encontrada.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="table"><thead><tr><th>Pod</th><th>Namespace</th><th>Container</th><th>Tipo</th><th>Severidade</th><th>Mensagem</th></tr></thead><tbody>';
|
||||
|
||||
validations.forEach(validation => {
|
||||
const severityClass = `severity-${validation.severity}`;
|
||||
html += `
|
||||
<tr>
|
||||
<td>${validation.pod_name}</td>
|
||||
<td>${validation.namespace}</td>
|
||||
<td>${validation.container_name}</td>
|
||||
<td>${validation.validation_type}</td>
|
||||
<td><span class="severity-badge ${severityClass}">${validation.severity}</span></td>
|
||||
<td>${validation.message}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function displayVPARecommendations(recommendations) {
|
||||
const container = document.getElementById('vpaList');
|
||||
|
||||
if (recommendations.length === 0) {
|
||||
container.innerHTML = '<p>Nenhuma recomendação VPA encontrada.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<table class="table"><thead><tr><th>Nome</th><th>Namespace</th><th>Target</th><th>Recomendações</th></tr></thead><tbody>';
|
||||
|
||||
recommendations.forEach(rec => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${rec.name}</td>
|
||||
<td>${rec.namespace}</td>
|
||||
<td>${rec.target_ref?.kind}/${rec.target_ref?.name || 'N/A'}</td>
|
||||
<td>${JSON.stringify(rec.recommendations, null, 2)}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('loading').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('error');
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
setTimeout(() => errorDiv.classList.add('hidden'), 5000);
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const successDiv = document.getElementById('success');
|
||||
successDiv.textContent = message;
|
||||
successDiv.classList.remove('hidden');
|
||||
setTimeout(() => successDiv.classList.add('hidden'), 3000);
|
||||
}
|
||||
|
||||
function hideMessages() {
|
||||
document.getElementById('error').classList.add('hidden');
|
||||
document.getElementById('success').classList.add('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user