kube-score is a tool that performs static code analysis of Kubernetes manifests to improve reliability and security. This guide provides a complete Java implementation for analyzing Kubernetes YAML files.
Kube-Score Overview
What is Kube-Score?
- Static analysis tool for Kubernetes manifests
- Checks for security, reliability, and best practices
- Provides actionable recommendations
- Supports various resource types (Deployments, Services, etc.)
Key Checks:
- Container security context
- Resource requests and limits
- Pod disruption budgets
- Network policies
- Liveness/readiness probes
Dependencies and Setup
Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<snakeyaml.version>2.0</snakeyaml.version>
<kubernetes-client.version>6.7.2</kubernetes-client.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- YAML Processing -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.kubernetes</groupId>
<artifactId>client-java</artifactId>
<version>${kubernetes-client.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml app: kube-score: # Severity levels: CRITICAL, WARNING, INFO min-severity: WARNING # Enable/disable specific checks checks: container-security-context: true resource-requests-limits: true pod-disruption-budget: true network-policy: true liveness-readiness-probes: true topology-spread-constraints: true # Custom scoring weights scoring: critical: 10 warning: 5 info: 1 server: port: 8080 logging: level: com.example.kubescore: DEBUG
Core Models
1. Analysis Models
// KubeScoreRequest.java
package com.example.kubescore.model;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.NotNull;
import java.util.Map;
@Data
public class KubeScoreRequest {
@NotNull(message = "Kubernetes manifest is required")
private MultipartFile manifest;
private AnalysisOptions options = new AnalysisOptions();
@Data
public static class AnalysisOptions {
private Severity minSeverity = Severity.WARNING;
private boolean enableAllChecks = true;
private Map<String, Boolean> enabledChecks;
private boolean ignoreTestPods = true;
private String outputFormat = "json";
}
}
// KubeScoreResponse.java
package com.example.kubescore.model;
import lombok.Data;
import lombok.Builder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
public class KubeScoreResponse {
private boolean success;
private String message;
private LocalDateTime timestamp;
private AnalysisSummary summary;
private List<CheckResult> results;
private String rawOutput;
@Data
@Builder
public static class AnalysisSummary {
private int totalChecks;
private int criticalIssues;
private int warnings;
private int infoIssues;
private int passedChecks;
private double score;
private String grade;
}
}
// CheckResult.java
package com.example.kubescore.model;
import lombok.Data;
import lombok.Builder;
@Data
@Builder
public class CheckResult {
private String checkName;
private String checkType;
private Severity severity;
private String message;
private String resourceType;
private String resourceName;
private String namespace;
private String comment;
private List<String> suggestions;
private String documentationUrl;
public boolean isCritical() {
return severity == Severity.CRITICAL;
}
public boolean isWarning() {
return severity == Severity.WARNING;
}
public boolean isInfo() {
return severity == Severity.INFO;
}
}
// Severity.java
package com.example.kubescore.model;
public enum Severity {
CRITICAL,
WARNING,
INFO,
OK
}
2. Kubernetes Resource Models
// KubernetesResource.java
package com.example.kubescore.model;
import lombok.Data;
import java.util.Map;
@Data
public class KubernetesResource {
private String apiVersion;
private String kind;
private Metadata metadata;
private Map<String, Object> spec;
@Data
public static class Metadata {
private String name;
private String namespace;
private Map<String, String> labels;
private Map<String, String> annotations;
}
}
// ContainerSpec.java
package com.example.kubescore.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class ContainerSpec {
private String name;
private String image;
private List<String> command;
private List<String> args;
private List<EnvVar> env;
private List<VolumeMount> volumeMounts;
private ResourceRequirements resources;
private SecurityContext securityContext;
private Probe livenessProbe;
private Probe readinessProbe;
private Probe startupProbe;
@Data
public static class EnvVar {
private String name;
private String value;
private EnvVarSource valueFrom;
}
@Data
public static class EnvVarSource {
private ConfigMapKeySelector configMapKeyRef;
private SecretKeySelector secretKeyRef;
private FieldRef fieldRef;
private ResourceFieldRef resourceFieldRef;
}
@Data
public static class ConfigMapKeySelector {
private String name;
private String key;
}
@Data
public static class SecretKeySelector {
private String name;
private String key;
}
@Data
public static class FieldRef {
private String fieldPath;
}
@Data
public static class ResourceFieldRef {
private String resource;
}
@Data
public static class VolumeMount {
private String name;
private String mountPath;
private boolean readOnly;
}
}
// ResourceRequirements.java
package com.example.kubescore.model;
import lombok.Data;
import java.util.Map;
@Data
public class ResourceRequirements {
private Map<String, String> requests;
private Map<String, String> limits;
public boolean hasRequests() {
return requests != null && !requests.isEmpty();
}
public boolean hasLimits() {
return limits != null && !limits.isEmpty();
}
public boolean hasCpuRequests() {
return hasRequests() && requests.containsKey("cpu");
}
public boolean hasMemoryRequests() {
return hasRequests() && requests.containsKey("memory");
}
public boolean hasCpuLimits() {
return hasLimits() && limits.containsKey("cpu");
}
public boolean hasMemoryLimits() {
return hasLimits() && limits.containsKey("memory");
}
}
// SecurityContext.java
package com.example.kubescore.model;
import lombok.Data;
@Data
public class SecurityContext {
private Boolean runAsNonRoot;
private Long runAsUser;
private Long runAsGroup;
private Boolean readOnlyRootFilesystem;
private Boolean allowPrivilegeEscalation;
private Capabilities capabilities;
private SeLinuxOptions seLinuxOptions;
private SeccompProfile seccompProfile;
@Data
public static class Capabilities {
private java.util.List<String> add;
private java.util.List<String> drop;
}
@Data
public static class SeLinuxOptions {
private String level;
}
@Data
public static class SeccompProfile {
private String type;
}
}
// Probe.java
package com.example.kubescore.model;
import lombok.Data;
@Data
public class Probe {
private ExecAction exec;
private HttpGetAction httpGet;
private TcpSocketAction tcpSocket;
private int initialDelaySeconds;
private int periodSeconds;
private int timeoutSeconds;
private int successThreshold;
private int failureThreshold;
@Data
public static class ExecAction {
private java.util.List<String> command;
}
@Data
public static class HttpGetAction {
private String path;
private int port;
private String scheme;
private java.util.Map<String, String> httpHeaders;
}
@Data
public static class TcpSocketAction {
private int port;
}
}
Analysis Checks
1. Base Check Interface
// ResourceCheck.java
package com.example.kubescore.checks;
import com.example.kubescore.model.CheckResult;
import com.example.kubescore.model.KubernetesResource;
import java.util.List;
public interface ResourceCheck {
String getName();
String getDescription();
List<String> getSupportedResourceTypes();
List<CheckResult> check(KubernetesResource resource);
boolean isEnabled();
}
2. Container Security Context Check
// ContainerSecurityContextCheck.java
package com.example.kubescore.checks;
import com.example.kubescore.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class ContainerSecurityContextCheck implements ResourceCheck {
@Override
public String getName() {
return "container-security-context";
}
@Override
public String getDescription() {
return "Checks container security context settings";
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of("Deployment", "StatefulSet", "DaemonSet", "Pod", "ReplicaSet", "CronJob", "Job");
}
@Override
public List<CheckResult> check(KubernetesResource resource) {
List<CheckResult> results = new ArrayList<>();
try {
Map<String, Object> spec = resource.getSpec();
if (spec == null) return results;
// Extract containers from different resource types
List<Map<String, Object>> containers = extractContainers(resource);
if (containers == null || containers.isEmpty()) return results;
for (Map<String, Object> container : containers) {
String containerName = (String) container.get("name");
checkContainerSecurityContext(resource, containerName, container, results);
}
} catch (Exception e) {
log.error("Error checking security context for resource: {}", resource.getMetadata().getName(), e);
}
return results;
}
private void checkContainerSecurityContext(KubernetesResource resource,
String containerName,
Map<String, Object> container,
List<CheckResult> results) {
Map<String, Object> securityContext = (Map<String, Object>) container.get("securityContext");
String resourceName = resource.getMetadata().getName();
String resourceType = resource.getKind();
// Check if security context is set
if (securityContext == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.CRITICAL)
.message("Container is missing security context")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Add securityContext to container specification",
"Set runAsNonRoot to true",
"Set readOnlyRootFilesystem to true",
"Drop all capabilities and add only required ones"
))
.documentationUrl("https://kubernetes.io/docs/concepts/security/security-context/")
.build());
return;
}
// Check runAsNonRoot
Boolean runAsNonRoot = (Boolean) securityContext.get("runAsNonRoot");
if (runAsNonRoot == null || !runAsNonRoot) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.CRITICAL)
.message("Container should run as non-root user")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set runAsNonRoot to true in securityContext"))
.build());
}
// Check readOnlyRootFilesystem
Boolean readOnlyRootFilesystem = (Boolean) securityContext.get("readOnlyRootFilesystem");
if (readOnlyRootFilesystem == null || !readOnlyRootFilesystem) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.WARNING)
.message("Container should use read-only root filesystem")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set readOnlyRootFilesystem to true in securityContext"))
.build());
}
// Check allowPrivilegeEscalation
Boolean allowPrivilegeEscalation = (Boolean) securityContext.get("allowPrivilegeEscalation");
if (allowPrivilegeEscalation == null || allowPrivilegeEscalation) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.CRITICAL)
.message("Container should not allow privilege escalation")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set allowPrivilegeEscalation to false in securityContext"))
.build());
}
// Check capabilities
Map<String, Object> capabilities = (Map<String, Object>) securityContext.get("capabilities");
if (capabilities != null) {
List<String> addCapabilities = (List<String>) capabilities.get("add");
if (addCapabilities != null && !addCapabilities.isEmpty()) {
if (addCapabilities.contains("ALL")) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.CRITICAL)
.message("Container should not have ALL capabilities")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Drop ALL capabilities and add only required ones"))
.build());
}
// Check for dangerous capabilities
List<String> dangerousCaps = List.of("SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", "SYS_MODULE");
for (String cap : dangerousCaps) {
if (addCapabilities.contains(cap)) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Security")
.severity(Severity.WARNING)
.message("Container has dangerous capability: " + cap)
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Remove capability " + cap + " unless absolutely necessary"))
.build());
}
}
}
}
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> extractContainers(KubernetesResource resource) {
Map<String, Object> spec = resource.getSpec();
if (spec == null) return null;
// Handle different resource types
switch (resource.getKind()) {
case "Pod":
return (List<Map<String, Object>>) spec.get("containers");
case "Deployment":
case "StatefulSet":
case "DaemonSet":
case "ReplicaSet":
Map<String, Object> template = (Map<String, Object>) spec.get("template");
if (template != null) {
Map<String, Object> podSpec = (Map<String, Object>) template.get("spec");
return podSpec != null ? (List<Map<String, Object>>) podSpec.get("containers") : null;
}
break;
case "CronJob":
Map<String, Object> jobTemplate = (Map<String, Object>) spec.get("jobTemplate");
if (jobTemplate != null) {
Map<String, Object> jobSpec = (Map<String, Object>) jobTemplate.get("spec");
if (jobSpec != null) {
Map<String, Object> podTemplate = (Map<String, Object>) jobSpec.get("template");
if (podTemplate != null) {
Map<String, Object> podSpec = (Map<String, Object>) podTemplate.get("spec");
return podSpec != null ? (List<Map<String, Object>>) podSpec.get("containers") : null;
}
}
}
break;
case "Job":
Map<String, Object> jobSpec = (Map<String, Object>) spec.get("spec");
if (jobSpec != null) {
Map<String, Object> podTemplate = (Map<String, Object>) jobSpec.get("template");
if (podTemplate != null) {
Map<String, Object> podSpec = (Map<String, Object>) podTemplate.get("spec");
return podSpec != null ? (List<Map<String, Object>>) podSpec.get("containers") : null;
}
}
break;
}
return null;
}
@Override
public boolean isEnabled() {
return true;
}
}
3. Resource Requests and Limits Check
// ResourceRequestsLimitsCheck.java
package com.example.kubescore.checks;
import com.example.kubescore.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class ResourceRequestsLimitsCheck implements ResourceCheck {
@Override
public String getName() {
return "resource-requests-limits";
}
@Override
public String getDescription() {
return "Checks resource requests and limits configuration";
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of("Deployment", "StatefulSet", "DaemonSet", "Pod", "ReplicaSet", "CronJob", "Job");
}
@Override
public List<CheckResult> check(KubernetesResource resource) {
List<CheckResult> results = new ArrayList<>();
try {
List<Map<String, Object>> containers = extractContainers(resource);
if (containers == null || containers.isEmpty()) return results;
for (Map<String, Object> container : containers) {
String containerName = (String) container.get("name");
checkContainerResources(resource, containerName, container, results);
}
} catch (Exception e) {
log.error("Error checking resources for resource: {}", resource.getMetadata().getName(), e);
}
return results;
}
@SuppressWarnings("unchecked")
private void checkContainerResources(KubernetesResource resource,
String containerName,
Map<String, Object> container,
List<CheckResult> results) {
Map<String, Object> resources = (Map<String, Object>) container.get("resources");
String resourceName = resource.getMetadata().getName();
String resourceType = resource.getKind();
if (resources == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.CRITICAL)
.message("Container is missing resource requests and limits")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Add resource requests and limits to container specification",
"Set CPU and memory requests based on application requirements",
"Set CPU and memory limits to prevent resource exhaustion"
))
.documentationUrl("https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/")
.build());
return;
}
Map<String, String> requests = (Map<String, String>) resources.get("requests");
Map<String, String> limits = (Map<String, String>) resources.get("limits");
// Check requests
if (requests == null || requests.isEmpty()) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.CRITICAL)
.message("Container is missing resource requests")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add CPU and memory requests to container specification"))
.build());
} else {
if (!requests.containsKey("cpu")) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container is missing CPU requests")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add CPU requests to container specification"))
.build());
}
if (!requests.containsKey("memory")) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container is missing memory requests")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add memory requests to container specification"))
.build());
}
}
// Check limits
if (limits == null || limits.isEmpty()) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container is missing resource limits")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add CPU and memory limits to container specification"))
.build());
} else {
if (!limits.containsKey("cpu")) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container is missing CPU limits")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add CPU limits to container specification"))
.build());
}
if (!limits.containsKey("memory")) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container is missing memory limits")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Add memory limits to container specification"))
.build());
}
}
// Validate resource values
if (requests != null) {
validateResourceValues(resource, containerName, "requests", requests, results);
}
if (limits != null) {
validateResourceValues(resource, containerName, "limits", limits, results);
}
}
private void validateResourceValues(KubernetesResource resource,
String containerName,
String resourceType,
Map<String, String> values,
List<CheckResult> results) {
String resourceName = resource.getMetadata().getName();
String kind = resource.getKind();
for (Map.Entry<String, String> entry : values.entrySet()) {
String resourceNameKey = entry.getKey();
String value = entry.getValue();
if (value == null || value.trim().isEmpty()) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container has empty " + resourceNameKey + " " + resourceType)
.resourceType(kind)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set a valid value for " + resourceNameKey + " " + resourceType))
.build());
}
// Validate CPU format
if ("cpu".equals(resourceNameKey)) {
if (!isValidCpuValue(value)) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container has invalid CPU " + resourceType + " value: " + value)
.resourceType(kind)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Use valid CPU values: millicores (e.g., 100m, 500m) or cores (e.g., 0.5, 1, 2)"
))
.build());
}
}
// Validate memory format
if ("memory".equals(resourceNameKey)) {
if (!isValidMemoryValue(value)) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Resources")
.severity(Severity.WARNING)
.message("Container has invalid memory " + resourceType + " value: " + value)
.resourceType(kind)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Use valid memory values: KiB (e.g., 64Ki), MiB (e.g., 128Mi), GiB (e.g., 1Gi)"
))
.build());
}
}
}
}
private boolean isValidCpuValue(String value) {
if (value == null) return false;
return value.matches("^[0-9]+m$") || // millicores: 100m, 500m
value.matches("^[0-9]+(\\.[0-9]+)?$"); // cores: 0.5, 1, 2
}
private boolean isValidMemoryValue(String value) {
if (value == null) return false;
return value.matches("^[0-9]+(\\.[0-9]+)?[KMGTPE]i?$") || // KiB, MiB, GiB, etc.
value.matches("^[0-9]+(\\.[0-9]+)?[KMGTPE]?$"); // K, M, G, etc.
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> extractContainers(KubernetesResource resource) {
// Same implementation as in ContainerSecurityContextCheck
Map<String, Object> spec = resource.getSpec();
if (spec == null) return null;
switch (resource.getKind()) {
case "Pod":
return (List<Map<String, Object>>) spec.get("containers");
case "Deployment":
case "StatefulSet":
case "DaemonSet":
case "ReplicaSet":
Map<String, Object> template = (Map<String, Object>) spec.get("template");
if (template != null) {
Map<String, Object> podSpec = (Map<String, Object>) template.get("spec");
return podSpec != null ? (List<Map<String, Object>>) podSpec.get("containers") : null;
}
break;
// ... other resource types
}
return null;
}
@Override
public boolean isEnabled() {
return true;
}
}
4. Liveness and Readiness Probes Check
// ProbeCheck.java
package com.example.kubescore.checks;
import com.example.kubescore.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Component
public class ProbeCheck implements ResourceCheck {
@Override
public String getName() {
return "liveness-readiness-probes";
}
@Override
public String getDescription() {
return "Checks liveness and readiness probes configuration";
}
@Override
public List<String> getSupportedResourceTypes() {
return List.of("Deployment", "StatefulSet", "DaemonSet", "Pod", "ReplicaSet");
}
@Override
public List<CheckResult> check(KubernetesResource resource) {
List<CheckResult> results = new ArrayList<>();
try {
List<Map<String, Object>> containers = extractContainers(resource);
if (containers == null || containers.isEmpty()) return results;
for (Map<String, Object> container : containers) {
String containerName = (String) container.get("name");
checkContainerProbes(resource, containerName, container, results);
}
} catch (Exception e) {
log.error("Error checking probes for resource: {}", resource.getMetadata().getName(), e);
}
return results;
}
@SuppressWarnings("unchecked")
private void checkContainerProbes(KubernetesResource resource,
String containerName,
Map<String, Object> container,
List<CheckResult> results) {
Map<String, Object> livenessProbe = (Map<String, Object>) container.get("livenessProbe");
Map<String, Object> readinessProbe = (Map<String, Object>) container.get("readinessProbe");
String resourceName = resource.getMetadata().getName();
String resourceType = resource.getKind();
// Check liveness probe
if (livenessProbe == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.WARNING)
.message("Container is missing liveness probe")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Add liveness probe to detect and restart unhealthy containers",
"Use HTTP GET, TCP socket, or command execution for health checks"
))
.documentationUrl("https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/")
.build());
} else {
validateProbeConfiguration(resource, containerName, "liveness", livenessProbe, results);
}
// Check readiness probe
if (readinessProbe == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.WARNING)
.message("Container is missing readiness probe")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Add readiness probe to control traffic to the container",
"Use HTTP GET, TCP socket, or command execution for readiness checks"
))
.build());
} else {
validateProbeConfiguration(resource, containerName, "readiness", readinessProbe, results);
}
// Check for startup probe (optional but recommended for slow-starting containers)
Map<String, Object> startupProbe = (Map<String, Object>) container.get("startupProbe");
if (startupProbe == null) {
// Only suggest startup probe if the app has long initialization
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.INFO)
.message("Consider adding startup probe for slow-starting containers")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Add startup probe for applications with long initialization times",
"Startup probe prevents killing containers during startup"
))
.build());
}
}
private void validateProbeConfiguration(KubernetesResource resource,
String containerName,
String probeType,
Map<String, Object> probe,
List<CheckResult> results) {
String resourceName = resource.getMetadata().getName();
String resourceType = resource.getKind();
// Check probe type
boolean hasHttpGet = probe.containsKey("httpGet");
boolean hasTcpSocket = probe.containsKey("tcpSocket");
boolean hasExec = probe.containsKey("exec");
if (!hasHttpGet && !hasTcpSocket && !hasExec) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.WARNING)
.message(probeType + " probe is missing action configuration")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Configure HTTP GET, TCP socket, or command execution for the probe"
))
.build());
}
// Check initial delay
Integer initialDelaySeconds = (Integer) probe.get("initialDelaySeconds");
if (initialDelaySeconds == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.INFO)
.message(probeType + " probe is missing initial delay")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of(
"Set initialDelaySeconds to account for application startup time"
))
.build());
}
// Check period seconds
Integer periodSeconds = (Integer) probe.get("periodSeconds");
if (periodSeconds == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.INFO)
.message(probeType + " probe is missing period configuration")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set periodSeconds to configure probe frequency"))
.build());
} else if (periodSeconds < 1) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.WARNING)
.message(probeType + " probe has very short period: " + periodSeconds + "s")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Increase periodSeconds to reduce probe frequency"))
.build());
}
// Check timeout
Integer timeoutSeconds = (Integer) probe.get("timeoutSeconds");
if (timeoutSeconds == null) {
results.add(CheckResult.builder()
.checkName(getName())
.checkType("Health")
.severity(Severity.INFO)
.message(probeType + " probe is missing timeout configuration")
.resourceType(resourceType)
.resourceName(resourceName)
.comment("Container: " + containerName)
.suggestions(List.of("Set timeoutSeconds for probe execution"))
.build());
}
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> extractContainers(KubernetesResource resource) {
// Same implementation as previous checks
Map<String, Object> spec = resource.getSpec();
if (spec == null) return null;
switch (resource.getKind()) {
case "Pod":
return (List<Map<String, Object>>) spec.get("containers");
case "Deployment":
case "StatefulSet":
case "DaemonSet":
case "ReplicaSet":
Map<String, Object> template = (Map<String, Object>) spec.get("template");
if (template != null) {
Map<String, Object> podSpec = (Map<String, Object>) template.get("spec");
return podSpec != null ? (List<Map<String, Object>>) podSpec.get("containers") : null;
}
break;
}
return null;
}
@Override
public boolean isEnabled() {
return true;
}
}
YAML Parser and Resource Extractor
// KubernetesManifestParser.java
package com.example.kubescore.service;
import com.example.kubescore.model.KubernetesResource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class KubernetesManifestParser {
private final Yaml yaml;
public KubernetesManifestParser() {
this.yaml = new Yaml(new SafeConstructor());
}
/**
* Parse Kubernetes manifest from input stream
*/
public List<KubernetesResource> parseManifest(InputStream inputStream) {
List<KubernetesResource> resources = new ArrayList<>();
try {
Iterable<Object> documents = yaml.loadAll(inputStream);
for (Object document : documents) {
if (document instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> resourceMap = (Map<String, Object>) document;
KubernetesResource resource = mapToKubernetesResource(resourceMap);
if (resource != null) {
resources.add(resource);
}
}
}
log.info("Parsed {} Kubernetes resources from manifest", resources.size());
} catch (Exception e) {
log.error("Error parsing Kubernetes manifest", e);
throw new ManifestParseException("Failed to parse Kubernetes manifest", e);
}
return resources;
}
/**
* Convert YAML map to KubernetesResource object
*/
@SuppressWarnings("unchecked")
private KubernetesResource mapToKubernetesResource(Map<String, Object> resourceMap) {
KubernetesResource resource = new KubernetesResource();
try {
// Set basic properties
resource.setApiVersion((String) resourceMap.get("apiVersion"));
resource.setKind((String) resourceMap.get("kind"));
// Set metadata
Map<String, Object> metadata = (Map<String, Object>) resourceMap.get("metadata");
if (metadata != null) {
KubernetesResource.Metadata resourceMetadata = new KubernetesResource.Metadata();
resourceMetadata.setName((String) metadata.get("name"));
resourceMetadata.setNamespace((String) metadata.get("namespace"));
resourceMetadata.setLabels((Map<String, String>) metadata.get("labels"));
resourceMetadata.setAnnotations((Map<String, String>) metadata.get("annotations"));
resource.setMetadata(resourceMetadata);
}
// Set spec
resource.setSpec((Map<String, Object>) resourceMap.get("spec"));
// Validate required fields
if (resource.getKind() == null || resource.getApiVersion() == null ||
resource.getMetadata() == null || resource.getMetadata().getName() == null) {
log.warn("Skipping invalid Kubernetes resource: missing required fields");
return null;
}
return resource;
} catch (Exception e) {
log.warn("Error mapping Kubernetes resource", e);
return null;
}
}
/**
* Validate Kubernetes resource kind
*/
public boolean isValidResourceKind(String kind) {
if (kind == null) return false;
List<String> supportedKinds = List.of(
"Pod", "Deployment", "StatefulSet", "DaemonSet", "ReplicaSet",
"Service", "ConfigMap", "Secret", "PersistentVolume", "PersistentVolumeClaim",
"ServiceAccount", "Role", "RoleBinding", "ClusterRole", "ClusterRoleBinding",
"Ingress", "NetworkPolicy", "PodDisruptionBudget", "CronJob", "Job",
"HorizontalPodAutoscaler", "VerticalPodAutoscaler"
);
return supportedKinds.contains(kind);
}
public static class ManifestParseException extends RuntimeException {
public ManifestParseException(String message) {
super(message);
}
public ManifestParseException(String message, Throwable cause) {
super(message, cause);
}
}
}
Main Analysis Service
// KubeScoreService.java
package com.example.kubescore.service;
import com.example.kubescore.checks.ResourceCheck;
import com.example.kubescore.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
public class KubeScoreService {
private final KubernetesManifestParser manifestParser;
private final List<ResourceCheck> resourceChecks;
public KubeScoreService(KubernetesManifestParser manifestParser,
List<ResourceCheck> resourceChecks) {
this.manifestParser = manifestParser;
this.resourceChecks = resourceChecks;
}
/**
* Analyze Kubernetes manifest
*/
public KubeScoreResponse analyzeManifest(KubeScoreRequest request) {
log.info("Starting kube-score analysis");
try (InputStream inputStream = request.getManifest().getInputStream()) {
// Parse Kubernetes manifest
List<KubernetesResource> resources = manifestParser.parseManifest(inputStream);
if (resources.isEmpty()) {
return KubeScoreResponse.builder()
.success(false)
.message("No valid Kubernetes resources found in manifest")
.timestamp(LocalDateTime.now())
.build();
}
// Perform analysis
List<CheckResult> allResults = new ArrayList<>();
for (KubernetesResource resource : resources) {
List<CheckResult> resourceResults = analyzeResource(resource, request.getOptions());
allResults.addAll(resourceResults);
}
// Filter by severity
List<CheckResult> filteredResults = filterResultsBySeverity(
allResults, request.getOptions().getMinSeverity()
);
// Generate summary
AnalysisSummary summary = generateSummary(filteredResults);
log.info("Kube-score analysis completed: {} issues found", filteredResults.size());
return KubeScoreResponse.builder()
.success(true)
.message("Analysis completed successfully")
.timestamp(LocalDateTime.now())
.summary(summary)
.results(filteredResults)
.build();
} catch (Exception e) {
log.error("Kube-score analysis failed", e);
return KubeScoreResponse.builder()
.success(false)
.message("Analysis failed: " + e.getMessage())
.timestamp(LocalDateTime.now())
.build();
}
}
/**
* Analyze single Kubernetes resource
*/
private List<CheckResult> analyzeResource(KubernetesResource resource,
KubeScoreRequest.AnalysisOptions options) {
List<CheckResult> results = new ArrayList<>();
// Skip test pods if configured
if (options.isIgnoreTestPods() && isTestPod(resource)) {
log.debug("Skipping test pod: {}", resource.getMetadata().getName());
return results;
}
// Run all applicable checks
for (ResourceCheck check : resourceChecks) {
if (check.isEnabled() && isCheckApplicable(check, resource)) {
try {
List<CheckResult> checkResults = check.check(resource);
results.addAll(checkResults);
} catch (Exception e) {
log.error("Check {} failed for resource {}", check.getName(),
resource.getMetadata().getName(), e);
}
}
}
return results;
}
/**
* Check if a check is applicable to the resource
*/
private boolean isCheckApplicable(ResourceCheck check, KubernetesResource resource) {
return check.getSupportedResourceTypes().contains(resource.getKind());
}
/**
* Check if resource is a test pod
*/
private boolean isTestPod(KubernetesResource resource) {
if (!"Pod".equals(resource.getKind())) {
return false;
}
KubernetesResource.Metadata metadata = resource.getMetadata();
if (metadata == null) {
return false;
}
String name = metadata.getName();
Map<String, String> labels = metadata.getLabels();
// Common test pod patterns
if (name != null && (name.contains("test") || name.contains("debug"))) {
return true;
}
if (labels != null) {
return labels.keySet().stream()
.anyMatch(key -> key.contains("test") || key.contains("debug"));
}
return false;
}
/**
* Filter results by minimum severity
*/
private List<CheckResult> filterResultsBySeverity(List<CheckResult> results, Severity minSeverity) {
return results.stream()
.filter(result -> {
switch (minSeverity) {
case CRITICAL:
return result.isCritical();
case WARNING:
return result.isCritical() || result.isWarning();
case INFO:
return true; // Include all
default:
return true;
}
})
.collect(Collectors.toList());
}
/**
* Generate analysis summary
*/
private AnalysisSummary generateSummary(List<CheckResult> results) {
int critical = (int) results.stream().filter(CheckResult::isCritical).count();
int warnings = (int) results.stream().filter(CheckResult::isWarning).count();
int info = (int) results.stream().filter(CheckResult::isInfo).count();
int total = results.size();
// Calculate score (0-100)
double score = calculateScore(results);
String grade = calculateGrade(score);
return AnalysisSummary.builder()
.totalChecks(total)
.criticalIssues(critical)
.warnings(warnings)
.infoIssues(info)
.passedChecks(0) // This would require tracking passed checks
.score(score)
.grade(grade)
.build();
}
/**
* Calculate overall score (0-100)
*/
private double calculateScore(List<CheckResult> results) {
if (results.isEmpty()) {
return 100.0;
}
// Weighted scoring based on severity
double totalWeight = 0;
double score = 100;
for (CheckResult result : results) {
double weight = getSeverityWeight(result.getSeverity());
totalWeight += weight;
score -= weight;
}
// Ensure score doesn't go below 0
return Math.max(0, score);
}
private double getSeverityWeight(Severity severity) {
switch (severity) {
case CRITICAL: return 10.0;
case WARNING: return 5.0;
case INFO: return 1.0;
default: return 0.0;
}
}
private String calculateGrade(double score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
if (score >= 60) return "D";
return "F";
}
/**
* Get analysis report in different formats
*/
public String generateReport(KubeScoreResponse response, String format) {
switch (format.toLowerCase()) {
case "json":
return generateJsonReport(response);
case "yaml":
return generateYamlReport(response);
case "html":
return generateHtmlReport(response);
case "text":
default:
return generateTextReport(response);
}
}
private String generateTextReport(KubeScoreResponse response) {
StringBuilder report = new StringBuilder();
AnalysisSummary summary = response.getSummary();
report.append("Kube-Score Analysis Report\n");
report.append("==========================\n\n");
report.append(String.format("Overall Score: %.1f/100 (%s)\n", summary.getScore(), summary.getGrade()));
report.append(String.format("Critical Issues: %d\n", summary.getCriticalIssues()));
report.append(String.format("Warnings: %d\n", summary.getWarnings()));
report.append(String.format("Info Issues: %d\n\n", summary.getInfoIssues()));
// Group by resource
response.getResults().stream()
.collect(Collectors.groupingBy(CheckResult::getResourceType))
.forEach((resourceType, results) -> {
report.append(resourceType).append(":\n");
results.forEach(result -> {
report.append(String.format(" [%s] %s: %s\n",
result.getSeverity(),
result.getResourceName(),
result.getMessage()));
});
report.append("\n");
});
return report.toString();
}
private String generateJsonReport(KubeScoreResponse response) {
// Use Jackson ObjectMapper for JSON serialization
try {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(response);
} catch (Exception e) {
log.error("Error generating JSON report", e);
return "{\"error\": \"Failed to generate JSON report\"}";
}
}
private String generateYamlReport(KubeScoreResponse response) {
// Use SnakeYAML for YAML serialization
try {
org.yaml.snakeyaml.Yaml yaml = new org.yaml.snakeyaml.Yaml();
return yaml.dump(response);
} catch (Exception e) {
log.error("Error generating YAML report", e);
return "error: Failed to generate YAML report";
}
}
private String generateHtmlReport(KubeScoreResponse response) {
// Simple HTML report generation
StringBuilder html = new StringBuilder();
html.append("""
<!DOCTYPE html>
<html>
<head>
<title>Kube-Score Analysis Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.critical { color: #d73a49; font-weight: bold; }
.warning { color: #f66a0a; }
.info { color: #0366d6; }
.score { font-size: 24px; font-weight: bold; }
.grade-A { color: #28a745; }
.grade-B { color: #17a2b8; }
.grade-C { color: #ffc107; }
.grade-D { color: #fd7e14; }
.grade-F { color: #dc3545; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Kube-Score Analysis Report</h1>
""");
AnalysisSummary summary = response.getSummary();
html.append(String.format("""
<div class="score grade-%s">Overall Score: %.1f/100 (%s)</div>
<p>Critical Issues: %d | Warnings: %d | Info Issues: %d</p>
""",
summary.getGrade(), summary.getScore(), summary.getGrade(),
summary.getCriticalIssues(), summary.getWarnings(), summary.getInfoIssues()));
html.append("<table><tr><th>Severity</th><th>Resource</th><th>Message</th><th>Suggestions</th></tr>");
for (CheckResult result : response.getResults()) {
String severityClass = result.getSeverity().name().toLowerCase();
String suggestions = result.getSuggestions() != null ?
String.join("<br>", result.getSuggestions()) : "";
html.append(String.format("""
<tr class="%s">
<td>%s</td>
<td>%s/%s</td>
<td>%s</td>
<td>%s</td>
</tr>
""",
severityClass, result.getSeverity(),
result.getResourceType(), result.getResourceName(),
result.getMessage(), suggestions));
}
html.append("</table></body></html>");
return html.toString();
}
}
REST Controller
// KubeScoreController.java
package com.example.kubescore.controller;
import com.example.kubescore.model.KubeScoreRequest;
import com.example.kubescore.model.KubeScoreResponse;
import com.example.kubescore.service.KubeScoreService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/kube-score")
public class KubeScoreController {
private final KubeScoreService kubeScoreService;
public KubeScoreController(KubeScoreService kubeScoreService) {
this.kubeScoreService = kubeScoreService;
}
@PostMapping(value = "/analyze", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<KubeScoreResponse> analyzeManifest(@ModelAttribute KubeScoreRequest request) {
log.info("Received kube-score analysis request");
try {
KubeScoreResponse response = kubeScoreService.analyzeManifest(request);
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("Analysis request failed", e);
return ResponseEntity.badRequest().body(
KubeScoreResponse.builder()
.success(false)
.message("Analysis failed: " + e.getMessage())
.build()
);
}
}
@PostMapping(value = "/analyze/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Map<String, Object>> analyzeManifestFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "minSeverity", defaultValue = "WARNING") String minSeverity,
@RequestParam(value = "outputFormat", defaultValue = "json") String outputFormat) {
log.info("Received file analysis request: {}", file.getOriginalFilename());
try {
KubeScoreRequest request = new KubeScoreRequest();
request.setManifest(file);
request.getOptions().setMinSeverity(
com.example.kubescore.model.Severity.valueOf(minSeverity.toUpperCase())
);
request.getOptions().setOutputFormat(outputFormat);
KubeScoreResponse response = kubeScoreService.analyzeManifest(request);
return ResponseEntity.ok(Map.of(
"success", response.isSuccess(),
"summary", response.getSummary(),
"results", response.getResults(),
"timestamp", response.getTimestamp()
));
} catch (Exception e) {
log.error("File analysis failed", e);
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@GetMapping("/report")
public ResponseEntity<String> generateReport(
@RequestParam String format,
@RequestBody KubeScoreResponse analysisResult) {
try {
String report = kubeScoreService.generateReport(analysisResult, format);
HttpHeaders headers = new HttpHeaders();
switch (format.toLowerCase()) {
case "json":
headers.setContentType(MediaType.APPLICATION_JSON);
break;
case "yaml":
headers.setContentType(MediaType.parseMediaType("application/yaml"));
break;
case "html":
headers.setContentType(MediaType.TEXT_HTML);
break;
default:
headers.setContentType(MediaType.TEXT_PLAIN);
}
return ResponseEntity.ok()
.headers(headers)
.body(report);
} catch (Exception e) {
log.error("Report generation failed", e);
return ResponseEntity.badRequest().body("Report generation failed: " + e.getMessage());
}
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
return ResponseEntity.ok(Map.of(
"status", "healthy",
"service", "kube-score",
"timestamp", java.time.LocalDateTime.now()
));
}
}
Testing
1. Unit Tests
// KubeScoreServiceTest.java
package com.example.kubescore.service;
import com.example.kubescore.model.KubeScoreRequest;
import com.example.kubescore.model.KubeScoreResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class KubeScoreServiceTest {
@Mock
private KubernetesManifestParser manifestParser;
@Mock
private MultipartFile multipartFile;
@InjectMocks
private KubeScoreService kubeScoreService;
@Test
void testAnalyzeManifest_Success() throws Exception {
// Setup
String yamlContent = """
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
spec:
replicas: 1
template:
spec:
containers:
- name: test-container
image: nginx:latest
""";
InputStream inputStream = new ByteArrayInputStream(yamlContent.getBytes(StandardCharsets.UTF_8));
when(multipartFile.getInputStream()).thenReturn(inputStream);
KubeScoreRequest request = new KubeScoreRequest();
request.setManifest(multipartFile);
// Execute
KubeScoreResponse response = kubeScoreService.analyzeManifest(request);
// Verify
assertNotNull(response);
// Add more assertions based on your implementation
}
}
// ContainerSecurityContextCheckTest.java
package com.example.kubescore.checks;
import com.example.kubescore.model.KubernetesResource;
import com.example.kubescore.model.CheckResult;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class ContainerSecurityContextCheckTest {
private final ContainerSecurityContextCheck check = new ContainerSecurityContextCheck();
@Test
void testCheck_NoSecurityContext() {
// Setup
KubernetesResource resource = new KubernetesResource();
resource.setKind("Deployment");
resource.setApiVersion("apps/v1");
KubernetesResource.Metadata metadata = new KubernetesResource.Metadata();
metadata.setName("test-deployment");
resource.setMetadata(metadata);
Map<String, Object> spec = Map.of(
"template", Map.of(
"spec", Map.of(
"containers", List.of(
Map.of("name", "test-container", "image", "nginx:latest")
)
)
)
);
resource.setSpec(spec);
// Execute
List<CheckResult> results = check.check(resource);
// Verify
assertFalse(results.isEmpty());
assertTrue(results.stream().anyMatch(r -> r.getMessage().contains("missing security context")));
}
}
2. Integration Test
// KubeScoreIntegrationTest.java
package com.example.kubescore.integration;
import com.example.kubescore.controller.KubeScoreController;
import com.example.kubescore.model.KubeScoreRequest;
import com.example.kubescore.model.Severity;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
class KubeScoreIntegrationTest {
@Autowired
private KubeScoreController kubeScoreController;
@Test
void testFullAnalysisWorkflow() {
// Setup
String yamlContent = """
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
""";
MockMultipartFile file = new MockMultipartFile(
"file", "test.yaml", "text/yaml", yamlContent.getBytes()
);
// Execute
var response = kubeScoreController.analyzeManifestFile(file, "WARNING", "json");
// Verify
assertTrue(response.getStatusCode().is2xxSuccessful());
assertNotNull(response.getBody());
// Add more specific assertions
}
}
Production Considerations
1. Configuration
// KubeScoreConfig.java
package com.example.kubescore.config;
import com.example.kubescore.checks.ResourceCheck;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class KubeScoreConfig {
@Bean
public List<ResourceCheck> resourceChecks(List<ResourceCheck> checks) {
// All ResourceCheck implementations are automatically injected
return checks;
}
}
2. Error Handling
// GlobalExceptionHandler.java
package com.example.kubescore.controller;
import com.example.kubescore.service.KubernetesManifestParser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(KubernetesManifestParser.ManifestParseException.class)
public ResponseEntity<Map<String, Object>> handleManifestParseException(
KubernetesManifestParser.ManifestParseException e) {
log.error("Manifest parse error", e);
return ResponseEntity.badRequest().body(Map.of(
"error", "Manifest parse failed",
"message", e.getMessage()
));
}
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<Map<String, Object>> handleMaxSizeException(MaxUploadSizeExceededException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "File too large",
"message", "Maximum upload size exceeded"
));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception e) {
log.error("Unexpected error", e);
return ResponseEntity.internalServerError().body(Map.of(
"error", "Internal server error",
"message", "An unexpected error occurred"
));
}
}
Best Practices
- Performance:
- Use streaming YAML parsing for large manifests
- Implement caching for frequently analyzed manifests
- Use parallel processing for multiple resources
- Extensibility:
- Easy to add new checks via ResourceCheck interface
- Configurable severity levels and weights
- Plugin architecture for custom checks
- Security:
- Validate YAML input to prevent YAML bombs
- Limit file upload sizes
- Sanitize output for different formats
- Usability:
- Multiple output formats (JSON, YAML, HTML, Text)
- Detailed suggestions for fixing issues
- Clear severity levels and scoring
Conclusion
This kube-score implementation provides:
- Comprehensive Kubernetes manifest analysis with multiple checks
- Extensible architecture for adding new analysis rules
- Multiple output formats for different use cases
- Production-ready features including error handling and validation
- REST API for easy integration with CI/CD pipelines
The solution can be extended with:
- Custom check plugins
- Integration with Kubernetes admission controllers
- Historical analysis and trend reporting
- Team-specific rule sets
- Automated fix suggestions