项目4:Namespace 资源报表生成器

项目4:Namespace 资源报表生成器

项目背景

在多租户 Kubernetes 集群中,需要了解每个 Namespace 的资源使用情况,用于:

  • 💰 成本核算和分摊
  • 📊 资源配额调整
  • 🔍 资源使用分析
  • 📈 趋势预测

解决方案
定期生成各 Namespace 的资源使用报表,支持多种输出格式(HTML、PDF、Excel、JSON)。

功能需求

核心功能

  • ✅ 统计 CPU、内存、存储使用量
  • ✅ Pod 数量统计
  • ✅ Service、ConfigMap、Secret 统计
  • ✅ 资源配额使用情况
  • ✅ 多种输出格式(HTML、JSON、CSV、Excel)
  • ✅ 定时生成报表

高级功能

  • ✅ 同比和环比分析
  • ✅ 成本估算
  • ✅ 异常检测
  • ✅ 报表邮件推送
  • ✅ Dashboard 可视化

Go 完整实现

main.go

package main

import (
    "context"
    "flag"
    "fmt"
    "os"
    "time"
    
    "github.com/sirupsen/logrus"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
    metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned"
    
    "namespace-reporter/pkg/config"
    "namespace-reporter/pkg/reporter"
)

var log = logrus.New()

func main() {
    kubeconfig := flag.String("kubeconfig", "", "path to kubeconfig")
    configFile := flag.String("config", "", "path to config file")
    outputFormat := flag.String("format", "html", "output format: html, json, csv, excel")
    outputPath := flag.String("output", "report.html", "output file path")
    flag.Parse()
    
    // 加载配置
    cfg, err := config.LoadConfig(*configFile)
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }
    
    // 设置日志
    level, _ := logrus.ParseLevel(cfg.LogLevel)
    log.SetLevel(level)
    log.SetFormatter(&logrus.JSONFormatter{})
    
    // 创建 Kubernetes 客户端
    var k8sConfig *rest.Config
    if *kubeconfig != "" {
        k8sConfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
    } else {
        k8sConfig, err = rest.InClusterConfig()
    }
    if err != nil {
        log.Fatalf("Failed to create k8s config: %v", err)
    }
    
    clientset, err := kubernetes.NewForConfig(k8sConfig)
    if err != nil {
        log.Fatalf("Failed to create clientset: %v", err)
    }
    
    metricsClient, err := metricsv1beta1.NewForConfig(k8sConfig)
    if err != nil {
        log.Fatalf("Failed to create metrics client: %v", err)
    }
    
    // 创建报表生成器
    nsReporter := reporter.NewNamespaceReporter(clientset, metricsClient, cfg, log)
    
    // 生成报表
    report, err := nsReporter.GenerateReport()
    if err != nil {
        log.Fatalf("Failed to generate report: %v", err)
    }
    
    // 输出报表
    if err := nsReporter.ExportReport(report, *outputFormat, *outputPath); err != nil {
        log.Fatalf("Failed to export report: %v", err)
    }
    
    log.Infof("Report generated successfully: %s", *outputPath)
    
    // 如果配置了邮件,发送报表
    if cfg.Email.Enabled {
        if err := nsReporter.SendEmailReport(report, *outputPath); err != nil {
            log.Errorf("Failed to send email: %v", err)
        } else {
            log.Info("Report sent via email")
        }
    }
}

pkg/reporter/reporter.go

package reporter

import (
    "context"
    "fmt"
    "sort"
    "time"
    
    "github.com/sirupsen/logrus"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned"
    
    "namespace-reporter/pkg/config"
)

type NamespaceReporter struct {
    clientset     *kubernetes.Clientset
    metricsClient *metricsv1beta1.Clientset
    config        *config.Config
    logger        *logrus.Logger
}

type Report struct {
    GeneratedAt       time.Time
    ClusterName       string
    TotalNamespaces   int
    NamespaceReports  []NamespaceReport
    Summary           Summary
}

type NamespaceReport struct {
    Name              string
    CreationTimestamp time.Time
    
    // 资源使用
    CPURequest        int64  // millicores
    CPULimit          int64
    CPUUsage          int64
    MemoryRequest     int64  // bytes
    MemoryLimit       int64
    MemoryUsage       int64
    StorageUsage      int64
    
    // 资源配额
    ResourceQuota     *ResourceQuotaInfo
    
    // 对象统计
    PodCount          int
    RunningPods       int
    ServiceCount      int
    ConfigMapCount    int
    SecretCount       int
    PVCCount          int
    
    // 成本估算
    EstimatedCost     float64
    
    // 标签
    Labels            map[string]string
}

