Custom Resource Definitions (CRDs) in Java

Comprehensive CRD Implementation Guide

1. CRD Definition for Java Application

Custom Resource Definition YAML

# crds/javapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: javapps.example.com
spec:
group: example.com
names:
kind: JavApp
listKind: JavAppList
plural: javapps
singular: javapp
shortNames:
- ja
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
applicationName:
type: string
description: Name of the Java application
version:
type: string
description: Application version
replicas:
type: integer
minimum: 1
maximum: 10
default: 1
image:
type: string
description: Container image for the application
jvmOptions:
type: string
description: JVM options
resources:
type: object
properties:
requests:
type: object
properties:
cpu:
type: string
default: "100m"
memory:
type: string
default: "256Mi"
limits:
type: object
properties:
cpu:
type: string
default: "500m"
memory:
type: string
default: "512Mi"
service:
type: object
properties:
type:
type: string
enum: [ClusterIP, NodePort, LoadBalancer]
default: ClusterIP
port:
type: integer
minimum: 1
maximum: 65535
default: 8080
ingress:
type: object
properties:
enabled:
type: boolean
default: false
host:
type: string
config:
type: object
additionalProperties:
type: string
required:
- applicationName
- version
- image
status:
type: object
properties:
phase:
type: string
enum: [Pending, Deploying, Ready, Failed, Updating]
message:
type: string
deployedReplicas:
type: integer
availableReplicas:
type: integer
serviceUrl:
type: string
lastUpdated:
type: string
format: date-time
subresources:
status: {}
additionalPrinterColumns:
- name: Phase
type: string
jsonPath: .status.phase
- name: Version
type: string
jsonPath: .spec.version
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Age
type: date
jsonPath: .metadata.creationTimestamp

2. Java Model Classes for CRD

Custom Resource Model

