Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions deploy/examples/sample-patternlibrary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
apiVersion: podmortem.redhat.com/v1alpha1
kind: PatternLibrary
metadata:
name: community-patterns
namespace: podmortem-system
spec:
repositories:
- name: "podmortem-community"
url: "https://github.com/podmortem/pattern-libraries.git"
branch: "main"
refreshInterval: "1h"
enabledLibraries:
- "spring-boot-core"
- "kubernetes-events"
- "postgresql-errors"
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.3</surefire-plugin.version>

<podmortem.common.lib.version>1.0-1db4e13-SNAPSHOT</podmortem.common.lib.version>
<podmortem.common.lib.version>1.0-36588fe-SNAPSHOT</podmortem.common.lib.version>
</properties>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.redhat.podmortem.operator.reconcile;

import com.redhat.podmortem.common.model.kube.patternlibrary.PatternLibrary;
import com.redhat.podmortem.common.model.kube.patternlibrary.PatternLibraryStatus;
import com.redhat.podmortem.common.model.kube.patternlibrary.PatternRepository;
import com.redhat.podmortem.operator.service.PatternSyncService;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.time.Instant;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ControllerConfiguration
@ApplicationScoped
public class PatternLibraryReconciler implements Reconciler<PatternLibrary> {

private static final Logger log = LoggerFactory.getLogger(PatternLibraryReconciler.class);

@Inject KubernetesClient client;

@Inject PatternSyncService patternSyncService;

@Override
public UpdateControl<PatternLibrary> reconcile(
PatternLibrary resource, Context<PatternLibrary> context) {
log.info("Reconciling PatternLibrary: {}", resource.getMetadata().getName());

// Initialize status if not present
if (resource.getStatus() == null) {
resource.setStatus(new PatternLibraryStatus());
}

try {
// Update status to syncing
updatePatternLibraryStatus(resource, "Syncing", "Synchronizing pattern repositories");

// Sync each configured repository
List<PatternRepository> repositories = resource.getSpec().getRepositories();
if (repositories != null) {
for (PatternRepository repo : repositories) {
syncRepository(resource, repo);
}
}

// Discover available libraries from synced repositories
List<String> availableLibraries =
patternSyncService.getAvailableLibraries(resource.getMetadata().getName());

// Update status with available libraries
resource.getStatus().setAvailableLibraries(availableLibraries);
updatePatternLibraryStatus(
resource,
"Ready",
String.format(
"Successfully synced %d repositories, %d libraries available",
repositories != null ? repositories.size() : 0,
availableLibraries.size()));

return UpdateControl.patchStatus(resource);

} catch (Exception e) {
log.error("Error reconciling PatternLibrary: {}", resource.getMetadata().getName(), e);
updatePatternLibraryStatus(
resource, "Failed", "Failed to reconcile: " + e.getMessage());
return UpdateControl.patchStatus(resource);
}
}

private void syncRepository(PatternLibrary resource, PatternRepository repo) {
try {
log.info(
"Syncing repository {} for PatternLibrary {}",
repo.getName(),
resource.getMetadata().getName());

// Get credentials from secret if specified
String credentials = null;
if (repo.getCredentials() != null && repo.getCredentials().getSecretRef() != null) {
credentials = getCredentialsFromSecret(repo.getCredentials().getSecretRef());
}

// Sync repository using PatternSyncService
patternSyncService.syncRepository(resource.getMetadata().getName(), repo, credentials);

// Update repository sync status
updateRepositoryStatus(resource, repo, "Success", null);

} catch (Exception e) {
log.error("Failed to sync repository {}: {}", repo.getName(), e.getMessage(), e);
updateRepositoryStatus(resource, repo, "Failed", e.getMessage());
}
}

private String getCredentialsFromSecret(String secretName) {
try {
// Get secret from the same namespace as PatternLibrary
var secret =
client.secrets()
.inNamespace("podmortem-system") // TODO: use resource namespace
.withName(secretName)
.get();

if (secret != null && secret.getData() != null) {
// Return base64 decoded credentials
// This is a simplified version - in practice you'd handle username/password
// or token-based auth
return new String(secret.getData().get("token"));
}
} catch (Exception e) {
log.warn("Failed to get credentials from secret {}: {}", secretName, e.getMessage());
}
return null;
}

private void updateRepositoryStatus(
PatternLibrary resource, PatternRepository repo, String status, String error) {
// TODO: Implement repository status tracking in PatternLibraryStatus
// This would update the syncedRepositories field with individual repo status
log.info("Repository {} status: {}", repo.getName(), status);
}

private void updatePatternLibraryStatus(PatternLibrary resource, String phase, String message) {
PatternLibraryStatus status = resource.getStatus();
if (status == null) {
status = new PatternLibraryStatus();
resource.setStatus(status);
}

status.setPhase(phase);
status.setMessage(message);
status.setLastSyncTime(Instant.now());
status.setObservedGeneration(resource.getMetadata().getGeneration());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.redhat.podmortem.reconcile.config;
package com.redhat.podmortem.operator.reconcile;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.redhat.podmortem.operator.service;

import com.redhat.podmortem.common.model.kube.patternlibrary.PatternRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ApplicationScoped
public class PatternSyncService {

private static final Logger log = LoggerFactory.getLogger(PatternSyncService.class);
private static final String PATTERN_CACHE_DIR = "/tmp/pattern-cache";

public void syncRepository(String libraryName, PatternRepository repo, String credentials) {
try {
log.info("Syncing repository {} for library {}", repo.getName(), libraryName);

// Create cache directory structure
Path libraryPath = Paths.get(PATTERN_CACHE_DIR, libraryName);
Files.createDirectories(libraryPath);

Path repoPath = libraryPath.resolve(repo.getName());

if (Files.exists(repoPath)) {
// Pull latest changes
pullRepository(repoPath, repo, credentials);
} else {
// Clone repository
cloneRepository(repoPath, repo, credentials);
}

// Validate patterns in the repository
validatePatterns(repoPath);

log.info(
"Successfully synced repository {} for library {}",
repo.getName(),
libraryName);

} catch (Exception e) {
log.error(
"Failed to sync repository {} for library {}: {}",
repo.getName(),
libraryName,
e.getMessage(),
e);
throw new RuntimeException("Repository sync failed", e);
}
}

public List<String> getAvailableLibraries(String libraryName) {
List<String> libraries = new ArrayList<>();
try {
Path libraryPath = Paths.get(PATTERN_CACHE_DIR, libraryName);
if (Files.exists(libraryPath)) {
// Scan for pattern library directories and files
try (Stream<Path> paths = Files.walk(libraryPath, 2)) {
paths.filter(Files::isDirectory)
.filter(path -> !path.equals(libraryPath))
.map(path -> path.getFileName().toString())
.forEach(libraries::add);
}

// Also scan for YAML files that might be libraries
try (Stream<Path> yamlFiles = Files.walk(libraryPath)) {
yamlFiles
.filter(Files::isRegularFile)
.filter(
path ->
path.toString().endsWith(".yaml")
|| path.toString().endsWith(".yml"))
.map(
path ->
path.getFileName()
.toString()
.replaceAll("\\.(yaml|yml)$", ""))
.forEach(libraries::add);
}
}
} catch (Exception e) {
log.error(
"Failed to get available libraries for {}: {}", libraryName, e.getMessage(), e);
}
return libraries;
}

private void cloneRepository(Path repoPath, PatternRepository repo, String credentials) {
try {
log.info("Cloning repository {} to {}", repo.getUrl(), repoPath);

// For now, using simple git command execution
// In a full implementation, you'd use JGit library
ProcessBuilder pb = new ProcessBuilder();
pb.directory(repoPath.getParent().toFile());

if (credentials != null) {
// Handle authentication - this is simplified
String authUrl = repo.getUrl().replace("https://", "https://" + credentials + "@");
pb.command(
"git",
"clone",
"-b",
repo.getBranch() != null ? repo.getBranch() : "main",
authUrl,
repoPath.getFileName().toString());
} else {
pb.command(
"git",
"clone",
"-b",
repo.getBranch() != null ? repo.getBranch() : "main",
repo.getUrl(),
repoPath.getFileName().toString());
}

Process process = pb.start();
int exitCode = process.waitFor();

if (exitCode != 0) {
throw new RuntimeException("Git clone failed with exit code: " + exitCode);
}

} catch (Exception e) {
log.error("Failed to clone repository {}: {}", repo.getUrl(), e.getMessage(), e);
throw new RuntimeException("Git clone failed", e);
}
}

private void pullRepository(Path repoPath, PatternRepository repo, String credentials) {
try {
log.info("Pulling latest changes for repository at {}", repoPath);

ProcessBuilder pb = new ProcessBuilder();
pb.directory(repoPath.toFile());
pb.command(
"git", "pull", "origin", repo.getBranch() != null ? repo.getBranch() : "main");

Process process = pb.start();
int exitCode = process.waitFor();

if (exitCode != 0) {
throw new RuntimeException("Git pull failed with exit code: " + exitCode);
}

} catch (Exception e) {
log.error("Failed to pull repository at {}: {}", repoPath, e.getMessage(), e);
throw new RuntimeException("Git pull failed", e);
}
}

private void validatePatterns(Path repoPath) {
try {
// Basic validation - check if YAML files are present
try (Stream<Path> files = Files.walk(repoPath)) {
long yamlCount =
files.filter(Files::isRegularFile)
.filter(
path ->
path.toString().endsWith(".yaml")
|| path.toString().endsWith(".yml"))
.count();

if (yamlCount == 0) {
log.warn("No YAML pattern files found in repository at {}", repoPath);
}

log.info("Found {} YAML pattern files in repository at {}", yamlCount, repoPath);
}
} catch (Exception e) {
log.error(
"Failed to validate patterns in repository at {}: {}",
repoPath,
e.getMessage(),
e);
}
}
}
Loading
Loading