type ResourceQuotaInfo struct {
    Name              string
    CPUHard           int64
    CPUUsed           int64
    MemoryHard        int64
    MemoryUsed        int64
    PodsHard          int
    PodsUsed          int
}

type Summary struct {
    TotalCPURequest   int64
    TotalCPULimit     int64
    TotalCPUUsage     int64
    TotalMemoryRequest int64
    TotalMemoryLimit   int64
    TotalMemoryUsage   int64
    TotalStorageUsage  int64
    TotalPods          int
    TotalEstimatedCost float64
}

func NewNamespaceReporter(
    clientset *kubernetes.Clientset,
    metricsClient *metricsv1beta1.Clientset,
    cfg *config.Config,
    logger *logrus.Logger,
) *NamespaceReporter {
    return &NamespaceReporter{
        clientset:     clientset,
        metricsClient: metricsClient,
        config:        cfg,
        logger:        logger,
    }
}

func (nr *NamespaceReporter) GenerateReport() (*Report, error) {
    nr.logger.Info("Starting report generation")
    
    // 获取所有命名空间
    namespaces, err := nr.clientset.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        return nil, fmt.Errorf("failed to list namespaces: %v", err)
    }
    
    report := &Report{
        GeneratedAt:     time.Now(),
        ClusterName:     nr.config.ClusterName,
        TotalNamespaces: len(namespaces.Items),
    }
    
    // 生成每个命名空间的报表
    for _, ns := range namespaces.Items {
        // 跳过系统命名空间
        if nr.shouldSkipNamespace(ns.Name) {
            continue
        }
        
        nsReport, err := nr.generateNamespaceReport(&ns)
        if err != nil {
            nr.logger.WithError(err).Warnf("Failed to generate report for namespace %s", ns.Name)
            continue
        }
        
        report.NamespaceReports = append(report.NamespaceReports, *nsReport)
        
        // 更新汇总
        report.Summary.TotalCPURequest += nsReport.CPURequest
        report.Summary.TotalCPULimit += nsReport.CPULimit
        report.Summary.TotalCPUUsage += nsReport.CPUUsage
        report.Summary.TotalMemoryRequest += nsReport.MemoryRequest
        report.Summary.TotalMemoryLimit += nsReport.MemoryLimit
        report.Summary.TotalMemoryUsage += nsReport.MemoryUsage
        report.Summary.TotalStorageUsage += nsReport.StorageUsage
        report.Summary.TotalPods += nsReport.PodCount
        report.Summary.TotalEstimatedCost += nsReport.EstimatedCost
    }
    
    // 按 CPU 使用量排序
    sort.Slice(report.NamespaceReports, func(i, j int) bool {
        return report.NamespaceReports[i].CPUUsage > report.NamespaceReports[j].CPUUsage
    })
    
    nr.logger.Infof("Report generated for %d namespaces", len(report.NamespaceReports))
    
    return report, nil
}

func (nr *NamespaceReporter) shouldSkipNamespace(namespace string) bool {
    skipNamespaces := []string{"kube-system", "kube-public", "kube-node-lease"}
    
    for _, skip := range skipNamespaces {
        if namespace == skip {
            return true
        }
    }
    
    return false
}

