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:
23
.env.example
Normal file
23
.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# Configurações do OpenShift/Kubernetes
|
||||
KUBECONFIG_PATH=
|
||||
CLUSTER_URL=
|
||||
TOKEN=
|
||||
|
||||
# Configurações do Prometheus
|
||||
PROMETHEUS_URL=http://prometheus.openshift-monitoring.svc.cluster.local:9090
|
||||
|
||||
# Configurações de validação
|
||||
CPU_LIMIT_RATIO=3.0
|
||||
MEMORY_LIMIT_RATIO=3.0
|
||||
MIN_CPU_REQUEST=10m
|
||||
MIN_MEMORY_REQUEST=32Mi
|
||||
|
||||
# Namespaces críticos para VPA (separados por vírgula)
|
||||
CRITICAL_NAMESPACES=openshift-monitoring,openshift-ingress,openshift-apiserver,openshift-controller-manager,openshift-sdn
|
||||
|
||||
# Configurações de relatório
|
||||
REPORT_EXPORT_PATH=/tmp/reports
|
||||
|
||||
# Configurações de segurança
|
||||
ENABLE_RBAC=true
|
||||
SERVICE_ACCOUNT_NAME=resource-governance-sa
|
||||
161
.gitignore
vendored
Normal file
161
.gitignore
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
reports/
|
||||
*.json
|
||||
*.csv
|
||||
*.pdf
|
||||
logs/
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Kubernetes
|
||||
kubeconfig
|
||||
*.kubeconfig
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
58
Dockerfile
Normal file
58
Dockerfile
Normal file
@@ -0,0 +1,58 @@
|
||||
# Multi-stage build para otimizar tamanho da imagem
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
# Instalar dependências do sistema necessárias para compilação
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Criar diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar requirements e instalar dependências Python
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# Stage final - imagem de produção
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Instalar dependências de runtime
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Criar usuário não-root
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||
|
||||
# Criar diretórios necessários
|
||||
RUN mkdir -p /app /tmp/reports && \
|
||||
chown -R appuser:appuser /app /tmp/reports
|
||||
|
||||
# Copiar dependências Python do stage anterior
|
||||
COPY --from=builder /root/.local /home/appuser/.local
|
||||
|
||||
# Definir PATH para incluir dependências locais
|
||||
ENV PATH=/home/appuser/.local/bin:$PATH
|
||||
|
||||
# Definir diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar código da aplicação
|
||||
COPY app/ ./app/
|
||||
|
||||
# Alterar propriedade dos arquivos
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
# Mudar para usuário não-root
|
||||
USER appuser
|
||||
|
||||
# Expor porta
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
# Comando para executar a aplicação
|
||||
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
139
Makefile
Normal file
139
Makefile
Normal file
@@ -0,0 +1,139 @@
|
||||
# Makefile para OpenShift Resource Governance Tool
|
||||
|
||||
# Configurações
|
||||
IMAGE_NAME = resource-governance
|
||||
TAG = latest
|
||||
REGISTRY = quay.io/openshift
|
||||
FULL_IMAGE_NAME = $(REGISTRY)/$(IMAGE_NAME):$(TAG)
|
||||
NAMESPACE = resource-governance
|
||||
|
||||
# Cores para output
|
||||
RED = \033[0;31m
|
||||
GREEN = \033[0;32m
|
||||
YELLOW = \033[1;33m
|
||||
BLUE = \033[0;34m
|
||||
NC = \033[0m # No Color
|
||||
|
||||
.PHONY: help build test deploy undeploy clean dev logs status
|
||||
|
||||
help: ## Mostrar ajuda
|
||||
@echo "$(BLUE)OpenShift Resource Governance Tool$(NC)"
|
||||
@echo ""
|
||||
@echo "Comandos disponíveis:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-15s$(NC) %s\n", $$1, $$2}'
|
||||
|
||||
build: ## Build da imagem Docker
|
||||
@echo "$(YELLOW)📦 Building Docker image...$(NC)"
|
||||
@./scripts/build.sh $(TAG) $(REGISTRY)
|
||||
|
||||
test: ## Testar a aplicação
|
||||
@echo "$(YELLOW)🧪 Testing application...$(NC)"
|
||||
@python -c "import app.main; print('$(GREEN)✅ App imports successfully$(NC)')"
|
||||
@echo "$(YELLOW)🧪 Testing API...$(NC)"
|
||||
@python -m uvicorn app.main:app --host 0.0.0.0 --port 8080 &
|
||||
@sleep 5
|
||||
@curl -f http://localhost:8080/health || (echo "$(RED)❌ Health check failed$(NC)" && exit 1)
|
||||
@pkill -f uvicorn
|
||||
@echo "$(GREEN)✅ Tests passed$(NC)"
|
||||
|
||||
deploy: ## Deploy no OpenShift
|
||||
@echo "$(YELLOW)🚀 Deploying to OpenShift...$(NC)"
|
||||
@./scripts/deploy.sh $(TAG) $(REGISTRY)
|
||||
|
||||
undeploy: ## Remover do OpenShift
|
||||
@echo "$(YELLOW)🗑️ Undeploying from OpenShift...$(NC)"
|
||||
@./scripts/undeploy.sh
|
||||
|
||||
clean: ## Limpar recursos locais
|
||||
@echo "$(YELLOW)🧹 Cleaning up...$(NC)"
|
||||
@docker rmi $(FULL_IMAGE_NAME) 2>/dev/null || true
|
||||
@docker system prune -f
|
||||
@echo "$(GREEN)✅ Cleanup completed$(NC)"
|
||||
|
||||
dev: ## Executar em modo desenvolvimento
|
||||
@echo "$(YELLOW)🔧 Starting development server...$(NC)"
|
||||
@python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8080
|
||||
|
||||
logs: ## Ver logs da aplicação
|
||||
@echo "$(YELLOW)📋 Showing application logs...$(NC)"
|
||||
@oc logs -f daemonset/$(IMAGE_NAME) -n $(NAMESPACE)
|
||||
|
||||
status: ## Ver status da aplicação
|
||||
@echo "$(YELLOW)📊 Application status:$(NC)"
|
||||
@oc get all -n $(NAMESPACE)
|
||||
@echo ""
|
||||
@echo "$(YELLOW)🌐 Route URL:$(NC)"
|
||||
@oc get route $(IMAGE_NAME)-route -n $(NAMESPACE) -o jsonpath='{.spec.host}' 2>/dev/null || echo "Route not found"
|
||||
|
||||
install-deps: ## Instalar dependências Python
|
||||
@echo "$(YELLOW)📦 Installing Python dependencies...$(NC)"
|
||||
@pip install -r requirements.txt
|
||||
@echo "$(GREEN)✅ Dependencies installed$(NC)"
|
||||
|
||||
format: ## Formatar código Python
|
||||
@echo "$(YELLOW)🎨 Formatting Python code...$(NC)"
|
||||
@python -m black app/
|
||||
@python -m isort app/
|
||||
@echo "$(GREEN)✅ Code formatted$(NC)"
|
||||
|
||||
lint: ## Verificar código Python
|
||||
@echo "$(YELLOW)🔍 Linting Python code...$(NC)"
|
||||
@python -m flake8 app/
|
||||
@python -m mypy app/
|
||||
@echo "$(GREEN)✅ Linting completed$(NC)"
|
||||
|
||||
security: ## Verificar segurança
|
||||
@echo "$(YELLOW)🔒 Security check...$(NC)"
|
||||
@python -m bandit -r app/
|
||||
@echo "$(GREEN)✅ Security check completed$(NC)"
|
||||
|
||||
all: clean install-deps format lint test build ## Executar pipeline completo
|
||||
|
||||
# Comandos específicos do OpenShift
|
||||
oc-login: ## Fazer login no OpenShift
|
||||
@echo "$(YELLOW)🔐 Logging into OpenShift...$(NC)"
|
||||
@oc login
|
||||
|
||||
oc-projects: ## Listar projetos OpenShift
|
||||
@echo "$(YELLOW)📋 OpenShift projects:$(NC)"
|
||||
@oc get projects
|
||||
|
||||
oc-ns: ## Criar namespace
|
||||
@echo "$(YELLOW)📁 Creating namespace...$(NC)"
|
||||
@oc apply -f k8s/namespace.yaml
|
||||
|
||||
oc-rbac: ## Aplicar RBAC
|
||||
@echo "$(YELLOW)🔐 Applying RBAC...$(NC)"
|
||||
@oc apply -f k8s/rbac.yaml
|
||||
|
||||
oc-config: ## Aplicar ConfigMap
|
||||
@echo "$(YELLOW)⚙️ Applying ConfigMap...$(NC)"
|
||||
@oc apply -f k8s/configmap.yaml
|
||||
|
||||
oc-deploy: ## Aplicar DaemonSet
|
||||
@echo "$(YELLOW)📦 Applying DaemonSet...$(NC)"
|
||||
@oc apply -f k8s/daemonset.yaml
|
||||
|
||||
oc-service: ## Aplicar Service
|
||||
@echo "$(YELLOW)🌐 Applying Service...$(NC)"
|
||||
@oc apply -f k8s/service.yaml
|
||||
|
||||
oc-route: ## Aplicar Route
|
||||
@echo "$(YELLOW)🛣️ Applying Route...$(NC)"
|
||||
@oc apply -f k8s/route.yaml
|
||||
|
||||
oc-apply: oc-ns oc-rbac oc-config oc-deploy oc-service oc-route ## Aplicar todos os recursos
|
||||
|
||||
# Comandos de monitoramento
|
||||
monitor: ## Monitorar aplicação
|
||||
@echo "$(YELLOW)📊 Monitoring application...$(NC)"
|
||||
@watch -n 5 'oc get pods -n $(NAMESPACE) && echo "" && oc get route $(IMAGE_NAME)-route -n $(NAMESPACE)'
|
||||
|
||||
health: ## Verificar saúde da aplicação
|
||||
@echo "$(YELLOW)🏥 Health check...$(NC)"
|
||||
@ROUTE_URL=$$(oc get route $(IMAGE_NAME)-route -n $(NAMESPACE) -o jsonpath='{.spec.host}' 2>/dev/null); \
|
||||
if [ -n "$$ROUTE_URL" ]; then \
|
||||
curl -f https://$$ROUTE_URL/health || echo "$(RED)❌ Health check failed$(NC)"; \
|
||||
else \
|
||||
echo "$(RED)❌ Route not found$(NC)"; \
|
||||
fi
|
||||
301
README.md
Normal file
301
README.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# OpenShift Resource Governance Tool
|
||||
|
||||
Uma ferramenta de governança de recursos para clusters OpenShift que vai além do que o Metrics Server e VPA oferecem, fornecendo validações, relatórios e recomendações consolidadas.
|
||||
|
||||
## 🚀 Características
|
||||
|
||||
- **Coleta Automática**: Coleta requests/limits de todos os pods/containers no cluster
|
||||
- **Validações Red Hat**: Valida best practices de capacity management
|
||||
- **Integração VPA**: Consome recomendações do VPA em modo Off
|
||||
- **Integração Prometheus**: Coleta métricas reais de consumo
|
||||
- **Relatórios Consolidados**: Gera relatórios em JSON, CSV e PDF
|
||||
- **UI Web**: Interface simples para visualização e interação
|
||||
- **Aplicação de Recomendações**: Permite aprovar e aplicar recomendações
|
||||
|
||||
## 📋 Requisitos
|
||||
|
||||
- OpenShift 4.x
|
||||
- Prometheus (nativo no OCP)
|
||||
- VPA (opcional, para recomendações)
|
||||
- Python 3.11+
|
||||
- Docker
|
||||
- OpenShift CLI (oc)
|
||||
|
||||
## 🛠️ Instalação
|
||||
|
||||
### 1. Build da Imagem
|
||||
|
||||
```bash
|
||||
# Build local
|
||||
./scripts/build.sh
|
||||
|
||||
# Build com tag específica
|
||||
./scripts/build.sh v1.0.0
|
||||
|
||||
# Build para registry específico
|
||||
./scripts/build.sh latest quay.io/seu-usuario
|
||||
```
|
||||
|
||||
### 2. Deploy no OpenShift
|
||||
|
||||
```bash
|
||||
# Deploy padrão
|
||||
./scripts/deploy.sh
|
||||
|
||||
# Deploy com tag específica
|
||||
./scripts/deploy.sh v1.0.0
|
||||
|
||||
# Deploy para registry específico
|
||||
./scripts/deploy.sh latest quay.io/seu-usuario
|
||||
```
|
||||
|
||||
### 3. Acesso à Aplicação
|
||||
|
||||
Após o deploy, acesse a aplicação através da rota criada:
|
||||
|
||||
```bash
|
||||
# Obter URL da rota
|
||||
oc get route resource-governance-route -n resource-governance
|
||||
|
||||
# Acessar via browser
|
||||
# https://resource-governance-route-resource-governance.apps.openshift.local
|
||||
```
|
||||
|
||||
## 🔧 Configuração
|
||||
|
||||
### ConfigMap
|
||||
|
||||
A aplicação é configurada através do ConfigMap `resource-governance-config`:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
CPU_LIMIT_RATIO: "3.0" # Ratio padrão limit:request para CPU
|
||||
MEMORY_LIMIT_RATIO: "3.0" # Ratio padrão limit:request para memória
|
||||
MIN_CPU_REQUEST: "10m" # Mínimo de CPU request
|
||||
MIN_MEMORY_REQUEST: "32Mi" # Mínimo de memória request
|
||||
CRITICAL_NAMESPACES: | # Namespaces críticos para VPA
|
||||
openshift-monitoring
|
||||
openshift-ingress
|
||||
openshift-apiserver
|
||||
PROMETHEUS_URL: "http://prometheus.openshift-monitoring.svc.cluster.local:9090"
|
||||
```
|
||||
|
||||
### Variáveis de Ambiente
|
||||
|
||||
- `KUBECONFIG`: Caminho para kubeconfig (usado em desenvolvimento)
|
||||
- `PROMETHEUS_URL`: URL do Prometheus
|
||||
- `CPU_LIMIT_RATIO`: Ratio CPU limit:request
|
||||
- `MEMORY_LIMIT_RATIO`: Ratio memória limit:request
|
||||
- `MIN_CPU_REQUEST`: Mínimo de CPU request
|
||||
- `MIN_MEMORY_REQUEST`: Mínimo de memória request
|
||||
|
||||
## 📊 Uso
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Status do Cluster
|
||||
```bash
|
||||
GET /api/v1/cluster/status
|
||||
```
|
||||
|
||||
#### Status de Namespace
|
||||
```bash
|
||||
GET /api/v1/namespace/{namespace}/status
|
||||
```
|
||||
|
||||
#### Validações
|
||||
```bash
|
||||
GET /api/v1/validations?namespace=default&severity=error
|
||||
```
|
||||
|
||||
#### Recomendações VPA
|
||||
```bash
|
||||
GET /api/v1/vpa/recommendations?namespace=default
|
||||
```
|
||||
|
||||
#### Exportar Relatório
|
||||
```bash
|
||||
POST /api/v1/export
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"format": "json",
|
||||
"namespaces": ["default", "kube-system"],
|
||||
"includeVPA": true,
|
||||
"includeValidations": true
|
||||
}
|
||||
```
|
||||
|
||||
### Exemplos de Uso
|
||||
|
||||
#### 1. Verificar Status do Cluster
|
||||
```bash
|
||||
curl https://resource-governance-route-resource-governance.apps.openshift.local/api/v1/cluster/status
|
||||
```
|
||||
|
||||
#### 2. Exportar Relatório CSV
|
||||
```bash
|
||||
curl -X POST https://resource-governance-route-resource-governance.apps.openshift.local/api/v1/export \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"format": "csv", "includeVPA": true}'
|
||||
```
|
||||
|
||||
#### 3. Ver Validações Críticas
|
||||
```bash
|
||||
curl "https://resource-governance-route-resource-governance.apps.openshift.local/api/v1/validations?severity=critical"
|
||||
```
|
||||
|
||||
## 🔍 Validações Implementadas
|
||||
|
||||
### 1. Requests Obrigatórios
|
||||
- **Problema**: Pods sem requests definidos
|
||||
- **Severidade**: Error
|
||||
- **Recomendação**: Definir requests de CPU e memória
|
||||
|
||||
### 2. Limits Recomendados
|
||||
- **Problema**: Pods sem limits definidos
|
||||
- **Severidade**: Warning
|
||||
- **Recomendação**: Definir limits para evitar consumo excessivo
|
||||
|
||||
### 3. Ratio Limit:Request
|
||||
- **Problema**: Ratio muito alto ou baixo
|
||||
- **Severidade**: Warning/Error
|
||||
- **Recomendação**: Ajustar para ratio 3:1
|
||||
|
||||
### 4. Valores Mínimos
|
||||
- **Problema**: Requests muito baixos
|
||||
- **Severidade**: Warning
|
||||
- **Recomendação**: Aumentar para valores mínimos
|
||||
|
||||
### 5. Overcommit
|
||||
- **Problema**: Requests excedem capacidade do cluster
|
||||
- **Severidade**: Critical
|
||||
- **Recomendação**: Reduzir requests ou adicionar nós
|
||||
|
||||
## 📈 Relatórios
|
||||
|
||||
### Formato JSON
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"total_pods": 150,
|
||||
"total_namespaces": 25,
|
||||
"total_nodes": 3,
|
||||
"validations": [...],
|
||||
"vpa_recommendations": [...],
|
||||
"summary": {
|
||||
"total_validations": 45,
|
||||
"critical_issues": 5,
|
||||
"warnings": 25,
|
||||
"errors": 15
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Formato CSV
|
||||
```csv
|
||||
Pod Name,Namespace,Container Name,Validation Type,Severity,Message,Recommendation
|
||||
pod-1,default,nginx,missing_requests,error,Container sem requests definidos,Definir requests de CPU e memória
|
||||
```
|
||||
|
||||
## 🔐 Segurança
|
||||
|
||||
### RBAC
|
||||
A aplicação usa um ServiceAccount dedicado com permissões mínimas:
|
||||
|
||||
- **Pods**: get, list, watch, patch, update
|
||||
- **Namespaces**: get, list, watch
|
||||
- **Nodes**: get, list, watch
|
||||
- **VPA**: get, list, watch
|
||||
- **Deployments/ReplicaSets**: get, list, watch, patch, update
|
||||
|
||||
### Security Context
|
||||
- Executa como usuário não-root (UID 1000)
|
||||
- Usa SecurityContext com runAsNonRoot: true
|
||||
- Limita recursos com requests/limits
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Verificar Logs
|
||||
```bash
|
||||
oc logs -f daemonset/resource-governance -n resource-governance
|
||||
```
|
||||
|
||||
### Verificar Status dos Pods
|
||||
```bash
|
||||
oc get pods -n resource-governance
|
||||
oc describe pod <pod-name> -n resource-governance
|
||||
```
|
||||
|
||||
### Verificar RBAC
|
||||
```bash
|
||||
oc auth can-i get pods --as=system:serviceaccount:resource-governance:resource-governance-sa
|
||||
```
|
||||
|
||||
### Testar Conectividade
|
||||
```bash
|
||||
# Health check
|
||||
curl https://resource-governance-route-resource-governance.apps.openshift.local/health
|
||||
|
||||
# Teste de API
|
||||
curl https://resource-governance-route-resource-governance.apps.openshift.local/api/v1/cluster/status
|
||||
```
|
||||
|
||||
## 🚀 Desenvolvimento
|
||||
|
||||
### Executar Localmente
|
||||
```bash
|
||||
# Instalar dependências
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Executar aplicação
|
||||
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8080
|
||||
```
|
||||
|
||||
### Executar com Docker
|
||||
```bash
|
||||
# Build
|
||||
docker build -t resource-governance .
|
||||
|
||||
# Executar
|
||||
docker run -p 8080:8080 resource-governance
|
||||
```
|
||||
|
||||
### Testes
|
||||
```bash
|
||||
# Testar importação
|
||||
python -c "import app.main; print('OK')"
|
||||
|
||||
# Testar API
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## 📝 Roadmap
|
||||
|
||||
### Próximas Versões
|
||||
- [ ] UI Web com gráficos interativos
|
||||
- [ ] Relatórios PDF com gráficos
|
||||
- [ ] Regras customizadas por namespace
|
||||
- [ ] Integração com GitOps (ArgoCD)
|
||||
- [ ] Notificações via Slack/Teams
|
||||
- [ ] Métricas customizadas do Prometheus
|
||||
- [ ] Suporte a múltiplos clusters
|
||||
|
||||
## 🤝 Contribuição
|
||||
|
||||
1. Fork o projeto
|
||||
2. Crie uma branch para sua feature (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit suas mudanças (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Push para a branch (`git push origin feature/AmazingFeature`)
|
||||
5. Abra um Pull Request
|
||||
|
||||
## 📄 Licença
|
||||
|
||||
Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para detalhes.
|
||||
|
||||
## 📞 Suporte
|
||||
|
||||
Para suporte e dúvidas:
|
||||
- Abra uma issue no GitHub
|
||||
- Consulte a documentação do OpenShift
|
||||
- Verifique os logs da aplicação
|
||||
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>
|
||||
32
k8s/configmap.yaml
Normal file
32
k8s/configmap.yaml
Normal file
@@ -0,0 +1,32 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: resource-governance-config
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
data:
|
||||
# Configurações da aplicação
|
||||
CPU_LIMIT_RATIO: "3.0"
|
||||
MEMORY_LIMIT_RATIO: "3.0"
|
||||
MIN_CPU_REQUEST: "10m"
|
||||
MIN_MEMORY_REQUEST: "32Mi"
|
||||
|
||||
# Namespaces críticos para VPA
|
||||
CRITICAL_NAMESPACES: |
|
||||
openshift-monitoring
|
||||
openshift-ingress
|
||||
openshift-apiserver
|
||||
openshift-controller-manager
|
||||
openshift-sdn
|
||||
|
||||
# URL do Prometheus
|
||||
PROMETHEUS_URL: "http://prometheus.openshift-monitoring.svc.cluster.local:9090"
|
||||
|
||||
# Configurações de relatório
|
||||
REPORT_EXPORT_PATH: "/tmp/reports"
|
||||
|
||||
# Configurações de segurança
|
||||
ENABLE_RBAC: "true"
|
||||
SERVICE_ACCOUNT_NAME: "resource-governance-sa"
|
||||
122
k8s/daemonset.yaml
Normal file
122
k8s/daemonset.yaml
Normal file
@@ -0,0 +1,122 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: resource-governance
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
spec:
|
||||
serviceAccountName: resource-governance-sa
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: resource-governance
|
||||
image: resource-governance:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: KUBECONFIG
|
||||
value: "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
- name: CPU_LIMIT_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: CPU_LIMIT_RATIO
|
||||
- name: MEMORY_LIMIT_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: MEMORY_LIMIT_RATIO
|
||||
- name: MIN_CPU_REQUEST
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: MIN_CPU_REQUEST
|
||||
- name: MIN_MEMORY_REQUEST
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: MIN_MEMORY_REQUEST
|
||||
- name: CRITICAL_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: CRITICAL_NAMESPACES
|
||||
- name: PROMETHEUS_URL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: PROMETHEUS_URL
|
||||
- name: REPORT_EXPORT_PATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: REPORT_EXPORT_PATH
|
||||
- name: ENABLE_RBAC
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: ENABLE_RBAC
|
||||
- name: SERVICE_ACCOUNT_NAME
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: resource-governance-config
|
||||
key: SERVICE_ACCOUNT_NAME
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
volumeMounts:
|
||||
- name: reports-volume
|
||||
mountPath: /tmp/reports
|
||||
- name: tmp-volume
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: reports-volume
|
||||
emptyDir: {}
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
nodeSelector:
|
||||
kubernetes.io/os: linux
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
19
k8s/kustomization.yaml
Normal file
19
k8s/kustomization.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- rbac.yaml
|
||||
- configmap.yaml
|
||||
- daemonset.yaml
|
||||
- service.yaml
|
||||
- route.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
app.kubernetes.io/part-of: openshift-governance
|
||||
|
||||
images:
|
||||
- name: resource-governance
|
||||
newTag: latest
|
||||
36
k8s/namespace.yaml
Normal file
36
k8s/namespace.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: resource-governance
|
||||
labels:
|
||||
name: resource-governance
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: resource-governance-quota
|
||||
namespace: resource-governance
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "2"
|
||||
requests.memory: 4Gi
|
||||
limits.cpu: "4"
|
||||
limits.memory: 8Gi
|
||||
pods: "10"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: resource-governance-limits
|
||||
namespace: resource-governance
|
||||
spec:
|
||||
limits:
|
||||
- default:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
defaultRequest:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
type: Container
|
||||
93
k8s/rbac.yaml
Normal file
93
k8s/rbac.yaml
Normal file
@@ -0,0 +1,93 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: resource-governance-sa
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: resource-governance-role
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
rules:
|
||||
# Permissões para listar e ler pods
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Permissões para listar e ler namespaces
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Permissões para listar e ler nós
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Permissões para VPA (Vertical Pod Autoscaler)
|
||||
- apiGroups: ["autoscaling.k8s.io"]
|
||||
resources: ["verticalpodautoscalers"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
# Permissões para deployments e replicasets (para aplicar recomendações)
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "replicasets"]
|
||||
verbs: ["get", "list", "watch", "patch", "update"]
|
||||
# Permissões para pods (para aplicar recomendações)
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch", "patch", "update"]
|
||||
# Permissões para eventos (para logging)
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["get", "list", "watch", "create"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: resource-governance-binding
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: resource-governance-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: resource-governance-sa
|
||||
namespace: resource-governance
|
||||
---
|
||||
# Role para acessar recursos do Prometheus (se necessário)
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: resource-governance-prometheus-role
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
rules:
|
||||
# Permissões para acessar serviços do Prometheus
|
||||
- apiGroups: [""]
|
||||
resources: ["services", "endpoints"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: resource-governance-prometheus-binding
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: resource-governance-prometheus-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: resource-governance-sa
|
||||
namespace: resource-governance
|
||||
23
k8s/route.yaml
Normal file
23
k8s/route.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: route.openshift.io/v1
|
||||
kind: Route
|
||||
metadata:
|
||||
name: resource-governance-route
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
annotations:
|
||||
haproxy.router.openshift.io/timeout: "300s"
|
||||
haproxy.router.openshift.io/rate-limit: "100"
|
||||
spec:
|
||||
host: resource-governance.apps.openshift.local
|
||||
to:
|
||||
kind: Service
|
||||
name: resource-governance-service
|
||||
weight: 100
|
||||
port:
|
||||
targetPort: http
|
||||
tls:
|
||||
termination: edge
|
||||
insecureEdgeTerminationPolicy: Redirect
|
||||
wildcardPolicy: None
|
||||
18
k8s/service.yaml
Normal file
18
k8s/service.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: resource-governance-service
|
||||
namespace: resource-governance
|
||||
labels:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: resource-governance
|
||||
app.kubernetes.io/component: governance
|
||||
14
requirements.txt
Normal file
14
requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
kubernetes==28.1.0
|
||||
prometheus-client==0.19.0
|
||||
requests==2.31.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
jinja2==3.1.2
|
||||
aiofiles==23.2.1
|
||||
pandas==2.1.4
|
||||
reportlab==4.0.7
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-dotenv==1.0.0
|
||||
58
scripts/build.sh
Executable file
58
scripts/build.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de build para OpenShift Resource Governance Tool
|
||||
set -e
|
||||
|
||||
# Cores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configurações
|
||||
IMAGE_NAME="resource-governance"
|
||||
TAG="${1:-latest}"
|
||||
REGISTRY="${2:-quay.io/openshift}"
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
echo -e "${BLUE}🚀 Building OpenShift Resource Governance Tool${NC}"
|
||||
echo -e "${BLUE}Image: ${FULL_IMAGE_NAME}${NC}"
|
||||
|
||||
# Verificar se Docker está rodando
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Docker não está rodando. Inicie o Docker e tente novamente.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build da imagem
|
||||
echo -e "${YELLOW}📦 Building Docker image...${NC}"
|
||||
docker build -t "${FULL_IMAGE_NAME}" .
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Image built successfully!${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Testar a imagem
|
||||
echo -e "${YELLOW}🧪 Testing image...${NC}"
|
||||
docker run --rm "${FULL_IMAGE_NAME}" python -c "import app.main; print('✅ App imports successfully')"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}✅ Image test passed!${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Image test failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mostrar informações da imagem
|
||||
echo -e "${BLUE}📊 Image information:${NC}"
|
||||
docker images "${FULL_IMAGE_NAME}"
|
||||
|
||||
echo -e "${GREEN}🎉 Build completed successfully!${NC}"
|
||||
echo -e "${BLUE}To push to registry:${NC}"
|
||||
echo -e " docker push ${FULL_IMAGE_NAME}"
|
||||
echo -e "${BLUE}To run locally:${NC}"
|
||||
echo -e " docker run -p 8080:8080 ${FULL_IMAGE_NAME}"
|
||||
90
scripts/deploy.sh
Executable file
90
scripts/deploy.sh
Executable file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de deploy para OpenShift Resource Governance Tool
|
||||
set -e
|
||||
|
||||
# Cores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configurações
|
||||
NAMESPACE="resource-governance"
|
||||
IMAGE_NAME="resource-governance"
|
||||
TAG="${1:-latest}"
|
||||
REGISTRY="${2:-quay.io/openshift}"
|
||||
FULL_IMAGE_NAME="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
echo -e "${BLUE}🚀 Deploying OpenShift Resource Governance Tool${NC}"
|
||||
echo -e "${BLUE}Namespace: ${NAMESPACE}${NC}"
|
||||
echo -e "${BLUE}Image: ${FULL_IMAGE_NAME}${NC}"
|
||||
|
||||
# Verificar se oc está instalado
|
||||
if ! command -v oc &> /dev/null; then
|
||||
echo -e "${RED}❌ OpenShift CLI (oc) não está instalado.${NC}"
|
||||
echo -e "${YELLOW}Instale o oc CLI: https://docs.openshift.com/container-platform/latest/cli_reference/openshift_cli/getting-started-cli.html${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar se está logado no OpenShift
|
||||
if ! oc whoami &> /dev/null; then
|
||||
echo -e "${RED}❌ Não está logado no OpenShift.${NC}"
|
||||
echo -e "${YELLOW}Faça login com: oc login <cluster-url>${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Logado como: $(oc whoami)${NC}"
|
||||
|
||||
# Criar namespace se não existir
|
||||
echo -e "${YELLOW}📁 Creating namespace...${NC}"
|
||||
oc apply -f k8s/namespace.yaml
|
||||
|
||||
# Aplicar RBAC
|
||||
echo -e "${YELLOW}🔐 Applying RBAC...${NC}"
|
||||
oc apply -f k8s/rbac.yaml
|
||||
|
||||
# Aplicar ConfigMap
|
||||
echo -e "${YELLOW}⚙️ Applying ConfigMap...${NC}"
|
||||
oc apply -f k8s/configmap.yaml
|
||||
|
||||
# Atualizar imagem no DaemonSet
|
||||
echo -e "${YELLOW}🔄 Updating image in DaemonSet...${NC}"
|
||||
oc set image daemonset/resource-governance resource-governance="${FULL_IMAGE_NAME}" -n "${NAMESPACE}"
|
||||
|
||||
# Aplicar DaemonSet
|
||||
echo -e "${YELLOW}📦 Applying DaemonSet...${NC}"
|
||||
oc apply -f k8s/daemonset.yaml
|
||||
|
||||
# Aplicar Service
|
||||
echo -e "${YELLOW}🌐 Applying Service...${NC}"
|
||||
oc apply -f k8s/service.yaml
|
||||
|
||||
# Aplicar Route
|
||||
echo -e "${YELLOW}🛣️ Applying Route...${NC}"
|
||||
oc apply -f k8s/route.yaml
|
||||
|
||||
# Aguardar pods ficarem prontos
|
||||
echo -e "${YELLOW}⏳ Waiting for pods to be ready...${NC}"
|
||||
oc wait --for=condition=ready pod -l app.kubernetes.io/name=resource-governance -n "${NAMESPACE}" --timeout=300s
|
||||
|
||||
# Obter URL da rota
|
||||
ROUTE_URL=$(oc get route resource-governance-route -n "${NAMESPACE}" -o jsonpath='{.spec.host}')
|
||||
if [ -n "${ROUTE_URL}" ]; then
|
||||
echo -e "${GREEN}🎉 Deploy completed successfully!${NC}"
|
||||
echo -e "${BLUE}🌐 Application URL: https://${ROUTE_URL}${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Deploy completed, but route URL not found.${NC}"
|
||||
echo -e "${BLUE}Check with: oc get routes -n ${NAMESPACE}${NC}"
|
||||
fi
|
||||
|
||||
# Mostrar status
|
||||
echo -e "${BLUE}📊 Deployment status:${NC}"
|
||||
oc get all -n "${NAMESPACE}"
|
||||
|
||||
echo -e "${BLUE}🔍 To check logs:${NC}"
|
||||
echo -e " oc logs -f daemonset/resource-governance -n ${NAMESPACE}"
|
||||
|
||||
echo -e "${BLUE}🧪 To test health:${NC}"
|
||||
echo -e " curl https://${ROUTE_URL}/health"
|
||||
81
scripts/undeploy.sh
Executable file
81
scripts/undeploy.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de undeploy para OpenShift Resource Governance Tool
|
||||
set -e
|
||||
|
||||
# Cores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configurações
|
||||
NAMESPACE="resource-governance"
|
||||
|
||||
echo -e "${BLUE}🗑️ Undeploying OpenShift Resource Governance Tool${NC}"
|
||||
echo -e "${BLUE}Namespace: ${NAMESPACE}${NC}"
|
||||
|
||||
# Verificar se oc está instalado
|
||||
if ! command -v oc &> /dev/null; then
|
||||
echo -e "${RED}❌ OpenShift CLI (oc) não está instalado.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar se está logado no OpenShift
|
||||
if ! oc whoami &> /dev/null; then
|
||||
echo -e "${RED}❌ Não está logado no OpenShift.${NC}"
|
||||
echo -e "${YELLOW}Faça login com: oc login <cluster-url>${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Logado como: $(oc whoami)${NC}"
|
||||
|
||||
# Confirmar remoção
|
||||
read -p "Tem certeza que deseja remover a aplicação? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}❌ Operação cancelada.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Remover Route
|
||||
echo -e "${YELLOW}🛣️ Removing Route...${NC}"
|
||||
oc delete -f k8s/route.yaml --ignore-not-found=true
|
||||
|
||||
# Remover Service
|
||||
echo -e "${YELLOW}🌐 Removing Service...${NC}"
|
||||
oc delete -f k8s/service.yaml --ignore-not-found=true
|
||||
|
||||
# Remover DaemonSet
|
||||
echo -e "${YELLOW}📦 Removing DaemonSet...${NC}"
|
||||
oc delete -f k8s/daemonset.yaml --ignore-not-found=true
|
||||
|
||||
# Aguardar pods serem removidos
|
||||
echo -e "${YELLOW}⏳ Waiting for pods to be terminated...${NC}"
|
||||
oc wait --for=delete pod -l app.kubernetes.io/name=resource-governance -n "${NAMESPACE}" --timeout=60s || true
|
||||
|
||||
# Remover ConfigMap
|
||||
echo -e "${YELLOW}⚙️ Removing ConfigMap...${NC}"
|
||||
oc delete -f k8s/configmap.yaml --ignore-not-found=true
|
||||
|
||||
# Remover RBAC
|
||||
echo -e "${YELLOW}🔐 Removing RBAC...${NC}"
|
||||
oc delete -f k8s/rbac.yaml --ignore-not-found=true
|
||||
|
||||
# Remover namespace (opcional)
|
||||
read -p "Deseja remover o namespace também? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}📁 Removing namespace...${NC}"
|
||||
oc delete -f k8s/namespace.yaml --ignore-not-found=true
|
||||
echo -e "${GREEN}✅ Namespace removed.${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Namespace mantido.${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🎉 Undeploy completed successfully!${NC}"
|
||||
|
||||
# Verificar se ainda há recursos
|
||||
echo -e "${BLUE}🔍 Checking remaining resources:${NC}"
|
||||
oc get all -n "${NAMESPACE}" 2>/dev/null || echo -e "${GREEN}✅ No resources found in namespace.${NC}"
|
||||
67
setup.sh
Executable file
67
setup.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de setup para OpenShift Resource Governance Tool
|
||||
set -e
|
||||
|
||||
# Cores para output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}🚀 Setting up OpenShift Resource Governance Tool${NC}"
|
||||
|
||||
# Verificar se Python está instalado
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo -e "${RED}❌ Python 3 não está instalado.${NC}"
|
||||
echo -e "${YELLOW}Instale Python 3.11+ e tente novamente.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verificar se pip está instalado
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
echo -e "${RED}❌ pip3 não está instalado.${NC}"
|
||||
echo -e "${YELLOW}Instale pip3 e tente novamente.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Instalar dependências Python
|
||||
echo -e "${YELLOW}📦 Installing Python dependencies...${NC}"
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# Tornar scripts executáveis
|
||||
echo -e "${YELLOW}🔧 Making scripts executable...${NC}"
|
||||
chmod +x scripts/*.sh
|
||||
|
||||
# Criar diretório de relatórios
|
||||
echo -e "${YELLOW}📁 Creating reports directory...${NC}"
|
||||
mkdir -p reports
|
||||
|
||||
# Verificar se Docker está instalado
|
||||
if command -v docker &> /dev/null; then
|
||||
echo -e "${GREEN}✅ Docker encontrado${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Docker não encontrado. Instale para fazer build da imagem.${NC}"
|
||||
fi
|
||||
|
||||
# Verificar se oc está instalado
|
||||
if command -v oc &> /dev/null; then
|
||||
echo -e "${GREEN}✅ OpenShift CLI (oc) encontrado${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ OpenShift CLI (oc) não encontrado. Instale para fazer deploy.${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}🎉 Setup completed successfully!${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Próximos passos:${NC}"
|
||||
echo -e "1. ${YELLOW}Desenvolvimento local:${NC} make dev"
|
||||
echo -e "2. ${YELLOW}Build da imagem:${NC} make build"
|
||||
echo -e "3. ${YELLOW}Deploy no OpenShift:${NC} make deploy"
|
||||
echo -e "4. ${YELLOW}Ver documentação:${NC} cat README.md"
|
||||
echo ""
|
||||
echo -e "${BLUE}Comandos úteis:${NC}"
|
||||
echo -e " make help - Mostrar todos os comandos"
|
||||
echo -e " make test - Executar testes"
|
||||
echo -e " make logs - Ver logs da aplicação"
|
||||
echo -e " make status - Ver status da aplicação"
|
||||
Reference in New Issue
Block a user