diff --git a/deploy/examples/sample-patternlibrary.yaml b/deploy/examples/sample-patternlibrary.yaml
new file mode 100644
index 0000000..cb87b04
--- /dev/null
+++ b/deploy/examples/sample-patternlibrary.yaml
@@ -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"
diff --git a/pom.xml b/pom.xml
index c185f4d..8b554ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,7 +16,7 @@
true
3.5.3
- 1.0-1db4e13-SNAPSHOT
+ 1.0-36588fe-SNAPSHOT
diff --git a/src/main/java/com/redhat/podmortem/operator/reconcile/PatternLibraryReconciler.java b/src/main/java/com/redhat/podmortem/operator/reconcile/PatternLibraryReconciler.java
new file mode 100644
index 0000000..9cfb822
--- /dev/null
+++ b/src/main/java/com/redhat/podmortem/operator/reconcile/PatternLibraryReconciler.java
@@ -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 {
+
+ private static final Logger log = LoggerFactory.getLogger(PatternLibraryReconciler.class);
+
+ @Inject KubernetesClient client;
+
+ @Inject PatternSyncService patternSyncService;
+
+ @Override
+ public UpdateControl reconcile(
+ PatternLibrary resource, Context 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 repositories = resource.getSpec().getRepositories();
+ if (repositories != null) {
+ for (PatternRepository repo : repositories) {
+ syncRepository(resource, repo);
+ }
+ }
+
+ // Discover available libraries from synced repositories
+ List 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());
+ }
+}
diff --git a/src/main/java/com/redhat/podmortem/reconcile/config/PodmortemReconciler.java b/src/main/java/com/redhat/podmortem/operator/reconcile/PodmortemReconciler.java
similarity index 99%
rename from src/main/java/com/redhat/podmortem/reconcile/config/PodmortemReconciler.java
rename to src/main/java/com/redhat/podmortem/operator/reconcile/PodmortemReconciler.java
index a12e947..eee2984 100644
--- a/src/main/java/com/redhat/podmortem/reconcile/config/PodmortemReconciler.java
+++ b/src/main/java/com/redhat/podmortem/operator/reconcile/PodmortemReconciler.java
@@ -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;
diff --git a/src/main/java/com/redhat/podmortem/operator/service/PatternSyncService.java b/src/main/java/com/redhat/podmortem/operator/service/PatternSyncService.java
new file mode 100644
index 0000000..f8ea8c6
--- /dev/null
+++ b/src/main/java/com/redhat/podmortem/operator/service/PatternSyncService.java
@@ -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 getAvailableLibraries(String libraryName) {
+ List 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 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 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 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);
+ }
+ }
+}
diff --git a/src/main/kubernetes/operator-deployment.yaml b/src/main/kubernetes/operator-deployment.yaml
new file mode 100644
index 0000000..af86351
--- /dev/null
+++ b/src/main/kubernetes/operator-deployment.yaml
@@ -0,0 +1,92 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: podmortem-operator
+ namespace: podmortem-system
+ labels:
+ app: podmortem-operator
+ component: operator
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: podmortem-operator
+ template:
+ metadata:
+ labels:
+ app: podmortem-operator
+ component: operator
+ spec:
+ serviceAccountName: podmortem-operator
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1001
+ runAsGroup: 1001
+ seccompProfile:
+ type: RuntimeDefault
+ containers:
+ - name: operator
+ image: ghcr.io/podmortem/podmortem-operator:latest
+ imagePullPolicy: Always
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ - name: https
+ containerPort: 8443
+ protocol: TCP
+ env:
+ - name: KUBERNETES_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: OPERATOR_NAME
+ value: "podmortem-operator"
+ resources:
+ limits:
+ memory: "512Mi"
+ cpu: "500m"
+ requests:
+ memory: "256Mi"
+ cpu: "100m"
+ livenessProbe:
+ httpGet:
+ path: /q/health/live
+ port: 8080
+ scheme: HTTP
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 10
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /q/health/ready
+ port: 8080
+ scheme: HTTP
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 10
+ failureThreshold: 3
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ privileged: false
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ - name: pattern-cache
+ mountPath: /tmp/pattern-cache
+ volumes:
+ - name: tmp
+ emptyDir: {}
+ - name: pattern-cache
+ emptyDir: {}
+ terminationGracePeriodSeconds: 60
diff --git a/src/main/kubernetes/operator-serviceaccount.yaml b/src/main/kubernetes/operator-serviceaccount.yaml
new file mode 100644
index 0000000..931aa61
--- /dev/null
+++ b/src/main/kubernetes/operator-serviceaccount.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: podmortem-operator
+ namespace: podmortem-system
+ labels:
+ app: podmortem-operator
+ component: operator
+automountServiceAccountToken: true
diff --git a/src/main/kubernetes/patternlibrary-crd.yaml b/src/main/kubernetes/patternlibrary-crd.yaml
new file mode 100644
index 0000000..368e666
--- /dev/null
+++ b/src/main/kubernetes/patternlibrary-crd.yaml
@@ -0,0 +1,99 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: patternlibraries.podmortem.redhat.com
+spec:
+ group: podmortem.redhat.com
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ spec:
+ type: object
+ properties:
+ repositories:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ description: "Name identifier for the repository"
+ url:
+ type: string
+ description: "Git repository URL containing pattern libraries"
+ branch:
+ type: string
+ default: "main"
+ description: "Git branch to use"
+ credentials:
+ type: object
+ properties:
+ secretRef:
+ type: string
+ description: "Name of Kubernetes secret containing Git credentials"
+ description: "Authentication credentials for private repositories"
+ required: ["name", "url"]
+ refreshInterval:
+ type: string
+ default: "1h"
+ description: "How often to sync patterns from repositories (e.g., '30m', '1h')"
+ enabledLibraries:
+ type: array
+ items:
+ type: string
+ description: "List of pattern library IDs to enable from the repositories"
+ status:
+ type: object
+ properties:
+ phase:
+ type: string
+ enum: ["Pending", "Syncing", "Ready", "Failed"]
+ description: "Current state of the pattern library"
+ message:
+ type: string
+ description: "Human-readable status message"
+ lastSyncTime:
+ type: string
+ format: date-time
+ description: "When patterns were last synchronized"
+ syncedRepositories:
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ type: string
+ lastCommit:
+ type: string
+ syncTime:
+ type: string
+ format: date-time
+ status:
+ type: string
+ enum: ["Success", "Failed"]
+ error:
+ type: string
+ description: "Status of each configured repository"
+ availableLibraries:
+ type: array
+ items:
+ type: string
+ description: "List of discovered pattern libraries from all repositories"
+ observedGeneration:
+ type: integer
+ format: int64
+ subresources:
+ status: {}
+ scope: Namespaced
+ names:
+ plural: patternlibraries
+ singular: patternlibrary
+ kind: PatternLibrary
+ shortNames:
+ - pl