func (nr *NamespaceReporter) generateNamespaceReport(ns *corev1.Namespace) (*NamespaceReport, error) {
    nsReport := &NamespaceReport{
        Name:              ns.Name,
        CreationTimestamp: ns.CreationTimestamp.Time,
        Labels:            ns.Labels,
    }
    
    // 获取 Pods
    pods, err := nr.clientset.CoreV1().Pods(ns.Name).List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        return nil, err
    }
    
    nsReport.PodCount = len(pods.Items)
    
    // 统计资源请求和限制
    for _, pod := range pods.Items {
        if pod.Status.Phase == corev1.PodRunning {
            nsReport.RunningPods++
        }
        
        for _, container := range pod.Spec.Containers {
            // CPU Request
            if cpu, ok := container.Resources.Requests[corev1.ResourceCPU]; ok {
                nsReport.CPURequest += cpu.MilliValue()
            }
            
            // CPU Limit
            if cpu, ok := container.Resources.Limits[corev1.ResourceCPU]; ok {
                nsReport.CPULimit += cpu.MilliValue()
            }
            
            // Memory Request
            if mem, ok := container.Resources.Requests[corev1.ResourceMemory]; ok {
                nsReport.MemoryRequest += mem.Value()
            }
            
            // Memory Limit
            if mem, ok := container.Resources.Limits[corev1.ResourceMemory]; ok {
                nsReport.MemoryLimit += mem.Value()
            }
        }
    }
    
    // 获取实际使用量(从 metrics-server)
    podMetrics, err := nr.metricsClient.MetricsV1beta1().PodMetricses(ns.Name).List(
        context.TODO(),
        metav1.ListOptions{},
    )
    if err == nil {
        for _, pm := range podMetrics.Items {
            for _, container := range pm.Containers {
                nsReport.CPUUsage += container.Usage.Cpu().MilliValue()
                nsReport.MemoryUsage += container.Usage.Memory().Value()
            }
        }
    }
    
    // 获取 Services
    services, _ := nr.clientset.CoreV1().Services(ns.Name).List(context.TODO(), metav1.ListOptions{})
    nsReport.ServiceCount = len(services.Items)
    
    // 获取 ConfigMaps
    configMaps, _ := nr.clientset.CoreV1().ConfigMaps(ns.Name).List(context.TODO(), metav1.ListOptions{})
    nsReport.ConfigMapCount = len(configMaps.Items)
    
    // 获取 Secrets
    secrets, _ := nr.clientset.CoreV1().Secrets(ns.Name).List(context.TODO(), metav1.ListOptions{})
    nsReport.SecretCount = len(secrets.Items)
    
    // 获取 PVCs
    pvcs, _ := nr.clientset.CoreV1().PersistentVolumeClaims(ns.Name).List(context.TODO(), metav1.ListOptions{})
    nsReport.PVCCount = len(pvcs.Items)
    
    for _, pvc := range pvcs.Items {
        if storage, ok := pvc.Status.Capacity[corev1.ResourceStorage]; ok {
            nsReport.StorageUsage += storage.Value()
        }
    }
    
    // 获取资源配额
    quotas, _ := nr.clientset.CoreV1().ResourceQuotas(ns.Name).List(context.TODO(), metav1.ListOptions{})
    if len(quotas.Items) > 0 {
        quota := &quotas.Items[0]
        nsReport.ResourceQuota = &ResourceQuotaInfo{
            Name: quota.Name,
        }
        
        if cpu, ok := quota.Spec.Hard[corev1.ResourceRequestsCPU]; ok {
            nsReport.ResourceQuota.CPUHard = cpu.MilliValue()
        }
        if cpu, ok := quota.Status.Used[corev1.ResourceRequestsCPU]; ok {
            nsReport.ResourceQuota.CPUUsed = cpu.MilliValue()
        }
        if mem, ok := quota.Spec.Hard[corev1.ResourceRequestsMemory]; ok {
            nsReport.ResourceQuota.MemoryHard = mem.Value()
        }
        if mem, ok := quota.Status.Used[corev1.ResourceRequestsMemory]; ok {
            nsReport.ResourceQuota.MemoryUsed = mem.Value()
        }
        if pods, ok := quota.Spec.Hard[corev1.ResourcePods]; ok {
            nsReport.ResourceQuota.PodsHard = int(pods.Value())
        }
        if pods, ok := quota.Status.Used[corev1.ResourcePods]; ok {
            nsReport.ResourceQuota.PodsUsed = int(pods.Value())
        }
    }
    
    // 估算成本(简化计算)
    nsReport.EstimatedCost = nr.calculateCost(nsReport)
    
    return nsReport, nil
}

func (nr *NamespaceReporter) calculateCost(nsReport *NamespaceReport) float64 {
    // 简化的成本计算
    // CPU: $0.03 per core per hour
    // Memory: $0.004 per GB per hour
    // Storage: $0.10 per GB per month
    
    cpuCores := float64(nsReport.CPURequest) / 1000.0
    memoryGB := float64(nsReport.MemoryRequest) / (1024 * 1024 * 1024)
    storageGB := float64(nsReport.StorageUsage) / (1024 * 1024 * 1024)
    
    // 月成本
    cpuCost := cpuCores * 0.03 * 24 * 30
    memoryCost := memoryGB * 0.004 * 24 * 30
    storageCost := storageGB * 0.10
    
    return cpuCost + memoryCost + storageCost
}