// JavApp.java
package com.example.crd.model;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;
@Group("example.com")
@Version("v1alpha1")
public class JavApp extends CustomResource<JavAppSpec, JavAppStatus> {
@Override
protected JavAppSpec initSpec() {
return new JavAppSpec();
}
@Override
protected JavAppStatus initStatus() {
return new JavAppStatus();
}
}
// JavAppSpec.java
package com.example.crd.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize
public class JavAppSpec {
@JsonProperty("applicationName")
private String applicationName;
@JsonProperty("version")
private String version;
@JsonProperty("replicas")
private Integer replicas = 1;
@JsonProperty("image")
private String image;
@JsonProperty("jvmOptions")
private String jvmOptions = "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0";
@JsonProperty("resources")
private ResourceRequirements resources = new ResourceRequirements();
@JsonProperty("service")
private ServiceConfig service = new ServiceConfig();
@JsonProperty("ingress")
private IngressConfig ingress = new IngressConfig();
@JsonProperty("config")
private java.util.Map<String, String> config = new java.util.HashMap<>();
// Getters and Setters
public String getApplicationName() { return applicationName; }
public void setApplicationName(String applicationName) { this.applicationName = applicationName; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public Integer getReplicas() { return replicas; }
public void setReplicas(Integer replicas) { this.replicas = replicas; }
public String getImage() { return image; }
public void setImage(String image) { this.image = image; }
public String getJvmOptions() { return jvmOptions; }
public void setJvmOptions(String jvmOptions) { this.jvmOptions = jvmOptions; }
public ResourceRequirements getResources() { return resources; }
public void setResources(ResourceRequirements resources) { this.resources = resources; }
public ServiceConfig getService() { return service; }
public void setService(ServiceConfig service) { this.service = service; }
public IngressConfig getIngress() { return ingress; }
public void setIngress(IngressConfig ingress) { this.ingress = ingress; }
public java.util.Map<String, String> getConfig() { return config; }
public void setConfig(java.util.Map<String, String> config) { this.config = config; }
}
// JavAppStatus.java
package com.example.crd.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize
public class JavAppStatus {
public enum Phase {
Pending, Deploying, Ready, Failed, Updating
}
@JsonProperty("phase")
private Phase phase = Phase.Pending;
@JsonProperty("message")
private String message;
@JsonProperty("deployedReplicas")
private Integer deployedReplicas = 0;
@JsonProperty("availableReplicas")
private Integer availableReplicas = 0;
@JsonProperty("serviceUrl")
private String serviceUrl;
@JsonProperty("lastUpdated")
private String lastUpdated;
// Getters and Setters
public Phase getPhase() { return phase; }
public void setPhase(Phase phase) { this.phase = phase; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Integer getDeployedReplicas() { return deployedReplicas; }
public void setDeployedReplicas(Integer deployedReplicas) { this.deployedReplicas = deployedReplicas; }
public Integer getAvailableReplicas() { return availableReplicas; }
public void setAvailableReplicas(Integer availableReplicas) { this.availableReplicas = availableReplicas; }
public String getServiceUrl() { return serviceUrl; }
public void setServiceUrl(String serviceUrl) { this.serviceUrl = serviceUrl; }
public String getLastUpdated() { return lastUpdated; }
public void setLastUpdated(String lastUpdated) { this.lastUpdated = lastUpdated; }
}
// Supporting classes
class ResourceRequirements {
private Resource requests = new Resource("100m", "256Mi");
private Resource limits = new Resource("500m", "512Mi");
// Getters and Setters
public Resource getRequests() { return requests; }
public void setRequests(Resource requests) { this.requests = requests; }
public Resource getLimits() { return limits; }
public void setLimits(Resource limits) { this.limits = limits; }
}
class Resource {
private String cpu;
private String memory;
public Resource() {}
public Resource(String cpu, String memory) {
this.cpu = cpu;
this.memory = memory;
}
// Getters and Setters
public String getCpu() { return cpu; }
public void setCpu(String cpu) { this.cpu = cpu; }
public String getMemory() { return memory; }
public void setMemory(String memory) { this.memory = memory; }
}
class ServiceConfig {
private String type = "ClusterIP";
private Integer port = 8080;
// Getters and Setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public Integer getPort() { return port; }
public void setPort(Integer port) { this.port = port; }
}
class IngressConfig {
private Boolean enabled = false;
private String host;
// Getters and Setters
public Boolean getEnabled() { return enabled; }
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
}

3. Custom Controller Implementation

Main Controller Class

// JavAppController.java
package com.example.crd.controller;
import com.example.crd.model.JavApp;
import com.example.crd.model.JavAppStatus;
import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.informers.ResourceEventHandler;
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
import io.fabric8.kubernetes.client.informers.SharedInformerFactory;
import io.javaoperatorsdk.operator.api.*;
import io.javaoperatorsdk.operator.api.Context;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Controller
public class JavAppController implements ResourceController<JavApp> {
private static final Logger logger = LoggerFactory.getLogger(JavAppController.class);
private final KubernetesClient kubernetesClient;
private final Map<String, Long> lastProcessed = new ConcurrentHashMap<>();
public JavAppController(KubernetesClient kubernetesClient) {
this.kubernetesClient = kubernetesClient;
}
@Override
public UpdateControl<JavApp> createOrUpdateResource(JavApp javApp, Context<JavApp> context) {
String namespace = javApp.getMetadata().getNamespace();
String name = javApp.getMetadata().getName();
logger.info("Processing JavApp: {}/{}", namespace, name);
try {
// Check if we recently processed this resource
String key = namespace + "/" + name;
long currentTime = System.currentTimeMillis();
if (lastProcessed.containsKey(key)) {
long lastTime = lastProcessed.get(key);
if (currentTime - lastTime < 30000) { // 30 seconds cooldown
logger.debug("Skipping recently processed resource: {}", key);
return UpdateControl.noUpdate();
}
}
lastProcessed.put(key, currentTime);
// Create or update deployment
Deployment deployment = createDeployment(javApp);
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name)
.createOrReplace(deployment);
// Create or update service
Service service = createService(javApp);
kubernetesClient.services()
.inNamespace(namespace)
.withName(name)
.createOrReplace(service);
// Create ingress if enabled
if (javApp.getSpec().getIngress().getEnabled()) {
createIngress(javApp);
}
// Create config map for application configuration
createConfigMap(javApp);
// Update status
updateStatus(javApp, deployment);
logger.info("Successfully processed JavApp: {}/{}", namespace, name);
return UpdateControl.updateStatus(javApp);
} catch (Exception e) {
logger.error("Error processing JavApp: {}/{}", namespace, name, e);
updateStatusWithError(javApp, e.getMessage());
return UpdateControl.updateStatus(javApp);
}
}
private Deployment createDeployment(JavApp javApp) {
Map<String, String> labels = createLabels(javApp);
return new DeploymentBuilder()
.withNewMetadata()
.withName(javApp.getMetadata().getName())
.withNamespace(javApp.getMetadata().getNamespace())
.withLabels(labels)
.endMetadata()
.withNewSpec()
.withReplicas(javApp.getSpec().getReplicas())
.withNewSelector()
.withMatchLabels(labels)
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(labels)
.endMetadata()
.withNewSpec()
.addNewContainer()
.withName(javApp.getSpec().getApplicationName())
.withImage(javApp.getSpec().getImage())
.withPorts(new ContainerPortBuilder()
.withContainerPort(javApp.getSpec().getService().getPort())
.build())
.withEnv(createEnvironmentVariables(javApp))
.withResources(createResourceRequirements(javApp))
.withLivenessProbe(createLivenessProbe(javApp))
.withReadinessProbe(createReadinessProbe(javApp))
.withNewSecurityContext()
.withRunAsNonRoot(true)
.withNewRunAsUser(1000L)
.endSecurityContext()
.endContainer()
.withServiceAccountName(javApp.getMetadata().getName() + "-sa")
.endSpec()
.endTemplate()
.endSpec()
.build();
}
private Service createService(JavApp javApp) {
Map<String, String> labels = createLabels(javApp);
return new ServiceBuilder()
.withNewMetadata()
.withName(javApp.getMetadata().getName())
.withNamespace(javApp.getMetadata().getNamespace())
.withLabels(labels)
.endMetadata()
.withNewSpec()
.withType(javApp.getSpec().getService().getType())
.withPorts(new ServicePortBuilder()
.withPort(javApp.getSpec().getService().getPort())
.withTargetPort(new IntOrString(javApp.getSpec().getService().getPort()))
.build())
.withSelector(labels)
.endSpec()
.build();
}
private void createIngress(JavApp javApp) {
// Ingress creation implementation
logger.info("Creating ingress for: {}", javApp.getMetadata().getName());
}
private void createConfigMap(JavApp javApp) {
ConfigMap configMap = new ConfigMapBuilder()
.withNewMetadata()
.withName(javApp.getMetadata().getName() + "-config")
.withNamespace(javApp.getMetadata().getNamespace())
.endMetadata()
.withData(javApp.getSpec().getConfig())
.build();
kubernetesClient.configMaps()
.inNamespace(javApp.getMetadata().getNamespace())
.withName(javApp.getMetadata().getName() + "-config")
.createOrReplace(configMap);
}
private Map<String, String> createLabels(JavApp javApp) {
Map<String, String> labels = new HashMap<>();
labels.put("app", javApp.getMetadata().getName());
labels.put("version", javApp.getSpec().getVersion());
labels.put("managed-by", "javapp-controller");
return labels;
}
private EnvVar[] createEnvironmentVariables(JavApp javApp) {
return new EnvVar[] {
new EnvVarBuilder()
.withName("JAVA_OPTS")
.withValue(javApp.getSpec().getJvmOptions())
.build(),
new EnvVarBuilder()
.withName("SPRING_PROFILES_ACTIVE")
.withValue("kubernetes")
.build(),
new EnvVarBuilder()
.withName("APPLICATION_NAME")
.withValue(javApp.getSpec().getApplicationName())
.build()
};
}
private io.fabric8.kubernetes.api.model.ResourceRequirements createResourceRequirements(JavApp javApp) {
return new io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder()
.addToRequests("cpu", new Quantity(javApp.getSpec().getResources().getRequests().getCpu()))
.addToRequests("memory", new Quantity(javApp.getSpec().getResources().getRequests().getMemory()))
.addToLimits("cpu", new Quantity(javApp.getSpec().getResources().getLimits().getCpu()))
.addToLimits("memory", new Quantity(javApp.getSpec().getResources().getLimits().getMemory()))
.build();
}
private Probe createLivenessProbe(JavApp javApp) {
return new ProbeBuilder()
.withNewHttpGet()
.withPath("/actuator/health/liveness")
.withPort(new IntOrString(javApp.getSpec().getService().getPort()))
.endHttpGet()
.withInitialDelaySeconds(30)
.withPeriodSeconds(10)
.build();
}
private Probe createReadinessProbe(JavApp javApp) {
return new ProbeBuilder()
.withNewHttpGet()
.withPath("/actuator/health/readiness")
.withPort(new IntOrString(javApp.getSpec().getService().getPort()))
.endHttpGet()
.withInitialDelaySeconds(5)
.withPeriodSeconds(5)
.build();
}
private void updateStatus(JavApp javApp, Deployment deployment) {
JavAppStatus status = javApp.getStatus();
if (deployment != null) {
status.setDeployedReplicas(deployment.getStatus().getReplicas());
status.setAvailableReplicas(deployment.getStatus().getAvailableReplicas());
if (deployment.getStatus().getAvailableReplicas() != null &&
deployment.getStatus().getAvailableReplicas() >= javApp.getSpec().getReplicas()) {
status.setPhase(JavAppStatus.Phase.Ready);
status.setMessage("Application is ready and serving traffic");
} else {
status.setPhase(JavAppStatus.Phase.Deploying);
status.setMessage("Application is being deployed");
}
}
status.setServiceUrl(createServiceUrl(javApp));
status.setLastUpdated(Instant.now().toString());
}
private void updateStatusWithError(JavApp javApp, String errorMessage) {
JavAppStatus status = javApp.getStatus();
status.setPhase(JavAppStatus.Phase.Failed);
status.setMessage("Error: " + errorMessage);
status.setLastUpdated(Instant.now().toString());
}
private String createServiceUrl(JavApp javApp) {
return String.format("http://%s.%s:%d",
javApp.getMetadata().getName(),
javApp.getMetadata().getNamespace(),
javApp.getSpec().getService().getPort());
}
@Override
public DeleteControl deleteResource(JavApp javApp, Context<JavApp> context) {
String namespace = javApp.getMetadata().getNamespace();
String name = javApp.getMetadata().getName();
logger.info("Deleting JavApp resources: {}/{}", namespace, name);
// Clean up resources
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name)
.delete();
kubernetesClient.services()
.inNamespace(namespace)
.withName(name)
.delete();
kubernetesClient.configMaps()
.inNamespace(namespace)
.withName(name + "-config")
.delete();
logger.info("Successfully deleted JavApp resources: {}/{}", namespace, name);
return DeleteControl.DEFAULT_DELETE;
}
}

