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
- Run scans regularly in CI/CD pipelines and monitoring systems
- Exclude system namespaces (kube-system, kube-public) from scans
- Use appropriate severity thresholds for different environments
- Integrate with alerting systems for critical issues
- Maintain custom rules for organization-specific requirements
- Review and act on recommendations regularly
- 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.