diff --git a/apis/apps/v2beta1/emqx_types.go b/apis/apps/v2beta1/emqx_types.go index 7be7c5161..0484ba0d4 100644 --- a/apis/apps/v2beta1/emqx_types.go +++ b/apis/apps/v2beta1/emqx_types.go @@ -72,6 +72,12 @@ type EMQXSpec struct { //+kubebuilder:default:="cluster.local" ClusterDomain string `json:"clusterDomain,omitempty"` + // The number of old ReplicaSets, old StatefulSet and old PersistentVolumeClaim to retain to allow rollback. + // This is a pointer to distinguish between explicit zero and not specified. + // Defaults to 3. + // +kubebuilder:default:=3 + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` + // UpdateStrategy is the object that describes the EMQX blue-green update strategy //+kubebuilder:default={type:Recreate,initialDelaySeconds:10,evacuationStrategy:{waitTakeover:10,connEvictRate:1000,sessEvictRate:1000}} UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` @@ -79,6 +85,7 @@ type EMQXSpec struct { // CoreTemplate is the object that describes the EMQX core node that will be created //+kubebuilder:default={spec:{replicas:2}} CoreTemplate EMQXCoreTemplate `json:"coreTemplate,omitempty"` + // ReplicantTemplate is the object that describes the EMQX replicant node that will be created ReplicantTemplate *EMQXReplicantTemplate `json:"replicantTemplate,omitempty"` diff --git a/apis/apps/v2beta1/zz_generated.deepcopy.go b/apis/apps/v2beta1/zz_generated.deepcopy.go index d28cdcf89..f6ebc7618 100644 --- a/apis/apps/v2beta1/zz_generated.deepcopy.go +++ b/apis/apps/v2beta1/zz_generated.deepcopy.go @@ -343,6 +343,11 @@ func (in *EMQXSpec) DeepCopyInto(out *EMQXSpec) { copy(*out, *in) } out.Config = in.Config + if in.RevisionHistoryLimit != nil { + in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit + *out = new(int32) + **out = **in + } out.UpdateStrategy = in.UpdateStrategy in.CoreTemplate.DeepCopyInto(&out.CoreTemplate) if in.ReplicantTemplate != nil { diff --git a/config/crd/bases/apps.emqx.io_emqxes.yaml b/config/crd/bases/apps.emqx.io_emqxes.yaml index acaa1f8bb..cf4ec1ee0 100644 --- a/config/crd/bases/apps.emqx.io_emqxes.yaml +++ b/config/crd/bases/apps.emqx.io_emqxes.yaml @@ -12746,6 +12746,10 @@ spec: type: array type: object type: object + revisionHistoryLimit: + default: 3 + format: int32 + type: integer serviceAccountName: type: string updateStrategy: diff --git a/config/samples/emqx/v2beta1/emqx-full.yaml b/config/samples/emqx/v2beta1/emqx-full.yaml index fde8a605c..782109ef4 100644 --- a/config/samples/emqx/v2beta1/emqx-full.yaml +++ b/config/samples/emqx/v2beta1/emqx-full.yaml @@ -8,6 +8,7 @@ metadata: spec: image: "emqx:5.1" imagePullPolicy: Always + revisionHistoryLimit: 3 config: data: | dashboard.listeners.http.bind = 18083 diff --git a/controllers/apps/v2beta1/add_emqx_core.go b/controllers/apps/v2beta1/add_emqx_core.go index 673c19432..b6b089786 100644 --- a/controllers/apps/v2beta1/add_emqx_core.go +++ b/controllers/apps/v2beta1/add_emqx_core.go @@ -15,6 +15,7 @@ import ( k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -63,7 +64,7 @@ func (a *addCore) reconcile(ctx context.Context, instance *appsv2beta1.EMQX, _ i ) if !patchResult.IsEmpty() { logger := log.FromContext(ctx) - logger.Info("got different statefulSet for EMQX core nodes, will update statefulSet", "patch", string(patchResult.Patch)) + logger.Info("got different statefulSet for EMQX core nodes, will update statefulSet", "statefulSet", klog.KObj(preSts), "patch", string(patchResult.Patch)) if err := a.Handler.Update(preSts); err != nil { return subResult{err: emperror.Wrap(err, "failed to update statefulSet")} @@ -146,7 +147,7 @@ func (a *addCore) getNewStatefulSet(ctx context.Context, instance *appsv2beta1.E } logger := log.FromContext(ctx) - logger.Info("got different pod template for EMQX core nodes, will create new statefulSet", "patch", string(patchResult.Patch)) + logger.Info("got different pod template for EMQX core nodes, will create new statefulSet", "statefulSet", klog.KObj(preSts), "patch", string(patchResult.Patch)) return preSts, nil } diff --git a/controllers/apps/v2beta1/add_emqx_repl.go b/controllers/apps/v2beta1/add_emqx_repl.go index 4c113e44b..deec1f2cc 100644 --- a/controllers/apps/v2beta1/add_emqx_repl.go +++ b/controllers/apps/v2beta1/add_emqx_repl.go @@ -14,6 +14,7 @@ import ( k8sErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -69,7 +70,7 @@ func (a *addRepl) reconcile(ctx context.Context, instance *appsv2beta1.EMQX, _ i ) if !patchResult.IsEmpty() { logger := log.FromContext(ctx) - logger.Info("got different replicaSet for EMQX replicant nodes, will update replicaSet", "patch", string(patchResult.Patch)) + logger.Info("got different replicaSet for EMQX replicant nodes, will update replicaSet", "replicaSet", klog.KObj(preRs), "patch", string(patchResult.Patch)) if err := a.Handler.Update(preRs); err != nil { return subResult{err: emperror.Wrap(err, "failed to update replicaSet")} @@ -150,8 +151,9 @@ func (a *addRepl) getNewReplicaSet(ctx context.Context, instance *appsv2beta1.EM preRs.Spec.Selector = updateRs.DeepCopy().Spec.Selector return preRs, nil } + logger := log.FromContext(ctx) - logger.Info("got different pod template for EMQX replicant nodes, will create new replicaSet", "patch", string(patchResult.Patch)) + logger.Info("got different pod template for EMQX replicant nodes, will create new replicaSet", "replicaSet", klog.KObj(preRs), "patch", string(patchResult.Patch)) return preRs, nil } diff --git a/controllers/apps/v2beta1/emqx_controller.go b/controllers/apps/v2beta1/emqx_controller.go index e79a7e3dd..543a30819 100644 --- a/controllers/apps/v2beta1/emqx_controller.go +++ b/controllers/apps/v2beta1/emqx_controller.go @@ -120,6 +120,7 @@ func (r *EMQXReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. &updatePodConditions{r}, &updateStatus{r}, &syncPods{r}, + &syncSets{r}, } { subResult := subReconciler.reconcile(ctx, instance, requester) if !subResult.result.IsZero() { diff --git a/controllers/apps/v2beta1/sync_sets.go b/controllers/apps/v2beta1/sync_sets.go new file mode 100644 index 000000000..c2808e63a --- /dev/null +++ b/controllers/apps/v2beta1/sync_sets.go @@ -0,0 +1,76 @@ +package v2beta1 + +import ( + "context" + + appsv2beta1 "github.com/emqx/emqx-operator/apis/apps/v2beta1" + innerReq "github.com/emqx/emqx-operator/internal/requester" + corev1 "k8s.io/api/core/v1" + k8sErrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type syncSets struct { + *EMQXReconciler +} + +func (s *syncSets) reconcile(ctx context.Context, instance *appsv2beta1.EMQX, r innerReq.RequesterInterface) subResult { + if !instance.Status.IsConditionTrue(appsv2beta1.Ready) { + return subResult{} + } + logger := log.FromContext(ctx) + + _, _, oldRsList := getReplicaSetList(ctx, s.Client, instance) + rsDiff := int32(len(oldRsList)) - *instance.Spec.RevisionHistoryLimit + if rsDiff > 0 { + for i := 0; i < int(rsDiff); i++ { + rs := oldRsList[i].DeepCopy() + // Avoid delete replica set with non-zero replica counts + if rs.Status.Replicas != 0 || *(rs.Spec.Replicas) != 0 || rs.Generation > rs.Status.ObservedGeneration || rs.DeletionTimestamp != nil { + continue + } + logger.Info("trying to cleanup replica set for EMQX", "replicaSet", klog.KObj(rs), "EMQX", klog.KObj(instance)) + if err := s.Client.Delete(ctx, rs); err != nil && !k8sErrors.IsNotFound(err) { + return subResult{err: err} + } + } + } + + _, _, oldStsList := getStateFulSetList(ctx, s.Client, instance) + stsDiff := int32(len(oldStsList)) - *instance.Spec.RevisionHistoryLimit + if stsDiff > 0 { + for i := 0; i < int(rsDiff); i++ { + sts := oldStsList[i].DeepCopy() + // Avoid delete stateful set with non-zero replica counts + if sts.Status.Replicas != 0 || *(sts.Spec.Replicas) != 0 || sts.Generation > sts.Status.ObservedGeneration || sts.DeletionTimestamp != nil { + continue + } + logger.Info("trying to cleanup stateful set for EMQX", "statefulSet", klog.KObj(sts), "EMQX", klog.KObj(instance)) + if err := s.Client.Delete(ctx, sts); err != nil && !k8sErrors.IsNotFound(err) { + return subResult{err: err} + } + + // Delete PVCs + pvcList := &corev1.PersistentVolumeClaimList{} + _ = s.Client.List(ctx, pvcList, + client.InNamespace(instance.Namespace), + client.MatchingLabels(sts.Spec.Selector.MatchLabels), + ) + + for _, p := range pvcList.Items { + pvc := p.DeepCopy() + if pvc.DeletionTimestamp != nil { + continue + } + logger.Info("trying to cleanup pvc for EMQX", "pvc", klog.KObj(pvc), "EMQX", klog.KObj(instance)) + if err := s.Client.Delete(ctx, pvc); err != nil && !k8sErrors.IsNotFound(err) { + return subResult{err: err} + } + } + } + } + + return subResult{} +} diff --git a/controllers/apps/v2beta1/sync_sets_suite_test.go b/controllers/apps/v2beta1/sync_sets_suite_test.go new file mode 100644 index 000000000..6cc97a51b --- /dev/null +++ b/controllers/apps/v2beta1/sync_sets_suite_test.go @@ -0,0 +1,203 @@ +package v2beta1 + +import ( + "context" + "fmt" + "time" + + appsv2beta1 "github.com/emqx/emqx-operator/apis/apps/v2beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Check sync rs", func() { + var s *syncSets + + var instance *appsv2beta1.EMQX = new(appsv2beta1.EMQX) + var ns *corev1.Namespace = &corev1.Namespace{} + + BeforeEach(func() { + s = &syncSets{emqxReconciler} + ns = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "controller-v2beta1-sync-sets-test-" + rand.String(5), + Labels: map[string]string{ + "test": "e2e", + }, + }, + } + instance = emqx.DeepCopy() + instance.Namespace = ns.Name + instance.Spec.RevisionHistoryLimit = pointer.Int32(3) + instance.Status = appsv2beta1.EMQXStatus{ + Conditions: []metav1.Condition{ + { + Type: appsv2beta1.Ready, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Time{Time: time.Now().AddDate(0, 0, -1)}, + }, + }, + } + + Expect(k8sClient.Create(context.Background(), ns)).To(Succeed()) + for i := 0; i < 5; i++ { + name := fmt.Sprintf("%s-%d", instance.Name, i) + + rs := &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: instance.Namespace, + Labels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultReplicantLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: pointer.Int32Ptr(0), + Selector: &metav1.LabelSelector{ + MatchLabels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultReplicantLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultReplicantLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "emqx", Image: "emqx"}, + }, + }, + }, + }, + } + Expect(k8sClient.Create(context.Background(), rs.DeepCopy())).Should(Succeed()) + rs.Status.Replicas = 0 + rs.Status.ObservedGeneration = 1 + Expect(k8sClient.Status().Patch(context.Background(), rs.DeepCopy(), client.Merge)).Should(Succeed()) + + sts := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: instance.Namespace, + Labels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultCoreLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: pointer.Int32Ptr(0), + Selector: &metav1.LabelSelector{ + MatchLabels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultCoreLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: appsv2beta1.CloneAndAddLabel( + appsv2beta1.DefaultCoreLabels(instance), + appsv2beta1.LabelsPodTemplateHashKey, + fmt.Sprintf("fake-%d", i), + ), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "emqx", Image: "emqx"}, + }, + }, + }, + }, + } + Expect(k8sClient.Create(context.Background(), sts.DeepCopy())).Should(Succeed()) + sts.Status.Replicas = 0 + sts.Status.ObservedGeneration = 1 + Expect(k8sClient.Status().Patch(context.Background(), sts.DeepCopy(), client.Merge)).Should(Succeed()) + + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: sts.Name, + Namespace: sts.Namespace, + Labels: sts.Labels, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + } + Expect(k8sClient.Create(context.Background(), pvc.DeepCopy())).Should(Succeed()) + } + }) + + It("should delete rs sts and pvc", func() { + Expect(s.reconcile(context.Background(), instance, nil)).Should(Equal(subResult{})) + + Eventually(func() int { + list := &appsv1.ReplicaSetList{} + _ = k8sClient.List(context.Background(), list, + client.InNamespace(instance.Namespace), + client.MatchingLabels(appsv2beta1.DefaultReplicantLabels(instance)), + ) + count := 0 + for _, i := range list.Items { + item := i.DeepCopy() + if item.DeletionTimestamp == nil { + count++ + } + } + return count + }).WithTimeout(timeout).WithPolling(interval).Should(BeEquivalentTo(*instance.Spec.RevisionHistoryLimit)) + + Eventually(func() int { + list := &appsv1.StatefulSetList{} + _ = k8sClient.List(context.Background(), list, + client.InNamespace(instance.Namespace), + client.MatchingLabels(appsv2beta1.DefaultCoreLabels(instance)), + ) + count := 0 + for _, i := range list.Items { + item := i.DeepCopy() + if item.DeletionTimestamp == nil { + count++ + } + } + return count + }).WithTimeout(timeout).WithPolling(interval).Should(BeEquivalentTo(*instance.Spec.RevisionHistoryLimit)) + + Eventually(func() int { + list := &corev1.PersistentVolumeClaimList{} + _ = k8sClient.List(context.Background(), list, + client.InNamespace(instance.Namespace), + client.MatchingLabels(appsv2beta1.DefaultCoreLabels(instance)), + ) + count := 0 + for _, i := range list.Items { + item := i.DeepCopy() + if item.DeletionTimestamp == nil { + count++ + } + } + return count + }).WithTimeout(timeout).WithPolling(interval).Should(BeEquivalentTo(*instance.Spec.RevisionHistoryLimit)) + }) +}) diff --git a/controllers/apps/v2beta1/update_emqx_status.go b/controllers/apps/v2beta1/update_emqx_status.go index 12f01571d..217303fa4 100644 --- a/controllers/apps/v2beta1/update_emqx_status.go +++ b/controllers/apps/v2beta1/update_emqx_status.go @@ -27,32 +27,45 @@ func (u *updateStatus) reconcile(ctx context.Context, instance *appsv2beta1.EMQX } updateRs, currentRs, oldRsList := getReplicaSetList(ctx, u.Client, instance) - if updateRs != nil && currentRs == nil { - if len(oldRsList) > 0 { - currentRs = oldRsList[0] - } else { - currentRs = updateRs - } - instance.Status.ReplicantNodesStatus.CurrentRevision = currentRs.Labels[appsv2beta1.LabelsPodTemplateHashKey] - if err := u.Client.Status().Update(ctx, instance); err != nil { - return subResult{err: emperror.Wrap(err, "failed to update status")} + if updateRs != nil { + if currentRs == nil || currentRs.Status.Replicas == 0 { + var i int + for i = 0; i < len(oldRsList); i++ { + if oldRsList[i].Status.Replicas > 0 { + currentRs = oldRsList[i] + break + } + } + if i == len(oldRsList) { + currentRs = updateRs + } + instance.Status.ReplicantNodesStatus.CurrentRevision = currentRs.Labels[appsv2beta1.LabelsPodTemplateHashKey] + if err := u.Client.Status().Update(ctx, instance); err != nil { + return subResult{err: emperror.Wrap(err, "failed to update status")} + } + return subResult{} } - return subResult{} } updateSts, currentSts, oldStsList := getStateFulSetList(ctx, u.Client, instance) - if updateSts != nil && currentSts == nil { - if len(oldStsList) > 0 { - currentSts = oldStsList[0] - } else { - currentSts = updateSts - } - instance.Status.CoreNodesStatus.CurrentRevision = currentSts.Labels[appsv2beta1.LabelsPodTemplateHashKey] - - if err := u.Client.Status().Update(ctx, instance); err != nil { - return subResult{err: emperror.Wrap(err, "failed to update status")} + if updateSts != nil { + if currentSts == nil || currentSts.Status.Replicas == 0 { + var i int + for i = 0; i < len(oldStsList); i++ { + if oldStsList[i].Status.Replicas > 0 { + currentSts = oldStsList[i] + break + } + } + if i == len(oldStsList) { + currentSts = updateSts + } + instance.Status.CoreNodesStatus.CurrentRevision = currentSts.Labels[appsv2beta1.LabelsPodTemplateHashKey] + if err := u.Client.Status().Update(ctx, instance); err != nil { + return subResult{err: emperror.Wrap(err, "failed to update status")} + } + return subResult{} } - return subResult{} } if r == nil { diff --git a/controllers/apps/v2beta1/util.go b/controllers/apps/v2beta1/util.go index a88421258..2a6c3119d 100644 --- a/controllers/apps/v2beta1/util.go +++ b/controllers/apps/v2beta1/util.go @@ -72,13 +72,10 @@ func getStateFulSetList(ctx context.Context, k8sClient client.Client, instance * if hash == instance.Status.CoreNodesStatus.UpdateRevision { updateSts = sts.DeepCopy() } - if sts.Status.Replicas != 0 && - hash == instance.Status.CoreNodesStatus.CurrentRevision { + if hash == instance.Status.CoreNodesStatus.CurrentRevision { currentSts = sts.DeepCopy() } - if sts.Status.Replicas != 0 && - hash != instance.Status.CoreNodesStatus.UpdateRevision && - hash != instance.Status.CoreNodesStatus.CurrentRevision { + if hash != instance.Status.CoreNodesStatus.UpdateRevision && hash != instance.Status.CoreNodesStatus.CurrentRevision { oldStsList = append(oldStsList, sts.DeepCopy()) } } @@ -91,12 +88,12 @@ func getStateFulSetList(ctx context.Context, k8sClient client.Client, instance * func getReplicaSetList(ctx context.Context, k8sClient client.Client, instance *appsv2beta1.EMQX) (updateRs, currentRs *appsv1.ReplicaSet, oldRsList []*appsv1.ReplicaSet) { labels := appsv2beta1.DefaultReplicantLabels(instance) + list := &appsv1.ReplicaSetList{} + _ = k8sClient.List(ctx, list, + client.InNamespace(instance.Namespace), + client.MatchingLabels(labels), + ) if instance.Spec.ReplicantTemplate == nil { - list := &appsv1.ReplicaSetList{} - _ = k8sClient.List(ctx, list, - client.InNamespace(instance.Namespace), - client.MatchingLabels(labels), - ) for _, rs := range list.Items { oldRsList = append(oldRsList, rs.DeepCopy()) } @@ -104,23 +101,15 @@ func getReplicaSetList(ctx context.Context, k8sClient client.Client, instance *a return } - list := &appsv1.ReplicaSetList{} - _ = k8sClient.List(ctx, list, - client.InNamespace(instance.Namespace), - client.MatchingLabels(labels), - ) for _, rs := range list.Items { if hash, ok := rs.Labels[appsv2beta1.LabelsPodTemplateHashKey]; ok { if hash == instance.Status.ReplicantNodesStatus.UpdateRevision { updateRs = rs.DeepCopy() } - if rs.Status.Replicas != 0 && - hash == instance.Status.ReplicantNodesStatus.CurrentRevision { + if hash == instance.Status.ReplicantNodesStatus.CurrentRevision { currentRs = rs.DeepCopy() } - if rs.Status.Replicas != 0 && - hash != instance.Status.ReplicantNodesStatus.UpdateRevision && - hash != instance.Status.ReplicantNodesStatus.CurrentRevision { + if hash != instance.Status.ReplicantNodesStatus.UpdateRevision && hash != instance.Status.ReplicantNodesStatus.CurrentRevision { oldRsList = append(oldRsList, rs.DeepCopy()) } } diff --git a/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml b/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml index e5edad8ea..01130efbe 100644 --- a/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml +++ b/deploy/charts/emqx-operator/templates/crd.emqxes.apps.emqx.io.yaml @@ -12758,6 +12758,10 @@ spec: type: array type: object type: object + revisionHistoryLimit: + default: 3 + format: int32 + type: integer serviceAccountName: type: string updateStrategy: diff --git a/docs/en_US/reference/v2beta1-reference.md b/docs/en_US/reference/v2beta1-reference.md index 4aa188dc6..3bdc49465 100644 --- a/docs/en_US/reference/v2beta1-reference.md +++ b/docs/en_US/reference/v2beta1-reference.md @@ -242,6 +242,7 @@ _Appears in:_ | `bootstrapAPIKeys` _[BootstrapAPIKey](#bootstrapapikey) array_ | EMQX bootstrap user Cannot be updated. | | `config` _[Config](#config)_ | EMQX config | | `clusterDomain` _string_ | | +| `revisionHistoryLimit` _integer_ | The number of old ReplicaSets, old StatefulSet and old PersistentVolumeClaim to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 3. | | `updateStrategy` _[UpdateStrategy](#updatestrategy)_ | UpdateStrategy is the object that describes the EMQX blue-green update strategy | | `coreTemplate` _[EMQXCoreTemplate](#emqxcoretemplate)_ | CoreTemplate is the object that describes the EMQX core node that will be created | | `replicantTemplate` _[EMQXReplicantTemplate](#emqxreplicanttemplate)_ | ReplicantTemplate is the object that describes the EMQX replicant node that will be created | diff --git a/docs/zh_CN/reference/v2beta1-reference.md b/docs/zh_CN/reference/v2beta1-reference.md index 4aa188dc6..3bdc49465 100644 --- a/docs/zh_CN/reference/v2beta1-reference.md +++ b/docs/zh_CN/reference/v2beta1-reference.md @@ -242,6 +242,7 @@ _Appears in:_ | `bootstrapAPIKeys` _[BootstrapAPIKey](#bootstrapapikey) array_ | EMQX bootstrap user Cannot be updated. | | `config` _[Config](#config)_ | EMQX config | | `clusterDomain` _string_ | | +| `revisionHistoryLimit` _integer_ | The number of old ReplicaSets, old StatefulSet and old PersistentVolumeClaim to retain to allow rollback. This is a pointer to distinguish between explicit zero and not specified. Defaults to 3. | | `updateStrategy` _[UpdateStrategy](#updatestrategy)_ | UpdateStrategy is the object that describes the EMQX blue-green update strategy | | `coreTemplate` _[EMQXCoreTemplate](#emqxcoretemplate)_ | CoreTemplate is the object that describes the EMQX core node that will be created | | `replicantTemplate` _[EMQXReplicantTemplate](#emqxreplicanttemplate)_ | ReplicantTemplate is the object that describes the EMQX replicant node that will be created |