pkg/reporter/exporter.go

package reporter

import (
    "encoding/csv"
    "encoding/json"
    "fmt"
    "html/template"
    "os"
    
    "github.com/xuri/excelize/v2"
)

func (nr *NamespaceReporter) ExportReport(report *Report, format, outputPath string) error {
    switch format {
    case "html":
        return nr.exportHTML(report, outputPath)
    case "json":
        return nr.exportJSON(report, outputPath)
    case "csv":
        return nr.exportCSV(report, outputPath)
    case "excel":
        return nr.exportExcel(report, outputPath)
    default:
        return fmt.Errorf("unsupported format: %s", format)
    }
}

func (nr *NamespaceReporter) exportHTML(report *Report, outputPath string) error {
    tmpl := `
<!DOCTYPE html>
<html>
<head>
    <title>Kubernetes Namespace Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        h1 { color: #326CE5; }
        table { border-collapse: collapse; width: 100%; margin: 20px 0; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #326CE5; color: white; }
        tr:nth-child(even) { background-color: #f2f2f2; }
        .summary { background-color: #e8f4f8; padding: 15px; margin: 20px 0; }
    </style>
</head>
<body>
    <h1>Kubernetes Namespace Resource Report</h1>
    <p>Generated at: {{.GeneratedAt.Format "2006-01-02 15:04:05"}}</p>
    <p>Cluster: {{.ClusterName}}</p>
    
    <div class="summary">
        <h2>Summary</h2>
        <p>Total Namespaces: {{.TotalNamespaces}}</p>
        <p>Total CPU Request: {{divide .Summary.TotalCPURequest 1000}} cores</p>
        <p>Total CPU Usage: {{divide .Summary.TotalCPUUsage 1000}} cores</p>
        <p>Total Memory Request: {{bytesToGB .Summary.TotalMemoryRequest}} GB</p>
        <p>Total Memory Usage: {{bytesToGB .Summary.TotalMemoryUsage}} GB</p>
        <p>Total Storage: {{bytesToGB .Summary.TotalStorageUsage}} GB</p>
        <p>Total Pods: {{.Summary.TotalPods}}</p>
        <p>Total Estimated Cost: ${{printf "%.2f" .Summary.TotalEstimatedCost}}/month</p>
    </div>
    
    <h2>Namespace Details</h2>
    <table>
        <tr>
            <th>Namespace</th>
            <th>CPU Request (cores)</th>
            <th>CPU Usage (cores)</th>
            <th>Memory Request (GB)</th>
            <th>Memory Usage (GB)</th>
            <th>Storage (GB)</th>
            <th>Pods</th>
            <th>Estimated Cost</th>
        </tr>
        {{range .NamespaceReports}}
        <tr>
            <td>{{.Name}}</td>
            <td>{{divide .CPURequest 1000}}</td>
            <td>{{divide .CPUUsage 1000}}</td>
            <td>{{bytesToGB .MemoryRequest}}</td>
            <td>{{bytesToGB .MemoryUsage}}</td>
            <td>{{bytesToGB .StorageUsage}}</td>
            <td>{{.PodCount}}</td>
            <td>${{printf "%.2f" .EstimatedCost}}</td>
        </tr>
        {{end}}
    </table>
</body>
</html>
`
    
    funcMap := template.FuncMap{
        "divide": func(a, b int64) float64 {
            return float64(a) / float64(b)
        },
        "bytesToGB": func(bytes int64) float64 {
            return float64(bytes) / (1024 * 1024 * 1024)
        },
    }
    
    t, err := template.New("report").Funcs(funcMap).Parse(tmpl)
    if err != nil {
        return err
    }
    
    f, err := os.Create(outputPath)
    if err != nil {
        return err
    }
    defer f.Close()
    
    return t.Execute(f, report)
}

func (nr *NamespaceReporter) exportJSON(report *Report, outputPath string) error {
    f, err := os.Create(outputPath)
    if err != nil {
        return err
    }
    defer f.Close()
    
    encoder := json.NewEncoder(f)
    encoder.SetIndent("", "  ")
    
    return encoder.Encode(report)
}

