Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: kubectl cnpg report cluster --logs fails to gather
### Is there an existing issue already for this bug?

- [x] I have searched for an existing issue, and could not find anything. I believe this is a new bug.

### I have read the troubleshooting guide

- [x] I have read the troubleshooting guide and I think this is a new bug.

### I am running a supported version of CloudNativePG

- [x] I have read the troubleshooting guide and I think this is a new bug.

### Contact Details

_No response_

### Version

1.27 (latest patch), 1.26 (latest patch), 1.25 (latest patch), trunk (main)

### What version of Kubernetes are you using?

1.34

### What is your Kubernetes environment?

Self-managed: kind (evaluation)

### How did you install the operator?

YAML manifest

### What happened?

When running `kubectl cnpg report cluster --logs cluster-example`, the logs gathering always fails with the error:

```
Error: could not write report: when opening the log stream: previous terminated container "postgres" in pod "cluster-example-1" not found
```

This occurs in a perfectly healthy cluster (see the status output below), where I have cluster admin permissions. The issue occurs on all supported plugin versions (1.25.x, 1.26.x, 1.27.x) as well as main branch builds.

The resulting report contains no logs.

### Cluster resource

```shell

```

### Relevant log output

```shell
$ kubectl cnpg report cluster --logs cluster-example
Error: could not write report: when opening the log stream: previous terminated container "postgres" in pod "cluster-example-1" not found

$ kubectl cnpg status cluster-example
Cluster Summary
Name                     default/cluster-example
System ID:               7566247159144882211
PostgreSQL Image:        ghcr.io/cloudnative-pg/postgresql:18.0-system-trixie
Primary instance:        cluster-example-1
Primary promotion time:  2025-10-28 12:23:45 +0000 UTC (4h20m38s)
Status:                  Cluster in a healthy state 
Instances:               3
Ready instances:         3
Size:                    144M
Current Write LSN:       0/8000000 (Timeline: 1 - WAL File: 000000010000000000000008)

Continuous Backup status (Barman Cloud Plugin)
ObjectStore / Server name:      minio-store/cluster-example
First Point of Recoverability:  -
Last Successful Backup:         -
Last Failed Backup:             -
Working WAL archiving:          OK
WALs waiting to be archived:    0
Last Archived WAL:              000000010000000000000007   @   2025-10-28T12:35:19.649263Z
Last Failed WAL:                -

Streaming Replication status
Replication Slots Enabled
Name               Sent LSN   Write LSN  Flush LSN  Replay LSN  Write Lag  Flush Lag  Replay Lag  State      Sync State  Sync Priority  Replication Slot
----               --------   ---------  ---------  ----------  ---------  ---------  ----------  -----      ----------  -------------  ----------------
cluster-example-2  0/8000000  0/8000000  0/8000000  0/8000000   00:00:00   00:00:00   00:00:00    streaming  async       0              active
cluster-example-3  0/8000000  0/8000000  0/8000000  0/8000000   00:00:00   00:00:00   00:00:00    streaming  async       0              active

Instances status
Name               Current LSN  Replication role  Status  QoS         Manager Version  Node
----               -----------  ----------------  ------  ---         ---------------  ----
cluster-example-1  0/8000000    Primary           OK      BestEffort  1.27.1           pg-operator-e2e-v1-34-0-worker
cluster-example-2  0/8000000    Standby (async)   OK      BestEffort  1.27.1           pg-operator-e2e-v1-34-0-worker2
cluster-example-3  0/8000000    Standby (async)   OK      BestEffort  1.27.1           pg-operator-e2e-v1-34-0-worker3

Plugins status
Name                            Version  Status  Reported Operator Capabilities
----                            -------  ------  ------------------------------
barman-cloud.cloudnative-pg.io  0.8.0    N/A     Reconciler Hooks, Lifecycle Service

$ unzip report_cluster_cluster-example_2025-10-28T16\:54\:33Z.zip 
Archive:  report_cluster_cluster-example_2025-10-28T16:54:33Z.zip
   creating: report_cluster_cluster-example_2025-10-28T16:54:33Z/
   creating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/cluster.yaml  
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/cluster-pods.yaml  
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/cluster-jobs.yaml  
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/events.yaml  
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/manifests/cluster-pvcs.yaml  
   creating: report_cluster_cluster-example_2025-10-28T16:54:33Z/logs/
  inflating: report_cluster_cluster-example_2025-10-28T16:54:33Z/logs/cluster-example-1-postgres.jsonl  

$ cat report_cluster_cluster-example_2025-10-28T16:54:33Z/logs/cluster-example-1-postgres.jsonl
"====== Beginning of Previous Log ====="
"Error fetching previous logs: when opening the log stream: previous terminated container \"postgres\" in pod \"cluster-example-1\" not found"
"====== End of Previous Log ====="
```

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Discussion:
@dosubot: <!-- Answer -->
This error is expected when the plugin tries to fetch "previous" logs for a container that hasn't restarted—Kubernetes only provides previous logs if the container has terminated and restarted at least once. In a healthy cluster with no restarts, you'll see this message and the logs section will be empty, but this does not indicate a failure in the cluster or the plugin's logic. The documentation confirms this behavior and does not list it as a bug or provide a workaround; it's a limitation of how Kubernetes exposes logs and how the plugin retrieves them ([docs](https://cloudnative-pg.io/documentation/current/kubectl-plugin/)).

There are related issues about log gathering in multi-container pods (e.g., with sidecars), where the plugin may fail to retrieve logs if it doesn't enforce the container name or handle multiple containers properly ([issue #6632](https://github.com/cloudnative-pg/cloudnative-pg/issues/6632), [issue #5905](https://github.com/cloudnative-pg/cloudnative-pg/issues/5905)). However, for your scenario with a single healthy container, the absence of previous logs is normal.

If you need to investigate logs after a pod restart, you'll see previous logs only if the container has actually terminated and restarted. Otherwise, the report will show the error message you encountered, which is safe to ignore for healthy clusters.


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/db2c1e2c-d924-47b8-84ba-8a38df01860b?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/89b818f3-8b5e-449b-bfb3-64a9360d9665/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/cloudnative-pg/cloudnative-pg/issues/8985)

Solution patch:
diff --git a/pkg/podlogs/writer.go b/pkg/podlogs/writer.go
index 21bb572e..2d341885 100644
--- a/pkg/podlogs/writer.go
+++ b/pkg/podlogs/writer.go
@@ -69,22 +69,26 @@ func (spl *Writer) sendLogsToWriter(
 	writer io.Writer,
 	options *corev1.PodLogOptions,
 ) error {
-	request := spl.Client.CoreV1().Pods(spl.Pod.Namespace).GetLogs(spl.Pod.Name, options)
-
 	if options.Previous {
 		jsWriter := json.NewEncoder(writer)
 		if err := jsWriter.Encode("====== Beginning of Previous Log ====="); err != nil {
 			return err
 		}
 		// getting the Previous logs can fail (as with `kubectl logs -p`). Don't error out
-		if err := executeGetLogRequest(ctx, request, writer); err != nil {
+		previousOpts := options.DeepCopy()
+		previousRequest := spl.Client.CoreV1().Pods(spl.Pod.Namespace).GetLogs(spl.Pod.Name, previousOpts)
+		if err := executeGetLogRequest(ctx, previousRequest, writer); err != nil {
 			// we try to print the json-safe error message. We don't exit on error
 			_ = json.NewEncoder(writer).Encode("Error fetching previous logs: " + err.Error())
 		}
 		if err := jsWriter.Encode("====== End of Previous Log ====="); err != nil {
 			return err
 		}
+		// Now fetch current logs with Previous set to false
+		options.Previous = false
 	}
+
+	request := spl.Client.CoreV1().Pods(spl.Pod.Namespace).GetLogs(spl.Pod.Name, options)
 	return executeGetLogRequest(ctx, request, writer)
 }
 
@@ -107,7 +111,7 @@ func (spl *Writer) Multiple(
 		containerOpts := opts.DeepCopy()
 		containerOpts.Container = container.Name
 
-		if err := spl.sendLogsToWriter(ctx, writer, opts); err != nil {
+		if err := spl.sendLogsToWriter(ctx, writer, containerOpts); err != nil {
 			return err
 		}
 	}



Test patch:
diff --git a/pkg/podlogs/writer_test.go b/pkg/podlogs/writer_test.go
index fa5d3d73..cb46f45c 100644
--- a/pkg/podlogs/writer_test.go
+++ b/pkg/podlogs/writer_test.go
@@ -250,5 +250,87 @@ fake logs
 fake logs
 `))
 		})
+
+		It("ensures containerOpts with correct container name is passed to sendLogsToWriter", func(ctx context.Context) {
+			client := fake.NewClientset(&podWithSidecar)
+			streamPodLog := Writer{
+				Pod:    podWithSidecar,
+				Client: client,
+			}
+
+			namer := func(container string) string {
+				return fmt.Sprintf("%s-%s.log", streamPodLog.Pod.Name, container)
+			}
+			mw := newMultiWriter()
+
+			// This is a regression test for bug where opts was passed instead of containerOpts
+			// If container name isn't set correctly, the fake client might not behave as expected
+			// The test verifies that each container gets its own log file
+			err := streamPodLog.Multiple(ctx, &corev1.PodLogOptions{}, mw, namer)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(mw.writers).To(HaveLen(2))
+			Expect(mw.writers).To(HaveKey("pod-name-test-postgres.log"))
+			Expect(mw.writers).To(HaveKey("pod-name-test-sidecar.log"))
+		})
+	})
+
+	When("handling Previous logs edge cases", func() {
+		It("should fetch current logs even when Previous is true (regression test for #8985)", func(ctx context.Context) {
+			// This test verifies that when Previous: true is set, the function attempts
+			// to fetch both previous and current logs, not previous logs twice
+			client := fake.NewClientset(&pod)
+			streamPodLog := Writer{
+				Pod:    pod,
+				Client: client,
+			}
+
+			var logBuffer bytes.Buffer
+			err := streamPodLog.Single(ctx, &logBuffer, &corev1.PodLogOptions{
+				Previous: true,
+			})
+			Expect(err).NotTo(HaveOccurred())
+
+			// The output should contain the previous log markers and both log entries
+			output := logBuffer.String()
+			Expect(output).To(ContainSubstring("====== Beginning of Previous Log ====="))
+			Expect(output).To(ContainSubstring("====== End of Previous Log ====="))
+			// Should have logs from both previous and current (fake client returns "fake logs" for both)
+			Expect(output).To(ContainSubstring("fake logs"))
+		})
+
+		It("should handle Previous logs in Multiple with correct container options", func(ctx context.Context) {
+			// Regression test to ensure containerOpts is passed (not opts) in Multiple
+			// when Previous is enabled
+			client := fake.NewClientset(&podWithSidecar)
+			streamPodLog := Writer{
+				Pod:    podWithSidecar,
+				Client: client,
+			}
+
+			namer := func(container string) string {
+				return fmt.Sprintf("%s-%s.log", streamPodLog.Pod.Name, container)
+			}
+			mw := newMultiWriter()
+
+			err := streamPodLog.Multiple(ctx, &corev1.PodLogOptions{
+				Previous:   true,
+				Timestamps: true,
+			}, mw, namer)
+			Expect(err).ToNot(HaveOccurred())
+
+			// Both containers should have their logs collected
+			Expect(mw.writers).To(HaveLen(2))
+
+			// Each should contain the previous log markers and actual logs
+			for containerName, writer := range mw.writers {
+				output := writer.String()
+				Expect(output).To(ContainSubstring("====== Beginning of Previous Log ====="),
+					"Container %s should have previous log marker", containerName)
+				Expect(output).To(ContainSubstring("====== End of Previous Log ====="),
+					"Container %s should have end marker", containerName)
+				Expect(output).To(ContainSubstring("fake logs"),
+					"Container %s should have actual logs", containerName)
+			}
+		})
 	})
 })