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.