GitOps is a methodology that uses Git as the single source of truth for infrastructure and application deployment. Flux is a popular GitOps tool that automatically syncs Kubernetes clusters with configuration stored in Git repositories.
Project Setup and Dependencies
1. Maven Dependencies
<dependencies> <!-- Kubernetes Client --> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java</artifactId> <version>18.0.0</version> </dependency> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java-api-fluent</artifactId> <version>18.0.0</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>3.1.0</version> </dependency> <!-- Git Integration --> <dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> <version>6.7.0.202309050840-r</version> </dependency> <!-- YAML Processing --> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>2.0</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Metrics --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>3.1.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.kubernetes</groupId> <artifactId>client-java-spring-integration</artifactId> <version>18.0.0</version> <scope>test</scope> </dependency> </dependencies>
Core Models and Configuration
1. GitOps Configuration
// GitOpsConfig.java
@Data
@Configuration
@ConfigurationProperties(prefix = "gitops")
public class GitOpsConfig {
private boolean enabled = true;
private String clusterName;
private String environment;
private GitConfig git = new GitConfig();
private FluxConfig flux = new FluxConfig();
private SyncConfig sync = new SyncConfig();
private NotificationConfig notification = new NotificationConfig();
@Data
public static class GitConfig {
private String url;
private String branch = "main";
private String path = "./manifests";
private String username;
private String token;
private String email;
private int timeoutSeconds = 30;
}
@Data
public static class FluxConfig {
private String namespace = "flux-system";
private String version = "v2.0.0";
private boolean autoCreation = true;
private List<FluxRepository> repositories = new ArrayList<>();
}
@Data
public static class SyncConfig {
private boolean autoSync = true;
private Duration syncInterval = Duration.ofMinutes(5);
private Duration timeout = Duration.ofMinutes(10);
private List<String> pruneLabels = List.of("app.kubernetes.io/part-of");
private boolean dryRun = false;
}
@Data
public static class NotificationConfig {
private boolean enabled = true;
private List<String> providers = List.of("slack", "webhook");
private String slackWebhookUrl;
private String webhookUrl;
}
}
// FluxRepository.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FluxRepository {
private String name;
private String url;
private String branch;
private String path;
private String secretRef;
private SyncInterval interval;
private List<FluxKustomization> kustomizations = new ArrayList<>();
public enum SyncInterval {
ONE_MINUTE("1m"),
FIVE_MINUTES("5m"),
FIFTEEN_MINUTES("15m"),
THIRTY_MINUTES("30m"),
ONE_HOUR("1h");
private final String value;
SyncInterval(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
// FluxKustomization.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FluxKustomization {
private String name;
private String namespace;
private String path;
private String targetNamespace;
private List<String> dependsOn;
private List<FluxHealthCheck> healthChecks;
private PruneConfig prune;
private ValidationConfig validation;
private List<FluxPatch> patches;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class PruneConfig {
private boolean enabled = true;
private List<String> labels;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ValidationConfig {
private boolean enabled = true;
private List<String> ignorePatterns;
}
}
// FluxHealthCheck.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FluxHealthCheck {
private String apiVersion;
private String kind;
private String name;
private String namespace;
private Duration timeout;
}
2. Kubernetes Resource Models
// FluxCustomResources.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GitRepository {
private String apiVersion = "source.toolkit.fluxcd.io/v1";
private String kind = "GitRepository";
private Metadata metadata;
private GitRepositorySpec spec;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class GitRepositorySpec {
private String url;
private String branch;
private String interval;
private SecretRef secretRef;
private List<Include> include;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SecretRef {
private String name;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Include {
private String repository;
private String fromPath;
private String toPath;
}
}
// Kustomization.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Kustomization {
private String apiVersion = "kustomize.toolkit.fluxcd.io/v1";
private String kind = "Kustomization";
private Metadata metadata;
private KustomizationSpec spec;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class KustomizationSpec {
private String interval;
private String path;
private String sourceRef;
private String targetNamespace;
private List<String> dependsOn;
private HealthChecks healthChecks;
private Prune prune;
private Validation validation;
private List<JSONPatch> patches;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class HealthChecks {
private List<HealthCheck> deployments;
private List<HealthCheck> services;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class HealthCheck {
private String apiVersion;
private String kind;
private String name;
private String namespace;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Prune {
private boolean enabled = true;
private List<String> labels;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Validation {
private boolean enabled = true;
private List<String> ignorePatterns;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class JSONPatch {
private String op;
private String path;
private Object value;
}
}
// Metadata.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Metadata {
private String name;
private String namespace;
private Map<String, String> labels;
private Map<String, String> annotations;
public Metadata(String name, String namespace) {
this.name = name;
this.namespace = namespace;
this.labels = new HashMap<>();
this.annotations = new HashMap<>();
}
}
3. Sync and Deployment Status
// SyncStatus.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SyncStatus {
private String repository;
private String branch;
private String commit;
private SyncState state;
private ZonedDateTime lastSync;
private ZonedDateTime lastAttempt;
private String message;
private List<SyncResource> resources;
private List<SyncCondition> conditions;
public enum SyncState {
SYNCED,
OUT_OF_SYNC,
ERROR,
PROGRESSING,
UNKNOWN
}
}
// SyncResource.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SyncResource {
private String name;
private String namespace;
private String kind;
private String apiVersion;
private SyncState state;
private String message;
private ZonedDateTime lastApplied;
}
// SyncCondition.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SyncCondition {
private String type;
private String status;
private String reason;
private String message;
private ZonedDateTime lastTransitionTime;
}
// DeploymentStatus.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeploymentStatus {
private String name;
private String namespace;
private String image;
private int replicas;
private int readyReplicas;
private int availableReplicas;
private int unavailableReplicas;
private ZonedDateTime lastUpdateTime;
private List<DeploymentCondition> conditions;
public boolean isReady() {
return readyReplicas >= replicas && replicas > 0;
}
public double getAvailability() {
return replicas > 0 ? (double) availableReplicas / replicas * 100 : 100.0;
}
}
// DeploymentCondition.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeploymentCondition {
private String type;
private String status;
private String reason;
private String message;
private ZonedDateTime lastUpdateTime;
}
Core GitOps Service
1. Flux Manager Service
// FluxManagerService.java
@Service
@Slf4j
public class FluxManagerService {
private final KubernetesClient kubernetesClient;
private final GitOpsConfig gitOpsConfig;
private final GitService gitService;
private final ObjectMapper objectMapper;
private final Yaml yaml;
public FluxManagerService(KubernetesClient kubernetesClient,
GitOpsConfig gitOpsConfig,
GitService gitService,
ObjectMapper objectMapper) {
this.kubernetesClient = kubernetesClient;
this.gitOpsConfig = gitOpsConfig;
this.gitService = gitService;
this.objectMapper = objectMapper;
this.yaml = new Yaml();
}
/**
* Bootstrap Flux in the cluster
*/
public BootstrapResult bootstrapFlux() {
try {
log.info("Bootstrapping Flux in cluster: {}", gitOpsConfig.getClusterName());
// Check if Flux is already installed
if (isFluxInstalled()) {
log.info("Flux is already installed in the cluster");
return BootstrapResult.alreadyInstalled();
}
// Create Flux namespace
createFluxNamespace();
// Install Flux components
installFluxControllers();
// Configure Git repository
configureGitRepository();
log.info("Flux bootstrap completed successfully");
return BootstrapResult.success();
} catch (Exception e) {
log.error("Flux bootstrap failed", e);
return BootstrapResult.failure(e.getMessage());
}
}
/**
* Create or update GitRepository resource
*/
public GitRepository createGitRepository(FluxRepository repository) {
try {
GitRepository gitRepo = buildGitRepository(repository);
// Apply GitRepository resource
kubernetesClient.resource(gitRepo)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.createOrReplace();
log.info("Created/Updated GitRepository: {}", repository.getName());
return gitRepo;
} catch (Exception e) {
log.error("Failed to create GitRepository: {}", repository.getName(), e);
throw new FluxOperationException("Failed to create GitRepository", e);
}
}
/**
* Create or update Kustomization resource
*/
public Kustomization createKustomization(FluxKustomization kustomization) {
try {
Kustomization kustomizationResource = buildKustomization(kustomization);
// Apply Kustomization resource
kubernetesClient.resource(kustomizationResource)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.createOrReplace();
log.info("Created/Updated Kustomization: {}", kustomization.getName());
return kustomizationResource;
} catch (Exception e) {
log.error("Failed to create Kustomization: {}", kustomization.getName(), e);
throw new FluxOperationException("Failed to create Kustomization", e);
}
}
/**
* Get sync status for all repositories
*/
public List<SyncStatus> getSyncStatus() {
List<SyncStatus> statusList = new ArrayList<>();
try {
// Get GitRepository resources
List<GitRepository> gitRepos = kubernetesClient.resources(GitRepository.class)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.list()
.getItems();
for (GitRepository gitRepo : gitRepos) {
SyncStatus status = getRepositorySyncStatus(gitRepo);
statusList.add(status);
}
} catch (Exception e) {
log.error("Failed to get sync status", e);
}
return statusList;
}
/**
* Trigger manual sync for a repository
*/
public SyncResult triggerSync(String repositoryName) {
try {
// Annotate the Kustomization to trigger reconciliation
String annotation = String.format(
"{\"metadata\":{\"annotations\":{\"reconcile.fluxcd.io/requestedAt\":\"%s\"}}}",
ZonedDateTime.now().format(DateTimeFormatter.ISO_INSTANT)
);
kubernetesClient.resources(Kustomization.class)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.withName(repositoryName)
.patch(annotation);
log.info("Triggered manual sync for repository: {}", repositoryName);
return SyncResult.success(repositoryName);
} catch (Exception e) {
log.error("Failed to trigger sync for repository: {}", repositoryName, e);
return SyncResult.failure(repositoryName, e.getMessage());
}
}
/**
* Suspend reconciliation for a repository
*/
public boolean suspendReconciliation(String repositoryName) {
try {
Kustomization kustomization = kubernetesClient.resources(Kustomization.class)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.withName(repositoryName)
.get();
if (kustomization != null) {
kustomization.getSpec().setSuspend(true);
kubernetesClient.resource(kustomization).update();
log.info("Suspended reconciliation for: {}", repositoryName);
return true;
}
return false;
} catch (Exception e) {
log.error("Failed to suspend reconciliation for: {}", repositoryName, e);
return false;
}
}
/**
* Resume reconciliation for a repository
*/
public boolean resumeReconciliation(String repositoryName) {
try {
Kustomization kustomization = kubernetesClient.resources(Kustomization.class)
.inNamespace(gitOpsConfig.getFlux().getNamespace())
.withName(repositoryName)
.get();
if (kustomization != null) {
kustomization.getSpec().setSuspend(false);
kubernetesClient.resource(kustomization).update();
log.info("Resumed reconciliation for: {}", repositoryName);
return true;
}
return false;
} catch (Exception e) {
log.error("Failed to resume reconciliation for: {}", repositoryName, e);
return false;
}
}
private GitRepository buildGitRepository(FluxRepository repository) {
return GitRepository.builder()
.metadata(Metadata.builder()
.name(repository.getName())
.namespace(gitOpsConfig.getFlux().getNamespace())
.build())
.spec(GitRepository.GitRepositorySpec.builder()
.url(repository.getUrl())
.branch(repository.getBranch())
.interval(repository.getInterval().getValue())
.secretRef(repository.getSecretRef() != null ?
new GitRepository.SecretRef(repository.getSecretRef()) : null)
.build())
.build();
}
private Kustomization buildKustomization(FluxKustomization kustomization) {
return Kustomization.builder()
.metadata(Metadata.builder()
.name(kustomization.getName())
.namespace(gitOpsConfig.getFlux().getNamespace())
.build())
.spec(Kustomization.KustomizationSpec.builder()
.interval(gitOpsConfig.getSync().getSyncInterval().toString())
.path(kustomization.getPath())
.sourceRef(new Kustomization.SourceRef("GitRepository", kustomization.getName()))
.targetNamespace(kustomization.getTargetNamespace())
.dependsOn(kustomization.getDependsOn())
.healthChecks(buildHealthChecks(kustomization.getHealthChecks()))
.prune(new Kustomization.Prune(
kustomization.getPrune() != null ? kustomization.getPrune().isEnabled() : true,
kustomization.getPrune() != null ? kustomization.getPrune().getLabels() : null
))
.validation(new Kustomization.Validation(
kustomization.getValidation() != null ? kustomization.getValidation().isEnabled() : true,
kustomization.getValidation() != null ? kustomization.getValidation().getIgnorePatterns() : null
))
.build())
.build();
}
private Kustomization.HealthChecks buildHealthChecks(List<FluxHealthCheck> healthChecks) {
if (healthChecks == null || healthChecks.isEmpty()) {
return null;
}
List<Kustomization.HealthCheck> deployments = new ArrayList<>();
List<Kustomization.HealthCheck> services = new ArrayList<>();
for (FluxHealthCheck check : healthChecks) {
Kustomization.HealthCheck healthCheck = new Kustomization.HealthCheck(
check.getApiVersion(),
check.getKind(),
check.getName(),
check.getNamespace()
);
if ("Deployment".equals(check.getKind())) {
deployments.add(healthCheck);
} else if ("Service".equals(check.getKind())) {
services.add(healthCheck);
}
}
return new Kustomization.HealthChecks(deployments, services);
}
private boolean isFluxInstalled() {
try {
// Check if flux-system namespace exists
return kubernetesClient.namespaces()
.withName(gitOpsConfig.getFlux().getNamespace())
.get() != null;
} catch (Exception e) {
return false;
}
}
private void createFluxNamespace() {
try {
Namespace namespace = new Namespace();
namespace.setMetadata(new io.kubernetes.client.openapi.models.V1ObjectMeta()
.name(gitOpsConfig.getFlux().getNamespace())
.labels(Map.of(
"app.kubernetes.io/name", "flux",
"app.kubernetes.io/instance", "flux-system"
)));
kubernetesClient.namespaces().create(namespace);
log.info("Created namespace: {}", gitOpsConfig.getFlux().getNamespace());
} catch (Exception e) {
log.warn("Namespace {} may already exist", gitOpsConfig.getFlux().getNamespace());
}
}
private void installFluxControllers() {
// In a real implementation, this would apply the Flux manifests
// For now, we'll assume manual installation or use the Flux CLI
log.info("Installing Flux controllers...");
// Implementation would apply YAML manifests for source-controller, kustomize-controller, etc.
}
private void configureGitRepository() {
if (gitOpsConfig.getGit().getUrl() != null) {
FluxRepository defaultRepo = FluxRepository.builder()
.name("flux-system")
.url(gitOpsConfig.getGit().getUrl())
.branch(gitOpsConfig.getGit().getBranch())
.interval(FluxRepository.SyncInterval.FIVE_MINUTES)
.build();
createGitRepository(defaultRepo);
}
}
private SyncStatus getRepositorySyncStatus(GitRepository gitRepo) {
// Implementation to get detailed sync status from Flux resources
// This is a simplified version
return SyncStatus.builder()
.repository(gitRepo.getMetadata().getName())
.branch(gitRepo.getSpec().getBranch())
.state(SyncStatus.SyncState.UNKNOWN)
.lastSync(ZonedDateTime.now())
.build();
}
}
// BootstrapResult.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BootstrapResult {
private boolean success;
private String message;
private ZonedDateTime timestamp;
public static BootstrapResult success() {
return BootstrapResult.builder()
.success(true)
.message("Flux bootstrap completed successfully")
.timestamp(ZonedDateTime.now())
.build();
}
public static BootstrapResult failure(String error) {
return BootstrapResult.builder()
.success(false)
.message("Flux bootstrap failed: " + error)
.timestamp(ZonedDateTime.now())
.build();
}
public static BootstrapResult alreadyInstalled() {
return BootstrapResult.builder()
.success(true)
.message("Flux is already installed")
.timestamp(ZonedDateTime.now())
.build();
}
}
// SyncResult.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SyncResult {
private boolean success;
private String repository;
private String message;
private ZonedDateTime timestamp;
public static SyncResult success(String repository) {
return SyncResult.builder()
.success(true)
.repository(repository)
.message("Sync triggered successfully")
.timestamp(ZonedDateTime.now())
.build();
}
public static SyncResult failure(String repository, String error) {
return SyncResult.builder()
.success(false)
.repository(repository)
.message("Sync failed: " + error)
.timestamp(ZonedDateTime.now())
.build();
}
}
2. Git Service for Repository Management
// GitService.java
@Service
@Slf4j
public class GitService {
private final GitOpsConfig gitOpsConfig;
private final ObjectMapper objectMapper;
public GitService(GitOpsConfig gitOpsConfig, ObjectMapper objectMapper) {
this.gitOpsConfig = gitOpsConfig;
this.objectMapper = objectMapper;
}
/**
* Clone or pull Git repository
*/
public GitOperationResult syncRepository(String localPath) {
try {
File repoDir = new File(localPath);
Git git = null;
if (repoDir.exists() && new File(repoDir, ".git").exists()) {
// Pull latest changes
git = Git.open(repoDir);
PullResult pullResult = git.pull()
.setCredentialsProvider(createCredentialsProvider())
.call();
log.info("Pulled latest changes from repository");
return GitOperationResult.success("Pulled latest changes",
pullResult.getFetchResult().getMessages());
} else {
// Clone repository
CloneCommand cloneCommand = Git.cloneRepository()
.setURI(gitOpsConfig.getGit().getUrl())
.setBranch(gitOpsConfig.getGit().getBranch())
.setDirectory(repoDir)
.setCredentialsProvider(createCredentialsProvider());
git = cloneCommand.call();
log.info("Cloned repository to: {}", localPath);
return GitOperationResult.success("Repository cloned successfully");
}
} catch (Exception e) {
log.error("Failed to sync repository", e);
return GitOperationResult.failure("Sync failed: " + e.getMessage());
}
}
/**
* Commit and push changes to Git
*/
public GitOperationResult commitAndPush(String localPath,
String commitMessage,
List<String> files) {
try {
Git git = Git.open(new File(localPath));
// Add files to staging
AddCommand add = git.add();
for (String file : files) {
add.addFilepattern(file);
}
add.call();
// Commit changes
CommitCommand commit = git.commit();
commit.setMessage(commitMessage)
.setAuthor(gitOpsConfig.getGit().getUsername(), gitOpsConfig.getGit().getEmail())
.setCommitter(gitOpsConfig.getGit().getUsername(), gitOpsConfig.getGit().getEmail())
.call();
// Push changes
PushCommand push = git.push();
push.setCredentialsProvider(createCredentialsProvider())
.call();
log.info("Committed and pushed changes: {}", commitMessage);
return GitOperationResult.success("Changes committed and pushed");
} catch (Exception e) {
log.error("Failed to commit and push changes", e);
return GitOperationResult.failure("Commit and push failed: " + e.getMessage());
}
}
/**
* Create Kubernetes manifests from application configuration
*/
public boolean generateManifests(ApplicationConfig appConfig, String outputPath) {
try {
File manifestsDir = new File(outputPath);
if (!manifestsDir.exists()) {
manifestsDir.mkdirs();
}
// Generate Deployment
generateDeploymentManifest(appConfig, outputPath);
// Generate Service
generateServiceManifest(appConfig, outputPath);
// Generate Kustomization
generateKustomizationManifest(appConfig, outputPath);
log.info("Generated manifests for application: {}", appConfig.getName());
return true;
} catch (Exception e) {
log.error("Failed to generate manifests for application: {}", appConfig.getName(), e);
return false;
}
}
/**
* Validate Kubernetes manifests
*/
public ValidationResult validateManifests(String manifestsPath) {
List<ValidationIssue> issues = new ArrayList<>();
try {
File manifestsDir = new File(manifestsPath);
File[] manifestFiles = manifestsDir.listFiles((dir, name) ->
name.endsWith(".yaml") || name.endsWith(".yml"));
if (manifestFiles == null) {
return ValidationResult.error("No manifest files found");
}
for (File file : manifestFiles) {
try {
String content = Files.readString(file.toPath());
Object manifest = yaml.load(content);
// Basic validation
if (manifest instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> manifestMap = (Map<String, Object>) manifest;
// Check required fields
if (!manifestMap.containsKey("apiVersion")) {
issues.add(ValidationIssue.error(file.getName(),
"Missing apiVersion"));
}
if (!manifestMap.containsKey("kind")) {
issues.add(ValidationIssue.error(file.getName(),
"Missing kind"));
}
}
} catch (Exception e) {
issues.add(ValidationIssue.error(file.getName(),
"Invalid YAML: " + e.getMessage()));
}
}
return ValidationResult.builder()
.valid(issues.isEmpty())
.issues(issues)
.build();
} catch (Exception e) {
log.error("Failed to validate manifests", e);
return ValidationResult.error("Validation failed: " + e.getMessage());
}
}
private CredentialsProvider createCredentialsProvider() {
if (gitOpsConfig.getGit().getToken() != null) {
return new UsernamePasswordCredentialsProvider(
gitOpsConfig.getGit().getUsername(),
gitOpsConfig.getGit().getToken()
);
}
return null;
}
private void generateDeploymentManifest(ApplicationConfig appConfig, String outputPath)
throws Exception {
Map<String, Object> deployment = Map.of(
"apiVersion", "apps/v1",
"kind", "Deployment",
"metadata", Map.of(
"name", appConfig.getName(),
"namespace", appConfig.getNamespace(),
"labels", Map.of(
"app", appConfig.getName(),
"version", appConfig.getVersion()
)
),
"spec", Map.of(
"replicas", appConfig.getReplicas(),
"selector", Map.of(
"matchLabels", Map.of("app", appConfig.getName())
),
"template", Map.of(
"metadata", Map.of(
"labels", Map.of("app", appConfig.getName())
),
"spec", Map.of(
"containers", List.of(Map.of(
"name", appConfig.getName(),
"image", appConfig.getImage(),
"ports", List.of(Map.of(
"containerPort", appConfig.getPort()
)),
"env", appConfig.getEnvironmentVariables()
))
)
)
)
);
writeYamlFile(deployment, outputPath + "/deployment.yaml");
}
private void generateServiceManifest(ApplicationConfig appConfig, String outputPath)
throws Exception {
Map<String, Object> service = Map.of(
"apiVersion", "v1",
"kind", "Service",
"metadata", Map.of(
"name", appConfig.getName(),
"namespace", appConfig.getNamespace()
),
"spec", Map.of(
"selector", Map.of("app", appConfig.getName()),
"ports", List.of(Map.of(
"port", appConfig.getPort(),
"targetPort", appConfig.getPort()
)),
"type", appConfig.getServiceType()
)
);
writeYamlFile(service, outputPath + "/service.yaml");
}
private void generateKustomizationManifest(ApplicationConfig appConfig, String outputPath)
throws Exception {
Map<String, Object> kustomization = Map.of(
"apiVersion", "kustomize.config.k8s.io/v1beta1",
"kind", "Kustomization",
"resources", List.of(
"deployment.yaml",
"service.yaml"
),
"images", List.of(Map.of(
"name", appConfig.getImage().split(":")[0],
"newTag", appConfig.getVersion()
))
);
writeYamlFile(kustomization, outputPath + "/kustomization.yaml");
}
private void writeYamlFile(Object data, String filePath) throws Exception {
String yamlContent = yaml.dump(data);
Files.writeString(Paths.get(filePath), yamlContent);
}
}
// GitOperationResult.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GitOperationResult {
private boolean success;
private String message;
private List<String> details;
private ZonedDateTime timestamp;
public static GitOperationResult success(String message) {
return GitOperationResult.builder()
.success(true)
.message(message)
.timestamp(ZonedDateTime.now())
.build();
}
public static GitOperationResult success(String message, List<String> details) {
return GitOperationResult.builder()
.success(true)
.message(message)
.details(details)
.timestamp(ZonedDateTime.now())
.build();
}
public static GitOperationResult failure(String message) {
return GitOperationResult.builder()
.success(false)
.message(message)
.timestamp(ZonedDateTime.now())
.build();
}
}
// ValidationResult.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ValidationResult {
private boolean valid;
private List<ValidationIssue> issues;
private String message;
public static ValidationResult error(String message) {
return ValidationResult.builder()
.valid(false)
.message(message)
.issues(new ArrayList<>())
.build();
}
}
// ValidationIssue.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ValidationIssue {
private String file;
private String severity; // ERROR, WARNING, INFO
private String message;
private int line;
public static ValidationIssue error(String file, String message) {
return ValidationIssue.builder()
.file(file)
.severity("ERROR")
.message(message)
.build();
}
public static ValidationIssue warning(String file, String message) {
return ValidationIssue.builder()
.file(file)
.severity("WARNING")
.message(message)
.build();
}
}
3. Application Configuration Model
// ApplicationConfig.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationConfig {
private String name;
private String namespace;
private String version;
private String image;
private int replicas = 1;
private int port = 8080;
private String serviceType = "ClusterIP";
private Map<String, String> labels = new HashMap<>();
private Map<String, String> annotations = new HashMap<>();
private List<EnvVar> environmentVariables = new ArrayList<>();
private ResourceRequirements resources = new ResourceRequirements();
private List<VolumeMount> volumeMounts = new ArrayList<>();
private List<Probe> livenessProbe;
private List<Probe> readinessProbe;
private List<String> command;
private List<String> args;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class EnvVar {
private String name;
private String value;
private ValueFrom valueFrom;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ValueFrom {
private SecretKeyRef secretKeyRef;
private ConfigMapKeyRef configMapKeyRef;
private FieldRef fieldRef;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class ResourceRequirements {
private Map<String, String> requests = new HashMap<>();
private Map<String, String> limits = new HashMap<>();
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class VolumeMount {
private String name;
private String mountPath;
private boolean readOnly = false;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Probe {
private String type; // HTTP, TCP, EXEC
private HttpGet httpGet;
private TcpSocket tcpSocket;
private Exec exec;
private int initialDelaySeconds = 0;
private int periodSeconds = 10;
private int timeoutSeconds = 1;
private int successThreshold = 1;
private int failureThreshold = 3;
}
}
REST API Controllers
1. GitOps Management API
// GitOpsController.java
@RestController
@RequestMapping("/api/v1/gitops")
@Slf4j
@Validated
public class GitOpsController {
private final FluxManagerService fluxManagerService;
private final GitService gitService;
public GitOpsController(FluxManagerService fluxManagerService,
GitService gitService) {
this.fluxManagerService = fluxManagerService;
this.gitService = gitService;
}
@PostMapping("/bootstrap")
public ResponseEntity<BootstrapResult> bootstrapFlux() {
log.info("Received Flux bootstrap request");
BootstrapResult result = fluxManagerService.bootstrapFlux();
if (result.isSuccess()) {
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
}
}
@PostMapping("/repositories")
public ResponseEntity<GitRepository> createRepository(
@Valid @RequestBody FluxRepository repository) {
log.info("Creating GitRepository: {}", repository.getName());
try {
GitRepository result = fluxManagerService.createGitRepository(repository);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
} catch (FluxOperationException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/kustomizations")
public ResponseEntity<Kustomization> createKustomization(
@Valid @RequestBody FluxKustomization kustomization) {
log.info("Creating Kustomization: {}", kustomization.getName());
try {
Kustomization result = fluxManagerService.createKustomization(kustomization);
return ResponseEntity.status(HttpStatus.CREATED).body(result);
} catch (FluxOperationException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/sync-status")
public ResponseEntity<List<SyncStatus>> getSyncStatus() {
List<SyncStatus> status = fluxManagerService.getSyncStatus();
return ResponseEntity.ok(status);
}
@PostMapping("/sync/{repository}")
public ResponseEntity<SyncResult> triggerSync(@PathVariable String repository) {
log.info("Triggering sync for repository: {}", repository);
SyncResult result = fluxManagerService.triggerSync(repository);
if (result.isSuccess()) {
return ResponseEntity.accepted().body(result);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
}
}
@PostMapping("/{repository}/suspend")
public ResponseEntity<Void> suspendReconciliation(@PathVariable String repository) {
log.info("Suspending reconciliation for: {}", repository);
boolean success = fluxManagerService.suspendReconciliation(repository);
if (success) {
return ResponseEntity.accepted().build();
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/{repository}/resume")
public ResponseEntity<Void> resumeReconciliation(@PathVariable String repository) {
log.info("Resuming reconciliation for: {}", repository);
boolean success = fluxManagerService.resumeReconciliation(repository);
if (success) {
return ResponseEntity.accepted().build();
} else {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/applications")
public ResponseEntity<ApplicationDeploymentResult> deployApplication(
@Valid @RequestBody ApplicationDeploymentRequest request) {
log.info("Deploying application: {}", request.getApplication().getName());
try {
// Generate manifests
boolean manifestsGenerated = gitService.generateManifests(
request.getApplication(),
request.getManifestsPath()
);
if (!manifestsGenerated) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApplicationDeploymentResult.failure("Failed to generate manifests"));
}
// Validate manifests
ValidationResult validation = gitService.validateManifests(request.getManifestsPath());
if (!validation.isValid()) {
return ResponseEntity.badRequest()
.body(ApplicationDeploymentResult.failure("Manifest validation failed: " +
validation.getMessage()));
}
// Commit and push changes
GitOperationResult gitResult = gitService.commitAndPush(
request.getRepositoryPath(),
request.getCommitMessage(),
List.of(request.getManifestsPath())
);
if (!gitResult.isSuccess()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApplicationDeploymentResult.failure("Git operation failed: " +
gitResult.getMessage()));
}
return ResponseEntity.accepted()
.body(ApplicationDeploymentResult.success(request.getApplication().getName()));
} catch (Exception e) {
log.error("Application deployment failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApplicationDeploymentResult.failure("Deployment failed: " + e.getMessage()));
}
}
}
// ApplicationDeploymentRequest.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationDeploymentRequest {
@Valid
private ApplicationConfig application;
@NotBlank
private String repositoryPath;
@NotBlank
private String manifestsPath;
private String commitMessage;
public String getCommitMessage() {
if (commitMessage == null || commitMessage.isBlank()) {
return String.format("Deploy %s version %s",
application.getName(), application.getVersion());
}
return commitMessage;
}
}
// ApplicationDeploymentResult.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationDeploymentResult {
private boolean success;
private String application;
private String message;
private ZonedDateTime timestamp;
public static ApplicationDeploymentResult success(String application) {
return ApplicationDeploymentResult.builder()
.success(true)
.application(application)
.message("Application deployment initiated")
.timestamp(ZonedDateTime.now())
.build();
}
public static ApplicationDeploymentResult failure(String message) {
return ApplicationDeploymentResult.builder()
.success(false)
.message(message)
.timestamp(ZonedDateTime.now())
.build();
}
}
2. Dashboard and Monitoring API
// GitOpsDashboardController.java
@RestController
@RequestMapping("/api/v1/dashboard")
@Slf4j
public class GitOpsDashboardController {
private final FluxManagerService fluxManagerService;
private final KubernetesClient kubernetesClient;
public GitOpsDashboardController(FluxManagerService fluxManagerService,
KubernetesClient kubernetesClient) {
this.fluxManagerService = fluxManagerService;
this.kubernetesClient = kubernetesClient;
}
@GetMapping("/overview")
public ResponseEntity<GitOpsOverview> getOverview() {
try {
List<SyncStatus> syncStatus = fluxManagerService.getSyncStatus();
long totalRepositories = syncStatus.size();
long syncedRepositories = syncStatus.stream()
.filter(status -> status.getState() == SyncStatus.SyncState.SYNCED)
.count();
long outOfSyncRepositories = syncStatus.stream()
.filter(status -> status.getState() == SyncStatus.SyncState.OUT_OF_SYNC)
.count();
long errorRepositories = syncStatus.stream()
.filter(status -> status.getState() == SyncStatus.SyncState.ERROR)
.count();
GitOpsOverview overview = GitOpsOverview.builder()
.totalRepositories(totalRepositories)
.syncedRepositories(syncedRepositories)
.outOfSyncRepositories(outOfSyncRepositories)
.errorRepositories(errorRepositories)
.syncStatus(syncStatus)
.lastUpdated(ZonedDateTime.now())
.build();
return ResponseEntity.ok(overview);
} catch (Exception e) {
log.error("Failed to get GitOps overview", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/applications")
public ResponseEntity<List<ApplicationStatus>> getApplicationStatus() {
try {
List<ApplicationStatus> applications = new ArrayList<>();
// Get deployments from all namespaces
List<Deployment> deployments = kubernetesClient.apps().deployments()
.inAnyNamespace()
.list()
.getItems();
for (Deployment deployment : deployments) {
ApplicationStatus status = buildApplicationStatus(deployment);
applications.add(status);
}
return ResponseEntity.ok(applications);
} catch (Exception e) {
log.error("Failed to get application status", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
private ApplicationStatus buildApplicationStatus(Deployment deployment) {
String namespace = deployment.getMetadata().getNamespace();
String name = deployment.getMetadata().getName();
// Get deployment status
int replicas = deployment.getStatus().getReplicas() != null ?
deployment.getStatus().getReplicas() : 0;
int readyReplicas = deployment.getStatus().getReadyReplicas() != null ?
deployment.getStatus().getReadyReplicas() : 0;
int availableReplicas = deployment.getStatus().getAvailableReplicas() != null ?
deployment.getStatus().getAvailableReplicas() : 0;
boolean healthy = readyReplicas >= replicas && replicas > 0;
// Get image version
String image = "unknown";
if (deployment.getSpec().getTemplate().getSpec().getContainers() != null &&
!deployment.getSpec().getTemplate().getSpec().getContainers().isEmpty()) {
image = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage();
}
return ApplicationStatus.builder()
.name(name)
.namespace(namespace)
.image(image)
.replicas(replicas)
.readyReplicas(readyReplicas)
.availableReplicas(availableReplicas)
.healthy(healthy)
.lastUpdated(ZonedDateTime.now())
.build();
}
}
// GitOpsOverview.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GitOpsOverview {
private long totalRepositories;
private long syncedRepositories;
private long outOfSyncRepositories;
private long errorRepositories;
private List<SyncStatus> syncStatus;
private ZonedDateTime lastUpdated;
public double getSyncPercentage() {
return totalRepositories > 0 ?
(double) syncedRepositories / totalRepositories * 100 : 100.0;
}
}
// ApplicationStatus.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApplicationStatus {
private String name;
private String namespace;
private String image;
private int replicas;
private int readyReplicas;
private int availableReplicas;
private boolean healthy;
private ZonedDateTime lastUpdated;
public double getAvailability() {
return replicas > 0 ? (double) availableReplicas / replicas * 100 : 100.0;
}
}
Configuration and Setup
1. Application Properties
# application.yml
gitops:
enabled: true
cluster-name: ${CLUSTER_NAME:production}
environment: ${ENVIRONMENT:prod}
git:
url: ${GIT_REPO_URL:https://github.com/company/gitops-repo.git}
branch: ${GIT_BRANCH:main}
path: ./manifests
username: ${GIT_USERNAME:gitops-bot}
token: ${GIT_TOKEN:}
email: ${GIT_EMAIL:[email protected]}
timeout-seconds: 30
flux:
namespace: flux-system
version: v2.0.0
auto-creation: true
repositories:
- name: flux-system
url: ${GIT_REPO_URL:https://github.com/company/gitops-repo.git}
branch: ${GIT_BRANCH:main}
interval: FIVE_MINUTES
sync:
auto-sync: true
sync-interval: PT5M
timeout: PT10M
prune-labels:
- app.kubernetes.io/part-of
dry-run: false
notification:
enabled: true
providers:
- slack
- webhook
slack-webhook-url: ${SLACK_WEBHOOK_URL:}
webhook-url: ${WEBHOOK_URL:}
# Kubernetes
kubernetes:
master-url: ${K8S_MASTER_URL:}
namespace: default
trust-certs: false
# Security
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
# Logging
logging:
level:
com.company.gitops: INFO
file:
name: logs/gitops-manager.log
2. Spring Configuration
// GitOpsConfig.java
@Configuration
@EnableConfigurationProperties(GitOpsConfig.class)
public class GitOpsConfiguration {
@Bean
@ConditionalOnMissingBean
public KubernetesClient kubernetesClient(GitOpsConfig gitOpsConfig) {
Config config;
if (gitOpsConfig.getKubernetes().getMasterUrl() != null) {
config = new ConfigBuilder()
.withMasterUrl(gitOpsConfig.getKubernetes().getMasterUrl())
.withTrustCerts(gitOpsConfig.getKubernetes().isTrustCerts())
.build();
} else {
// Use default configuration (in-cluster or ~/.kube/config)
config = Config.autoConfigure();
}
return new DefaultKubernetesClient(config);
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
@Bean
public Yaml yaml() {
return new Yaml();
}
}
// Custom Exceptions
class FluxOperationException extends RuntimeException {
public FluxOperationException(String message) {
super(message);
}
public FluxOperationException(String message, Throwable cause) {
super(message, cause);
}
}
This comprehensive GitOps with Flux implementation provides:
- Flux bootstrap and management for Kubernetes clusters
- Git repository synchronization with automated deployment
- Kustomization management for application deployment
- Manifest generation and validation from application configurations
- Sync status monitoring and health checks
- REST API for GitOps operations
- Dashboard and monitoring capabilities
- Multi-application support with dependency management
The system enables full GitOps workflows where changes to the Git repository automatically trigger deployments to Kubernetes clusters through Flux, providing a robust and auditable deployment pipeline.
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.