mirror of
https://github.com/lukaszraczylo/kubernetes-images-sync-operator.git
synced 2026-10-12 00:45:27 +00:00
Migrate the lint config to the v2 format. Replace the deprecated scheme.Builder with runtime.NewSchemeBuilder. Handle Close errors in the worker and replace repeated strings with constants. Remove unused parameters and use switch statements where the linter asks.
592 lines
22 KiB
Go
592 lines
22 KiB
Go
package raczylocom
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5" // #nosec G501 - MD5 used for non-cryptographic unique identifiers only
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/util/retry"
|
|
"k8s.io/utils/ptr"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
raczylocomv1 "github.com/lukaszraczylo/kubernetes-images-sync-operator/api/raczylo.com/v1"
|
|
shared "github.com/lukaszraczylo/kubernetes-images-sync-operator/internal/shared"
|
|
)
|
|
|
|
// ClusterImageExportReconciler reconciles a ClusterImageExport object
|
|
type ClusterImageExportReconciler struct {
|
|
client.Client
|
|
Scheme *runtime.Scheme
|
|
podAnnotations map[string]string
|
|
}
|
|
|
|
func (r *ClusterImageExportReconciler) InjectPodAnnotations(annotations map[string]string) {
|
|
r.podAnnotations = annotations
|
|
}
|
|
|
|
// +kubebuilder:rbac:groups=raczylo.com,resources=*,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=raczylo.com,resources=*/status,verbs=get;update;patch
|
|
// +kubebuilder:rbac:groups=raczylo.com,resources=*/finalizers,verbs=update
|
|
// additional RBAC rules
|
|
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch
|
|
// +kubebuilder:rbac:groups=apps,resources=daemonsets,verbs=get;list;watch
|
|
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
|
|
// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch
|
|
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
|
|
|
|
const clusterImageExportFinalizer = "raczylo.com/clusterimageexport-finalizer"
|
|
|
|
//nolint:gocyclo // single reconcile state machine; splitting is out of scope
|
|
func (r *ClusterImageExportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
|
l := log.FromContext(ctx)
|
|
// l.Info("Reconciling ClusterImageExport")
|
|
|
|
// Fetch the ClusterImageExport instance
|
|
clusterImageExport := &raczylocomv1.ClusterImageExport{}
|
|
if err := r.Get(ctx, req.NamespacedName, clusterImageExport); err != nil {
|
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
|
}
|
|
|
|
if !clusterImageExport.DeletionTimestamp.IsZero() {
|
|
return r.handleDeletion(ctx, clusterImageExport)
|
|
}
|
|
|
|
// Check if this export should be deleted by TTL
|
|
if r.shouldDeleteByTTL(clusterImageExport) {
|
|
l.Info("Deleting export due to TTL expiration",
|
|
"export", clusterImageExport.Name,
|
|
"ttlDays", *clusterImageExport.Spec.TTLDaysAfterFinished,
|
|
"completedAt", clusterImageExport.Status.CompletedAt)
|
|
if err := r.Delete(ctx, clusterImageExport); err != nil && !errors.IsNotFound(err) {
|
|
l.Error(err, "Failed to delete export by TTL")
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
// Add finalizer and creation timestamp annotation if they don't exist
|
|
needsUpdate := false
|
|
if !controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) {
|
|
controllerutil.AddFinalizer(clusterImageExport, clusterImageExportFinalizer)
|
|
needsUpdate = true
|
|
}
|
|
|
|
// Add creation timestamp annotation if it doesn't exist
|
|
if clusterImageExport.Annotations == nil {
|
|
clusterImageExport.Annotations = make(map[string]string)
|
|
}
|
|
if _, exists := clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"]; !exists {
|
|
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"] = clusterImageExport.CreationTimestamp.String()
|
|
needsUpdate = true
|
|
}
|
|
|
|
if needsUpdate {
|
|
if err := r.Update(ctx, clusterImageExport); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
// Proceed with reconciliation logic
|
|
// Get list of all images to be exported
|
|
fullImagesList, err := r.listImagesInCluster(ctx, clusterImageExport)
|
|
if err != nil {
|
|
l.Error(err, "unable to list images in the cluster")
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
// Add additional images if specified
|
|
if len(clusterImageExport.Spec.AdditionalImages) > 0 {
|
|
for _, image := range clusterImageExport.Spec.AdditionalImages {
|
|
img, err := shared.ProcessContainerName(image)
|
|
if err != nil {
|
|
l.Error(err, "unable to process additional image", "image", image)
|
|
continue
|
|
}
|
|
fullImagesList.Containers = append(fullImagesList.Containers, img)
|
|
}
|
|
}
|
|
|
|
// Update total image count and status
|
|
totalImages := len(fullImagesList.Containers)
|
|
if err := r.updateStatusWithRetry(ctx, clusterImageExport, func(export *raczylocomv1.ClusterImageExport) error {
|
|
if export.Status.Progress == "" {
|
|
export.Status.Progress = shared.STATUS_PENDING
|
|
}
|
|
if export.Status.Progress == shared.STATUS_PENDING {
|
|
export.Status.Progress = shared.STATUS_RUNNING
|
|
}
|
|
export.Status.TotalImages = totalImages
|
|
return nil
|
|
}); err != nil {
|
|
l.Error(err, "unable to update ClusterImageExport status")
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
for _, image := range fullImagesList.Containers {
|
|
// Include creation timestamp in the hash to differentiate between exports with the same name
|
|
// #nosec G401 - MD5 used for non-cryptographic unique identifier generation, not security
|
|
nameHash := fmt.Sprintf("%x", md5.Sum([]byte(clusterImageExport.Name+image.Image+image.Tag+image.Sha+
|
|
clusterImageExport.Annotations["export.raczylo.com/creation-timestamp"])))[:14]
|
|
|
|
// Check if the ClusterImage already exists
|
|
clusterImage := &raczylocomv1.ClusterImage{}
|
|
err := r.Get(ctx, client.ObjectKey{Namespace: clusterImageExport.Namespace, Name: nameHash}, clusterImage)
|
|
if err == nil {
|
|
// ClusterImage exists, check its status
|
|
if clusterImage.Status.Progress == shared.STATUS_FAILED {
|
|
if err := r.updateStatusWithRetry(ctx, clusterImageExport, func(export *raczylocomv1.ClusterImageExport) error {
|
|
export.Status.Progress = shared.STATUS_FAILED
|
|
return nil
|
|
}); err != nil {
|
|
l.Error(err, "unable to update ClusterImageExport status to FAILED")
|
|
return ctrl.Result{}, err
|
|
}
|
|
return ctrl.Result{}, nil
|
|
}
|
|
continue
|
|
} else if !errors.IsNotFound(err) {
|
|
l.Error(err, "unable to get ClusterImage")
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
// Create a new ClusterImage
|
|
newClusterImage := &raczylocomv1.ClusterImage{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: nameHash,
|
|
Namespace: clusterImageExport.Namespace,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: clusterImageExport.APIVersion,
|
|
Kind: clusterImageExport.Kind,
|
|
Name: clusterImageExport.Name,
|
|
UID: clusterImageExport.UID,
|
|
Controller: ptr.To(true),
|
|
},
|
|
},
|
|
},
|
|
Spec: raczylocomv1.ClusterImageSpec{
|
|
Image: image.Image,
|
|
Tag: image.Tag,
|
|
Sha: image.Sha,
|
|
FullName: image.FullName,
|
|
ImageNamespace: image.ImageNamespace,
|
|
Storage: clusterImageExport.Spec.Storage.StorageTarget,
|
|
ExportName: clusterImageExport.Name,
|
|
ExportPath: clusterImageExport.Spec.BasePath,
|
|
JobAnnotations: clusterImageExport.Spec.JobAnnotations,
|
|
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
|
},
|
|
}
|
|
|
|
if err := r.Create(ctx, newClusterImage); err != nil {
|
|
l.Error(err, "unable to create ClusterImage", "image", image)
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
// Check completion status and update counts
|
|
successCount := 0
|
|
failedCount := 0
|
|
pendingCount := 0
|
|
clusterImageList := &raczylocomv1.ClusterImageList{}
|
|
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
|
client.MatchingFields{shared.FIELD_EXPORT_NAME: clusterImageExport.Name}); err != nil {
|
|
l.Error(err, "unable to list ClusterImages")
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
for _, ci := range clusterImageList.Items {
|
|
switch ci.Status.Progress {
|
|
case shared.STATUS_SUCCESS, shared.STATUS_PRESENT:
|
|
successCount++
|
|
case shared.STATUS_FAILED:
|
|
failedCount++
|
|
case shared.STATUS_PENDING, shared.STATUS_RUNNING, shared.STATUS_RETRYING:
|
|
pendingCount++
|
|
}
|
|
}
|
|
|
|
completedCount := successCount + failedCount
|
|
|
|
// Update status with completion info
|
|
if err := r.updateStatusWithRetry(ctx, clusterImageExport, func(export *raczylocomv1.ClusterImageExport) error {
|
|
export.Status.CompletedImages = completedCount
|
|
if completedCount == totalImages && totalImages > 0 {
|
|
if failedCount > 0 {
|
|
export.Status.Progress = shared.STATUS_FAILED
|
|
} else {
|
|
export.Status.Progress = shared.STATUS_SUCCESS
|
|
}
|
|
// Set CompletedAt timestamp when export completes
|
|
if export.Status.CompletedAt == nil {
|
|
now := metav1.Now()
|
|
export.Status.CompletedAt = &now
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
l.Error(err, "unable to update ClusterImageExport status")
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
// If export is complete, run retention cleanup
|
|
if clusterImageExport.Status.Progress == shared.STATUS_SUCCESS ||
|
|
clusterImageExport.Status.Progress == shared.STATUS_FAILED {
|
|
if err := r.cleanupByRetention(ctx, clusterImageExport); err != nil {
|
|
l.Error(err, "Failed to cleanup by retention policy")
|
|
// Don't return error - this is non-critical
|
|
}
|
|
}
|
|
|
|
// If there are still pending images, requeue
|
|
if pendingCount > 0 {
|
|
return ctrl.Result{Requeue: true}, nil
|
|
}
|
|
|
|
return ctrl.Result{Requeue: true}, nil
|
|
}
|
|
|
|
// updateStatusWithRetry attempts to update the status of a ClusterImageExport with retries
|
|
func (r *ClusterImageExportReconciler) updateStatusWithRetry(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport, updateFn func(*raczylocomv1.ClusterImageExport) error) error {
|
|
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
|
// Get the latest version of the resource
|
|
latest := &raczylocomv1.ClusterImageExport{}
|
|
if err := r.Get(ctx, client.ObjectKey{
|
|
Namespace: clusterImageExport.Namespace,
|
|
Name: clusterImageExport.Name,
|
|
}, latest); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Apply the update function to the latest version
|
|
if err := updateFn(latest); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the status
|
|
return r.Status().Update(ctx, latest)
|
|
})
|
|
}
|
|
|
|
// SetupWithManager sets up the controller with the Manager.
|
|
|
|
func (r *ClusterImageExportReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
For(&raczylocomv1.ClusterImageExport{}).
|
|
Owns(&raczylocomv1.ClusterImage{}).
|
|
Complete(r)
|
|
}
|
|
|
|
func (r *ClusterImageExportReconciler) listImagesInCluster(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (shared.ContainersList, error) {
|
|
containersList := shared.ContainersList{}
|
|
if err := shared.ListAndProcessResources[*shared.DeploymentWrapper](ctx, r.Client, &appsv1.DeploymentList{}, &containersList); err != nil {
|
|
return shared.ContainersList{}, err
|
|
}
|
|
if err := shared.ListAndProcessResources[*shared.JobWrapper](ctx, r.Client, &batchv1.JobList{}, &containersList); err != nil {
|
|
return shared.ContainersList{}, err
|
|
}
|
|
if err := shared.ListAndProcessResources[*shared.DaemonSetWrapper](ctx, r.Client, &appsv1.DaemonSetList{}, &containersList); err != nil {
|
|
return shared.ContainersList{}, err
|
|
}
|
|
if err := shared.ListAndProcessResources[*shared.CronJobWrapper](ctx, r.Client, &batchv1.CronJobList{}, &containersList); err != nil {
|
|
return shared.ContainersList{}, err
|
|
}
|
|
|
|
if len(clusterImageExport.Spec.Includes) > 0 {
|
|
containersList = shared.IncludeOnlyImages(containersList, clusterImageExport.Spec.Includes)
|
|
}
|
|
|
|
if len(clusterImageExport.Spec.Excludes) > 0 {
|
|
containersList = shared.RemoveExcludedImages(containersList, clusterImageExport.Spec.Excludes)
|
|
}
|
|
|
|
if len(clusterImageExport.Spec.Namespaces) > 0 {
|
|
containersList = shared.FilterOnlyFromNamespaces(containersList, clusterImageExport.Spec.Namespaces)
|
|
}
|
|
|
|
if len(clusterImageExport.Spec.ExcludedNamespaces) > 0 {
|
|
containersList = shared.FilterOutWholeNamespaces(containersList, clusterImageExport.Spec.ExcludedNamespaces)
|
|
}
|
|
|
|
containersList = shared.RemoveDuplicates(containersList)
|
|
// l.Info("List of containers in the cluster", "containers", containersList)
|
|
|
|
return containersList, nil
|
|
}
|
|
|
|
func (r *ClusterImageExportReconciler) handleDeletion(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) (ctrl.Result, error) {
|
|
l := log.FromContext(ctx)
|
|
|
|
if controllerutil.ContainsFinalizer(clusterImageExport, clusterImageExportFinalizer) {
|
|
// Delete all associated ClusterImages first
|
|
if err := r.deleteAssociatedClusterImages(ctx, clusterImageExport); err != nil {
|
|
l.Error(err, "Failed to delete associated ClusterImages")
|
|
// Continue with deletion even if cleanup fails
|
|
}
|
|
|
|
// Create or recreate cleanup job
|
|
jobName := "cleanup-" + shared.NormalizeImageName(clusterImageExport.Name)
|
|
existingJob := &batchv1.Job{}
|
|
err := r.Get(ctx, client.ObjectKey{
|
|
Namespace: clusterImageExport.Namespace,
|
|
Name: jobName,
|
|
}, existingJob)
|
|
|
|
if err == nil {
|
|
// Job exists, delete it first
|
|
deletePolicy := metav1.DeletePropagationForeground
|
|
deleteOptions := client.DeleteOptions{
|
|
PropagationPolicy: &deletePolicy,
|
|
}
|
|
if err := r.Delete(ctx, existingJob, &deleteOptions); err != nil && !errors.IsNotFound(err) {
|
|
l.Error(err, "Failed to delete existing cleanup job")
|
|
return ctrl.Result{Requeue: true}, nil
|
|
}
|
|
l.Info("Successfully deleted existing cleanup job", "job", jobName)
|
|
// Requeue to wait for job deletion to complete
|
|
return ctrl.Result{Requeue: true}, nil
|
|
} else if !errors.IsNotFound(err) {
|
|
// Unexpected error
|
|
l.Error(err, "Error checking for existing cleanup job")
|
|
return ctrl.Result{Requeue: true}, nil
|
|
}
|
|
|
|
// Create new cleanup job
|
|
if err := r.runCleanupJob(ctx, clusterImageExport); err != nil {
|
|
l.Error(err, "Failed to create cleanup job")
|
|
return ctrl.Result{Requeue: true}, nil
|
|
}
|
|
|
|
// Only remove finalizer after cleanup job is created successfully
|
|
controllerutil.RemoveFinalizer(clusterImageExport, clusterImageExportFinalizer)
|
|
if err := r.Update(ctx, clusterImageExport); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
// Object is already gone, which is fine
|
|
return ctrl.Result{}, nil
|
|
}
|
|
return ctrl.Result{}, err
|
|
}
|
|
}
|
|
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
func (r *ClusterImageExportReconciler) deleteAssociatedClusterImages(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) error {
|
|
l := log.FromContext(ctx)
|
|
|
|
// List all ClusterImages associated with this export
|
|
clusterImageList := &raczylocomv1.ClusterImageList{}
|
|
if err := r.List(ctx, clusterImageList, client.InNamespace(clusterImageExport.Namespace),
|
|
client.MatchingFields{shared.FIELD_EXPORT_NAME: clusterImageExport.Name}); err != nil {
|
|
return fmt.Errorf("failed to list ClusterImages: %w", err)
|
|
}
|
|
|
|
// Delete each ClusterImage
|
|
for _, ci := range clusterImageList.Items {
|
|
if err := r.Delete(ctx, &ci); err != nil && !errors.IsNotFound(err) {
|
|
l.Error(err, "Failed to delete ClusterImage", "name", ci.Name)
|
|
// Continue deleting other ClusterImages even if one fails
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *ClusterImageExportReconciler) runCleanupJob(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) error {
|
|
l := log.FromContext(ctx)
|
|
normalisedImageName := "cleanup-" + shared.NormalizeImageName(clusterImageExport.Name)
|
|
|
|
defaultCommands := []string{}
|
|
|
|
switch clusterImageExport.Spec.Storage.StorageTarget {
|
|
case shared.STORAGE_S3:
|
|
s3Params := shared.SetupS3Params(clusterImageExport.Spec.Storage.S3)
|
|
additionalCommands := []string{
|
|
"./worker cleanup " + strings.Join(s3Params, " ") + " 's3://" + clusterImageExport.Spec.Storage.S3.Bucket + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.Name + "/'",
|
|
}
|
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
|
case shared.STORAGE_FILE:
|
|
additionalCommands := []string{
|
|
"./worker cleanup '" + clusterImageExport.Spec.BasePath + "/" + clusterImageExport.Name + "/'",
|
|
}
|
|
defaultCommands = append(defaultCommands, additionalCommands...)
|
|
}
|
|
|
|
// Set up the cleanup job with retry limits and TTL
|
|
backoffLimit := int32(2) // 3 total attempts (initial + 2 retries)
|
|
ttlSecondsAfterFinished := int32(300) // Delete job 5 minutes after completion
|
|
|
|
// Merge annotations from different sources
|
|
mergedAnnotations := make(map[string]string)
|
|
// 1. Add CRD metadata annotations
|
|
for k, v := range clusterImageExport.Annotations {
|
|
mergedAnnotations[k] = v
|
|
}
|
|
// 2. Add controller pod annotations
|
|
for k, v := range r.podAnnotations {
|
|
mergedAnnotations[k] = v
|
|
}
|
|
// 3. Add job-specific annotations from spec (these take precedence)
|
|
for k, v := range clusterImageExport.Spec.JobAnnotations {
|
|
mergedAnnotations[k] = v
|
|
}
|
|
|
|
// Set up environment variables for AWS configuration
|
|
envVars := []corev1.EnvVar{}
|
|
if clusterImageExport.Spec.Storage.StorageTarget == shared.STORAGE_S3 {
|
|
if clusterImageExport.Spec.Storage.S3.Region != "" {
|
|
envVars = append(envVars, corev1.EnvVar{
|
|
Name: "AWS_REGION",
|
|
Value: clusterImageExport.Spec.Storage.S3.Region,
|
|
})
|
|
envVars = append(envVars, corev1.EnvVar{
|
|
Name: "AWS_DEFAULT_REGION",
|
|
Value: clusterImageExport.Spec.Storage.S3.Region,
|
|
})
|
|
}
|
|
}
|
|
|
|
jobParams := shared.JobParams{
|
|
Name: normalisedImageName,
|
|
Namespace: clusterImageExport.Namespace,
|
|
Image: shared.BACKUP_JOB_IMAGE,
|
|
Commands: defaultCommands,
|
|
Annotations: mergedAnnotations,
|
|
ServiceAccount: "",
|
|
ImagePullSecrets: clusterImageExport.Spec.ImagePullSecrets,
|
|
BackoffLimit: &backoffLimit,
|
|
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
|
|
EnvVars: envVars,
|
|
}
|
|
|
|
cleanupJob := shared.CreateJob(jobParams, func(raczylocomv1.ClusterImageExport) []string { return nil })
|
|
|
|
// Try to create the cleanup job
|
|
if err := r.Create(ctx, cleanupJob); err != nil {
|
|
l.Error(err, "Failed to create cleanup job")
|
|
return err
|
|
}
|
|
|
|
l.Info("Created cleanup job with retry limit and TTL")
|
|
return nil
|
|
}
|
|
|
|
// shouldDeleteByTTL checks if the export should be deleted based on TTL (in days)
|
|
func (r *ClusterImageExportReconciler) shouldDeleteByTTL(clusterImageExport *raczylocomv1.ClusterImageExport) bool {
|
|
// Only apply TTL to completed exports
|
|
if clusterImageExport.Status.Progress != shared.STATUS_SUCCESS &&
|
|
clusterImageExport.Status.Progress != shared.STATUS_FAILED {
|
|
return false
|
|
}
|
|
|
|
// Check if TTL is configured
|
|
if clusterImageExport.Spec.TTLDaysAfterFinished == nil {
|
|
return false
|
|
}
|
|
|
|
// Check if CompletedAt is set
|
|
if clusterImageExport.Status.CompletedAt == nil {
|
|
return false
|
|
}
|
|
|
|
// Convert days to duration (24 hours per day)
|
|
ttlDuration := time.Duration(*clusterImageExport.Spec.TTLDaysAfterFinished) * 24 * time.Hour
|
|
expirationTime := clusterImageExport.Status.CompletedAt.Add(ttlDuration)
|
|
|
|
return time.Now().After(expirationTime)
|
|
}
|
|
|
|
// cleanupByRetention enforces the retention policy for completed exports
|
|
func (r *ClusterImageExportReconciler) cleanupByRetention(ctx context.Context, clusterImageExport *raczylocomv1.ClusterImageExport) error {
|
|
l := log.FromContext(ctx)
|
|
|
|
// Check if retention policy is configured
|
|
if clusterImageExport.Spec.Retention == nil {
|
|
return nil
|
|
}
|
|
|
|
// List all ClusterImageExports in the same namespace
|
|
exportList := &raczylocomv1.ClusterImageExportList{}
|
|
if err := r.List(ctx, exportList, client.InNamespace(clusterImageExport.Namespace)); err != nil {
|
|
return fmt.Errorf("failed to list ClusterImageExports: %w", err)
|
|
}
|
|
|
|
// Separate successful and failed exports, sorted by completion time
|
|
var successfulExports, failedExports []*raczylocomv1.ClusterImageExport
|
|
for i := range exportList.Items {
|
|
export := &exportList.Items[i]
|
|
// Skip exports that don't have the same base path (different backup sets)
|
|
if export.Spec.BasePath != clusterImageExport.Spec.BasePath {
|
|
continue
|
|
}
|
|
// Skip exports that are still running
|
|
if export.Status.Progress != shared.STATUS_SUCCESS &&
|
|
export.Status.Progress != shared.STATUS_FAILED {
|
|
continue
|
|
}
|
|
switch export.Status.Progress {
|
|
case shared.STATUS_SUCCESS:
|
|
successfulExports = append(successfulExports, export)
|
|
case shared.STATUS_FAILED:
|
|
failedExports = append(failedExports, export)
|
|
}
|
|
}
|
|
|
|
// Sort by CompletedAt (newest first)
|
|
sortByCompletionTime := func(exports []*raczylocomv1.ClusterImageExport) {
|
|
for i := 0; i < len(exports); i++ {
|
|
for j := i + 1; j < len(exports); j++ {
|
|
iTime := exports[i].Status.CompletedAt
|
|
jTime := exports[j].Status.CompletedAt
|
|
if iTime == nil || (jTime != nil && jTime.After(iTime.Time)) {
|
|
exports[i], exports[j] = exports[j], exports[i]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sortByCompletionTime(successfulExports)
|
|
sortByCompletionTime(failedExports)
|
|
|
|
// Delete excess successful exports
|
|
if clusterImageExport.Spec.Retention.MaxSuccessful != nil {
|
|
maxSuccessful := int(*clusterImageExport.Spec.Retention.MaxSuccessful)
|
|
if len(successfulExports) > maxSuccessful {
|
|
for _, export := range successfulExports[maxSuccessful:] {
|
|
l.Info("Deleting export due to retention policy (maxSuccessful exceeded)",
|
|
"export", export.Name, "maxSuccessful", maxSuccessful)
|
|
if err := r.Delete(ctx, export); err != nil && !errors.IsNotFound(err) {
|
|
l.Error(err, "Failed to delete export for retention", "export", export.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete excess failed exports
|
|
if clusterImageExport.Spec.Retention.MaxFailed != nil {
|
|
maxFailed := int(*clusterImageExport.Spec.Retention.MaxFailed)
|
|
if len(failedExports) > maxFailed {
|
|
for _, export := range failedExports[maxFailed:] {
|
|
l.Info("Deleting export due to retention policy (maxFailed exceeded)",
|
|
"export", export.Name, "maxFailed", maxFailed)
|
|
if err := r.Delete(ctx, export); err != nil && !errors.IsNotFound(err) {
|
|
l.Error(err, "Failed to delete export for retention", "export", export.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|