You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ho...@apache.org on 2021/07/08 16:30:06 UTC

[solr-operator] branch main updated: Enable automatic scheduled restarts managed by the Operator (#279)

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 36a6207  Enable automatic scheduled restarts managed by the Operator (#279)
36a6207 is described below

commit 36a62072a28ae7482dc90fee2754616d8c51a1d3
Author: Houston Putman <ho...@apache.org>
AuthorDate: Thu Jul 8 12:29:57 2021 -0400

    Enable automatic scheduled restarts managed by the Operator (#279)
    
    These restarts are scheduled via a CRON string in the SolrCloud spec.
---
 api/v1beta1/solrcloud_types.go                   | 13 ++++++
 config/crd/bases/solr.apache.org_solrclouds.yaml |  3 ++
 controllers/solrcloud_controller.go              | 34 ++++++++++++++--
 controllers/solrcloud_controller_test.go         |  7 +++-
 controllers/util/solr_update_util.go             | 51 +++++++++++++++++++++++
 controllers/util/solr_update_util_test.go        | 52 ++++++++++++++++++++++++
 dependency_licenses.csv                          |  1 +
 docs/solr-cloud/solr-cloud-crd.md                |  4 +-
 go.mod                                           |  1 +
 go.sum                                           |  3 ++
 helm/solr-operator/Chart.yaml                    |  9 ++--
 helm/solr-operator/crds/crds.yaml                |  3 ++
 helm/solr/README.md                              |  3 +-
 helm/solr/templates/solrcloud.yaml               | 11 ++++-
 helm/solr/values.yaml                            |  3 ++
 15 files changed, 184 insertions(+), 14 deletions(-)

diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index e60839c..6509d46 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -475,6 +475,19 @@ type SolrUpdateStrategy struct {
 	// Options for Solr Operator Managed rolling updates.
 	// +optional
 	ManagedUpdateOptions ManagedUpdateOptions `json:"managed,omitempty"`
+
+	// Perform a scheduled restart 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", 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
+	//
+	// +optional
+	RestartSchedule string `json:"restartSchedule,omitempty"`
 }
 
 // SolrUpdateMethod is a string enumeration type that enumerates
diff --git a/config/crd/bases/solr.apache.org_solrclouds.yaml b/config/crd/bases/solr.apache.org_solrclouds.yaml
index c6172e3..25d39c0 100644
--- a/config/crd/bases/solr.apache.org_solrclouds.yaml
+++ b/config/crd/bases/solr.apache.org_solrclouds.yaml
@@ -4668,6 +4668,9 @@ spec:
                     - StatefulSet
                     - Manual
                     type: string
+                  restartSchedule:
+                    description: "Perform a scheduled restart 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\", 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
                 type: object
               zookeeperRef:
                 description: The information for the Zookeeper this SolrCloud should connect to Can be a zookeeper that is running, or one that is created by the solr operator
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index c2dae2f..a7624a3 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -414,6 +414,26 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 		statefulSetLogger := logger.WithValues("statefulSet", statefulSet.Name)
 		foundStatefulSet := &appsv1.StatefulSet{}
 		err = r.Get(context.TODO(), types.NamespacedName{Name: statefulSet.Name, Namespace: statefulSet.Namespace}, foundStatefulSet)
+
+		// 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)
+		} else {
+			if nextRestartAnnotation != "" {
+				// Set the new restart time annotation
+				statefulSet.Spec.Template.Annotations[util.SolrScheduledRestartAnnotation] = nextRestartAnnotation
+				// TODO: Create event for the CRD.
+			} else if existingRestartAnnotation, exists := foundStatefulSet.Spec.Template.Annotations[util.SolrScheduledRestartAnnotation]; exists {
+				// Keep the existing nextRestart annotation if it exists and we aren't setting a new one.
+				statefulSet.Spec.Template.Annotations[util.SolrScheduledRestartAnnotation] = existingRestartAnnotation
+			}
+			if reconcileWaitDuration != nil {
+				// Set the requeueAfter if it has not been set, or is greater than the time we need to wait to restart again
+				updateRequeueAfter(&requeueOrNot, *reconcileWaitDuration)
+			}
+		}
+
+		// Update or Create the StatefulSet
 		if err != nil && errors.IsNotFound(err) {
 			statefulSetLogger.Info("Creating StatefulSet")
 			if err = controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err == nil {
@@ -458,7 +478,8 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	// Otherwise it will delete all PVCs possibly
 	if len(pvcLabelSelector) > 0 {
 		if err := r.reconcileStorageFinalizer(instance, pvcLabelSelector, logger); err != nil {
-			return reconcile.Result{RequeueAfter: time.Second * 10}, nil
+			logger.Error(err, "Cannot delete PVCs while garbage collecting after deletion.")
+			updateRequeueAfter(&requeueOrNot, time.Second*15)
 		}
 	}
 
@@ -502,9 +523,7 @@ func (r *SolrCloudReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 			// TODO: Create event for the CRD.
 		}
 		if err != nil || retryLater {
-			if requeueOrNot.RequeueAfter <= 0 || requeueOrNot.RequeueAfter > time.Second*15 {
-				requeueOrNot.RequeueAfter = time.Second * 15
-			}
+			updateRequeueAfter(&requeueOrNot, time.Second*15)
 		}
 	}
 
@@ -1021,3 +1040,10 @@ func (r *SolrCloudReconciler) verifyTLSSecretConfig(secretName string, secretNam
 
 	return foundTLSSecret, nil
 }
+
+// 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
+	}
+}
diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go
index 21eabfc..ec285c8 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -192,7 +192,8 @@ func TestCustomKubeOptionsCloudReconcile(t *testing.T) {
 				},
 			},
 			UpdateStrategy: solr.SolrUpdateStrategy{
-				Method: solr.StatefulSetUpdate,
+				Method:          solr.StatefulSetUpdate,
+				RestartSchedule: "@every 30m",
 			},
 			SolrGCTune: "gc Options",
 			CustomSolrKubeOptions: solr.CustomSolrKubeOptions{
@@ -285,6 +286,10 @@ func TestCustomKubeOptionsCloudReconcile(t *testing.T) {
 	testMapsEqual(t, "statefulSet labels", util.MergeLabelsOrAnnotations(expectedStatefulSetLabels, testSSLabels), statefulSet.Labels)
 	testMapsEqual(t, "statefulSet annotations", util.MergeLabelsOrAnnotations(expectedStatefulSetAnnotations, testSSAnnotations), statefulSet.Annotations)
 	testMapsEqual(t, "pod labels", util.MergeLabelsOrAnnotations(expectedStatefulSetLabels, testPodLabels), statefulSet.Spec.Template.ObjectMeta.Labels)
+	if assert.True(t, metav1.HasAnnotation(statefulSet.Spec.Template.ObjectMeta, util.SolrScheduledRestartAnnotation), "Pod Template does not have scheduled restart annotation when it should") {
+		// Remove the annotation when we know that it exists, we don't know the exact value so we can't check it below.
+		delete(statefulSet.Spec.Template.Annotations, util.SolrScheduledRestartAnnotation)
+	}
 	testMapsEqual(t, "pod annotations", util.MergeLabelsOrAnnotations(map[string]string{"solr.apache.org/solrXmlMd5": fmt.Sprintf("%x", md5.Sum([]byte(configMap.Data["solr.xml"])))}, testPodAnnotations), statefulSet.Spec.Template.Annotations)
 	testMapsEqual(t, "pod node selectors", testNodeSelectors, statefulSet.Spec.Template.Spec.NodeSelector)
 	testPodProbe(t, testProbeLivenessNonDefaults, statefulSet.Spec.Template.Spec.Containers[0].LivenessProbe)
diff --git a/controllers/util/solr_update_util.go b/controllers/util/solr_update_util.go
index ba441d2..a825d26 100644
--- a/controllers/util/solr_update_util.go
+++ b/controllers/util/solr_update_util.go
@@ -22,17 +22,68 @@ import (
 	solr "github.com/apache/solr-operator/api/v1beta1"
 	"github.com/apache/solr-operator/controllers/util/solr_api"
 	"github.com/go-logr/logr"
+	cron "github.com/robfig/cron/v3"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"net/url"
 	"sort"
+	"time"
 )
 
 const (
 	DefaultMaxPodsUnavailable          = "25%"
 	DefaultMaxShardReplicasUnavailable = 1
+
+	SolrScheduledRestartAnnotation = "solr.apache.org/nextScheduledRestart"
 )
 
+func ScheduleNextRestart(restartSchedule string, podTemplateAnnotations map[string]string) (nextRestart string, reconcileWaitDuration *time.Duration, err error) {
+	return scheduleNextRestartWithTime(restartSchedule, podTemplateAnnotations, time.Now())
+}
+
+func scheduleNextRestartWithTime(restartSchedule string, podTemplateAnnotations map[string]string, currentTime time.Time) (nextRestart string, reconcileWaitDuration *time.Duration, err error) {
+	lastScheduledTime := currentTime.UTC()
+	if restartSchedule == "" {
+		return
+	}
+	scheduledTime, hasScheduled := podTemplateAnnotations[SolrScheduledRestartAnnotation]
+
+	scheduleNextRestart := false
+
+	if hasScheduled {
+		parsedScheduledTime, parseErr := time.Parse(time.RFC3339, scheduledTime)
+		if parseErr != nil {
+			// If the scheduled time cannot be parsed, then go ahead and create a new time.
+			scheduleNextRestart = true
+		} else {
+			parsedScheduledTime = parsedScheduledTime.UTC()
+			if parsedScheduledTime.Before(currentTime) {
+				// If the already-scheduled time is passed, then schedule a new one.
+				scheduleNextRestart = true
+				lastScheduledTime = parsedScheduledTime
+			} else {
+				// If the already-scheduled time is in the future, re-reconcile at that time
+				reconcileWaitDurationTmp := parsedScheduledTime.Sub(currentTime)
+				reconcileWaitDuration = &reconcileWaitDurationTmp
+			}
+		}
+	} else {
+		scheduleNextRestart = true
+	}
+
+	if scheduleNextRestart {
+		if parsedSchedule, parseErr := cron.ParseStandard(restartSchedule); parseErr != nil {
+			err = parseErr
+		} else {
+			nextRestartTime := parsedSchedule.Next(lastScheduledTime)
+			nextRestart = parsedSchedule.Next(lastScheduledTime).Format(time.RFC3339)
+			reconcileWaitDurationTmp := nextRestartTime.Sub(currentTime)
+			reconcileWaitDuration = &reconcileWaitDurationTmp
+		}
+	}
+	return
+}
+
 // DeterminePodsSafeToUpdate takes a list of solr Pods and returns a list of pods that are safe to upgrade now.
 // This function MUST be idempotent and return the same list of pods given the same kubernetes/solr state.
 //
diff --git a/controllers/util/solr_update_util_test.go b/controllers/util/solr_update_util_test.go
index cd799cc..76c3f61 100644
--- a/controllers/util/solr_update_util_test.go
+++ b/controllers/util/solr_update_util_test.go
@@ -26,6 +26,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"strconv"
 	"testing"
+	"time"
 )
 
 func TestPickPodsToUpgrade(t *testing.T) {
@@ -944,3 +945,54 @@ func getPodNames(pods []corev1.Pod) []string {
 	}
 	return names
 }
+
+func TestScheduleNextRestart(t *testing.T) {
+	var schedule, nextRestart string
+	var lastRestart time.Time
+	var reconcileWaitDuration *time.Duration
+	var err error
+	now := time.Date(2020, 8, 10, 20, 10, 22, 0, time.Local)
+	utcNow := now.UTC()
+
+	// Test that no restart should happen
+	nextRestart, reconcileWaitDuration, err = scheduleNextRestartWithTime(schedule, map[string]string{}, now)
+	assert.Empty(t, nextRestart, "There should be no restart when the schedule is blank")
+	assert.Nil(t, reconcileWaitDuration, "There should be no reconcile wait when the schedule is blank")
+	assert.Empty(t, err, "There should be no error when the schedule is blank")
+
+	// Test a bad schedule string
+	schedule = "adasfdsdas"
+	nextRestart, reconcileWaitDuration, err = scheduleNextRestartWithTime(schedule, map[string]string{}, now)
+	assert.Empty(t, nextRestart, "There should be no restart when the schedule is blank")
+	assert.Nil(t, reconcileWaitDuration, "There should be no reconcile wait when the schedule is blank")
+	assert.Error(t, err, "There should be a parsing error for a bad schedule")
+
+	// Test first restart
+	schedule = "@every 10m"
+	nextRestart, reconcileWaitDuration, err = scheduleNextRestartWithTime(schedule, map[string]string{}, now)
+	assert.EqualValuesf(t, utcNow.Add(time.Minute*10).Format(time.RFC3339), nextRestart, "The restart is incorrect for schedule: %s", schedule)
+	if assert.NotNil(t, reconcileWaitDuration, "There should be a reconcile wait when there is a non-empty schedule") {
+		assert.EqualValues(t, time.Minute*10, *reconcileWaitDuration, "The reconcile wait is incorrect for the first scheduled restart")
+	}
+	assert.Emptyf(t, err, "There should be no error when the schedule is: %s", schedule)
+
+	// Test new restart scheduled
+	schedule = "@every 10m"
+	lastRestart = utcNow.Add(time.Minute * -1)
+	nextRestart, reconcileWaitDuration, err = scheduleNextRestartWithTime(schedule, map[string]string{SolrScheduledRestartAnnotation: lastRestart.Format(time.RFC3339)}, now)
+	assert.EqualValuesf(t, utcNow.Add(time.Minute*9).Format(time.RFC3339), nextRestart, "The new next restart time is incorrect for previous nextRestart at \"%s\" and schedule: %s", lastRestart.Format(time.RFC3339), schedule)
+	if assert.NotNil(t, reconcileWaitDuration, "There should be a reconcile wait when there is a non-empty schedule") {
+		assert.EqualValuesf(t, time.Minute*9, *reconcileWaitDuration, "The reconcile wait is incorrect for a next restart at \"%s\" and a current time of \"%s\"", nextRestart, now)
+	}
+	assert.Emptyf(t, err, "There should be no error when the schedule is: %s", schedule)
+
+	// Test new restart scheduled
+	schedule = "@every 10m"
+	lastRestart = utcNow.Add(time.Minute * 6)
+	nextRestart, reconcileWaitDuration, err = scheduleNextRestartWithTime(schedule, map[string]string{SolrScheduledRestartAnnotation: lastRestart.Format(time.RFC3339)}, now)
+	assert.Emptyf(t, nextRestart, "There should be no new restart time when the nextRestart is in the future: \"%s\"", lastRestart)
+	if assert.NotNil(t, reconcileWaitDuration, "There should be a reconcile wait when there is a non-empty schedule") {
+		assert.EqualValuesf(t, time.Minute*6, *reconcileWaitDuration, "The reconcile wait is incorrect for a next restart at \"%s\" and a current time of \"%s\"", nextRestart, now)
+	}
+	assert.Emptyf(t, err, "There should be no error when the schedule is: %s", schedule)
+}
diff --git a/dependency_licenses.csv b/dependency_licenses.csv
index a1ff2fc..2252eab 100644
--- a/dependency_licenses.csv
+++ b/dependency_licenses.csv
@@ -27,6 +27,7 @@ github.com/prometheus/client_model/go,https://github.com/prometheus/client_model
 github.com/prometheus/common,https://github.com/prometheus/common/blob/master/LICENSE,Apache-2.0
 github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg,https://github.com/prometheus/common/blob/master/internal/bitbucket.org/ww/goautoneg/README.txt,BSD-3-Clause
 github.com/prometheus/procfs,https://github.com/prometheus/procfs/blob/master/LICENSE,Apache-2.0
+github.com/robfig/cron/v3,https://github.com/robfig/cron/blob/master/v3/LICENSE,MIT
 github.com/spf13/pflag,https://github.com/spf13/pflag/blob/master/LICENSE,BSD-3-Clause
 go.uber.org/atomic,Unknown,MIT
 go.uber.org/multierr,Unknown,MIT
diff --git a/docs/solr-cloud/solr-cloud-crd.md b/docs/solr-cloud/solr-cloud-crd.md
index 9d0a049..06c6453 100644
--- a/docs/solr-cloud/solr-cloud-crd.md
+++ b/docs/solr-cloud/solr-cloud-crd.md
@@ -58,7 +58,9 @@ Under `SolrCloud.Spec.updateStrategy`:
   - **`maxPodsUnavailable`** - (Defaults to `"25%"`) The number of Solr pods in a Solr Cloud that are allowed to be unavailable during the rolling restart.
   More pods may become unavailable during the restart, however the Solr Operator will not kill pods if the limit has already been reached.  
   - **`maxShardReplicasUnavailable`** - (Defaults to `1`) The number of replicas for each shard allowed to be unavailable during the restart.
-  
+- **`restartSchedule`** - A [CRON](https://en.wikipedia.org/wiki/Cron) schedule for automatically restarting the Solr Cloud.
+  [Multiple CRON syntaxes](https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format) are supported, such as intervals (e.g. `@every 10h`) or predefined schedules (e.g. `@yearly`, `@weekly`, etc.).
+
 **Note:** Both `maxPodsUnavailable` and `maxShardReplicasUnavailable` are intOrString fields. So either an int or string can be provided for the field.
 - **int** - The parameter is treated as an absolute value, unless the value is <= 0 which is interpreted as unlimited.
 - **string** - Only percentage string values (`"0%"` - `"100%"`) are accepted, all other values will be ignored.
diff --git a/go.mod b/go.mod
index c9de22b..7abb01e 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/onsi/gomega v1.10.1
 	github.com/pravega/zookeeper-operator v0.2.9
+	github.com/robfig/cron/v3 v3.0.1
 	github.com/stretchr/testify v1.6.1
 	golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
 	golang.org/x/tools v0.0.0-20200616195046-dc31b401abb5 // indirect
diff --git a/go.sum b/go.sum
index 9fc2b2e..de0e5eb 100644
--- a/go.sum
+++ b/go.sum
@@ -766,7 +766,10 @@ github.com/prometheus/prometheus v2.3.2+incompatible/go.mod h1:oAIUtOny2rjMX0OWN
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
+github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1 h1:NZInwlJPD/G44mJDgBEMFvBfbv/QQKCrpo+az/QXn8c=
 github.com/robfig/cron v0.0.0-20170526150127-736158dc09e1/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
diff --git a/helm/solr-operator/Chart.yaml b/helm/solr-operator/Chart.yaml
index 85e7114..2b951c9 100644
--- a/helm/solr-operator/Chart.yaml
+++ b/helm/solr-operator/Chart.yaml
@@ -55,15 +55,12 @@ annotations:
   # Allowed syntax is described at: https://artifacthub.io/docs/topics/annotations/helm/#example
   artifacthub.io/changes: |
     - kind: added
-      description: Addition 1
+      description: Ability to schedule automatic restarts
       links:
         - name: Github Issue
-          url: https://github.com/issue-url
-    - kind: changed
-      description: Change 2
-      links:
+          url: https://github.com/apache/solr-operator/issues/281
         - name: Github PR
-          url: https://github.com/pr-url
+          url: https://github.com/apache/solr-operator/pull/279
   artifacthub.io/images: |
     - name: solr-operator
       image: apache/solr-operator:v0.4.0-prerelease
diff --git a/helm/solr-operator/crds/crds.yaml b/helm/solr-operator/crds/crds.yaml
index 105a28b..0a9eca6 100644
--- a/helm/solr-operator/crds/crds.yaml
+++ b/helm/solr-operator/crds/crds.yaml
@@ -5795,6 +5795,9 @@ spec:
                     - StatefulSet
                     - Manual
                     type: string
+                  restartSchedule:
+                    description: "Perform a scheduled restart 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\", 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
                 type: object
               zookeeperRef:
                 description: The information for the Zookeeper this SolrCloud should connect to Can be a zookeeper that is running, or one that is created by the solr operator
diff --git a/helm/solr/README.md b/helm/solr/README.md
index 001cf5f..94c8938 100644
--- a/helm/solr/README.md
+++ b/helm/solr/README.md
@@ -41,7 +41,7 @@ To install the Solr Operator for the first time in your cluster, you can use the
 helm install example apache-solr/solr --version 0.4.0-prerelease --set image.tag=8.8.0
 ```
 
-The command deploys the a SolrCloud object on the Kubernetes cluster with the default configuration.
+The command deploys a SolrCloud object on the Kubernetes cluster with the default configuration.
 The [Solr Operator](https://solr.apache.org/operator) is then in charge of creating the necessary Kubernetes resources to run that Solr Cloud.
 The [configuration](#chart-values) section lists the parameters that can be configured during installation.
 
@@ -96,6 +96,7 @@ The command removes the SolrCloud resource, and then Kubernetes will garbage col
 | updateStrategy.method | string | `"Managed"` | The method for conducting updates of Solr pods. Either `Managed`, `StatefulSet` or `Manual`. See the [docs](https://apache.github.io/solr-operator/docs/solr-cloud/solr-cloud-crd.html#update-strategy) for more information |
 | updateStrategy.managedUpdate.maxPodsUnavailable | int-or-string | `"25%"` | The number of Solr pods in a Solr Cloud that are allowed to be unavailable during the rolling restart. Either a static number, or a percentage representing the percentage of total pods requested for the statefulSet. |
 | updateStrategy.managedUpdate.maxShardReplicasUnavailable | int-or-string | `1` | The number of replicas for each shard allowed to be unavailable during the restart. Either a static number, or a percentage representing the percentage of the number of replicas for a shard. |
+| updateStrategy.restartSchedule | [string (CRON)](https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format) | | A CRON schedule for automatically restarting the Solr Cloud. [Refer here](https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format) for all possible CRON syntaxes accepted. |
 
 ### Data Storage Options
 
diff --git a/helm/solr/templates/solrcloud.yaml b/helm/solr/templates/solrcloud.yaml
index b4efce1..ce001a9 100644
--- a/helm/solr/templates/solrcloud.yaml
+++ b/helm/solr/templates/solrcloud.yaml
@@ -92,7 +92,16 @@ spec:
 
   {{- if .Values.updateStrategy }}
   updateStrategy:
-    {{- toYaml .Values.updateStrategy | nindent 4 }}
+    {{- if .Values.updateStrategy.method }}
+    method: {{ .Values.updateStrategy.method }}
+    {{- end }}
+    {{- if .Values.updateStrategy.managed }}
+    managed:
+      {{- toYaml .Values.updateStrategy.managed | nindent 6 }}
+    {{- end }}
+    {{- if .Values.updateStrategy.restartSchedule }}
+    restartSchedule: {{ .Values.updateStrategy.restartSchedule | quote }}
+    {{- end }}
   {{- end }}
 
   {{- if .Values.dataStorage }}
diff --git a/helm/solr/values.yaml b/helm/solr/values.yaml
index d29f46e..bb80840 100644
--- a/helm/solr/values.yaml
+++ b/helm/solr/values.yaml
@@ -86,6 +86,9 @@ updateStrategy:
     # Either a static number, or a percentage representing the percentage of the number of replicas for a shard.
     # Defaults to 1
     # maxShardReplicasUnavailable: 1
+  # Cron schedule for automatically restarting the Solr Cloud
+  # For available CRON syntaxes, check here: https://pkg.go.dev/github.com/robfig/cron/v3?utm_source=godoc#hdr-CRON_Expression_Format
+  restartSchedule: ""
 
 # More information can be found at:
 # https://apache.github.io/solr-operator/docs/solr-cloud/solr-cloud-crd.html#data-storage