func (nr *NamespaceReporter) exportCSV(report *Report, outputPath string) error {
    f, err := os.Create(outputPath)
    if err != nil {
        return err
    }
    defer f.Close()
    
    writer := csv.NewWriter(f)
    defer writer.Flush()
    
    // Header
    writer.Write([]string{
        "Namespace",
        "CPU Request (cores)",
        "CPU Usage (cores)",
        "Memory Request (GB)",
        "Memory Usage (GB)",
        "Storage (GB)",
        "Pods",
        "Estimated Cost ($)",
    })
    
    // Data
    for _, ns := range report.NamespaceReports {
        writer.Write([]string{
            ns.Name,
            fmt.Sprintf("%.2f", float64(ns.CPURequest)/1000),
            fmt.Sprintf("%.2f", float64(ns.CPUUsage)/1000),
            fmt.Sprintf("%.2f", float64(ns.MemoryRequest)/(1024*1024*1024)),
            fmt.Sprintf("%.2f", float64(ns.MemoryUsage)/(1024*1024*1024)),
            fmt.Sprintf("%.2f", float64(ns.StorageUsage)/(1024*1024*1024)),
            fmt.Sprintf("%d", ns.PodCount),
            fmt.Sprintf("%.2f", ns.EstimatedCost),
        })
    }
    
    return nil
}

func (nr *NamespaceReporter) exportExcel(report *Report, outputPath string) error {
    f := excelize.NewFile()
    defer f.Close()
    
    sheetName := "Resource Report"
    index, _ := f.NewSheet(sheetName)
    
    // Header
    headers := []string{"Namespace", "CPU Request", "CPU Usage", "Memory Request", "Memory Usage", "Storage", "Pods", "Cost"}
    for i, h := range headers {
        cell := fmt.Sprintf("%c1", 'A'+i)
        f.SetCellValue(sheetName, cell, h)
    }
    
    // Data
    for i, ns := range report.NamespaceReports {
        row := i + 2
        f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), ns.Name)
        f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), float64(ns.CPURequest)/1000)
        f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), float64(ns.CPUUsage)/1000)
        f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), float64(ns.MemoryRequest)/(1024*1024*1024))
        f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), float64(ns.MemoryUsage)/(1024*1024*1024))
        f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), float64(ns.StorageUsage)/(1024*1024*1024))
        f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), ns.PodCount)
        f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), ns.EstimatedCost)
    }
    
    f.SetActiveSheet(index)
    
    return f.SaveAs(outputPath)
}

Python 实现

namespace_reporter.py

#!/usr/bin/env python3

import argparse
import json
import csv
from datetime import datetime
from kubernetes import client, config
from jinja2 import Template
import openpyxl
from openpyxl.styles import Font, PatternFill

