From d70d6d63bd3b96de0dec4c0448e4f5ea9a93abed Mon Sep 17 00:00:00 2001 From: Wenting Wu Date: Thu, 19 Mar 2026 15:17:52 -0400 Subject: [PATCH] fix: add restart annotation when TLS gateway secret name changes When spec.tls.gateway.provided.secretName changes, the operator updates the gatewayTLSSecret plugin parameter on the CNPG Cluster but does not force a pod restart. CNPG does not auto-restart pods for plugin parameter changes (only for PodSpec divergence like ImageVolume changes). Since the sidecar injector mounts the TLS secret as a volume, pods must be restarted to pick up the new secret name. Add kubectl.kubernetes.io/restartedAt annotation (the same mechanism used for gateway-only image updates) to force a rolling restart when the TLS secret name changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Wenting Wu --- .../preview/configuration/tls.md | 3 + .../controller/documentdb_controller.go | 6 +- .../controller/documentdb_controller_test.go | 91 +++++++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/operator-public-documentation/preview/configuration/tls.md b/docs/operator-public-documentation/preview/configuration/tls.md index 0c634094..72a8d4de 100644 --- a/docs/operator-public-documentation/preview/configuration/tls.md +++ b/docs/operator-public-documentation/preview/configuration/tls.md @@ -263,6 +263,9 @@ Certificate rotation is automatic and zero-downtime. When a certificate is renew | **CertManager** | cert-manager auto-renews based on the Certificate CR's `renewBefore` | None | | **Provided** | You update the Secret contents (manually or via CSI driver sync) | Update the Secret | +!!! note + Changing `spec.tls.gateway.provided.secretName` to point to a **different** Secret triggers a rolling restart of the DocumentDB cluster pods, which causes a brief period of downtime. To rotate certificates without downtime, update the contents of the **existing** Secret instead of changing the Secret name. + ### Monitoring Certificate Expiration ```bash diff --git a/operator/src/internal/controller/documentdb_controller.go b/operator/src/internal/controller/documentdb_controller.go index b5a7633c..0b265ebc 100644 --- a/operator/src/internal/controller/documentdb_controller.go +++ b/operator/src/internal/controller/documentdb_controller.go @@ -224,7 +224,11 @@ func (r *DocumentDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) if currentCnpgCluster.Annotations == nil { currentCnpgCluster.Annotations = map[string]string{} } - currentCnpgCluster.Annotations["documentdb.io/gateway-tls-rev"] = time.Now().Format(time.RFC3339Nano) + // CNPG does not auto-restart pods for plugin parameter changes. + // The gatewayTLSSecret is mounted as a volume by the sidecar injector, + // so pods must be restarted to pick up the new secret name. + // CNPG specifically handles kubectl.kubernetes.io/restartedAt for pod restarts. + currentCnpgCluster.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339Nano) if err := r.Client.Update(ctx, currentCnpgCluster); err == nil { logger.Info("Patched CNPG Cluster with TLS settings; requeueing for pod update") return ctrl.Result{RequeueAfter: RequeueAfterShort}, nil diff --git a/operator/src/internal/controller/documentdb_controller_test.go b/operator/src/internal/controller/documentdb_controller_test.go index 69b9d886..59ef63b5 100644 --- a/operator/src/internal/controller/documentdb_controller_test.go +++ b/operator/src/internal/controller/documentdb_controller_test.go @@ -3562,6 +3562,97 @@ var _ = Describe("DocumentDB Controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(result.Requeue).To(BeFalse()) }) + + It("should add restart annotation when TLS secret name changes", func() { + Expect(rbacv1.AddToScheme(scheme)).To(Succeed()) + + documentdb := &dbpreview.DocumentDB{ + ObjectMeta: metav1.ObjectMeta{ + Name: documentDBName, + Namespace: documentDBNamespace, + Finalizers: []string{documentDBFinalizer}, + }, + Spec: dbpreview.DocumentDBSpec{ + InstancesPerNode: 1, + Resource: dbpreview.Resource{ + Storage: dbpreview.StorageConfiguration{ + PvcSize: "1Gi", + }, + }, + }, + Status: dbpreview.DocumentDBStatus{ + TLS: &dbpreview.TLSStatus{ + Ready: true, + SecretName: "new-tls-secret", + }, + }, + } + + cnpgCluster := &cnpgv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: documentDBName, + Namespace: documentDBNamespace, + }, + Spec: cnpgv1.ClusterSpec{ + Instances: 1, + PostgresConfiguration: cnpgv1.PostgresConfiguration{ + Extensions: []cnpgv1.ExtensionConfiguration{ + { + Name: "documentdb", + ImageVolumeSource: corev1.ImageVolumeSource{ + Reference: util.DEFAULT_DOCUMENTDB_IMAGE, + }, + }, + }, + }, + Plugins: []cnpgv1.PluginConfiguration{ + { + Name: util.DEFAULT_SIDECAR_INJECTOR_PLUGIN, + Parameters: map[string]string{ + "gatewayImage": util.DEFAULT_GATEWAY_IMAGE, + "documentDbCredentialSecret": util.DEFAULT_DOCUMENTDB_CREDENTIALS_SECRET, + "gatewayTLSSecret": "old-tls-secret", + }, + }, + }, + }, + Status: cnpgv1.ClusterStatus{ + CurrentPrimary: documentDBName + "-1", + TargetPrimary: documentDBName + "-1", + InstancesStatus: map[cnpgv1.PodStatus][]string{ + cnpgv1.PodHealthy: {documentDBName + "-1"}, + }, + }, + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(documentdb, cnpgCluster). + WithStatusSubresource(&dbpreview.DocumentDB{}). + Build() + + reconciler := &DocumentDBReconciler{ + Client: fakeClient, + Scheme: scheme, + Recorder: recorder, + } + + result, err := reconciler.Reconcile(ctx, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: documentDBName, + Namespace: documentDBNamespace, + }, + }) + + Expect(err).ToNot(HaveOccurred()) + Expect(result.RequeueAfter).To(Equal(RequeueAfterShort)) + + // Verify CNPG cluster was updated with new TLS secret and restart annotation + updatedCluster := &cnpgv1.Cluster{} + Expect(fakeClient.Get(ctx, types.NamespacedName{Name: documentDBName, Namespace: documentDBNamespace}, updatedCluster)).To(Succeed()) + Expect(updatedCluster.Spec.Plugins[0].Parameters["gatewayTLSSecret"]).To(Equal("new-tls-secret")) + Expect(updatedCluster.Annotations).To(HaveKey("kubectl.kubernetes.io/restartedAt")) + }) }) Describe("validateK8sVersion", func() {