4. Operator Configuration

Spring Boot Configuration

// OperatorConfiguration.java
package com.example.crd.config;
import com.example.crd.model.JavApp;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
import io.javaoperatorsdk.operator.config.runtime.DefaultConfigurationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OperatorConfiguration {
@Bean
public Operator operator(KubernetesClient kubernetesClient, 
ConfigurationService configurationService) {
return new Operator(kubernetesClient, configurationService);
}
@Bean
public ConfigurationService configurationService() {
return DefaultConfigurationService.instance();
}
}
// KubernetesClientConfiguration.java
package com.example.crd.config;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class KubernetesClientConfiguration {
@Value("${kubernetes.master.url:}")
private String masterUrl;
@Value("${kubernetes.namespace:default}")
private String namespace;
@Bean
@Profile("!test")
public KubernetesClient kubernetesClient() {
Config config = new ConfigBuilder()
.withMasterUrl(masterUrl)
.withNamespace(namespace)
.withTrustCerts(true)
.build();
return new DefaultKubernetesClient(config);
}
@Bean
@Profile("test")
public KubernetesClient mockKubernetesClient() {
// Return mock client for testing
return new DefaultKubernetesClient();
}
}

5. Custom Resource Examples

Sample JavApp Resource

# examples/javapp-example.yaml
apiVersion: example.com/v1alpha1
kind: JavApp
metadata:
name: user-service
namespace: production
labels:
environment: production
team: backend
spec:
applicationName: user-service
version: "1.2.0"
replicas: 3
image: "ghcr.io/my-org/user-service:1.2.0"
jvmOptions: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Xmx512m"
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1024Mi"
service:
type: ClusterIP
port: 8080
ingress:
enabled: true
host: users.example.com
config:
SPRING_DATASOURCE_URL: "jdbc:postgresql://postgresql:5432/users"
SPRING_DATASOURCE_USERNAME: "user"
LOGGING_LEVEL_COM_EXAMPLE: "INFO"