class NamespaceReporter:
    def __init__(self, cluster_name='default'):
        # 加载 Kubernetes 配置
        try:
            config.load_incluster_config()
        except:
            config.load_kube_config()
        
        self.v1 = client.CoreV1Api()
        self.metrics_api = client.CustomObjectsApi()
        self.cluster_name = cluster_name
    
    def generate_report(self):
        """生成报表"""
        print("Generating namespace report...")
        
        # 获取所有命名空间
        namespaces = self.v1.list_namespace()
        
        report = {
            'generated_at': datetime.now(),
            'cluster_name': self.cluster_name,
            'total_namespaces': len(namespaces.items),
            'namespace_reports': [],
            'summary': {
                'total_cpu_request': 0,
                'total_cpu_usage': 0,
                'total_memory_request': 0,
                'total_memory_usage': 0,
                'total_storage': 0,
                'total_pods': 0,
                'total_cost': 0
            }
        }
        
        # 生成每个命名空间的报表
        for ns in namespaces.items:
            if self.should_skip_namespace(ns.metadata.name):
                continue
            
            ns_report = self.generate_namespace_report(ns)
            report['namespace_reports'].append(ns_report)
            
            # 更新汇总
            report['summary']['total_cpu_request'] += ns_report['cpu_request']
            report['summary']['total_cpu_usage'] += ns_report['cpu_usage']
            report['summary']['total_memory_request'] += ns_report['memory_request']
            report['summary']['total_memory_usage'] += ns_report['memory_usage']
            report['summary']['total_storage'] += ns_report['storage_usage']
            report['summary']['total_pods'] += ns_report['pod_count']
            report['summary']['total_cost'] += ns_report['estimated_cost']
        
        # 按 CPU 使用量排序
        report['namespace_reports'].sort(
            key=lambda x: x['cpu_usage'],
            reverse=True
        )
        
        print(f"Report generated for {len(report['namespace_reports'])} namespaces")
        
        return report
    
    def should_skip_namespace(self, namespace):
        """检查是否跳过命名空间"""
        skip_namespaces = ['kube-system', 'kube-public', 'kube-node-lease']
        return namespace in skip_namespaces
    
    def generate_namespace_report(self, ns):
        """生成单个命名空间报表"""
        ns_name = ns.metadata.name
        
        report = {
            'name': ns_name,
            'creation_timestamp': ns.metadata.creation_timestamp,
            'cpu_request': 0,
            'cpu_limit': 0,
            'cpu_usage': 0,
            'memory_request': 0,
            'memory_limit': 0,
            'memory_usage': 0,
            'storage_usage': 0,
            'pod_count': 0,
            'running_pods': 0,
            'service_count': 0,
            'configmap_count': 0,
            'secret_count': 0,
            'pvc_count': 0,
            'estimated_cost': 0
        }
        
        # 获取 Pods
        try:
            pods = self.v1.list_namespaced_pod(ns_name)
            report['pod_count'] = len(pods.items)
            
            for pod in pods.items:
                if pod.status.phase == 'Running':
                    report['running_pods'] += 1
                
                for container in pod.spec.containers:
                    if container.resources.requests:
                        if 'cpu' in container.resources.requests:
                            report['cpu_request'] += self.parse_cpu(
                                container.resources.requests['cpu']
                            )
                        if 'memory' in container.resources.requests:
                            report['memory_request'] += self.parse_memory(
                                container.resources.requests['memory']
                            )
                    
                    if container.resources.limits:
                        if 'cpu' in container.resources.limits:
                            report['cpu_limit'] += self.parse_cpu(
                                container.resources.limits['cpu']
                            )
                        if 'memory' in container.resources.limits:
                            report['memory_limit'] += self.parse_memory(
                                container.resources.limits['memory']
                            )
        except Exception as e:
            print(f"Error getting pods for {ns_name}: {e}")
        
        # 获取实际使用量
        try:
            pod_metrics = self.metrics_api.list_namespaced_custom_object(
                group="metrics.k8s.io",
                version="v1beta1",
                namespace=ns_name,
                plural="pods"
            )
            
            for pm in pod_metrics.get('items', []):
                for container in pm.get('containers', []):
                    usage = container.get('usage', {})
                    if 'cpu' in usage:
                        report['cpu_usage'] += self.parse_cpu(usage['cpu'])
                    if 'memory' in usage:
                        report['memory_usage'] += self.parse_memory(usage['memory'])
        except Exception as e:
            print(f"Error getting metrics for {ns_name}: {e}")
        
        # 获取其他资源
        try:
            services = self.v1.list_namespaced_service(ns_name)
            report['service_count'] = len(services.items)
        except:
            pass
        
        try:
            configmaps = self.v1.list_namespaced_config_map(ns_name)
            report['configmap_count'] = len(configmaps.items)
        except:
            pass
        
        try:
            secrets = self.v1.list_namespaced_secret(ns_name)
            report['secret_count'] = len(secrets.items)
        except:
            pass
        
        try:
            pvcs = self.v1.list_namespaced_persistent_volume_claim(ns_name)
            report['pvc_count'] = len(pvcs.items)
            
            for pvc in pvcs.items:
                if pvc.status.capacity and 'storage' in pvc.status.capacity:
                    report['storage_usage'] += self.parse_memory(
                        pvc.status.capacity['storage']
                    )
        except:
            pass
        
        # 计算成本
        report['estimated_cost'] = self.calculate_cost(report)
        
        return report
    
    def parse_cpu(self, cpu_str):
        """解析 CPU(转换为 millicores)"""
        if cpu_str.endswith('m'):
            return int(cpu_str[:-1])
        elif cpu_str.endswith('n'):
            return int(cpu_str[:-1]) // 1000000
        else:
            return int(float(cpu_str) * 1000)
    
    def parse_memory(self, mem_str):
        """解析内存(转换为 bytes)"""
        units = {
            'Ki': 1024, 'Mi': 1024**2, 'Gi': 1024**3, 'Ti': 1024**4,
            'K': 1000, 'M': 1000**2, 'G': 1000**3, 'T': 1000**4
        }
        
        for unit, multiplier in units.items():
            if mem_str.endswith(unit):
                return int(mem_str[:-len(unit)]) * multiplier
        
        return int(mem_str)
    
    def calculate_cost(self, report):
        """计算成本"""
        cpu_cores = report['cpu_request'] / 1000.0
        memory_gb = report['memory_request'] / (1024**3)
        storage_gb = report['storage_usage'] / (1024**3)
        
        # 月成本
        cpu_cost = cpu_cores * 0.03 * 24 * 30
        memory_cost = memory_gb * 0.004 * 24 * 30
        storage_cost = storage_gb * 0.10
        
        return cpu_cost + memory_cost + storage_cost
    
    def export_html(self, report, output_path):
        """导出 HTML"""
        template = Template('''
<!DOCTYPE html>
<html>
<head>
    <title>Kubernetes Namespace Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        h1 { color: #326CE5; }
        table { border-collapse: collapse; width: 100%; margin: 20px 0; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #326CE5; color: white; }
        tr:nth-child(even) { background-color: #f2f2f2; }
        .summary { background-color: #e8f4f8; padding: 15px; margin: 20px 0; }
    </style>
</head>
<body>
    <h1>Kubernetes Namespace Resource Report</h1>
    <p>Generated at: {{ report.generated_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
    <p>Cluster: {{ report.cluster_name }}</p>
    
    <div class="summary">
        <h2>Summary</h2>
        <p>Total Namespaces: {{ report.total_namespaces }}</p>
        <p>Total CPU Request: {{ "%.2f"|format(report.summary.total_cpu_request / 1000) }} cores</p>
        <p>Total CPU Usage: {{ "%.2f"|format(report.summary.total_cpu_usage / 1000) }} cores</p>
        <p>Total Memory Request: {{ "%.2f"|format(report.summary.total_memory_request / (1024**3)) }} GB</p>
        <p>Total Memory Usage: {{ "%.2f"|format(report.summary.total_memory_usage / (1024**3)) }} GB</p>
        <p>Total Storage: {{ "%.2f"|format(report.summary.total_storage / (1024**3)) }} GB</p>
        <p>Total Pods: {{ report.summary.total_pods }}</p>
        <p>Total Estimated Cost: ${{ "%.2f"|format(report.summary.total_cost) }}/month</p>
    </div>
    
    <h2>Namespace Details</h2>
    <table>
        <tr>
            <th>Namespace</th>
            <th>CPU Request (cores)</th>
            <th>CPU Usage (cores)</th>
            <th>Memory Request (GB)</th>
            <th>Memory Usage (GB)</th>
            <th>Storage (GB)</th>
            <th>Pods</th>
            <th>Estimated Cost</th>
        </tr>
        {% for ns in report.namespace_reports %}
        <tr>
            <td>{{ ns.name }}</td>
            <td>{{ "%.2f"|format(ns.cpu_request / 1000) }}</td>
            <td>{{ "%.2f"|format(ns.cpu_usage / 1000) }}</td>
            <td>{{ "%.2f"|format(ns.memory_request / (1024**3)) }}</td>
            <td>{{ "%.2f"|format(ns.memory_usage / (1024**3)) }}</td>
            <td>{{ "%.2f"|format(ns.storage_usage / (1024**3)) }}</td>
            <td>{{ ns.pod_count }}</td>
            <td>${{ "%.2f"|format(ns.estimated_cost) }}</td>
        </tr>
        {% endfor %}
    </table>
</body>
</html>
''')
        
        with open(output_path, 'w') as f:
            f.write(template.render(report=report))
    
    def export_json(self, report, output_path):
        """导出 JSON"""
        # 转换 datetime 为字符串
        report_copy = report.copy()
        report_copy['generated_at'] = report['generated_at'].isoformat()
        
        for ns in report_copy['namespace_reports']:
            if 'creation_timestamp' in ns and ns['creation_timestamp']:
                ns['creation_timestamp'] = ns['creation_timestamp'].isoformat()
        
        with open(output_path, 'w') as f:
            json.dump(report_copy, f, indent=2)
    
    def export_csv(self, report, output_path):
        """导出 CSV"""
        with open(output_path, 'w', newline='') as f:
            writer = csv.writer(f)
            
            # Header
            writer.writerow([
                'Namespace', 'CPU Request (cores)', 'CPU Usage (cores)',
                'Memory Request (GB)', 'Memory Usage (GB)', 'Storage (GB)',
                'Pods', 'Estimated Cost ($)'
            ])
            
            # Data
            for ns in report['namespace_reports']:
                writer.writerow([
                    ns['name'],
                    f"{ns['cpu_request']/1000:.2f}",
                    f"{ns['cpu_usage']/1000:.2f}",
                    f"{ns['memory_request']/(1024**3):.2f}",
                    f"{ns['memory_usage']/(1024**3):.2f}",
                    f"{ns['storage_usage']/(1024**3):.2f}",
                    ns['pod_count'],
                    f"{ns['estimated_cost']:.2f}"
                ])
    
    def export_excel(self, report, output_path):
        """导出 Excel"""
        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "Resource Report"
        
        # Header
        headers = [
            'Namespace', 'CPU Request', 'CPU Usage', 'Memory Request',
            'Memory Usage', 'Storage', 'Pods', 'Cost'
        ]
        
        for col, header in enumerate(headers, 1):
            cell = ws.cell(row=1, column=col, value=header)
            cell.font = Font(bold=True)
            cell.fill = PatternFill(start_color="326CE5", end_color="326CE5", fill_type="solid")
        
        # Data
        for row, ns in enumerate(report['namespace_reports'], 2):
            ws.cell(row=row, column=1, value=ns['name'])
            ws.cell(row=row, column=2, value=ns['cpu_request']/1000)
            ws.cell(row=row, column=3, value=ns['cpu_usage']/1000)
            ws.cell(row=row, column=4, value=ns['memory_request']/(1024**3))
            ws.cell(row=row, column=5, value=ns['memory_usage']/(1024**3))
            ws.cell(row=row, column=6, value=ns['storage_usage']/(1024**3))
            ws.cell(row=row, column=7, value=ns['pod_count'])
            ws.cell(row=row, column=8, value=ns['estimated_cost'])
        
        wb.save(output_path)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Kubernetes Namespace Reporter')
    parser.add_argument('--format', default='html', choices=['html', 'json', 'csv', 'excel'],
                        help='Output format')
    parser.add_argument('--output', default='report.html', help='Output file path')
    parser.add_argument('--cluster-name', default='default', help='Cluster name')
    
    args = parser.parse_args()
    
    reporter = NamespaceReporter(cluster_name=args.cluster_name)
    report = reporter.generate_report()
    
    if args.format == 'html':
        reporter.export_html(report, args.output)
    elif args.format == 'json':
        reporter.export_json(report, args.output)
    elif args.format == 'csv':
        reporter.export_csv(report, args.output)
    elif args.format == 'excel':
        reporter.export_excel(report, args.output)
    
    print(f"Report exported to {args.output}")

