@@ -22,6 +22,7 @@ import (
2222 "encoding/json"
2323 "fmt"
2424 "io"
25+ "strconv"
2526 "strings"
2627 "time"
2728
@@ -96,6 +97,10 @@ type KubeVirtDataUploadReconciler struct {
9697 // DatamoverImagePullPolicy is the pull policy for the datamover image
9798 DatamoverImagePullPolicy corev1.PullPolicy
9899
100+ // MaxIncrementalBackups is the maximum number of incremental backups per VM
101+ // before forcing a full backup. 0 means unlimited.
102+ MaxIncrementalBackups int
103+
99104 // ObjectStoreFactory creates an ObjectStore from an UploaderConfig.
100105 // Defaults to uploader.InitObjectStore if nil. Override in tests to inject mocks.
101106 ObjectStoreFactory func (cfg * uploader.UploaderConfig ) (velero.ObjectStore , error )
@@ -499,9 +504,10 @@ func (r *KubeVirtDataUploadReconciler) evaluateVMBackupStatus(
499504// resolveBackupMode determines whether to force a full backup or allow incremental.
500505// Returns (forceFullBackup, checkpointLookup) where checkpointLookup is the BSL
501506// chain validation result (nil if BSL was unreachable or validation was skipped).
502- // This covers two scenarios:
507+ // This covers three scenarios:
503508// 1. User explicitly requested force-full-backup via annotation on DataUpload.
504509// 2. BSL checkpoint validation found a broken chain, requiring a forced full backup.
510+ // 3. Max incremental backups limit reached (global or per-VM override).
505511func (r * KubeVirtDataUploadReconciler ) resolveBackupMode (ctx context.Context , logger logr.Logger , du * velerov2alpha1.DataUpload , vmRef * common.VMReference ) (bool , * uploader.CheckpointLookupResult ) {
506512 // Check if force full backup is requested via annotation.
507513 if du .Annotations [common .AnnotationForceFullBackup ] == bslValidatedValue {
@@ -518,7 +524,47 @@ func (r *KubeVirtDataUploadReconciler) resolveBackupMode(ctx context.Context, lo
518524 return false , nil
519525 }
520526
521- return r .validateBSLCheckpoint (ctx , logger , du , vmRef )
527+ forceFullBackup , checkpointLookup := r .validateBSLCheckpoint (ctx , logger , du , vmRef )
528+
529+ // Check if max incremental backups limit is reached.
530+ // ChainLength includes the root full backup, so incrementals = ChainLength - 1.
531+ if ! forceFullBackup &&
532+ checkpointLookup != nil && checkpointLookup .Found && checkpointLookup .IsChainValid {
533+ maxInc := r .getEffectiveMaxIncrementalBackups (ctx , logger , vmRef )
534+ if maxInc > 0 {
535+ incrementalCount := checkpointLookup .ChainLength - 1
536+ if incrementalCount >= maxInc {
537+ logger .Info ("Max incremental backups reached, forcing full backup" ,
538+ "incrementalCount" , incrementalCount ,
539+ "maxIncrementalBackups" , maxInc ,
540+ "chainLength" , checkpointLookup .ChainLength )
541+ forceFullBackup = true
542+ }
543+ }
544+ }
545+
546+ return forceFullBackup , checkpointLookup
547+ }
548+
549+ // getEffectiveMaxIncrementalBackups returns the max incremental backups limit
550+ // for the given VM. A per-VM annotation takes precedence over the global setting.
551+ func (r * KubeVirtDataUploadReconciler ) getEffectiveMaxIncrementalBackups (ctx context.Context , logger logr.Logger , vmRef * common.VMReference ) int {
552+ vm := & kubevirtcorev1.VirtualMachine {}
553+ if err := r .Get (ctx , types.NamespacedName {Name : vmRef .Name , Namespace : vmRef .Namespace }, vm ); err != nil {
554+ logger .V (1 ).Info ("Could not fetch VM for max-incremental-backups annotation, using global setting" ,
555+ "reason" , err .Error ())
556+ return r .MaxIncrementalBackups
557+ }
558+ if val , ok := vm .Annotations [common .AnnotationMaxIncrementalBackups ]; ok {
559+ if parsed , err := strconv .Atoi (val ); err == nil && parsed >= 0 {
560+ logger .Info ("Using per-VM max incremental backups override" ,
561+ "vm" , vmRef .Name , "maxIncrementalBackups" , parsed )
562+ return parsed
563+ }
564+ logger .Info ("Invalid max-incremental-backups annotation value, using global setting" ,
565+ "vm" , vmRef .Name , "annotationValue" , val )
566+ }
567+ return r .MaxIncrementalBackups
522568}
523569
524570// validateBSLCheckpoint queries the BSL for a valid checkpoint chain and determines
0 commit comments