6. Webhook Validation

Validation Webhook

// JavAppValidationWebhook.java
package com.example.crd.webhook;
import com.example.crd.model.JavApp;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.regex.Pattern;
@RestController
public class JavAppValidationWebhook {
private static final Logger logger = LoggerFactory.getLogger(JavAppValidationWebhook.class);
private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$");
@PostMapping("/validate")
public AdmissionReview validate(@RequestBody AdmissionReview admissionReview) {
AdmissionRequest request = admissionReview.getRequest();
AdmissionResponse response = new AdmissionResponse();
try {
if (request.getObject() instanceof JavApp) {
JavApp javApp = (JavApp) request.getObject();
ValidationResult result = validateJavApp(javApp);
response.setUid(request.getUid());
response.setAllowed(result.isValid());
if (!result.isValid()) {
response.setStatus(new io.fabric8.kubernetes.api.model.Status(
"Failure",
null,
result.getMessage(),
422,
null
));
}
}
} catch (Exception e) {
logger.error("Error validating JavApp", e);
response.setUid(request.getUid());
response.setAllowed(false);
response.setStatus(new io.fabric8.kubernetes.api.model.Status(
"Error",
null,
"Validation error: " + e.getMessage(),
500,
null
));
}
admissionReview.setResponse(response);
return admissionReview;
}
private ValidationResult validateJavApp(JavApp javApp) {
// Validate name
if (!NAME_PATTERN.matcher(javApp.getMetadata().getName()).matches()) {
return ValidationResult.invalid("Name must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character");
}
// Validate replicas
if (javApp.getSpec().getReplicas() < 1 || javApp.getSpec().getReplicas() > 10) {
return ValidationResult.invalid("Replicas must be between 1 and 10");
}
// Validate image format
if (!isValidImage(javApp.getSpec().getImage())) {
return ValidationResult.invalid("Image must be in the format: [registry/]repository[:tag]");
}
// Validate resource requests
if (!isValidResource(javApp.getSpec().getResources().getRequests().getCpu()) ||
!isValidResource(javApp.getSpec().getResources().getRequests().getMemory()) ||
!isValidResource(javApp.getSpec().getResources().getLimits().getCpu()) ||
!isValidResource(javApp.getSpec().getResources().getLimits().getMemory())) {
return ValidationResult.invalid("Resource requests and limits must be valid Kubernetes quantities");
}
return ValidationResult.valid();
}
private boolean isValidImage(String image) {
return image != null && !image.trim().isEmpty() && image.contains(":");
}
private boolean isValidResource(String resource) {
try {
new io.fabric8.kubernetes.api.model.Quantity(resource);
return true;
} catch (Exception e) {
return false;
}
}
private static class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult valid() {
return new ValidationResult(true, null);
}
public static ValidationResult invalid(String message) {
return new ValidationResult(false, message);
}
public boolean isValid() { return valid; }
public String getMessage() { return message; }
}
}

