Operator Pattern in Java: Building Kubernetes Native Applications

The Operator Pattern extends Kubernetes to manage complex, stateful applications using custom controllers and custom resources. This guide covers implementing Kubernetes Operators in Java using the Fabric8 Kubernetes Client and Java Operator SDK.


Core Concepts

What is a Kubernetes Operator?

  • Custom controller that extends Kubernetes API
  • Manages complex, stateful applications
  • Encodes operational knowledge into software
  • Uses Custom Resource Definitions (CRDs)

Key Components:

  • Custom Resource Definition (CRD): Extends Kubernetes API
  • Custom Resource (CR): Instance of the CRD
  • Controller/Operator: Reconciliation loop
  • Reconciliation: Desired state vs. current state

Dependencies and Setup

1. Maven Dependencies
<properties>
<java-operator-sdk.version>4.4.0</java-operator-sdk.version>
<fabric8.version>6.7.2</fabric8.version>
<quarkus.version>3.2.3.Final</quarkus.version>
</properties>
<dependencies>
<!-- Java Operator SDK -->
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework</artifactId>
<version>${java-operator-sdk.version}</version>
</dependency>
<!-- Kubernetes Client -->
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kubernetes-client</artifactId>
<version>${fabric8.version}</version>
</dependency>
<!-- Quarkus (optional, for runtime) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<version>${quarkus.version}</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.javaoperatorsdk</groupId>
<artifactId>operator-framework-junit</artifactId>
<version>${java-operator-sdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
2. Project Structure
java-operator/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── operator/
│   │   │               ├── crd/
│   │   │               ├── controller/
│   │   │               ├── service/
│   │   │               └── OperatorApplication.java
│   │   └── resources/
│   │       └── crds/
│   └── test/
│       └── java/
│           └── com/
│               └── example/
│                   └── operator/
└── pom.xml

Core Implementation

1. Custom Resource Definition (CRD)
// src/main/java/com/example/operator/crd/JavaApp.java
package com.example.operator.crd;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.Version;
import io.fabric8.kubernetes.model.annotation.Kind;
@Group("operator.java.example.com")
@Version("v1alpha1")
@Kind("JavaApp")
public class JavaApp extends CustomResource<JavaAppSpec, JavaAppStatus> {
@Override
protected JavaAppStatus initStatus() {
return new JavaAppStatus();
}
}
// JavaAppSpec - Desired state
package com.example.operator.crd;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.fabric8.kubernetes.model.annotation.PrinterColumn;
@JsonDeserialize
public class JavaAppSpec {
@JsonProperty
@PrinterColumn(name = "APPLICATION", description = "The application name", priority = 1)
private String applicationName;
@JsonProperty
@PrinterColumn(name = "VERSION", description = "The application version", priority = 2)
private String version;
@JsonProperty
private int replicas = 1;
@JsonProperty
private ResourceRequirements resources;
@JsonProperty
private DatabaseConfig database;
@JsonProperty
private CacheConfig cache;
@JsonProperty
private ServiceConfig service;
// 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 int getReplicas() { return replicas; }
public void setReplicas(int replicas) { this.replicas = replicas; }
public ResourceRequirements getResources() { return resources; }
public void setResources(ResourceRequirements resources) { this.resources = resources; }
public DatabaseConfig getDatabase() { return database; }
public void setDatabase(DatabaseConfig database) { this.database = database; }
public CacheConfig getCache() { return cache; }
public void setCache(CacheConfig cache) { this.cache = cache; }
public ServiceConfig getService() { return service; }
public void setService(ServiceConfig service) { this.service = service; }
}
// Supporting classes
package com.example.operator.crd;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize
public class ResourceRequirements {
@JsonProperty
private String memory = "512Mi";
@JsonProperty
private String cpu = "250m";
@JsonProperty
private String storage = "10Gi";
// Getters and setters
public String getMemory() { return memory; }
public void setMemory(String memory) { this.memory = memory; }
public String getCpu() { return cpu; }
public void setCpu(String cpu) { this.cpu = cpu; }
public String getStorage() { return storage; }
public void setStorage(String storage) { this.storage = storage; }
}
@JsonDeserialize
public class DatabaseConfig {
@JsonProperty
private boolean enabled = false;
@JsonProperty
private String type = "postgresql";
@JsonProperty
private String version = "13";
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
}
@JsonDeserialize
public class CacheConfig {
@JsonProperty
private boolean enabled = false;
@JsonProperty
private String type = "redis";
@JsonProperty
private String version = "7.0";
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
}
@JsonDeserialize
public class ServiceConfig {
@JsonProperty
private String type = "ClusterIP";
@JsonProperty
private int port = 8080;
@JsonProperty
private boolean metricsEnabled = true;
@JsonProperty
private boolean healthChecksEnabled = true;
// Getters and setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public boolean isMetricsEnabled() { return metricsEnabled; }
public void setMetricsEnabled(boolean metricsEnabled) { this.metricsEnabled = metricsEnabled; }
public boolean isHealthChecksEnabled() { return healthChecksEnabled; }
public void setHealthChecksEnabled(boolean healthChecksEnabled) { this.healthChecksEnabled = healthChecksEnabled; }
}
// JavaAppStatus - Actual state
package com.example.operator.crd;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.fabric8.kubernetes.model.annotation.PrinterColumn;
import java.util.List;
@JsonDeserialize
public class JavaAppStatus {
@JsonProperty
@PrinterColumn(name = "PHASE", description = "The current phase", priority = 0)
private String phase = "Pending";
@JsonProperty
private String message;
@JsonProperty
private int availableReplicas;
@JsonProperty
private List<Condition> conditions;
@JsonProperty
private String lastUpdateTime;
// Getters and setters
public String getPhase() { return phase; }
public void setPhase(String phase) { this.phase = phase; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public int getAvailableReplicas() { return availableReplicas; }
public void setAvailableReplicas(int availableReplicas) { this.availableReplicas = availableReplicas; }
public List<Condition> getConditions() { return conditions; }
public void setConditions(List<Condition> conditions) { this.conditions = conditions; }
public String getLastUpdateTime() { return lastUpdateTime; }
public void setLastUpdateTime(String lastUpdateTime) { this.lastUpdateTime = lastUpdateTime; }
}
@JsonDeserialize
public class Condition {
@JsonProperty
private String type;
@JsonProperty
private String status;
@JsonProperty
private String reason;
@JsonProperty
private String message;
@JsonProperty
private String lastTransitionTime;
// Getters and setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getLastTransitionTime() { return lastTransitionTime; }
public void setLastTransitionTime(String lastTransitionTime) { this.lastTransitionTime = lastTransitionTime; }
}
2. CRD YAML Definition
# src/main/resources/crds/javaapp-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: javaapps.operator.java.example.com
labels:
app.kubernetes.io/name: javaapp
app.kubernetes.io/version: "1.0.0"
spec:
group: operator.java.example.com
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
applicationName:
type: string
version:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
resources:
type: object
properties:
memory:
type: string
cpu:
type: string
storage:
type: string
database:
type: object
properties:
enabled:
type: boolean
type:
type: string
version:
type: string
cache:
type: object
properties:
enabled:
type: boolean
type:
type: string
version:
type: string
service:
type: object
properties:
type:
type: string
port:
type: integer
metricsEnabled:
type: boolean
healthChecksEnabled:
type: boolean
status:
type: object
properties:
phase:
type: string
message:
type: string
availableReplicas:
type: integer
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
reason:
type: string
message:
type: string
lastTransitionTime:
type: string
subresources:
status: {}
scope: Namespaced
names:
plural: javaapps
singular: javaapp
kind: JavaApp
shortNames:
- japp
categories:
- all
3. Operator Controller
// src/main/java/com/example/operator/controller/JavaAppController.java
package com.example.operator.controller;
import com.example.operator.crd.JavaApp;
import com.example.operator.crd.JavaAppStatus;
import com.example.operator.service.KubernetesService;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
@ControllerConfiguration(
namespaces = Constants.WATCH_CURRENT_NAMESPACE,
name = "javaapp",
retry = @Retry(maxAttempts = 5, backoff = @Backoff(delay = 2, delayUnit = TimeUnit.SECONDS))
)
public class JavaAppController implements Reconciler<JavaApp> {
private static final Logger log = LoggerFactory.getLogger(JavaAppController.class);
private final KubernetesService kubernetesService;
public JavaAppController(KubernetesService kubernetesService) {
this.kubernetesService = kubernetesService;
}
@Override
public UpdateControl<JavaApp> reconcile(JavaApp javaApp, Context<JavaApp> context) {
log.info("Reconciling JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName());
try {
// Update status to Processing
updateStatus(javaApp, "Processing", "Reconciliation in progress");
// 1. Create or update deployment
kubernetesService.createOrUpdateDeployment(javaApp);
// 2. Create or update service
kubernetesService.createOrUpdateService(javaApp);
// 3. Create or update config maps
kubernetesService.createOrUpdateConfigMaps(javaApp);
// 4. Handle database if enabled
if (javaApp.getSpec().getDatabase().isEnabled()) {
kubernetesService.createOrUpdateDatabase(javaApp);
}
// 5. Handle cache if enabled
if (javaApp.getSpec().getCache().isEnabled()) {
kubernetesService.createOrUpdateCache(javaApp);
}
// 6. Update status with success
updateStatus(javaApp, "Ready", "Application deployed successfully");
log.info("Successfully reconciled JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName());
return UpdateControl.updateStatus(javaApp);
} catch (Exception e) {
log.error("Error reconciling JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName(), e);
updateStatus(javaApp, "Error", "Reconciliation failed: " + e.getMessage());
return UpdateControl.updateStatus(javaApp);
}
}
@Override
public DeleteControl cleanup(JavaApp javaApp, Context<JavaApp> context) {
log.info("Cleaning up JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName());
try {
kubernetesService.cleanupResources(javaApp);
log.info("Successfully cleaned up JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName());
} catch (Exception e) {
log.error("Error cleaning up JavaApp: {}/{}", 
javaApp.getMetadata().getNamespace(), 
javaApp.getMetadata().getName(), e);
}
return DeleteControl.defaultDelete();
}
private void updateStatus(JavaApp javaApp, String phase, String message) {
JavaAppStatus status = javaApp.getStatus();
if (status == null) {
status = new JavaAppStatus();
javaApp.setStatus(status);
}
status.setPhase(phase);
status.setMessage(message);
status.setLastUpdateTime(Instant.now().toString());
// Update available replicas from deployment status
int availableReplicas = kubernetesService.getAvailableReplicas(
javaApp.getMetadata().getNamespace(),
javaApp.getMetadata().getName()
);
status.setAvailableReplicas(availableReplicas);
}
}
4. Kubernetes Service
// src/main/java/com/example/operator/service/KubernetesService.java
package com.example.operator.service;
import com.example.operator.crd.JavaApp;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
@ApplicationScoped
public class KubernetesService {
private static final Logger log = LoggerFactory.getLogger(KubernetesService.class);
@Inject
KubernetesClient kubernetesClient;
private static final String APP_LABEL = "app.kubernetes.io/name";
private static final String INSTANCE_LABEL = "app.kubernetes.io/instance";
private static final String MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
private static final String MANAGED_BY_VALUE = "java-operator";
public void createOrUpdateDeployment(JavaApp javaApp) {
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
Deployment deployment = new DeploymentBuilder()
.withNewMetadata()
.withName(name)
.withNamespace(namespace)
.withLabels(createLabels(javaApp))
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withNewSpec()
.withReplicas(javaApp.getSpec().getReplicas())
.withNewSelector()
.withMatchLabels(createMatchLabels(javaApp))
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(createLabels(javaApp))
.endMetadata()
.withNewSpec()
.addNewContainer()
.withName("java-app")
.withImage("eclipse-temurin:17-jre")
.withImagePullPolicy("IfNotPresent")
.withPorts(createContainerPorts(javaApp))
.withEnv(createEnvironmentVariables(javaApp))
.withResources(createResourceRequirements(javaApp))
.withLivenessProbe(createLivenessProbe(javaApp))
.withReadinessProbe(createReadinessProbe(javaApp))
.withVolumeMounts(createVolumeMounts(javaApp))
.endContainer()
.withVolumes(createVolumes(javaApp))
.withRestartPolicy("Always")
.endSpec()
.endTemplate()
.endSpec()
.build();
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.resource(deployment)
.createOrReplace();
log.info("Created/Updated deployment for JavaApp: {}/{}", namespace, name);
}
public void createOrUpdateService(JavaApp javaApp) {
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
Service service = new ServiceBuilder()
.withNewMetadata()
.withName(name + "-service")
.withNamespace(namespace)
.withLabels(createLabels(javaApp))
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withNewSpec()
.withType(javaApp.getSpec().getService().getType())
.withSelector(createMatchLabels(javaApp))
.addNewPort()
.withPort(javaApp.getSpec().getService().getPort())
.withTargetPort(new IntOrString(javaApp.getSpec().getService().getPort()))
.withProtocol("TCP")
.endPort()
.endSpec()
.build();
kubernetesClient.services()
.inNamespace(namespace)
.resource(service)
.createOrReplace();
log.info("Created/Updated service for JavaApp: {}/{}", namespace, name);
}
public void createOrUpdateConfigMaps(JavaApp javaApp) {
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
// Application configuration
ConfigMap appConfig = new ConfigMapBuilder()
.withNewMetadata()
.withName(name + "-config")
.withNamespace(namespace)
.withLabels(createLabels(javaApp))
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withData(createConfigMapData(javaApp))
.build();
kubernetesClient.configMaps()
.inNamespace(namespace)
.resource(appConfig)
.createOrReplace();
log.info("Created/Updated config maps for JavaApp: {}/{}", namespace, name);
}
public void createOrUpdateDatabase(JavaApp javaApp) {
if (!javaApp.getSpec().getDatabase().isEnabled()) {
return;
}
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
// Create database deployment
Deployment dbDeployment = new DeploymentBuilder()
.withNewMetadata()
.withName(name + "-database")
.withNamespace(namespace)
.withLabels(createLabels(javaApp))
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withNewSpec()
.withReplicas(1)
.withNewSelector()
.withMatchLabels(createDatabaseLabels(javaApp))
.endSelector()
.withNewTemplate()
.withNewMetadata()
.withLabels(createDatabaseLabels(javaApp))
.endMetadata()
.withNewSpec()
.addNewContainer()
.withName("database")
.withImage(javaApp.getSpec().getDatabase().getType() + ":" + 
javaApp.getSpec().getDatabase().getVersion())
.withEnv(createDatabaseEnvVars(javaApp))
.withPorts(new ContainerPortBuilder()
.withContainerPort(5432)
.build())
.withResources(createDatabaseResourceRequirements(javaApp))
.withVolumeMounts(new VolumeMountBuilder()
.withName("db-data")
.withMountPath("/var/lib/postgresql/data")
.build())
.endContainer()
.withVolumes(new VolumeBuilder()
.withName("db-data")
.withNewPersistentVolumeClaim()
.withClaimName(name + "-db-pvc")
.endPersistentVolumeClaim()
.build())
.endSpec()
.endTemplate()
.endSpec()
.build();
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.resource(dbDeployment)
.createOrReplace();
// Create database service
Service dbService = new ServiceBuilder()
.withNewMetadata()
.withName(name + "-database-service")
.withNamespace(namespace)
.withLabels(createDatabaseLabels(javaApp))
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withNewSpec()
.withSelector(createDatabaseLabels(javaApp))
.addNewPort()
.withPort(5432)
.withTargetPort(new IntOrString(5432))
.endPort()
.endSpec()
.build();
kubernetesClient.services()
.inNamespace(namespace)
.resource(dbService)
.createOrReplace();
// Create PVC
PersistentVolumeClaim pvc = new PersistentVolumeClaimBuilder()
.withNewMetadata()
.withName(name + "-db-pvc")
.withNamespace(namespace)
.addToOwnerReferences(createOwnerReference(javaApp))
.endMetadata()
.withNewSpec()
.withAccessModes("ReadWriteOnce")
.withNewResources()
.withRequests(Map.of("storage", new Quantity(javaApp.getSpec().getResources().getStorage())))
.endResources()
.endSpec()
.build();
kubernetesClient.persistentVolumeClaims()
.inNamespace(namespace)
.resource(pvc)
.createOrReplace();
log.info("Created/Updated database for JavaApp: {}/{}", namespace, name);
}
public void createOrUpdateCache(JavaApp javaApp) {
if (!javaApp.getSpec().getCache().isEnabled()) {
return;
}
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
// Similar implementation for cache (Redis, etc.)
log.info("Cache deployment for JavaApp: {}/{}", namespace, name);
}
public void cleanupResources(JavaApp javaApp) {
String namespace = javaApp.getMetadata().getNamespace();
String name = javaApp.getMetadata().getName();
// Delete deployment
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name)
.delete();
// Delete service
kubernetesClient.services()
.inNamespace(namespace)
.withName(name + "-service")
.delete();
// Delete config maps
kubernetesClient.configMaps()
.inNamespace(namespace)
.withName(name + "-config")
.delete();
// Delete database resources if they exist
if (javaApp.getSpec().getDatabase().isEnabled()) {
kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name + "-database")
.delete();
kubernetesClient.services()
.inNamespace(namespace)
.withName(name + "-database-service")
.delete();
kubernetesClient.persistentVolumeClaims()
.inNamespace(namespace)
.withName(name + "-db-pvc")
.delete();
}
log.info("Cleaned up resources for JavaApp: {}/{}", namespace, name);
}
public int getAvailableReplicas(String namespace, String name) {
try {
Deployment deployment = kubernetesClient.apps().deployments()
.inNamespace(namespace)
.withName(name)
.get();
return deployment != null && deployment.getStatus() != null ? 
deployment.getStatus().getAvailableReplicas() : 0;
} catch (Exception e) {
log.warn("Could not get available replicas for {}/{}", namespace, name, e);
return 0;
}
}
// Helper methods for creating Kubernetes objects
private Map<String, String> createLabels(JavaApp javaApp) {
Map<String, String> labels = new HashMap<>();
labels.put(APP_LABEL, "java-app");
labels.put(INSTANCE_LABEL, javaApp.getMetadata().getName());
labels.put(MANAGED_BY_LABEL, MANAGED_BY_VALUE);
labels.put("app", javaApp.getMetadata().getName());
return labels;
}
private Map<String, String> createMatchLabels(JavaApp javaApp) {
Map<String, String> labels = new HashMap<>();
labels.put("app", javaApp.getMetadata().getName());
return labels;
}
private Map<String, String> createDatabaseLabels(JavaApp javaApp) {
Map<String, String> labels = new HashMap<>();
labels.put(APP_LABEL, "database");
labels.put(INSTANCE_LABEL, javaApp.getMetadata().getName() + "-database");
labels.put(MANAGED_BY_LABEL, MANAGED_BY_VALUE);
return labels;
}
private OwnerReference createOwnerReference(JavaApp javaApp) {
return new OwnerReferenceBuilder()
.withApiVersion(javaApp.getApiVersion())
.withKind(javaApp.getKind())
.withName(javaApp.getMetadata().getName())
.withUid(javaApp.getMetadata().getUid())
.withBlockOwnerDeletion(true)
.withController(true)
.build();
}
private List<ContainerPort> createContainerPorts(JavaApp javaApp) {
ContainerPort port = new ContainerPortBuilder()
.withContainerPort(javaApp.getSpec().getService().getPort())
.withProtocol("TCP")
.build();
return List.of(port);
}
private List<EnvVar> createEnvironmentVariables(JavaApp javaApp) {
List<EnvVar> envVars = new ArrayList<>();
envVars.add(new EnvVarBuilder()
.withName("JAVA_OPTS")
.withValue("-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0")
.build());
envVars.add(new EnvVarBuilder()
.withName("SPRING_PROFILES_ACTIVE")
.withValue("kubernetes")
.build());
if (javaApp.getSpec().getDatabase().isEnabled()) {
envVars.add(new EnvVarBuilder()
.withName("SPRING_DATASOURCE_URL")
.withValue("jdbc:postgresql://" + javaApp.getMetadata().getName() + "-database-service:5432/" + javaApp.getMetadata().getName())
.build());
}
return envVars;
}
private List<EnvVar> createDatabaseEnvVars(JavaApp javaApp) {
List<EnvVar> envVars = new ArrayList<>();
envVars.add(new EnvVarBuilder()
.withName("POSTGRES_DB")
.withValue(javaApp.getMetadata().getName())
.build());
envVars.add(new EnvVarBuilder()
.withName("POSTGRES_USER")
.withValue("appuser")
.build());
envVars.add(new EnvVarBuilder()
.withName("POSTGRES_PASSWORD")
.withValue("password")
.build());
return envVars;
}
private ResourceRequirements createResourceRequirements(JavaApp javaApp) {
return new ResourceRequirementsBuilder()
.withRequests(Map.of(
"memory", new Quantity(javaApp.getSpec().getResources().getMemory()),
"cpu", new Quantity(javaApp.getSpec().getResources().getCpu())
))
.withLimits(Map.of(
"memory", new Quantity(javaApp.getSpec().getResources().getMemory()),
"cpu", new Quantity(javaApp.getSpec().getResources().getCpu())
))
.build();
}
private ResourceRequirements createDatabaseResourceRequirements(JavaApp javaApp) {
return new ResourceRequirementsBuilder()
.withRequests(Map.of(
"memory", new Quantity("256Mi"),
"cpu", new Quantity("100m")
))
.withLimits(Map.of(
"memory", new Quantity("512Mi"),
"cpu", new Quantity("200m")
))
.build();
}
private Probe createLivenessProbe(JavaApp javaApp) {
if (!javaApp.getSpec().getService().isHealthChecksEnabled()) {
return null;
}
return new ProbeBuilder()
.withNewHttpGet()
.withPath("/actuator/health/liveness")
.withPort(new IntOrString(javaApp.getSpec().getService().getPort()))
.endHttpGet()
.withInitialDelaySeconds(30)
.withPeriodSeconds(10)
.withTimeoutSeconds(5)
.withFailureThreshold(3)
.build();
}
private Probe createReadinessProbe(JavaApp javaApp) {
if (!javaApp.getSpec().getService().isHealthChecksEnabled()) {
return null;
}
return new ProbeBuilder()
.withNewHttpGet()
.withPath("/actuator/health/readiness")
.withPort(new IntOrString(javaApp.getSpec().getService().getPort()))
.endHttpGet()
.withInitialDelaySeconds(5)
.withPeriodSeconds(5)
.withTimeoutSeconds(3)
.withFailureThreshold(3)
.build();
}
private List<VolumeMount> createVolumeMounts(JavaApp javaApp) {
VolumeMount configMount = new VolumeMountBuilder()
.withName("config-volume")
.withMountPath("/app/config")
.withReadOnly(true)
.build();
return List.of(configMount);
}
private List<Volume> createVolumes(JavaApp javaApp) {
Volume configVolume = new VolumeBuilder()
.withName("config-volume")
.withNewConfigMap()
.withName(javaApp.getMetadata().getName() + "-config")
.endConfigMap()
.build();
return List.of(configVolume);
}
private Map<String, String> createConfigMapData(JavaApp javaApp) {
Map<String, String> data = new HashMap<>();
String applicationYml = """
server:
port: %d
spring:
application:
name: %s
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
""".formatted(javaApp.getSpec().getService().getPort(), javaApp.getSpec().getApplicationName());
data.put("application.yml", applicationYml);
return data;
}
}
5. Operator Application
// src/main/java/com/example/operator/OperatorApplication.java
package com.example.operator;
import com.example.operator.controller.JavaAppController;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientBuilder;
import io.javaoperatorsdk.operator.Operator;
import io.quarkus.runtime.Quarkus;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.annotations.QuarkusMain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@QuarkusMain
public class OperatorApplication {
public static void main(String[] args) {
Quarkus.run(OperatorRunner.class, args);
}
public static class OperatorRunner implements QuarkusApplication {
private static final Logger log = LoggerFactory.getLogger(OperatorRunner.class);
@Inject
Operator operator;
@Override
public int run(String... args) throws Exception {
log.info("Starting Java Operator...");
operator.start();
log.info("Java Operator started successfully");
Quarkus.waitForExit();
return 0;
}
}
}
// Operator Configuration
package com.example.operator;
import com.example.operator.controller.JavaAppController;
import io.javaoperatorsdk.operator.Operator;
import io.quarkus.runtime.StartupEvent;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
@ApplicationScoped
public class OperatorConfiguration {
@Inject
JavaAppController javaAppController;
void onStartup(@Observes StartupEvent ev) {
// Operator startup logic handled by Quarkus extension
}
@Produces
@ApplicationScoped
Operator operator(JavaAppController javaAppController) {
Operator operator = new Operator();
operator.register(javaAppController);
return operator;
}
}

Deployment Configuration

1. Operator Deployment
# src/main/resources/kubernetes/operator-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-operator
namespace: java-operator-system
labels:
app.kubernetes.io/name: java-operator
app.kubernetes.io/version: "1.0.0"
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: java-operator
template:
metadata:
labels:
app.kubernetes.io/name: java-operator
spec:
serviceAccountName: java-operator
containers:
- name: operator
image: ghcr.io/myorg/java-operator:latest
ports:
- containerPort: 8080
protocol: TCP
env:
- name: WATCH_NAMESPACE
value: ""
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /q/health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /q/health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: java-operator
namespace: java-operator-system
labels:
app.kubernetes.io/name: java-operator
spec:
ports:
- port: 8080
targetPort: 8080
protocol: TCP
name: http
selector:
app.kubernetes.io/name: java-operator
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: java-operator
namespace: java-operator-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: java-operator
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "persistentvolumeclaims"]
verbs: ["*"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["*"]
- apiGroups: ["operator.java.example.com"]
resources: ["javaapps"]
verbs: ["*"]
- apiGroups: ["operator.java.example.com"]
resources: ["javaapps/status"]
verbs: ["get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: java-operator
subjects:
- kind: ServiceAccount
name: java-operator
namespace: java-operator-system
roleRef:
kind: ClusterRole
name: java-operator
apiGroup: rbac.authorization.k8s.io
2. Custom Resource Example
# examples/my-java-app.yaml
apiVersion: operator.java.example.com/v1alpha1
kind: JavaApp
metadata:
name: my-java-application
namespace: default
spec:
applicationName: "my-java-app"
version: "1.0.0"
replicas: 3
resources:
memory: "1Gi"
cpu: "500m"
storage: "20Gi"
database:
enabled: true
type: "postgresql"
version: "13"
cache:
enabled: true
type: "redis"
version: "7.0"
service:
type: "ClusterIP"
port: 8080
metricsEnabled: true
healthChecksEnabled: true

Testing

1. Unit Tests
// src/test/java/com/example/operator/JavaAppControllerTest.java
package com.example.operator;
import com.example.operator.controller.JavaAppController;
import com.example.operator.crd.JavaApp;
import com.example.operator.crd.JavaAppSpec;
import com.example.operator.service.KubernetesService;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class JavaAppControllerTest {
@Mock
private KubernetesService kubernetesService;
private JavaAppController controller;
@BeforeEach
void setUp() {
controller = new JavaAppController(kubernetesService);
}
@Test
void shouldReconcileJavaAppSuccessfully() {
// Given
JavaApp javaApp = createTestJavaApp();
// When
UpdateControl<JavaApp> result = controller.reconcile(javaApp, null);
// Then
assertNotNull(result);
assertTrue(result.isUpdateStatusSubResource());
verify(kubernetesService).createOrUpdateDeployment(javaApp);
verify(kubernetesService).createOrUpdateService(javaApp);
verify(kubernetesService).createOrUpdateConfigMaps(javaApp);
}
@Test
void shouldHandleDatabaseWhenEnabled() {
// Given
JavaApp javaApp = createTestJavaApp();
javaApp.getSpec().getDatabase().setEnabled(true);
// When
controller.reconcile(javaApp, null);
// Then
verify(kubernetesService).createOrUpdateDatabase(javaApp);
}
@Test
void shouldCleanupResourcesOnDelete() {
// Given
JavaApp javaApp = createTestJavaApp();
// When
controller.cleanup(javaApp, null);
// Then
verify(kubernetesService).cleanupResources(javaApp);
}
private JavaApp createTestJavaApp() {
JavaApp javaApp = new JavaApp();
ObjectMeta metadata = new ObjectMeta();
metadata.setName("test-app");
metadata.setNamespace("default");
javaApp.setMetadata(metadata);
JavaAppSpec spec = new JavaAppSpec();
spec.setApplicationName("Test Application");
spec.setVersion("1.0.0");
spec.setReplicas(2);
javaApp.setSpec(spec);
return javaApp;
}
}
2. Integration Tests
// src/test/java/com/example/operator/JavaAppControllerIT.java
package com.example.operator;
import com.example.operator.crd.JavaApp;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.awaitility.Awaitility.await;
import static java.util.concurrent.TimeUnit.SECONDS;
class JavaAppControllerIT {
@RegisterExtension
LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder()
.withReconciler(new JavaAppController(operator.getKubernetesClient()))
.build();
@Test
void shouldCreateJavaAppResource() {
// Given
JavaApp javaApp = createTestJavaApp();
// When
operator.getKubernetesClient().resources(JavaApp.class)
.inNamespace("default")
.resource(javaApp)
.create();
// Then
await().atMost(30, SECONDS).until(() -> {
JavaApp updated = operator.getKubernetesClient().resources(JavaApp.class)
.inNamespace("default")
.withName("test-app")
.get();
return updated != null && "Ready".equals(updated.getStatus().getPhase());
});
}
private JavaApp createTestJavaApp() {
JavaApp javaApp = new JavaApp();
ObjectMeta metadata = new ObjectMeta();
metadata.setName("test-app");
metadata.setNamespace("default");
javaApp.setMetadata(metadata);
// Set spec as needed
return javaApp;
}
}

Advanced Features

1. Event Handling
// Event handling in controller
package com.example.operator.controller;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.timer.TimerEventSource;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class JavaAppController implements Reconciler<JavaApp>, EventSourceInitializer<JavaApp> {
private TimerEventSource timerEventSource;
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<JavaApp> context) {
timerEventSource = new TimerEventSource();
return Map.of("timer", timerEventSource);
}
public void scheduleReconciliation(JavaApp javaApp, long delay, TimeUnit unit) {
timerEventSource.schedule(javaApp, delay, unit);
}
}
2. Metrics Integration
// Metrics service
package com.example.operator.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
@ApplicationScoped
public class MetricsService {
private static final Logger log = LoggerFactory.getLogger(MetricsService.class);
@Inject
MeterRegistry meterRegistry;
private final Counter reconciliationCounter;
private final Counter errorCounter;
public MetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.reconciliationCounter = Counter.builder("java_operator_reconciliations_total")
.description("Total number of reconciliations")
.register(meterRegistry);
this.errorCounter = Counter.builder("java_operator_errors_total")
.description("Total number of reconciliation errors")
.register(meterRegistry);
}
public void recordReconciliation(String appName, String namespace) {
reconciliationCounter.increment();
log.info("Recorded reconciliation for {}/{}", namespace, appName);
}
public void recordError(String appName, String namespace) {
errorCounter.increment();
log.error("Recorded error for {}/{}", namespace, appName);
}
}

Best Practices

  1. Idempotent Operations: Ensure reconciliation can run multiple times safely
  2. Owner References: Set owner references for garbage collection
  3. Status Updates: Keep status updated with meaningful information
  4. Error Handling: Implement robust error handling and retry logic
  5. Resource Management: Clean up resources properly
  6. Testing: Comprehensive unit and integration tests
  7. Monitoring: Implement metrics and logging
  8. Security: Follow Kubernetes security best practices

Conclusion

The Operator Pattern in Java provides:

  • Custom Automation: Encode operational knowledge into software
  • Kubernetes Native: First-class Kubernetes citizens
  • Declarative API: Users declare desired state
  • Self-Healing: Automatic reconciliation
  • Complex Application Management: Handle stateful applications

By implementing operators in Java, you leverage the rich Java ecosystem while building powerful Kubernetes-native applications that can manage complex operational workflows automatically.

Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/

OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/

OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/

Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.

https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics

Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2

Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.

https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide

Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2

Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.

https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide

Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.

https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server

Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.

Leave a Reply

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


Macro Nepal Helper