使用示例

生成 HTML 报表

# Go
./namespace-reporter --format=html --output=report.html

# Python
python3 namespace_reporter.py --format=html --output=report.html

生成 Excel 报表

# Go
./namespace-reporter --format=excel --output=report.xlsx

# Python
python3 namespace_reporter.py --format=excel --output=report.xlsx

定时生成报表(CronJob)

apiVersion: batch/v1
kind: CronJob
metadata:
  name: namespace-reporter
  namespace: kube-system
spec:
  schedule: "0 0 * * *"  # 每天凌晨
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: namespace-reporter
          containers:
          - name: reporter
            image: your-registry/namespace-reporter:latest
            args:
            - --format=html
            - --output=/reports/report-$(date +%Y%m%d).html
            volumeMounts:
            - name: reports
              mountPath: /reports
          restartPolicy: OnFailure
          volumes:
          - name: reports
            persistentVolumeClaim:
              claimName: reports-pvc

总结

功能特性

多维度统计: CPU、内存、存储、Pod 数量
成本估算: 基于资源使用的成本计算
多种格式: HTML、JSON、CSV、Excel
定时生成: 支持 CronJob 定时运行
可视化: 美观的 HTML 报表

扩展方向

  1. 趋势分析: 对比历史数据
  2. 异常检测: 识别资源使用异常
  3. 邮件推送: 自动发送报表
  4. Dashboard: Grafana 可视化
  5. 成本优化建议: 基于使用情况给出优化建议

下一个项目将介绍应用健康检查自愈工具。