7. Monitoring and Metrics

Custom Metrics Collector

// JavAppMetricsCollector.java
package com.example.crd.metrics;
import com.example.crd.model.JavApp;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class JavAppMetricsCollector {
private final KubernetesClient kubernetesClient;
private final MeterRegistry meterRegistry;
private final AtomicInteger totalJavApps = new AtomicInteger(0);
private final AtomicInteger readyJavApps = new AtomicInteger(0);
public JavAppMetricsCollector(KubernetesClient kubernetesClient, MeterRegistry meterRegistry) {
this.kubernetesClient = kubernetesClient;
this.meterRegistry = meterRegistry;
initializeMetrics();
}
private void initializeMetrics() {
Gauge.builder("javapp_controller_resources_total")
.description("Total number of JavApp resources")
.register(meterRegistry, totalJavApps);
Gauge.builder("javapp_controller_resources_ready")
.description("Number of ready JavApp resources")
.register(meterRegistry, readyJavApps);
}
@Scheduled(fixedRate = 30000) // Every 30 seconds
public void collectMetrics() {
try {
List<JavApp> javApps = kubernetesClient.resources(JavApp.class)
.inAnyNamespace()
.list()
.getItems();
totalJavApps.set(javApps.size());
long readyCount = javApps.stream()
.filter(app -> app.getStatus() != null && 
app.getStatus().getPhase() == JavAppStatus.Phase.Ready)
.count();
readyJavApps.set((int) readyCount);
} catch (Exception e) {
// Log error but don't fail
}
}
}

8. Testing Framework

Controller Unit Tests

// JavAppControllerTest.java
package com.example.crd.controller;
import com.example.crd.model.JavApp;
import com.example.crd.model.JavAppSpec;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient;
import io.javaoperatorsdk.operator.api.Context;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@EnableKubernetesMockClient
class JavAppControllerTest {
private JavAppController controller;
private KubernetesClient kubernetesClient;
@BeforeEach
void setUp() {
controller = new JavAppController(kubernetesClient);
}
@Test
void testCreateOrUpdateResource() {
// Given
JavApp javApp = createTestJavApp();
// When
var result = controller.createOrUpdateResource(javApp, new TestContext());
// Then
assertNotNull(result);
assertEquals("Ready", javApp.getStatus().getPhase().name());
}
private JavApp createTestJavApp() {
JavApp javApp = new JavApp();
ObjectMeta metadata = new ObjectMeta();
metadata.setName("test-app");
metadata.setNamespace("default");
javApp.setMetadata(metadata);
JavAppSpec spec = new JavAppSpec();
spec.setApplicationName("test-app");
spec.setVersion("1.0.0");
spec.setReplicas(1);
spec.setImage("test/image:1.0.0");
javApp.setSpec(spec);
return javApp;
}
private static class TestContext implements Context<JavApp> {
@Override
public JavApp getResource() {
return null;
}
@Override
public <T> T getSecondaryResource(Class<T> aClass, String s) {
return null;
}
@Override
public RetryInfo getRetryInfo() {
return null;
}
}
}

This comprehensive CRD implementation provides:

  • Complete CRD definition with validation schema
  • Java operator using Java Operator SDK
  • Automatic resource management (Deployments, Services, ConfigMaps)
  • Status updates and health monitoring
  • Validation webhooks for admission control
  • Metrics collection for monitoring
  • Comprehensive testing framework
  • Spring Boot integration

The operator automatically manages the complete lifecycle of Java applications in Kubernetes based on the custom resource specifications.

Leave a Reply

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


Macro Nepal Helper