Popeye Cluster Cleaner in Java: Kubernetes Cluster Sanitization

Popeye is a utility that scans Kubernetes clusters and reports potential issues with deployed resources and configurations. This Java implementation provides similar functionality for sanitizing and optimizing Kubernetes clusters.

Popeye Overview

Popeye scans:

  • Cluster resources and their configurations
  • Resource utilization and limits
  • Best practices compliance
  • Security issues and misconfigurations
  • Deprecated APIs and resources

Dependencies and Setup

Maven Configuration:

<dependencies> <!-- Kubernetes Client --> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java</artifactId> <version>18.0.0</version> </dependency> <!-- YAML Processing --> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.0</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Apache Commons --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.12.0</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> </dependencies>

Configuration Management

PopeyeConfig.java:

@Configuration @ConfigurationProperties(prefix = "popeye") @Data public class PopeyeConfig { private ScanConfig scan = new ScanConfig(); private OutputConfig output = new OutputConfig(); private FilterConfig filters = new FilterConfig(); private RulesConfig rules = new RulesConfig(); @Data public static class ScanConfig { private boolean includeNamespaces = true; private boolean includeNodes = true; private boolean includePods = true; private boolean includeServices = true; private boolean includeDeployments = true; private boolean includeStatefulSets = true; private boolean includeDaemonSets = true; private boolean includeJobs = true; private boolean includeCronJobs = true; private boolean includeConfigMaps = true; private boolean includeSecrets = true; private boolean includePersistentVolumes = true; private boolean includePersistentVolumeClaims = true; private boolean includeStorageClasses = true; private boolean includeServiceAccounts = true; private boolean includeRoles = true; private boolean includeRoleBindings = true; private boolean includeClusterRoles = true; private boolean includeClusterRoleBindings = true; private boolean includeNetworkPolicies = true; private boolean includeIngresses = true; private int timeoutSeconds = 300; private int maxConcurrentScans = 10; } @Data public static class OutputConfig { private OutputFormat format = OutputFormat.TEXT; private boolean colorOutput = true; private boolean saveReport = true; private String reportPath = "./popeye-reports"; private boolean verbose = false; private int outputWidth = 120; } @Data public static class FilterConfig { private List<String> excludedNamespaces = Arrays.asList("kube-system", "kube-public"); private List<String> includedNamespaces = new ArrayList<>(); private Map<String, String> labelSelectors = new HashMap<>(); private Map<String, String> fieldSelectors = new HashMap<>(); private int minSeverity = 1; // 1=OK, 2=INFO, 3=WARNING, 4=ERROR } @Data public static class RulesConfig { private boolean checkResourceLimits = true; private boolean checkProbes = true; private boolean checkSecurityContext = true; private boolean checkDeprecatedAPIs = true; private boolean checkPodDisruptionBudgets = true; private boolean checkNetworkPolicies = true; private boolean checkHpa = true; private boolean checkQuotas = true; private boolean checkNodeResources = true; private Map<String, Object> customRules = new HashMap<>(); } public enum OutputFormat { TEXT, JSON, YAML, HTML, XML } }

Scan Results and Issues

ScanResult.java:

@Data @Builder public class ScanResult { private String scanId; private Instant scanTimestamp; private ClusterInfo clusterInfo; private List<NamespaceResult> namespaceResults; private List<NodeResult> nodeResults; private Summary summary; private List<Recommendation> recommendations; @Data @Builder public static class ClusterInfo { private String clusterName; private String serverVersion; private int nodeCount; private int namespaceCount; private int podCount; private Map<String, String> context; } @Data @Builder public static class Summary { private int totalIssues; private int okCount; private int infoCount; private int warningCount; private int errorCount; private double score; private Map<String, Integer> issuesBySeverity; private Map<String, Integer> issuesByResource; } } @Data @Builder public class NamespaceResult { private String name; private List<ResourceIssue> issues; private List<PodResult> pods; private List<ServiceResult> services; private List<DeploymentResult> deployments; private Summary summary; private ComplianceLevel compliance; public enum ComplianceLevel { EXCELLENT, GOOD, FAIR, POOR, CRITICAL } } @Data @Builder public class ResourceIssue { private String resourceType; private String resourceName; private Severity severity; private String category; private String message; private String description; private String remediation; private List<String> references; private Map<String, Object> metadata; public enum Severity { OK(1, "✅", "OK"), INFO(2, "ℹ️", "INFO"), WARNING(3, "⚠️", "WARNING"), ERROR(4, "❌", "ERROR"); private final int level; private final String icon; private final String label; Severity(int level, String icon, String label) { this.level = level; this.icon = icon; this.label = label; } public int getLevel() { return level; } public String getIcon() { return icon; } public String getLabel() { return label; } } } @Data @Builder public class Recommendation { private String category; private String description; private Severity severity; private String resourceType; private String resourceName; private String action; private String priority; private Double estimatedEffort; // in hours }

Core Scanner Service

PopeyeScanner.java:

@Service @Slf4j public class PopeyeScanner { private final KubernetesService kubernetesService; private final PopeyeConfig config; private final List<ResourceScanner> resourceScanners; private final ReportGenerator reportGenerator; public PopeyeScanner(KubernetesService kubernetesService, PopeyeConfig config, List<ResourceScanner> resourceScanners, ReportGenerator reportGenerator) { this.kubernetesService = kubernetesService; this.config = config; this.resourceScanners = resourceScanners; this.reportGenerator = reportGenerator; } public ScanResult scanCluster() { log.info("Starting cluster scan..."); Instant startTime = Instant.now(); try { ScanResult.ScanResultBuilder resultBuilder = ScanResult.builder() .scanId(generateScanId()) .scanTimestamp(startTime); // Get cluster info ClusterInfo clusterInfo = kubernetesService.getClusterInfo(); resultBuilder.clusterInfo(clusterInfo); // Scan namespaces List<NamespaceResult> namespaceResults = scanNamespaces(); resultBuilder.namespaceResults(namespaceResults); // Scan nodes if (config.getScan().isIncludeNodes()) { List<NodeResult> nodeResults = scanNodes(); resultBuilder.nodeResults(nodeResults); } // Generate summary and recommendations ScanResult scanResult = resultBuilder.build(); ScanResult.Summary summary = generateSummary(scanResult); List<Recommendation> recommendations = generateRecommendations(scanResult); scanResult.setSummary(summary); scanResult.setRecommendations(recommendations); // Generate report if (config.getOutput().isSaveReport()) { reportGenerator.generateReport(scanResult); } long duration = Duration.between(startTime, Instant.now()).getSeconds(); log.info("Cluster scan completed in {} seconds. Score: {}/100", duration, String.format("%.1f", summary.getScore())); return scanResult; } catch (Exception e) { log.error("Cluster scan failed", e); throw new ScanException("Cluster scan failed: " + e.getMessage(), e); } } public ScanResult scanNamespace(String namespace) { log.info("Scanning namespace: {}", namespace); try { if (isNamespaceExcluded(namespace)) { log.info("Namespace {} is excluded from scanning", namespace); return ScanResult.builder() .scanId(generateScanId()) .scanTimestamp(Instant.now()) .build(); } NamespaceResult namespaceResult = scanSingleNamespace(namespace); return ScanResult.builder() .scanId(generateScanId()) .scanTimestamp(Instant.now()) .namespaceResults(List.of(namespaceResult)) .summary(generateNamespaceSummary(namespaceResult)) .build(); } catch (Exception e) { log.error("Namespace scan failed: {}", namespace, e); throw new ScanException("Namespace scan failed: " + namespace, e); } } public ComplianceReport checkCompliance(String namespace) { try { ScanResult scanResult = scanNamespace(namespace); return generateComplianceReport(scanResult); } catch (Exception e) { throw new ComplianceException("Compliance check failed", e); } } private List<NamespaceResult> scanNamespaces() { List<String> namespaces = kubernetesService.getNamespaces(); return namespaces.parallelStream() .filter(this::shouldScanNamespace) .map(this::scanSingleNamespace) .collect(Collectors.toList()); } private NamespaceResult scanSingleNamespace(String namespace) { log.debug("Scanning namespace: {}", namespace); NamespaceResult.NamespaceResultBuilder builder = NamespaceResult.builder() .name(namespace); List<ResourceIssue> allIssues = new ArrayList<>(); // Scan different resource types if (config.getScan().isIncludePods()) { List<PodResult> podResults = scanPods(namespace); builder.pods(podResults); allIssues.addAll(extractIssuesFromPods(podResults)); } if (config.getScan().isIncludeServices()) { List<ServiceResult> serviceResults = scanServices(namespace); builder.services(serviceResults); allIssues.addAll(extractIssuesFromServices(serviceResults)); } if (config.getScan().isIncludeDeployments()) { List<DeploymentResult> deploymentResults = scanDeployments(namespace); builder.deployments(deploymentResults); allIssues.addAll(extractIssuesFromDeployments(deploymentResults)); } // Add more resource scans... NamespaceResult namespaceResult = builder.build(); namespaceResult.setIssues(allIssues); namespaceResult.setSummary(generateNamespaceSummary(namespaceResult)); namespaceResult.setCompliance(calculateComplianceLevel(namespaceResult)); return namespaceResult; } private List<NodeResult> scanNodes() { return kubernetesService.getNodes().stream() .map(this::scanNode) .collect(Collectors.toList()); } private NodeResult scanNode(V1Node node) { List<ResourceIssue> issues = new ArrayList<>(); // Check node conditions checkNodeConditions(node, issues); // Check node resources checkNodeResources(node, issues); // Check node taints and tolerations checkNodeTaints(node, issues); return NodeResult.builder() .name(node.getMetadata().getName()) .issues(issues) .build(); } private List<PodResult> scanPods(String namespace) { List<V1Pod> pods = kubernetesService.getPods(namespace); return pods.stream() .map(this::scanPod) .collect(Collectors.toList()); } private PodResult scanPod(V1Pod pod) { List<ResourceIssue> issues = new ArrayList<>(); // Check pod spec checkPodSpec(pod, issues); // Check containers checkContainers(pod, issues); // Check security context checkSecurityContext(pod, issues); // Check resource limits checkResourceLimits(pod, issues); return PodResult.builder() .name(pod.getMetadata().getName()) .namespace(pod.getMetadata().getNamespace()) .issues(issues) .build(); } private void checkPodSpec(V1Pod pod, List<ResourceIssue> issues) { V1PodSpec spec = pod.getSpec(); // Check restart policy if ("Always".equals(spec.getRestartPolicy()) && spec.getContainers().size() > 1) { issues.add(createIssue("Pod", pod.getMetadata().getName(), ResourceIssue.Severity.INFO, "Restart Policy", "Consider using 'OnFailure' restart policy for multi-container pods", "Multi-container pods with 'Always' restart policy may cause unnecessary restarts")); } // Check service account if (spec.getServiceAccountName() == null || "default".equals(spec.getServiceAccountName())) { issues.add(createIssue("Pod", pod.getMetadata().getName(), ResourceIssue.Severity.WARNING, "Service Account", "Pod using default service account", "Create and use dedicated service accounts for better security")); } // Check automount service account token if (spec.getAutomountServiceAccountToken() == null || Boolean.TRUE.equals(spec.getAutomountServiceAccountToken())) { issues.add(createIssue("Pod", pod.getMetadata().getName(), ResourceIssue.Severity.WARNING, "Security", "Service account token automatically mounted", "Set automountServiceAccountToken: false if not needed")); } } private void checkContainers(V1Pod pod, List<ResourceIssue> issues) { List<V1Container> containers = pod.getSpec().getContainers(); for (V1Container container : containers) { // Check image tag checkImageTag(container, pod, issues); // Check probes checkProbes(container, pod, issues); // Check security context checkContainerSecurityContext(container, pod, issues); // Check resources checkContainerResources(container, pod, issues); } } private void checkImageTag(V1Container container, V1Pod pod, List<ResourceIssue> issues) { String image = container.getImage(); if (image == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.ERROR, "Image", "Container image not specified", "Specify a container image")); return; } // Check for latest tag if (image.endsWith(":latest") || !image.contains(":")) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Image", "Container using 'latest' tag or no tag", "Use specific image tags for better version control")); } // Check image repository if (!image.contains("/")) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Image", "Container using Docker Hub official image", "Consider using images from trusted repositories")); } } private void checkProbes(V1Container container, V1Pod pod, List<ResourceIssue> issues) { if (container.getLivenessProbe() == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Health Checks", "Liveness probe not configured", "Configure liveness probe to detect and restart unhealthy containers")); } if (container.getReadinessProbe() == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Health Checks", "Readiness probe not configured", "Configure readiness probe to manage traffic during startup")); } // Check probe configurations if (container.getLivenessProbe() != null) { checkProbeConfiguration(container.getLivenessProbe(), "liveness", container, pod, issues); } if (container.getReadinessProbe() != null) { checkProbeConfiguration(container.getReadinessProbe(), "readiness", container, pod, issues); } } private void checkProbeConfiguration(V1Probe probe, String probeType, V1Container container, V1Pod pod, List<ResourceIssue> issues) { if (probe.getInitialDelaySeconds() == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Health Checks", probeType + " probe initial delay not set", "Set initialDelaySeconds to avoid false positives during container startup")); } if (probe.getPeriodSeconds() != null && probe.getPeriodSeconds() > 30) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Health Checks", probeType + " probe period is long (" + probe.getPeriodSeconds() + "s)", "Consider shorter period for faster failure detection")); } } private void checkContainerSecurityContext(V1Container container, V1Pod pod, List<ResourceIssue> issues) { V1SecurityContext securityContext = container.getSecurityContext(); if (securityContext == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Security", "Security context not configured", "Configure security context with least privileges")); return; } // Check runAsNonRoot if (securityContext.getRunAsNonRoot() == null || !securityContext.getRunAsNonRoot()) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Security", "Container may run as root", "Set runAsNonRoot: true or specify runAsUser")); } // Check readOnlyRootFilesystem if (securityContext.getReadOnlyRootFilesystem() == null || !securityContext.getReadOnlyRootFilesystem()) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Security", "Container has writable root filesystem", "Set readOnlyRootFilesystem: true if possible")); } // Check privilege escalation if (securityContext.getAllowPrivilegeEscalation() == null || securityContext.getAllowPrivilegeEscalation()) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Security", "Container allows privilege escalation", "Set allowPrivilegeEscalation: false")); } } private void checkContainerResources(V1Container container, V1Pod pod, List<ResourceIssue> issues) { V1ResourceRequirements resources = container.getResources(); if (resources == null) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Resources", "Resource limits and requests not configured", "Configure resource limits and requests for better scheduling and stability")); return; } // Check limits if (resources.getLimits() == null || resources.getLimits().isEmpty()) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Resources", "Resource limits not configured", "Configure resource limits to prevent resource exhaustion")); } // Check requests if (resources.getRequests() == null || resources.getRequests().isEmpty()) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Resources", "Resource requests not configured", "Configure resource requests for proper scheduling")); } // Check CPU and memory values checkResourceValues(resources, container, issues); } private void checkResourceValues(V1ResourceRequirements resources, V1Container container, List<ResourceIssue> issues) { Map<String, Quantity> limits = resources.getLimits(); Map<String, Quantity> requests = resources.getRequests(); if (limits != null) { Quantity cpuLimit = limits.get("cpu"); Quantity memoryLimit = limits.get("memory"); if (cpuLimit != null) { checkCpuValue(cpuLimit, "limit", container, issues); } if (memoryLimit != null) { checkMemoryValue(memoryLimit, "limit", container, issues); } } if (requests != null) { Quantity cpuRequest = requests.get("cpu"); Quantity memoryRequest = requests.get("memory"); if (cpuRequest != null) { checkCpuValue(cpuRequest, "request", container, issues); } if (memoryRequest != null) { checkMemoryValue(memoryRequest, "request", container, issues); } } // Check limits vs requests checkLimitsVsRequests(limits, requests, container, issues); } private void checkCpuValue(Quantity quantity, String type, V1Container container, List<ResourceIssue> issues) { try { BigDecimal cpu = new BigDecimal(quantity.getAmount()); if (cpu.compareTo(BigDecimal.ZERO) == 0) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Resources", "CPU " + type + " is zero", "Set appropriate CPU " + type + " for better performance")); } if (cpu.compareTo(new BigDecimal("8")) > 0) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Resources", "CPU " + type + " is high: " + cpu + " cores", "Consider optimizing application or splitting workload")); } } catch (Exception e) { log.warn("Failed to parse CPU quantity: {}", quantity, e); } } private void checkMemoryValue(Quantity quantity, String type, V1Container container, List<ResourceIssue> issues) { try { // Convert to megabytes for easier analysis long memoryMB = convertToMB(quantity); if (memoryMB == 0) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.WARNING, "Resources", "Memory " + type + " is zero", "Set appropriate memory " + type + " for better performance")); } if (memoryMB > 16384) { // 16GB issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Resources", "Memory " + type + " is high: " + memoryMB + "MB", "Consider optimizing application memory usage")); } } catch (Exception e) { log.warn("Failed to parse memory quantity: {}", quantity, e); } } private void checkLimitsVsRequests(Map<String, Quantity> limits, Map<String, Quantity> requests, V1Container container, List<ResourceIssue> issues) { if (limits == null || requests == null) return; Quantity cpuLimit = limits.get("cpu"); Quantity cpuRequest = requests.get("cpu"); Quantity memoryLimit = limits.get("memory"); Quantity memoryRequest = requests.get("memory"); if (cpuLimit != null && cpuRequest != null) { try { BigDecimal limit = new BigDecimal(cpuLimit.getAmount()); BigDecimal request = new BigDecimal(cpuRequest.getAmount()); if (limit.compareTo(request) == 0) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Resources", "CPU limit equals request", "Consider setting higher limits for burst capacity")); } } catch (Exception e) { log.warn("Failed to compare CPU values", e); } } if (memoryLimit != null && memoryRequest != null) { try { long limitMB = convertToMB(memoryLimit); long requestMB = convertToMB(memoryRequest); if (limitMB == requestMB) { issues.add(createIssue("Container", container.getName(), ResourceIssue.Severity.INFO, "Resources", "Memory limit equals request", "Consider setting higher limits for memory spikes")); } } catch (Exception e) { log.warn("Failed to compare memory values", e); } } } private long convertToMB(Quantity quantity) { String amount = quantity.getAmount(); String format = quantity.getFormat(); if (format == null) { format = quantity.getAmount().endsWith("Gi") ? "Gi" : quantity.getAmount().endsWith("Mi") ? "Mi" : ""; } try { BigDecimal value = new BigDecimal(amount.replaceAll("[^0-9.]", "")); switch (format) { case "Gi": return value.multiply(new BigDecimal("1024")).longValue(); case "Mi": return value.longValue(); default: // Assume bytes return value.divide(new BigDecimal("1048576"), 0, RoundingMode.HALF_UP).longValue(); } } catch (Exception e) { log.warn("Failed to convert quantity to MB: {}", quantity, e); return 0; } } private ResourceIssue createIssue(String resourceType, String resourceName, ResourceIssue.Severity severity, String category, String message, String remediation) { return ResourceIssue.builder() .resourceType(resourceType) .resourceName(resourceName) .severity(severity) .category(category) .message(message) .remediation(remediation) .build(); } private boolean shouldScanNamespace(String namespace) { if (isNamespaceExcluded(namespace)) { return false; } if (!config.getFilters().getIncludedNamespaces().isEmpty() && !config.getFilters().getIncludedNamespaces().contains(namespace)) { return false; } return true; } private boolean isNamespaceExcluded(String namespace) { return config.getFilters().getExcludedNamespaces().contains(namespace); } private ScanResult.Summary generateSummary(ScanResult scanResult) { Map<String, Integer> issuesBySeverity = new HashMap<>(); Map<String, Integer> issuesByResource = new HashMap<>(); int totalIssues = 0; int okCount = 0; int infoCount = 0; int warningCount = 0; int errorCount = 0; // Aggregate issues from all namespaces and nodes if (scanResult.getNamespaceResults() != null) { for (NamespaceResult nsResult : scanResult.getNamespaceResults()) { if (nsResult.getIssues() != null) { for (ResourceIssue issue : nsResult.getIssues()) { totalIssues++; issuesBySeverity.merge(issue.getSeverity().getLabel(), 1, Integer::sum); issuesByResource.merge(issue.getResourceType(), 1, Integer::sum); switch (issue.getSeverity()) { case OK: okCount++; break; case INFO: infoCount++; break; case WARNING: warningCount++; break; case ERROR: errorCount++; break; } } } } } // Calculate score (0-100, higher is better) double score = calculateScore(totalIssues, errorCount, warningCount); return ScanResult.Summary.builder() .totalIssues(totalIssues) .okCount(okCount) .infoCount(infoCount) .warningCount(warningCount) .errorCount(errorCount) .score(score) .issuesBySeverity(issuesBySeverity) .issuesByResource(issuesByResource) .build(); } private double calculateScore(int totalIssues, int errorCount, int warningCount) { if (totalIssues == 0) return 100.0; double errorPenalty = errorCount * 10.0; double warningPenalty = warningCount * 3.0; double totalPenalty = errorPenalty + warningPenalty; return Math.max(0.0, 100.0 - totalPenalty); } private List<Recommendation> generateRecommendations(ScanResult scanResult) { List<Recommendation> recommendations = new ArrayList<>(); // Generate recommendations based on issues if (scanResult.getNamespaceResults() != null) { for (NamespaceResult nsResult : scanResult.getNamespaceResults()) { if (nsResult.getIssues() != null) { for (ResourceIssue issue : nsResult.getIssues()) { if (issue.getSeverity().getLevel() >= ResourceIssue.Severity.WARNING.getLevel()) { recommendations.add(createRecommendation(issue)); } } } } } // Sort by severity and priority return recommendations.stream() .sorted(Comparator.comparing((Recommendation r) -> r.getSeverity().getLevel()) .reversed() .thenComparing(Recommendation::getPriority)) .collect(Collectors.toList()); } private Recommendation createRecommendation(ResourceIssue issue) { return Recommendation.builder() .category(issue.getCategory()) .description(issue.getMessage()) .severity(issue.getSeverity()) .resourceType(issue.getResourceType()) .resourceName(issue.getResourceName()) .action(issue.getRemediation()) .priority(issue.getSeverity().getLevel() >= ResourceIssue.Severity.ERROR.getLevel() ? "HIGH" : "MEDIUM") .estimatedEffort(estimateEffort(issue)) .build(); } private Double estimateEffort(ResourceIssue issue) { // Simple effort estimation based on severity and category double baseEffort = 1.0; // hours switch (issue.getSeverity()) { case ERROR: baseEffort = 4.0; break; case WARNING: baseEffort = 2.0; break; case INFO: baseEffort = 0.5; break; } // Adjust based on category switch (issue.getCategory()) { case "Security": baseEffort *= 1.5; break; case "Resources": baseEffort *= 1.2; break; case "Health Checks": baseEffort *= 0.8; break; } return baseEffort; } private String generateScanId() { return "scan-" + Instant.now().getEpochSecond() + "-" + UUID.randomUUID().toString().substring(0, 8); } }

Report Generation

ReportGenerator.java:

@Service @Slf4j public class ReportGenerator { private final PopeyeConfig config; private final ObjectMapper objectMapper; private final ObjectMapper yamlMapper; public ReportGenerator(PopeyeConfig config) { this.config = config; this.objectMapper = new ObjectMapper(); this.yamlMapper = new ObjectMapper(new YAMLFactory()); } public void generateReport(ScanResult scanResult) { try { ensureReportDirectory(); switch (config.getOutput().getFormat()) { case TEXT: generateTextReport(scanResult); break; case JSON: generateJsonReport(scanResult); break; case YAML: generateYamlReport(scanResult); break; case HTML: generateHtmlReport(scanResult); break; case XML: generateXmlReport(scanResult); break; } log.info("Report generated in {} format", config.getOutput().getFormat()); } catch (Exception e) { log.error("Report generation failed", e); throw new ReportException("Failed to generate report", e); } } private void generateTextReport(ScanResult scanResult) throws IOException { Path reportPath = getReportPath("txt"); try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(reportPath))) { writer.println("POPEYE SCAN REPORT"); writer.println("=================="); writer.println(); // Cluster info writer.println("CLUSTER INFORMATION"); writer.println("-------------------"); writer.printf("Scan ID: %s%n", scanResult.getScanId()); writer.printf("Scan Time: %s%n", scanResult.getScanTimestamp()); if (scanResult.getClusterInfo() != null) { writer.printf("Cluster: %s%n", scanResult.getClusterInfo().getClusterName()); writer.printf("Nodes: %d%n", scanResult.getClusterInfo().getNodeCount()); writer.printf("Namespaces: %d%n", scanResult.getClusterInfo().getNamespaceCount()); } writer.println(); // Summary if (scanResult.getSummary() != null) { writer.println("SCAN SUMMARY"); writer.println("------------"); writer.printf("Score: %.1f/100%n", scanResult.getSummary().getScore()); writer.printf("Total Issues: %d%n", scanResult.getSummary().getTotalIssues()); writer.printf("Errors: %d%n", scanResult.getSummary().getErrorCount()); writer.printf("Warnings: %d%n", scanResult.getSummary().getWarningCount()); writer.printf("Info: %d%n", scanResult.getSummary().getInfoCount()); writer.println(); } // Issues by namespace if (scanResult.getNamespaceResults() != null) { writer.println("ISSUES BY NAMESPACE"); writer.println("-------------------"); for (NamespaceResult nsResult : scanResult.getNamespaceResults()) { if (nsResult.getIssues() != null && !nsResult.getIssues().isEmpty()) { writer.printf("%nNamespace: %s%n", nsResult.getName()); writer.println("-".repeat(nsResult.getName().length() + 12)); for (ResourceIssue issue : nsResult.getIssues()) { if (issue.getSeverity().getLevel() >= config.getFilters().getMinSeverity()) { writer.printf("%s [%s] %s: %s%n", issue.getSeverity().getIcon(), issue.getResourceType(), issue.getResourceName(), issue.getMessage()); if (config.getOutput().isVerbose() && issue.getRemediation() != null) { writer.printf(" 💡 %s%n", issue.getRemediation()); } } } } } } // Recommendations if (scanResult.getRecommendations() != null && !scanResult.getRecommendations().isEmpty()) { writer.println(); writer.println("RECOMMENDATIONS"); writer.println("---------------"); for (Recommendation rec : scanResult.getRecommendations()) { writer.printf("%s [%s] %s: %s%n", rec.getSeverity().getIcon(), rec.getCategory(), rec.getResourceType() + "/" + rec.getResourceName(), rec.getDescription()); if (rec.getAction() != null) { writer.printf(" 🛠️ %s%n", rec.getAction()); } } } } } private void generateJsonReport(ScanResult scanResult) throws IOException { Path reportPath = getReportPath("json"); objectMapper.writerWithDefaultPrettyPrinter() .writeValue(reportPath.toFile(), scanResult); } private void generateYamlReport(ScanResult scanResult) throws IOException { Path reportPath = getReportPath("yaml"); yamlMapper.writeValue(reportPath.toFile(), scanResult); } private void generateHtmlReport(ScanResult scanResult) throws IOException { Path reportPath = getReportPath("html"); String html = generateHtmlContent(scanResult); Files.write(reportPath, html.getBytes()); } private void generateXmlReport(ScanResult scanResult) throws IOException { Path reportPath = getReportPath("xml"); // XML generation would use JAXB or similar log.warn("XML report generation not implemented"); } private String generateHtmlContent(ScanResult scanResult) { StringBuilder html = new StringBuilder(); html.append(""" <!DOCTYPE html> <html> <head> <title>Popeye Scan Report</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } .header { background: #f5f5f5; padding: 20px; border-radius: 5px; } .summary { margin: 20px 0; } .issue { margin: 10px 0; padding: 10px; border-left: 4px solid; } .error { border-color: #dc3545; background: #f8d7da; } .warning { border-color: #ffc107; background: #fff3cd; } .info { border-color: #17a2b8; background: #d1ecf1; } .recommendation { margin: 10px 0; padding: 10px; background: #e2e3e5; } </style> </head> <body> <div class="header"> <h1>Popeye Scan Report</h1> <p>Scan ID: %s | Date: %s</p> </div> """.formatted(scanResult.getScanId(), scanResult.getScanTimestamp())); // Add summary, issues, recommendations... html.append("</body></html>"); return html.toString(); } private void ensureReportDirectory() throws IOException { Path reportDir = Paths.get(config.getOutput().getReportPath()); if (!Files.exists(reportDir)) { Files.createDirectories(reportDir); } } private Path getReportPath(String extension) { String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss") .format(LocalDateTime.now()); String filename = String.format("popeye-report-%s.%s", timestamp, extension); return Paths.get(config.getOutput().getReportPath(), filename); } }

REST API Controller

PopeyeController.java:

@RestController @RequestMapping("/api/popeye") @Slf4j public class PopeyeController { private final PopeyeScanner popeyeScanner; private final ReportGenerator reportGenerator; public PopeyeController(PopeyeScanner popeyeScanner, ReportGenerator reportGenerator) { this.popeyeScanner = popeyeScanner; this.reportGenerator = reportGenerator; } @PostMapping("/scan/cluster") public ResponseEntity<ScanResult> scanCluster() { try { ScanResult result = popeyeScanner.scanCluster(); return ResponseEntity.ok(result); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(null); } } @PostMapping("/scan/namespace/{namespace}") public ResponseEntity<ScanResult> scanNamespace(@PathVariable String namespace) { try { ScanResult result = popeyeScanner.scanNamespace(namespace); return ResponseEntity.ok(result); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(null); } } @GetMapping("/report/{scanId}") public ResponseEntity<Resource> getReport(@PathVariable String scanId, @RequestParam(defaultValue = "json") String format) { try { // Implementation to retrieve saved report Path reportPath = findReportFile(scanId, format); if (reportPath == null || !Files.exists(reportPath)) { return ResponseEntity.notFound().build(); } Resource resource = new FileSystemResource(reportPath); return ResponseEntity.ok() .header("Content-Type", getContentType(format)) .header("Content-Disposition", "attachment; filename=\"" + reportPath.getFileName() + "\"") .body(resource); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { Map<String, Object> health = new HashMap<>(); health.put("status", "UP"); health.put("timestamp", Instant.now()); return ResponseEntity.ok(health); } private Path findReportFile(String scanId, String format) { // Implementation to find report file by scan ID and format return null; } private String getContentType(String format) { switch (format.toLowerCase()) { case "json": return "application/json"; case "yaml": return "application/yaml"; case "html": return "text/html"; case "txt": return "text/plain"; default: return "application/octet-stream"; } } }

Best Practices

  1. Run scans regularly in CI/CD pipelines and monitoring systems
  2. Exclude system namespaces (kube-system, kube-public) from scans
  3. Use appropriate severity thresholds for different environments
  4. Integrate with alerting systems for critical issues
  5. Maintain custom rules for organization-specific requirements
  6. Review and act on recommendations regularly
  7. Monitor scan performance and optimize as needed

Conclusion

Popeye Cluster Cleaner in Java provides:

  • Comprehensive Kubernetes cluster scanning
  • Best practices validation for resources
  • Security misconfiguration detection
  • Resource optimization recommendations
  • Multiple output formats for integration
  • Extensible rule system for custom checks

This implementation helps maintain clean, secure, and optimized Kubernetes clusters by identifying issues and providing actionable recommendations for improvement.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper