diff --git a/api/v1alpha1/conditions.go b/api/v1alpha1/conditions.go index a88bf9b7..df1074e7 100644 --- a/api/v1alpha1/conditions.go +++ b/api/v1alpha1/conditions.go @@ -52,7 +52,6 @@ const ( ConditionReasonMajorUpdateAvailable ConditionReason = "MajorUpdateAvailable" ConditionReasonVersionOutdated ConditionReason = "VersionOutdated" ConditionReasonUpgradeCheckFailed ConditionReason = "UpgradeCheckFailed" - // ConditionTypeReady indicates that cluster is ready to serve client requests. ConditionTypeReady ConditionType = "Ready" ClickHouseConditionAllShardsReady ConditionReason = "AllShardsReady" diff --git a/internal/controller/clickhouse/commands.go b/internal/controller/clickhouse/commands.go index bb9110af..64e8794d 100644 --- a/internal/controller/clickhouse/commands.go +++ b/internal/controller/clickhouse/commands.go @@ -102,6 +102,34 @@ func (cmd *commander) Version(ctx context.Context, id v1.ClickHouseReplicaID) (s return version, nil } +// ReloadConfig executes SYSTEM RELOAD CONFIG. +func (cmd *commander) ReloadConfig(ctx context.Context, id v1.ClickHouseReplicaID) error { + conn, err := cmd.getConn(id) + if err != nil { + return fmt.Errorf("failed to get connection for replica %s: %w", id, err) + } + + if err := conn.Exec(ctx, "SYSTEM RELOAD CONFIG"); err != nil { + return fmt.Errorf("reload config on replica %s: %w", id, err) + } + + return nil +} + +// ReloadUsers executes SYSTEM RELOAD USERS. +func (cmd *commander) ReloadUsers(ctx context.Context, id v1.ClickHouseReplicaID) error { + conn, err := cmd.getConn(id) + if err != nil { + return fmt.Errorf("failed to get connection for replica %s: %w", id, err) + } + + if err := conn.Exec(ctx, "SYSTEM RELOAD USERS"); err != nil { + return fmt.Errorf("reload users on replica %s: %w", id, err) + } + + return nil +} + func (cmd *commander) Databases(ctx context.Context, id v1.ClickHouseReplicaID) (map[string]databaseDescriptor, error) { conn, err := cmd.getConn(id) if err != nil { diff --git a/internal/controllerutil/annotations.go b/internal/controllerutil/annotations.go index 25ab0514..9fc84bea 100644 --- a/internal/controllerutil/annotations.go +++ b/internal/controllerutil/annotations.go @@ -9,7 +9,8 @@ const ( AnnotationConfigHash = "checksum/configuration" AnnotationRestartedAt = "kubectl.kubernetes.io/restartedAt" - AnnotationStatefulSetVersion = "clickhouse.com/statefulset-version" + AnnotationStatefulSetVersion = "clickhouse.com/statefulset-version" + AnnotationRestartRequiredConfigHash = "checksum/restart-required-config" ) // AddHashWithKeyToAnnotations adds given spec hash to object's annotations with given key. @@ -48,3 +49,18 @@ func GetConfigHashFromObject(found client.Object) string { func AddObjectConfigHash(obj client.Object, hash string) { AddHashWithKeyToAnnotations(obj, AnnotationConfigHash, hash) } + +// GetRestartRequiredConfigHashFromObject retrieves restart-required config hash from object's annotations. +func GetRestartRequiredConfigHashFromObject(found client.Object) string { + annotations := found.GetAnnotations() + if annotations == nil || annotations[AnnotationRestartRequiredConfigHash] == "" { + return "" + } + + return annotations[AnnotationRestartRequiredConfigHash] +} + +// AddObjectRestartRequiredConfigHash adds restart-required config hash to object's annotations. +func AddObjectRestartRequiredConfigHash(obj client.Object, hash string) { + AddHashWithKeyToAnnotations(obj, AnnotationRestartRequiredConfigHash, hash) +} diff --git a/test/e2e/clickhouse_e2e_test.go b/test/e2e/clickhouse_e2e_test.go index f4061742..8d81400b 100644 --- a/test/e2e/clickhouse_e2e_test.go +++ b/test/e2e/clickhouse_e2e_test.go @@ -112,6 +112,84 @@ var _ = Describe("ClickHouse controller", Label("clickhouse"), func() { Entry("scale up to 2 replicas", v1.ClickHouseClusterSpec{Replicas: ptr.To[int32](2)}), ) + It("should not restart pods when only ExtraUsersConfig changes", func(ctx context.Context) { + cr := v1.ClickHouseCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: fmt.Sprintf("test-%d", rand.Uint32()), //nolint:gosec + }, + Spec: v1.ClickHouseClusterSpec{ + Replicas: ptr.To[int32](1), + ContainerTemplate: v1.ContainerTemplateSpec{ + Image: v1.ContainerImage{Tag: ClickHouseBaseVersion}, + }, + DataVolumeClaimSpec: &defaultStorage, + KeeperClusterRef: &corev1.LocalObjectReference{Name: keeper.Name}, + Settings: v1.ClickHouseSettings{ + ExtraUsersConfig: runtime.RawExtension{Raw: []byte(`{}`)}, + }, + }, + } + checks := 0 + + By("creating cluster CR") + Expect(k8sClient.Create(ctx, &cr)).To(Succeed()) + DeferCleanup(func(ctx context.Context) { + Expect(k8sClient.Delete(ctx, &cr)).To(Succeed()) + }) + WaitClickHouseUpdatedAndReady(ctx, &cr, time.Minute, false) + ClickHouseRWChecks(ctx, &cr, &checks) + + By("recording pod UIDs before config change") + + var podsBefore corev1.PodList + + Expect(k8sClient.List(ctx, &podsBefore, client.InNamespace(testNamespace), + client.MatchingLabels{controllerutil.LabelAppKey: cr.SpecificName()})).To(Succeed()) + Expect(podsBefore.Items).NotTo(BeEmpty()) + + uidByPodName := make(map[string]types.UID) + for _, p := range podsBefore.Items { + uidByPodName[p.Name] = p.UID + } + + By("updating ExtraUsersConfig (reloadable, should not trigger restart)") + Expect(k8sClient.Get(ctx, cr.NamespacedName(), &cr)).To(Succeed()) + cr.Spec.Settings.ExtraUsersConfig = runtime.RawExtension{ + Raw: []byte(`{"users": {"e2e_test_user": {"password": "test", "profile": "default"}}}`), + } + Expect(k8sClient.Update(ctx, &cr)).To(Succeed()) + + By("waiting for configuration to sync") + EventuallyWithOffset(1, func() bool { + var cluster v1.ClickHouseCluster + + ExpectWithOffset(1, k8sClient.Get(ctx, cr.NamespacedName(), &cluster)).To(Succeed()) + + for _, cond := range cluster.Status.Conditions { + if cond.Type == string(v1.ConditionTypeConfigurationInSync) && cond.Status == metav1.ConditionTrue { + return true + } + } + + return false + }, 2*time.Minute).Should(BeTrue()) + + By("verifying pods were not restarted (same UIDs)") + + var podsAfter corev1.PodList + + Expect(k8sClient.List(ctx, &podsAfter, client.InNamespace(testNamespace), + client.MatchingLabels{controllerutil.LabelAppKey: cr.SpecificName()})).To(Succeed()) + + for _, p := range podsAfter.Items { + Expect(p.UID).To(Equal(uidByPodName[p.Name]), + "pod %s was restarted (UID changed)", p.Name) + } + + ClickHouseRWChecks(ctx, &cr, &checks) + }) + DescribeTable("ClickHouse cluster updates", func( ctx context.Context, baseReplicas int,