This is an automated email from the ASF dual-hosted git repository.

houston pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new ff3bcd1  Add ability to schedule recurring incremental backups (#359)
ff3bcd1 is described below

commit ff3bcd176f1ba6bf671970e33dc57a63c499a9b4
Author: Houston Putman <[email protected]>
AuthorDate: Tue Nov 9 12:06:27 2021 -0500

    Add ability to schedule recurring incremental backups (#359)
---
 api/v1beta1/solrbackup_types.go                    |  81 ++++++++++----
 api/v1beta1/solrcloud_types.go                     |   1 -
 api/v1beta1/zz_generated.deepcopy.go               |  82 +++++++++++---
 config/crd/bases/solr.apache.org_solrbackups.yaml  | 121 +++++++++++++++++++-
 controllers/common.go                              |   3 +
 controllers/solrbackup_controller.go               | 123 ++++++++++++++-------
 controllers/solrcloud_controller.go                |   4 +-
 controllers/solrprometheusexporter_controller.go   |  12 +-
 controllers/util/backup_util.go                    |  84 +++++++-------
 controllers/util/solr_api/api.go                   |  55 ++++++++-
 .../{common.go => util/solr_api/node_command.go}   |  18 ++-
 controllers/util/solr_update_util.go               |   2 +-
 docs/solr-backup/README.md                         |  87 ++++++++++++++-
 helm/solr-operator/Chart.yaml                      |  11 +-
 helm/solr-operator/crds/crds.yaml                  | 121 +++++++++++++++++++-
 main.go                                            |   3 +-
 16 files changed, 660 insertions(+), 148 deletions(-)

diff --git a/api/v1beta1/solrbackup_types.go b/api/v1beta1/solrbackup_types.go
index ec0d151..2665211 100644
--- a/api/v1beta1/solrbackup_types.go
+++ b/api/v1beta1/solrbackup_types.go
@@ -21,7 +21,6 @@ import (
        "fmt"
        corev1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-       "strings"
 )
 
 // SolrBackupSpec defines the desired state of SolrBackup
@@ -50,6 +49,13 @@ type SolrBackupSpec struct {
        // +optional
        Location string `json:"location,omitempty"`
 
+       // Set this backup to be taken recurrently, with options for scheduling 
and storage.
+       //
+       // NOTE: This is only supported for Solr Clouds version 8.9+, as it 
uses the incremental backup API.
+       //
+       // +optional
+       Recurrence *BackupRecurrence `json:"recurrence,omitempty"`
+
        // Persistence is the specification on how to persist the backup data.
        // This feature has been removed as of v0.5.0. Any options specified 
here will not be used.
        //
@@ -67,6 +73,39 @@ func (spec *SolrBackupSpec) withDefaults() (changed bool) {
        return changed
 }
 
+// BackupRecurrence defines the recurrence of the incremental backup
+type BackupRecurrence struct {
+       // Perform a backup on the given schedule, in CRON format.
+       //
+       // Multiple CRON syntaxes are supported
+       //   - Standard CRON (e.g. "CRON_TZ=Asia/Seoul 0 6 * * ?")
+       //   - Predefined Schedules (e.g. "@yearly", "@weekly", "@daily", etc.)
+       //   - Intervals (e.g. "@every 10h30m")
+       //
+       // For more information please check this reference:
+       // 
https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format
+       Schedule string `json:"schedule"`
+
+       // Define the number of backup points to save for this backup at any 
given time.
+       // The oldest backups will be deleted if too many exist when a backup 
is taken.
+       // If not provided, this defaults to 5.
+       //
+       // +kubebuilder:default:=5
+       // +kubebuilder:validation:Minimum:=1
+       // +optional
+       MaxSaved int `json:"maxSaved,omitempty"`
+
+       // Disable the recurring backups. Note this will not affect any 
currently-running backup.
+       //
+       // +kubebuilder:default:=false
+       // +optional
+       Disabled bool `json:"disabled,omitempty"`
+}
+
+func (recurrence *BackupRecurrence) IsEnabled() bool {
+       return recurrence != nil && !recurrence.Disabled
+}
+
 // PersistenceSource defines the location and method of persisting the backup 
data.
 // Exactly one member must be specified.
 //
@@ -160,27 +199,29 @@ type VolumePersistenceSource struct {
        BusyBoxImage ContainerImage `json:"busyBoxImage,omitempty"`
 }
 
-// Deprecated: Will be unused as of v0.5.0
-func (spec *VolumePersistenceSource) withDefaults(backupName string) (changed 
bool) {
-       changed = spec.BusyBoxImage.withDefaults(DefaultBusyBoxImageRepo, 
DefaultBusyBoxImageVersion, DefaultPullPolicy) || changed
-
-       if spec.Path != "" && strings.HasPrefix(spec.Path, "/") {
-               spec.Path = strings.TrimPrefix(spec.Path, "/")
-               changed = true
-       }
+// SolrBackupStatus defines the observed state of SolrBackup
+type SolrBackupStatus struct {
+       // The current Backup Status, which all fields are added to this struct
+       IndividualSolrBackupStatus `json:",inline"`
 
-       if spec.Filename == "" {
-               spec.Filename = backupName + ".tgz"
-               changed = true
-       }
+       // The scheduled time for the next backup to occur
+       // +optional
+       NextScheduledTime *metav1.Time `json:"nextScheduledTime,omitempty"`
 
-       return changed
+       // The status history of recurring backups
+       // +optional
+       History []IndividualSolrBackupStatus `json:"history,omitempty"`
 }
 
-// SolrBackupStatus defines the observed state of SolrBackup
-type SolrBackupStatus struct {
+// IndividualSolrBackupStatus defines the observed state of a single issued 
SolrBackup
+type IndividualSolrBackupStatus struct {
        // Version of the Solr being backed up
-       SolrVersion string `json:"solrVersion"`
+       // +optional
+       SolrVersion string `json:"solrVersion,omitempty"`
+
+       // The time that this backup was initiated
+       // +optional
+       StartTime metav1.Time `json:"startTimestamp,omitempty"`
 
        // The status of each collection's backup progress
        // +optional
@@ -289,8 +330,10 @@ func (sb *SolrBackup) PersistenceJobName() string {
 //+kubebuilder:categories=all
 //+kubebuilder:subresource:status
 
//+kubebuilder:printcolumn:name="Cloud",type="string",JSONPath=".spec.solrCloud",description="Solr
 Cloud"
-//+kubebuilder:printcolumn:name="Finished",type="boolean",JSONPath=".status.finished",description="Whether
 the backup has finished"
-//+kubebuilder:printcolumn:name="Successful",type="boolean",JSONPath=".status.successful",description="Whether
 the backup was successful"
+//+kubebuilder:printcolumn:name="Started",type="date",JSONPath=".status.startTimestamp",description="Most
 recent time the backup started"
+//+kubebuilder:printcolumn:name="Finished",type="boolean",JSONPath=".status.finished",description="Whether
 the most recent backup has finished"
+//+kubebuilder:printcolumn:name="Successful",type="boolean",JSONPath=".status.successful",description="Whether
 the most recent backup was successful"
+//+kubebuilder:printcolumn:name="NextBackup",type="string",JSONPath=".status.nextScheduledTime",description="Next
 scheduled time for a recurrent backup",format="date-time"
 
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
 
 // SolrBackup is the Schema for the solrbackups API
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index ff265ec..3c6b1ef 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -35,7 +35,6 @@ const (
        DefaultSolrReplicas = int32(3)
        DefaultSolrRepo     = "library/solr"
        DefaultSolrVersion  = "8.9"
-       DefaultSolrStorage  = "5Gi"
        DefaultSolrJavaMem  = "-Xms1g -Xmx2g"
        DefaultSolrOpts     = ""
        DefaultSolrLogLevel = "INFO"
diff --git a/api/v1beta1/zz_generated.deepcopy.go 
b/api/v1beta1/zz_generated.deepcopy.go
index 31031ee..4e3450b 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -77,6 +77,21 @@ func (in *BackupPersistenceStatus) DeepCopy() 
*BackupPersistenceStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
+func (in *BackupRecurrence) DeepCopyInto(out *BackupRecurrence) {
+       *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new BackupRecurrence.
+func (in *BackupRecurrence) DeepCopy() *BackupRecurrence {
+       if in == nil {
+               return nil
+       }
+       out := new(BackupRecurrence)
+       in.DeepCopyInto(out)
+       return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *CollectionBackupStatus) DeepCopyInto(out *CollectionBackupStatus) {
        *out = *in
        if in.StartTime != nil {
@@ -299,6 +314,43 @@ func (in *GcsRepository) DeepCopy() *GcsRepository {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
+func (in *IndividualSolrBackupStatus) DeepCopyInto(out 
*IndividualSolrBackupStatus) {
+       *out = *in
+       in.StartTime.DeepCopyInto(&out.StartTime)
+       if in.CollectionBackupStatuses != nil {
+               in, out := &in.CollectionBackupStatuses, 
&out.CollectionBackupStatuses
+               *out = make([]CollectionBackupStatus, len(*in))
+               for i := range *in {
+                       (*in)[i].DeepCopyInto(&(*out)[i])
+               }
+       }
+       if in.PersistenceStatus != nil {
+               in, out := &in.PersistenceStatus, &out.PersistenceStatus
+               *out = new(BackupPersistenceStatus)
+               (*in).DeepCopyInto(*out)
+       }
+       if in.FinishTime != nil {
+               in, out := &in.FinishTime, &out.FinishTime
+               *out = (*in).DeepCopy()
+       }
+       if in.Successful != nil {
+               in, out := &in.Successful, &out.Successful
+               *out = new(bool)
+               **out = **in
+       }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new IndividualSolrBackupStatus.
+func (in *IndividualSolrBackupStatus) DeepCopy() *IndividualSolrBackupStatus {
+       if in == nil {
+               return nil
+       }
+       out := new(IndividualSolrBackupStatus)
+       in.DeepCopyInto(out)
+       return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *IngressOptions) DeepCopyInto(out *IngressOptions) {
        *out = *in
        if in.Annotations != nil {
@@ -803,6 +855,11 @@ func (in *SolrBackupSpec) DeepCopyInto(out 
*SolrBackupSpec) {
                *out = make([]string, len(*in))
                copy(*out, *in)
        }
+       if in.Recurrence != nil {
+               in, out := &in.Recurrence, &out.Recurrence
+               *out = new(BackupRecurrence)
+               **out = **in
+       }
        if in.Persistence != nil {
                in, out := &in.Persistence, &out.Persistence
                *out = new(PersistenceSource)
@@ -823,27 +880,18 @@ func (in *SolrBackupSpec) DeepCopy() *SolrBackupSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, 
writing into out. in must be non-nil.
 func (in *SolrBackupStatus) DeepCopyInto(out *SolrBackupStatus) {
        *out = *in
-       if in.CollectionBackupStatuses != nil {
-               in, out := &in.CollectionBackupStatuses, 
&out.CollectionBackupStatuses
-               *out = make([]CollectionBackupStatus, len(*in))
+       
in.IndividualSolrBackupStatus.DeepCopyInto(&out.IndividualSolrBackupStatus)
+       if in.NextScheduledTime != nil {
+               in, out := &in.NextScheduledTime, &out.NextScheduledTime
+               *out = (*in).DeepCopy()
+       }
+       if in.History != nil {
+               in, out := &in.History, &out.History
+               *out = make([]IndividualSolrBackupStatus, len(*in))
                for i := range *in {
                        (*in)[i].DeepCopyInto(&(*out)[i])
                }
        }
-       if in.PersistenceStatus != nil {
-               in, out := &in.PersistenceStatus, &out.PersistenceStatus
-               *out = new(BackupPersistenceStatus)
-               (*in).DeepCopyInto(*out)
-       }
-       if in.FinishTime != nil {
-               in, out := &in.FinishTime, &out.FinishTime
-               *out = (*in).DeepCopy()
-       }
-       if in.Successful != nil {
-               in, out := &in.Successful, &out.Successful
-               *out = new(bool)
-               **out = **in
-       }
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, 
creating a new SolrBackupStatus.
diff --git a/config/crd/bases/solr.apache.org_solrbackups.yaml 
b/config/crd/bases/solr.apache.org_solrbackups.yaml
index 248515a..aa640fc 100644
--- a/config/crd/bases/solr.apache.org_solrbackups.yaml
+++ b/config/crd/bases/solr.apache.org_solrbackups.yaml
@@ -35,14 +35,23 @@ spec:
       jsonPath: .spec.solrCloud
       name: Cloud
       type: string
-    - description: Whether the backup has finished
+    - description: Most recent time the backup started
+      jsonPath: .status.startTimestamp
+      name: Started
+      type: date
+    - description: Whether the most recent backup has finished
       jsonPath: .status.finished
       name: Finished
       type: boolean
-    - description: Whether the backup was successful
+    - description: Whether the most recent backup was successful
       jsonPath: .status.successful
       name: Successful
       type: boolean
+    - description: Next scheduled time for a recurrent backup
+      format: date-time
+      jsonPath: .status.nextScheduledTime
+      name: NextBackup
+      type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
@@ -1051,6 +1060,24 @@ spec:
                     - source
                     type: object
                 type: object
+              recurrence:
+                description: "Set this backup to be taken recurrently, with 
options for scheduling and storage. \n NOTE: This is only supported for Solr 
Clouds version 8.9+, as it uses the incremental backup API."
+                properties:
+                  disabled:
+                    default: false
+                    description: Disable the recurring backups. Note this will 
not affect any currently-running backup.
+                    type: boolean
+                  maxSaved:
+                    default: 5
+                    description: Define the number of backup points to save 
for this backup at any given time. The oldest backups will be deleted if too 
many exist when a backup is taken. If not provided, this defaults to 5.
+                    minimum: 1
+                    type: integer
+                  schedule:
+                    description: "Perform a backup on the given schedule, in 
CRON format. \n Multiple CRON syntaxes are supported   - Standard CRON (e.g. 
\"CRON_TZ=Asia/Seoul 0 6 * * ?\")   - Predefined Schedules (e.g. \"@yearly\", 
\"@weekly\", \"@daily\", etc.)   - Intervals (e.g. \"@every 10h30m\") \n For 
more information please check this reference: 
https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format";
+                    type: string
+                required:
+                - schedule
+                type: object
               repositoryName:
                 description: The name of the repository to use for the backup. 
 Defaults to "legacy_local_repository" if not specified (the auto-configured 
repository for legacy singleton volumes).
                 maxLength: 100
@@ -1111,6 +1138,90 @@ spec:
               finished:
                 description: Whether the backup has finished
                 type: boolean
+              history:
+                description: The status history of recurring backups
+                items:
+                  description: IndividualSolrBackupStatus defines the observed 
state of a single issued SolrBackup
+                  properties:
+                    collectionBackupStatuses:
+                      description: The status of each collection's backup 
progress
+                      items:
+                        description: CollectionBackupStatus defines the 
progress of a Solr Collection's backup
+                        properties:
+                          asyncBackupStatus:
+                            description: The status of the asynchronous backup 
call to solr
+                            type: string
+                          backupName:
+                            description: BackupName of this collection's 
backup in Solr
+                            type: string
+                          collection:
+                            description: Solr Collection name
+                            type: string
+                          finishTimestamp:
+                            description: Time that the collection backup 
finished at
+                            format: date-time
+                            type: string
+                          finished:
+                            description: Whether the backup has finished
+                            type: boolean
+                          inProgress:
+                            description: Whether the collection is being 
backed up
+                            type: boolean
+                          startTimestamp:
+                            description: Time that the collection backup 
started at
+                            format: date-time
+                            type: string
+                          successful:
+                            description: Whether the backup was successful
+                            type: boolean
+                        required:
+                        - collection
+                        type: object
+                      type: array
+                    finishTimestamp:
+                      description: Version of the Solr being backed up
+                      format: date-time
+                      type: string
+                    finished:
+                      description: Whether the backup has finished
+                      type: boolean
+                    persistenceStatus:
+                      description: Whether the backups are in progress of 
being persisted. This feature has been removed as of v0.5.0.
+                      properties:
+                        finishTimestamp:
+                          description: Time that the collection backup 
finished at
+                          format: date-time
+                          type: string
+                        finished:
+                          description: Whether the persistence has finished
+                          type: boolean
+                        inProgress:
+                          description: Whether the collection is being backed 
up
+                          type: boolean
+                        startTimestamp:
+                          description: Time that the collection backup started 
at
+                          format: date-time
+                          type: string
+                        successful:
+                          description: Whether the backup was successful
+                          type: boolean
+                      type: object
+                    solrVersion:
+                      description: Version of the Solr being backed up
+                      type: string
+                    startTimestamp:
+                      description: The time that this backup was initiated
+                      format: date-time
+                      type: string
+                    successful:
+                      description: Whether the backup was successful
+                      type: boolean
+                  type: object
+                type: array
+              nextScheduledTime:
+                description: The scheduled time for the next backup to occur
+                format: date-time
+                type: string
               persistenceStatus:
                 description: Whether the backups are in progress of being 
persisted. This feature has been removed as of v0.5.0.
                 properties:
@@ -1135,11 +1246,13 @@ spec:
               solrVersion:
                 description: Version of the Solr being backed up
                 type: string
+              startTimestamp:
+                description: The time that this backup was initiated
+                format: date-time
+                type: string
               successful:
                 description: Whether the backup was successful
                 type: boolean
-            required:
-            - solrVersion
             type: object
         type: object
     served: true
diff --git a/controllers/common.go b/controllers/common.go
index 38a056e..b86fb00 100644
--- a/controllers/common.go
+++ b/controllers/common.go
@@ -24,6 +24,9 @@ import (
 
 // Set the requeueAfter if it has not been set, or is greater than the new 
time to requeue at
 func updateRequeueAfter(requeueOrNot *reconcile.Result, newWait time.Duration) 
{
+       if newWait <= 0 {
+               requeueOrNot.RequeueAfter = 0
+       }
        if requeueOrNot.RequeueAfter <= 0 || requeueOrNot.RequeueAfter > 
newWait {
                requeueOrNot.RequeueAfter = newWait
        }
diff --git a/controllers/solrbackup_controller.go 
b/controllers/solrbackup_controller.go
index 4e8d41b..c5905c1 100644
--- a/controllers/solrbackup_controller.go
+++ b/controllers/solrbackup_controller.go
@@ -30,7 +30,6 @@ import (
 
        "github.com/apache/solr-operator/controllers/util"
        "github.com/go-logr/logr"
-       batchv1 "k8s.io/api/batch/v1"
        "k8s.io/apimachinery/pkg/api/errors"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
@@ -48,7 +47,7 @@ import (
 type SolrBackupReconciler struct {
        client.Client
        Scheme *runtime.Scheme
-       config *rest.Config
+       Config *rest.Config
 }
 
 //+kubebuilder:rbac:groups="",resources=pods/exec,verbs=create
@@ -80,45 +79,93 @@ func (r *SolrBackupReconciler) Reconcile(ctx 
context.Context, req ctrl.Request)
                return reconcile.Result{}, err
        }
 
-       oldStatus := backup.Status.DeepCopy()
-
        changed := backup.WithDefaults()
        if changed {
                logger.Info("Setting default settings for solr-backup")
-               if err := r.Update(ctx, backup); err != nil {
+               if err = r.Update(ctx, backup); err != nil {
                        return reconcile.Result{}, err
                }
                return reconcile.Result{Requeue: true}, nil
        }
 
-       // When working with the collection backups, auto-requeue after 5 
seconds
-       // to check on the status of the async solr backup calls
+       oldStatus := backup.Status.DeepCopy()
+
        requeueOrNot := reconcile.Result{}
 
-       solrCloud, _, err := r.reconcileSolrCloudBackup(ctx, backup, logger)
-       if err != nil {
-               // TODO Should we be failing the backup for some sub-set of 
errors here?
-               logger.Error(err, "Error while taking SolrCloud backup")
-
-               // Requeue after 10 seconds for errors.
-               requeueOrNot = reconcile.Result{Requeue: true, RequeueAfter: 
time.Second * 10}
-       } else if solrCloud != nil && !backup.Status.Finished {
-               // Only requeue if the SolrCloud we are backing up exists and 
we are not finished with the backups.
-               requeueOrNot = reconcile.Result{Requeue: true, RequeueAfter: 
time.Second * 5}
-       } else if backup.Status.Finished && backup.Status.FinishTime == nil {
-               now := metav1.Now()
-               backup.Status.FinishTime = &now
+       var backupNeedsToWait bool
+
+       // Check if we should start the next backup
+       if backup.Status.NextScheduledTime != nil {
+               // If the backup no longer enabled, remove the next scheduled 
time
+               if !backup.Spec.Recurrence.IsEnabled() {
+                       backup.Status.NextScheduledTime = nil
+                       backupNeedsToWait = false
+               } else if 
backup.Status.NextScheduledTime.UTC().Before(time.Now().UTC()) {
+                       // We have hit the next scheduled restart time.
+                       backupNeedsToWait = false
+                       backup.Status.NextScheduledTime = nil
+
+                       // Add the current backup to the front of the history.
+                       // If there is no max
+                       backup.Status.History = 
append([]solrv1beta1.IndividualSolrBackupStatus{backup.Status.IndividualSolrBackupStatus},
 backup.Status.History...)
+
+                       // Remove history if we have too much saved
+                       if len(backup.Status.History) > 
backup.Spec.Recurrence.MaxSaved {
+                               backup.Status.History = 
backup.Status.History[:backup.Spec.Recurrence.MaxSaved]
+                       }
+
+                       // Reset Current, which is fine since it is now in the 
history.
+                       backup.Status.IndividualSolrBackupStatus = 
solrv1beta1.IndividualSolrBackupStatus{}
+               } else {
+                       // If we have not hit the next scheduled restart, wait 
to requeue until that is true.
+                       updateRequeueAfter(&requeueOrNot, 
backup.Status.NextScheduledTime.UTC().Sub(time.Now().UTC()))
+                       backupNeedsToWait = true
+               }
+       } else {
+               backupNeedsToWait = false
+       }
+
+       // Do backup work if we are not waiting and the current backup is not 
finished
+       if !backupNeedsToWait && 
!backup.Status.IndividualSolrBackupStatus.Finished {
+               solrCloud, _, err1 := r.reconcileSolrCloudBackup(ctx, backup, 
&backup.Status.IndividualSolrBackupStatus, logger)
+               if err1 != nil {
+                       // TODO Should we be failing the backup for some 
sub-set of errors here?
+                       logger.Error(err1, "Error while taking SolrCloud 
backup")
+
+                       // Requeue after 10 seconds for errors.
+                       updateRequeueAfter(&requeueOrNot, time.Second*10)
+               } else if backup.Status.IndividualSolrBackupStatus.Finished {
+                       // Set finish time
+                       now := metav1.Now()
+                       backup.Status.IndividualSolrBackupStatus.FinishTime = 
&now
+               } else if solrCloud != nil {
+                       // When working with the collection backups, 
auto-requeue after 5 seconds
+                       // to check on the status of the async solr backup calls
+                       updateRequeueAfter(&requeueOrNot, time.Second*5)
+               }
+       }
+
+       // Schedule the next backupTime, if it doesn't have a next scheduled 
time, it has recurrence and the current backup is finished
+       if backup.Status.NextScheduledTime == nil && 
backup.Spec.Recurrence.IsEnabled() && 
backup.Status.IndividualSolrBackupStatus.Finished {
+               if nextBackupTime, err1 := 
util.ScheduleNextBackup(backup.Spec.Recurrence.Schedule, 
backup.Status.IndividualSolrBackupStatus.StartTime.Time); err1 != nil {
+                       logger.Error(err1, "Could not schedule new backup due 
to bad cron schedule", "cron", backup.Spec.Recurrence.Schedule)
+               } else {
+                       logger.Info("Scheduling Next Backup", "time", 
nextBackupTime)
+                       convTime := metav1.NewTime(nextBackupTime)
+                       backup.Status.NextScheduledTime = &convTime
+                       updateRequeueAfter(&requeueOrNot, 
backup.Status.NextScheduledTime.Sub(time.Now()))
+               }
        }
 
-       if !reflect.DeepEqual(oldStatus, &backup.Status) {
-               logger.Info("Updating status for solr-backup")
+       if !reflect.DeepEqual(*oldStatus, backup.Status) {
+               logger.Info("Updating status for solr-backup", "newStatus", 
backup.Status, "oldStatus", oldStatus)
                err = r.Status().Update(ctx, backup)
        }
 
        return requeueOrNot, err
 }
 
-func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx context.Context, 
backup *solrv1beta1.SolrBackup, logger logr.Logger) (solrCloud 
*solrv1beta1.SolrCloud, actionTaken bool, err error) {
+func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx context.Context, 
backup *solrv1beta1.SolrBackup, currentBackupStatus 
*solrv1beta1.IndividualSolrBackupStatus, logger logr.Logger) (solrCloud 
*solrv1beta1.SolrCloud, actionTaken bool, err error) {
        // Get the solrCloud that this backup is for.
        solrCloud = &solrv1beta1.SolrCloud{}
 
@@ -139,7 +186,7 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx 
context.Context, bac
        }
 
        // First check if the collection backups have been completed
-       collectionBackupsFinished := 
util.UpdateStatusOfCollectionBackups(backup)
+       collectionBackupsFinished := 
util.UpdateStatusOfCollectionBackups(currentBackupStatus)
 
        // If the collectionBackups are complete, then nothing else has to be 
done here
        if collectionBackupsFinished {
@@ -156,9 +203,9 @@ func (r *SolrBackupReconciler) reconcileSolrCloudBackup(ctx 
context.Context, bac
        }
 
        // This should only occur before the backup processes have been started
-       if backup.Status.SolrVersion == "" {
+       if currentBackupStatus.StartTime.IsZero() {
                // Prep the backup directory in the persistentVolume
-               err = util.EnsureDirectoryForBackup(solrCloud, 
backupRepository, backup, r.config)
+               err = util.EnsureDirectoryForBackup(solrCloud, 
backupRepository, backup, r.Config)
                if err != nil {
                        return solrCloud, actionTaken, err
                }
@@ -170,30 +217,31 @@ func (r *SolrBackupReconciler) 
reconcileSolrCloudBackup(ctx context.Context, bac
                }
 
                // Only set the solr version at the start of the backup. This 
shouldn't change throughout the backup.
-               backup.Status.SolrVersion = solrCloud.Status.Version
+               currentBackupStatus.SolrVersion = solrCloud.Status.Version
+               currentBackupStatus.StartTime = metav1.Now()
        }
 
        // Go through each collection specified and reconcile the backup.
        for _, collection := range backup.Spec.Collections {
                // This will in-place update the CollectionBackupStatus in the 
backup object
-               if _, err = reconcileSolrCollectionBackup(ctx, backup, 
solrCloud, backupRepository, collection, logger); err != nil {
+               if _, err = reconcileSolrCollectionBackup(ctx, backup, 
currentBackupStatus, solrCloud, backupRepository, collection, logger); err != 
nil {
                        break
                }
        }
 
        // First check if the collection backups have been completed
-       util.UpdateStatusOfCollectionBackups(backup)
+       util.UpdateStatusOfCollectionBackups(currentBackupStatus)
 
        return solrCloud, actionTaken, err
 }
 
-func reconcileSolrCollectionBackup(ctx context.Context, backup 
*solrv1beta1.SolrBackup, solrCloud *solrv1beta1.SolrCloud, backupRepository 
*solrv1beta1.SolrBackupRepository, collection string, logger logr.Logger) 
(finished bool, err error) {
+func reconcileSolrCollectionBackup(ctx context.Context, backup 
*solrv1beta1.SolrBackup, currentBackupStatus 
*solrv1beta1.IndividualSolrBackupStatus, solrCloud *solrv1beta1.SolrCloud, 
backupRepository *solrv1beta1.SolrBackupRepository, collection string, logger 
logr.Logger) (finished bool, err error) {
        now := metav1.Now()
        collectionBackupStatus := solrv1beta1.CollectionBackupStatus{}
        collectionBackupStatus.Collection = collection
        backupIndex := -1
        // Get the backup status for this collection, if one exists
-       for i, status := range backup.Status.CollectionBackupStatuses {
+       for i, status := range currentBackupStatus.CollectionBackupStatuses {
                if status.Collection == collection {
                        collectionBackupStatus = status
                        backupIndex = i
@@ -201,7 +249,9 @@ func reconcileSolrCollectionBackup(ctx context.Context, 
backup *solrv1beta1.Solr
        }
 
        // If the collection backup hasn't started, start it
-       if !collectionBackupStatus.InProgress && 
!collectionBackupStatus.Finished {
+       if collectionBackupStatus.Finished {
+               return true, nil
+       } else if !collectionBackupStatus.InProgress {
                // Start the backup by calling solr
                var started bool
                started, err = util.StartBackupForCollection(ctx, solrCloud, 
backupRepository, backup, collection, logger)
@@ -239,9 +289,9 @@ func reconcileSolrCollectionBackup(ctx context.Context, 
backup *solrv1beta1.Solr
        }
 
        if backupIndex < 0 {
-               backup.Status.CollectionBackupStatuses = 
append(backup.Status.CollectionBackupStatuses, collectionBackupStatus)
+               currentBackupStatus.CollectionBackupStatuses = 
append(currentBackupStatus.CollectionBackupStatuses, collectionBackupStatus)
        } else {
-               backup.Status.CollectionBackupStatuses[backupIndex] = 
collectionBackupStatus
+               currentBackupStatus.CollectionBackupStatuses[backupIndex] = 
collectionBackupStatus
        }
 
        return collectionBackupStatus.Finished, err
@@ -249,11 +299,10 @@ func reconcileSolrCollectionBackup(ctx context.Context, 
backup *solrv1beta1.Solr
 
 // SetupWithManager sets up the controller with the Manager.
 func (r *SolrBackupReconciler) SetupWithManager(mgr ctrl.Manager) (err error) {
-       r.config = mgr.GetConfig()
+       r.Config = mgr.GetConfig()
 
        ctrlBuilder := ctrl.NewControllerManagedBy(mgr).
-               For(&solrv1beta1.SolrBackup{}).
-               Owns(&batchv1.Job{})
+               For(&solrv1beta1.SolrBackup{})
 
        ctrlBuilder, err = r.indexAndWatchForSolrClouds(mgr, ctrlBuilder)
        if err != nil {
diff --git a/controllers/solrcloud_controller.go 
b/controllers/solrcloud_controller.go
index 8d0153c..0674825 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -103,7 +103,7 @@ func (r *SolrCloudReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
        changed := instance.WithDefaults()
        if changed {
                logger.Info("Setting default settings for SolrCloud")
-               if err := r.Update(ctx, instance); err != nil {
+               if err = r.Update(ctx, instance); err != nil {
                        return reconcile.Result{}, err
                }
                return reconcile.Result{Requeue: true}, nil
@@ -323,7 +323,7 @@ func (r *SolrCloudReconciler) Reconcile(ctx 
context.Context, req ctrl.Request) (
 
                // Set the annotation for a scheduled restart, if necessary.
                if nextRestartAnnotation, reconcileWaitDuration, err := 
util.ScheduleNextRestart(instance.Spec.UpdateStrategy.RestartSchedule, 
foundStatefulSet.Spec.Template.Annotations); err != nil {
-                       logger.Error(err, "Cannot parse restartSchedule cron: 
%s", instance.Spec.UpdateStrategy.RestartSchedule)
+                       logger.Error(err, "Cannot parse restartSchedule cron", 
"cron", instance.Spec.UpdateStrategy.RestartSchedule)
                } else {
                        if nextRestartAnnotation != "" {
                                // Set the new restart time annotation
diff --git a/controllers/solrprometheusexporter_controller.go 
b/controllers/solrprometheusexporter_controller.go
index b6dd129..ae8f53a 100644
--- a/controllers/solrprometheusexporter_controller.go
+++ b/controllers/solrprometheusexporter_controller.go
@@ -217,7 +217,7 @@ func (r *SolrPrometheusExporterReconciler) Reconcile(ctx 
context.Context, req ct
 
        // Set the annotation for a scheduled restart, if necessary.
        if nextRestartAnnotation, reconcileWaitDuration, err := 
util.ScheduleNextRestart(prometheusExporter.Spec.RestartSchedule, 
foundDeploy.Spec.Template.Annotations); err != nil {
-               logger.Error(err, "Cannot parse restartSchedule cron: %s", 
prometheusExporter.Spec.RestartSchedule)
+               logger.Error(err, "Cannot parse restartSchedule cron", "cron", 
prometheusExporter.Spec.RestartSchedule)
        } else {
                if nextRestartAnnotation != "" {
                        if deploy.Spec.Template.Annotations == nil {
@@ -383,7 +383,7 @@ func (r *SolrPrometheusExporterReconciler) 
indexAndWatchForSolrClouds(mgr ctrl.M
        solrCloudField := ".spec.solrReference.cloud.name"
 
        if err := mgr.GetFieldIndexer().IndexField(context.Background(), 
&solrv1beta1.SolrPrometheusExporter{}, solrCloudField, func(rawObj 
client.Object) []string {
-               // grab the SolrCloud object, extract the used configMap...
+               // grab the SolrPrometheusExporter object, extract the used 
SolrCloud...
                exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
                if exporter.Spec.SolrReference.Cloud == nil {
                        return nil
@@ -429,7 +429,7 @@ func (r *SolrPrometheusExporterReconciler) 
indexAndWatchForProvidedConfigMaps(mg
        providedConfigMapField := 
".spec.customKubeOptions.configMapOptions.providedConfigMap"
 
        if err := mgr.GetFieldIndexer().IndexField(context.Background(), 
&solrv1beta1.SolrPrometheusExporter{}, providedConfigMapField, func(rawObj 
client.Object) []string {
-               // grab the SolrCloud object, extract the used configMap...
+               // grab the SolrPrometheusExporter object, extract the used 
configMap...
                exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
                if exporter.Spec.CustomKubeOptions.ConfigMapOptions == nil {
                        return nil
@@ -475,7 +475,7 @@ func (r *SolrPrometheusExporterReconciler) 
indexAndWatchForKeystoreSecret(mgr ct
        tlsSecretField := ".spec.solrReference.solrTLS.pkcs12Secret"
 
        if err := mgr.GetFieldIndexer().IndexField(context.Background(), 
&solrv1beta1.SolrPrometheusExporter{}, tlsSecretField, func(rawObj 
client.Object) []string {
-               // grab the SolrCloud object, extract the referenced TLS 
secret...
+               // grab the SolrPrometheusExporter object, extract the 
referenced TLS secret...
                exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
                if exporter.Spec.SolrReference.SolrTLS == nil || 
exporter.Spec.SolrReference.SolrTLS.PKCS12Secret == nil {
                        return nil
@@ -493,7 +493,7 @@ func (r *SolrPrometheusExporterReconciler) 
indexAndWatchForTruststoreSecret(mgr
        tlsSecretField := ".spec.solrReference.solrTLS.trustStoreSecret"
 
        if err := mgr.GetFieldIndexer().IndexField(context.Background(), 
&solrv1beta1.SolrPrometheusExporter{}, tlsSecretField, func(rawObj 
client.Object) []string {
-               // grab the SolrCloud object, extract the referenced truststore 
secret...
+               // grab the SolrPrometheusExporter object, extract the 
referenced truststore secret...
                exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
                if exporter.Spec.SolrReference.SolrTLS == nil || 
exporter.Spec.SolrReference.SolrTLS.TrustStoreSecret == nil {
                        return nil
@@ -511,7 +511,7 @@ func (r *SolrPrometheusExporterReconciler) 
indexAndWatchForBasicAuthSecret(mgr c
        secretField := ".spec.solrReference.basicAuthSecret"
 
        if err := mgr.GetFieldIndexer().IndexField(context.Background(), 
&solrv1beta1.SolrPrometheusExporter{}, secretField, func(rawObj client.Object) 
[]string {
-               // grab the SolrCloud object, extract the referenced TLS 
secret...
+               // grab the SolrPrometheusExporter object, extract the 
referenced BasicAuth secret...
                exporter := rawObj.(*solrv1beta1.SolrPrometheusExporter)
                if exporter.Spec.SolrReference.BasicAuthSecret == "" {
                        return nil
diff --git a/controllers/util/backup_util.go b/controllers/util/backup_util.go
index 0119665..ac89129 100644
--- a/controllers/util/backup_util.go
+++ b/controllers/util/backup_util.go
@@ -24,12 +24,15 @@ import (
        solr "github.com/apache/solr-operator/api/v1beta1"
        "github.com/apache/solr-operator/controllers/util/solr_api"
        "github.com/go-logr/logr"
+       "github.com/robfig/cron/v3"
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/client-go/kubernetes"
        "k8s.io/client-go/rest"
        "k8s.io/client-go/tools/remotecommand"
        "net/url"
+       "strconv"
+       "time"
 )
 
 func GetBackupRepositoryByName(backupRepos []solr.SolrBackupRepository, 
repositoryName string) *solr.SolrBackupRepository {
@@ -54,19 +57,20 @@ func AsyncIdForCollectionBackup(collection string, 
backupName string) string {
        return fmt.Sprintf("%s-%s", backupName, collection)
 }
 
-func UpdateStatusOfCollectionBackups(backup *solr.SolrBackup) (allFinished 
bool) {
+func UpdateStatusOfCollectionBackups(backupStatus 
*solr.IndividualSolrBackupStatus) (allFinished bool) {
        // Check if all collection backups have been completed, this is updated 
in the loop
-       allFinished = len(backup.Status.CollectionBackupStatuses) > 0
+       allFinished = len(backupStatus.CollectionBackupStatuses) > 0
 
-       allSuccessful := len(backup.Status.CollectionBackupStatuses) > 0
+       allSuccessful := len(backupStatus.CollectionBackupStatuses) > 0
 
-       for _, collectionStatus := range backup.Status.CollectionBackupStatuses 
{
+       for _, collectionStatus := range backupStatus.CollectionBackupStatuses {
                allFinished = allFinished && collectionStatus.Finished
                allSuccessful = allSuccessful && (collectionStatus.Successful 
!= nil && *collectionStatus.Successful)
        }
-       backup.Status.Finished = allFinished
-       if allFinished && backup.Status.Successful == nil {
-               backup.Status.Successful = &allSuccessful
+
+       backupStatus.Finished = allFinished
+       if allFinished && backupStatus.Successful == nil {
+               backupStatus.Successful = &allSuccessful
        }
        return
 }
@@ -79,6 +83,11 @@ func GenerateQueryParamsForBackup(backupRepository 
*solr.SolrBackupRepository, b
        queryParams.Add("async", AsyncIdForCollectionBackup(collection, 
backup.Name))
        queryParams.Add("location", BackupLocationPath(backupRepository, 
backup.Spec.Location))
        queryParams.Add("repository", backup.Spec.RepositoryName)
+
+       if backup.Spec.Recurrence.IsEnabled() {
+               queryParams.Add("maxNumBackupPoints", 
strconv.Itoa(backup.Spec.Recurrence.MaxSaved))
+       }
+
        return queryParams
 }
 
@@ -101,45 +110,34 @@ func StartBackupForCollection(ctx context.Context, cloud 
*solr.SolrCloud, backup
 }
 
 func CheckBackupForCollection(ctx context.Context, cloud *solr.SolrCloud, 
collection string, backupName string, logger logr.Logger) (finished bool, 
success bool, asyncStatus string, err error) {
-       queryParams := url.Values{}
-       queryParams.Add("action", "REQUESTSTATUS")
-       queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, 
backupName))
-
-       resp := &solr_api.SolrAsyncResponse{}
-
        logger.Info("Calling to check on collection backup", "solrCloud", 
cloud.Name, "collection", collection)
-       err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
+
+       var message string
+       asyncStatus, message, err = solr_api.CheckAsyncRequest(ctx, cloud, 
AsyncIdForCollectionBackup(collection, backupName))
 
        if err == nil {
-               if resp.ResponseHeader.Status == 0 {
-                       asyncStatus = resp.Status.AsyncState
-                       if resp.Status.AsyncState == "completed" {
-                               finished = true
-                               success = true
-                       }
-                       if resp.Status.AsyncState == "failed" {
-                               finished = true
-                               success = false
-                       }
+               if asyncStatus == "completed" {
+                       finished = true
+                       success = true
+               }
+               if asyncStatus == "failed" {
+                       finished = true
+                       success = false
                }
        } else {
-               logger.Error(err, "Error checking on collection backup", 
"solrCloud", cloud.Name, "collection", collection)
+               logger.Error(err, "Error checking on collection backup", 
"solrCloud", cloud.Name, "collection", collection, "message", message)
        }
 
        return finished, success, asyncStatus, err
 }
 
 func DeleteAsyncInfoForBackup(ctx context.Context, cloud *solr.SolrCloud, 
collection string, backupName string, logger logr.Logger) (err error) {
-       queryParams := url.Values{}
-       queryParams.Add("action", "DELETESTATUS")
-       queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, 
backupName))
-
-       resp := &solr_api.SolrAsyncResponse{}
-
        logger.Info("Calling to delete async info for backup command.", 
"solrCloud", cloud.Name, "collection", collection)
-       err = solr_api.CallCollectionsApi(ctx, cloud, queryParams, resp)
+       var message string
+       message, err = solr_api.DeleteAsyncRequest(ctx, cloud, 
AsyncIdForCollectionBackup(collection, backupName))
+
        if err != nil {
-               logger.Error(err, "Error deleting async data for collection 
backup", "solrCloud", cloud.Name, "collection", collection)
+               logger.Error(err, "Error deleting async data for collection 
backup", "solrCloud", cloud.Name, "collection", collection, "message", message)
        }
 
        return err
@@ -153,15 +151,15 @@ func EnsureDirectoryForBackup(solrCloud *solr.SolrCloud, 
backupRepository *solr.
                        solrCloud.GetAllSolrPodNames()[0],
                        solrCloud.Namespace,
                        []string{"/bin/bash", "-c", "rm -rf " + backupPath + " 
&& mkdir -p " + backupPath},
-                       *config,
+                       config,
                )
        }
        return nil
 }
 
-func RunExecForPod(podName string, namespace string, command []string, config 
rest.Config) (err error) {
+func RunExecForPod(podName string, namespace string, command []string, config 
*rest.Config) (err error) {
        client := &kubernetes.Clientset{}
-       if client, err = kubernetes.NewForConfig(&config); err != nil {
+       if client, err = kubernetes.NewForConfig(config); err != nil {
                return err
        }
        req := client.CoreV1().RESTClient().Post().
@@ -170,7 +168,7 @@ func RunExecForPod(podName string, namespace string, 
command []string, config re
                Namespace(namespace).
                SubResource("exec")
        scheme := runtime.NewScheme()
-       if err := corev1.AddToScheme(scheme); err != nil {
+       if err = corev1.AddToScheme(scheme); err != nil {
                return fmt.Errorf("error adding to scheme: %v", err)
        }
 
@@ -184,7 +182,8 @@ func RunExecForPod(podName string, namespace string, 
command []string, config re
                TTY:       false,
        }, parameterCodec)
 
-       exec, err := remotecommand.NewSPDYExecutor(&config, "POST", req.URL())
+       var exec remotecommand.Executor
+       exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL())
        if err != nil {
                return fmt.Errorf("error while creating Executor: %v", err)
        }
@@ -202,3 +201,12 @@ func RunExecForPod(podName string, namespace string, 
command []string, config re
 
        return nil
 }
+
+func ScheduleNextBackup(restartSchedule string, lastBackupTime time.Time) 
(nextBackup time.Time, err error) {
+       if parsedSchedule, parseErr := cron.ParseStandard(restartSchedule); 
parseErr != nil {
+               err = parseErr
+       } else {
+               nextBackup = parsedSchedule.Next(lastBackupTime)
+       }
+       return
+}
diff --git a/controllers/util/solr_api/api.go b/controllers/util/solr_api/api.go
index 5e4baa1..4ab0a07 100644
--- a/controllers/util/solr_api/api.go
+++ b/controllers/util/solr_api/api.go
@@ -51,10 +51,10 @@ type SolrAsyncResponse struct {
        ResponseHeader SolrResponseHeader `json:"responseHeader"`
 
        // +optional
-       RequestId string `json:"requestId"`
+       RequestId string `json:"requestId,omitempty"`
 
        // +optional
-       Status SolrAsyncStatus `json:"status"`
+       Status SolrAsyncStatus `json:"status,omitempty"`
 }
 
 type SolrResponseHeader struct {
@@ -65,9 +65,56 @@ type SolrResponseHeader struct {
 
 type SolrAsyncStatus struct {
        // Possible states can be found here: 
https://github.com/apache/solr/blob/releases/lucene-solr%2F8.8.1/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java
-       AsyncState string `json:"state"`
+       // +optional
+       AsyncState string `json:"state,omitempty"`
+
+       // +optional
+       Message string `json:"msg,omitempty"`
+}
+
+type SolrAsyncStatusResponse struct {
+       ResponseHeader SolrResponseHeader `json:"responseHeader"`
+
+       // +optional
+       Status SolrAsyncStatus `json:"status,omitempty"`
+}
+
+type SolrDeleteRequestStatus struct {
+       ResponseHeader SolrResponseHeader `json:"responseHeader"`
+
+       // Status of the delete request
+       // +optional
+       Status string `json:"status,omitempty"`
+}
+
+func CheckAsyncRequest(ctx context.Context, cloud *solr.SolrCloud, asyncId 
string) (asyncState string, message string, err error) {
+       asyncStatus := &SolrAsyncStatusResponse{}
+
+       queryParams := url.Values{}
+       queryParams.Set("action", "REQUESTSTATUS")
+       queryParams.Set("requestid", asyncId)
+       if err = CallCollectionsApi(ctx, cloud, queryParams, asyncStatus); err 
== nil {
+               if _, err = CheckForCollectionsApiError("REQUESTSTATUS", 
asyncStatus.ResponseHeader); err == nil {
+                       asyncState = asyncStatus.Status.AsyncState
+                       message = asyncStatus.Status.Message
+               }
+       }
+
+       return
+}
+
+func DeleteAsyncRequest(ctx context.Context, cloud *solr.SolrCloud, asyncId 
string) (message string, err error) {
+       deleteStatus := &SolrDeleteRequestStatus{}
+
+       queryParams := url.Values{}
+       queryParams.Set("action", "DELETESTATUS")
+       queryParams.Set("requestid", asyncId)
+       if err = CallCollectionsApi(ctx, cloud, queryParams, deleteStatus); err 
== nil {
+               _, err = CheckForCollectionsApiError("DELETESTATUS", 
deleteStatus.ResponseHeader)
+               message = deleteStatus.Status
+       }
 
-       Message string `json:"msg"`
+       return
 }
 
 func CallCollectionsApi(ctx context.Context, cloud *solr.SolrCloud, urlParams 
url.Values, response interface{}) (err error) {
diff --git a/controllers/common.go b/controllers/util/solr_api/node_command.go
similarity index 68%
copy from controllers/common.go
copy to controllers/util/solr_api/node_command.go
index 38a056e..d98336b 100644
--- a/controllers/common.go
+++ b/controllers/util/solr_api/node_command.go
@@ -15,16 +15,14 @@
  * limitations under the License.
  */
 
-package controllers
+package solr_api
 
-import (
-       "sigs.k8s.io/controller-runtime/pkg/reconcile"
-       "time"
-)
+type SolrReplaceNodeResponse struct {
+       ResponseHeader SolrResponseHeader `json:"responseHeader"`
 
-// Set the requeueAfter if it has not been set, or is greater than the new 
time to requeue at
-func updateRequeueAfter(requeueOrNot *reconcile.Result, newWait time.Duration) 
{
-       if requeueOrNot.RequeueAfter <= 0 || requeueOrNot.RequeueAfter > 
newWait {
-               requeueOrNot.RequeueAfter = newWait
-       }
+       // +optional
+       Success string `json:"success,omitempty"`
+
+       // +optional
+       Failure string `json:"failure,omitempty"`
 }
diff --git a/controllers/util/solr_update_util.go 
b/controllers/util/solr_update_util.go
index 5ce6038..199137f 100644
--- a/controllers/util/solr_update_util.go
+++ b/controllers/util/solr_update_util.go
@@ -77,7 +77,7 @@ func scheduleNextRestartWithTime(restartSchedule string, 
podTemplateAnnotations
                        err = parseErr
                } else {
                        nextRestartTime := 
parsedSchedule.Next(lastScheduledTime)
-                       nextRestart = 
parsedSchedule.Next(lastScheduledTime).Format(time.RFC3339)
+                       nextRestart = nextRestartTime.Format(time.RFC3339)
                        reconcileWaitDurationTmp := 
nextRestartTime.Sub(currentTime)
                        reconcileWaitDuration = &reconcileWaitDurationTmp
                }
diff --git a/docs/solr-backup/README.md b/docs/solr-backup/README.md
index d5fa604..23c9d3d 100644
--- a/docs/solr-backup/README.md
+++ b/docs/solr-backup/README.md
@@ -28,6 +28,7 @@ For detailed information on how to best configure backups for 
your use case, ple
 This page outlines how to create and delete a Kubernetes SolrBackup
 
 - [Creation](#creating-an-example-solrbackup)
+- [Recurring/Scheduled Backups](#recurring-backups)
 - [Deletion](#deleting-an-example-solrbackup)
 - [Repository Types](#supported-repository-types)
   - [GCS](#gcs-backup-repositories)
@@ -99,17 +100,97 @@ The status of our triggered backup can be checked with the 
command below.
 
 ```bash
 $ kubectl get solrbackups
-NAME                               CLOUD     FINISHED   SUCCESSFUL   AGE
-local-backup   example   true       true         72s
+NAME   CLOUD     STARTED   FINISHED   SUCCESSFUL   NEXTBACKUP  AGE
+test   example   123m      true       false                     161m
 ```
 
+## Recurring Backups
+_Since v0.5.0_
+
+The Solr Operator enables taking recurring updates, at a set interval.
+Note that this feature requires a SolrCloud running Solr `8.9.0` or older, 
because it relies on `Incremental` backups.
+
+By default the Solr Operator will save a maximum of **5** backups at a time, 
however users can override this using `SolrBackup.spec.recurrence.maxSaved`.
+When using `recurrence`, users must provide a Cron-style `schedule` for the 
interval at which backups should be taken.
+Please refer to the [GoLang 
cron-spec](https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format)
 for more information on allowed syntax.
+
+```yaml
+apiVersion: solr.apache.org/v1beta1
+kind: SolrBackup
+metadata:
+  name: local-backup
+  namespace: default
+spec:
+  repositoryName: "local-collection-backups-1"
+  solrCloud: example
+  collections:
+    - techproducts
+    - books
+  recurrence: # Store one backup daily, and keep a week at a time.
+    schedule: "@daily"
+    maxSaved: 7
+```
+
+If using `kubectl`, the standard `get` command will return the time the backup 
was last started and when the next backup will occur.
+
+```bash
+$ kubectl get solrbackups
+NAME   CLOUD     STARTED   FINISHED   SUCCESSFUL   NEXTBACKUP             AGE
+test   example   123m      true       true         2021-11-09T00:00:00Z   161m
+```
+
+Much like when not taking a recurring backup, `SolrBackup.status` will contain 
the information from the latest, or currently running, backup.
+The results of previous backup attempts are stored under 
`SolrBackup.status.history` (sorted from most recent to oldest).
+
+You are able to **add or remove** `recurrence` to/from an existing 
`SolrBackup` object, no matter what stage that `SolrBackup` object is in.
+If you add recurrence, then a new backup will be scheduled based on the 
`startTimestamp` of the last backup.
+If you remove recurrence, then the `nextBackupTime` will be removed.
+However, if the recurrent backup is already underway, it will not be stopped.
+
+### Backup Scheduling
+
+Backups are scheduled based on the `startTimestamp` of the last backup.
+Therefore if a interval schedule such as `@every 1h` is used, and a backup 
starts on `2021-11-09T03:10:00Z` and ends on `2021-11-09T05:30:00Z`, then the 
next backup will be started at `2021-11-09T04:10:00Z`.
+If the interval is shorter than the time it takes to complete a backup, then 
the next backup will started directly after the previous backup completes (even 
though it is delayed from its given schedule).
+And the next backup will be scheduled based on the `startTimestamp` of the 
delayed backup.
+So there is a possibility of skew overtime if backups take longer than the 
allotted schedule.
+
+If a guaranteed schedule is important, it is recommended to use intervals that 
are guaranteed to be longer than the time it takes to complete a backup.
+
+### Temporarily Disabling Recurring Backups
+
+It is also easy to temporarily disable backups for a time.
+Merely add `disabled: true` under the `recurrence` section of the `SolrBackup` 
resource.
+And set `disabled: false`, or just remove the property to re-enable backups.
+
+Since backups are scheduled based on the `startTimestamp` of the last backup, 
a new backup may start immediately after you re-enable the recurrence.
+
+```yaml
+apiVersion: solr.apache.org/v1beta1
+kind: SolrBackup
+metadata:
+  name: local-backup
+  namespace: default
+spec:
+  repositoryName: "local-collection-backups-1"
+  solrCloud: example
+  collections:
+    - techproducts
+    - books
+  recurrence: # Store one backup daily, and keep a week at a time.
+    schedule: "@daily"
+    maxSaved: 7
+    disabled: true
+```
+
+**Note: this will not stop any backups running at the time that `disabled: 
true` is set, it will only affect scheduling future backups.**
+
 ## Deleting an example SolrBackup
 
 Once the operator completes a backup, the SolrBackup instance can be safely 
deleted.
 
 ```bash
 $ kubectl delete solrbackup local-backup
-TODO command output
 ```
 
 Note that deleting SolrBackup instances doesn't delete the backed up data, 
which the operator views as already persisted and outside its control.
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 257cb62..71f9689 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -41,7 +41,7 @@ dependencies:
     condition: zookeeper-operator.install
 annotations:
   artifacthub.io/operator: "true"
-  artifacthub.io/operatorCapabilities: Seamless Upgrades
+  artifacthub.io/operatorCapabilities: Full Lifecycle
   artifacthub.io/prerelease: "true"
   artifacthub.io/recommendations: |
     - url: https://artifacthub.io/packages/helm/apache-solr/solr
@@ -183,6 +183,15 @@ annotations:
           url: https://github.com/apache/solr-operator/issues/326
         - name: Github PR
           url: https://github.com/apache/solr-operator/pull/358
+    - kind: added
+      description: Scheduled/Recurring SolrBackup support
+      links:
+        - name: Github Issue
+          url: https://github.com/apache/solr-operator/issues/303
+        - name: Github PR
+          url: https://github.com/apache/solr-operator/pull/359
+        - name: SolrBackup Documentation
+          url: 
https://apache.github.io/solr-operator/docs/solr-backup#recurring-backups
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.5.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml 
b/helm/solr-operator/crds/crds.yaml
index 6ff1bd1..7cd6c99 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -35,14 +35,23 @@ spec:
       jsonPath: .spec.solrCloud
       name: Cloud
       type: string
-    - description: Whether the backup has finished
+    - description: Most recent time the backup started
+      jsonPath: .status.startTimestamp
+      name: Started
+      type: date
+    - description: Whether the most recent backup has finished
       jsonPath: .status.finished
       name: Finished
       type: boolean
-    - description: Whether the backup was successful
+    - description: Whether the most recent backup was successful
       jsonPath: .status.successful
       name: Successful
       type: boolean
+    - description: Next scheduled time for a recurrent backup
+      format: date-time
+      jsonPath: .status.nextScheduledTime
+      name: NextBackup
+      type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
@@ -1051,6 +1060,24 @@ spec:
                     - source
                     type: object
                 type: object
+              recurrence:
+                description: "Set this backup to be taken recurrently, with 
options for scheduling and storage. \n NOTE: This is only supported for Solr 
Clouds version 8.9+, as it uses the incremental backup API."
+                properties:
+                  disabled:
+                    default: false
+                    description: Disable the recurring backups. Note this will 
not affect any currently-running backup.
+                    type: boolean
+                  maxSaved:
+                    default: 5
+                    description: Define the number of backup points to save 
for this backup at any given time. The oldest backups will be deleted if too 
many exist when a backup is taken. If not provided, this defaults to 5.
+                    minimum: 1
+                    type: integer
+                  schedule:
+                    description: "Perform a backup on the given schedule, in 
CRON format. \n Multiple CRON syntaxes are supported   - Standard CRON (e.g. 
\"CRON_TZ=Asia/Seoul 0 6 * * ?\")   - Predefined Schedules (e.g. \"@yearly\", 
\"@weekly\", \"@daily\", etc.)   - Intervals (e.g. \"@every 10h30m\") \n For 
more information please check this reference: 
https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format";
+                    type: string
+                required:
+                - schedule
+                type: object
               repositoryName:
                 description: The name of the repository to use for the backup. 
 Defaults to "legacy_local_repository" if not specified (the auto-configured 
repository for legacy singleton volumes).
                 maxLength: 100
@@ -1111,6 +1138,90 @@ spec:
               finished:
                 description: Whether the backup has finished
                 type: boolean
+              history:
+                description: The status history of recurring backups
+                items:
+                  description: IndividualSolrBackupStatus defines the observed 
state of a single issued SolrBackup
+                  properties:
+                    collectionBackupStatuses:
+                      description: The status of each collection's backup 
progress
+                      items:
+                        description: CollectionBackupStatus defines the 
progress of a Solr Collection's backup
+                        properties:
+                          asyncBackupStatus:
+                            description: The status of the asynchronous backup 
call to solr
+                            type: string
+                          backupName:
+                            description: BackupName of this collection's 
backup in Solr
+                            type: string
+                          collection:
+                            description: Solr Collection name
+                            type: string
+                          finishTimestamp:
+                            description: Time that the collection backup 
finished at
+                            format: date-time
+                            type: string
+                          finished:
+                            description: Whether the backup has finished
+                            type: boolean
+                          inProgress:
+                            description: Whether the collection is being 
backed up
+                            type: boolean
+                          startTimestamp:
+                            description: Time that the collection backup 
started at
+                            format: date-time
+                            type: string
+                          successful:
+                            description: Whether the backup was successful
+                            type: boolean
+                        required:
+                        - collection
+                        type: object
+                      type: array
+                    finishTimestamp:
+                      description: Version of the Solr being backed up
+                      format: date-time
+                      type: string
+                    finished:
+                      description: Whether the backup has finished
+                      type: boolean
+                    persistenceStatus:
+                      description: Whether the backups are in progress of 
being persisted. This feature has been removed as of v0.5.0.
+                      properties:
+                        finishTimestamp:
+                          description: Time that the collection backup 
finished at
+                          format: date-time
+                          type: string
+                        finished:
+                          description: Whether the persistence has finished
+                          type: boolean
+                        inProgress:
+                          description: Whether the collection is being backed 
up
+                          type: boolean
+                        startTimestamp:
+                          description: Time that the collection backup started 
at
+                          format: date-time
+                          type: string
+                        successful:
+                          description: Whether the backup was successful
+                          type: boolean
+                      type: object
+                    solrVersion:
+                      description: Version of the Solr being backed up
+                      type: string
+                    startTimestamp:
+                      description: The time that this backup was initiated
+                      format: date-time
+                      type: string
+                    successful:
+                      description: Whether the backup was successful
+                      type: boolean
+                  type: object
+                type: array
+              nextScheduledTime:
+                description: The scheduled time for the next backup to occur
+                format: date-time
+                type: string
               persistenceStatus:
                 description: Whether the backups are in progress of being 
persisted. This feature has been removed as of v0.5.0.
                 properties:
@@ -1135,11 +1246,13 @@ spec:
               solrVersion:
                 description: Version of the Solr being backed up
                 type: string
+              startTimestamp:
+                description: The time that this backup was initiated
+                format: date-time
+                type: string
               successful:
                 description: Whether the backup was successful
                 type: boolean
-            required:
-            - solrVersion
             type: object
         type: object
     served: true
diff --git a/main.go b/main.go
index ae118d0..11d8606 100644
--- a/main.go
+++ b/main.go
@@ -23,7 +23,7 @@ import (
        "flag"
        "fmt"
        "github.com/apache/solr-operator/controllers/util/solr_api"
-       zk_api "github.com/apache/solr-operator/controllers/zk_api"
+       "github.com/apache/solr-operator/controllers/zk_api"
        "github.com/apache/solr-operator/version"
        "github.com/fsnotify/fsnotify"
        "io/ioutil"
@@ -203,6 +203,7 @@ func main() {
        if err = (&controllers.SolrBackupReconciler{
                Client: mgr.GetClient(),
                Scheme: mgr.GetScheme(),
+               Config: mgr.GetConfig(),
        }).SetupWithManager(mgr); err != nil {
                setupLog.Error(err, "unable to create controller", 
"controller", "SolrBackup")
                os.Exit(1)

Reply via email to