Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: No logs if cluster is configured to use `ImageCatalog` but no CRD for ImageCatalog is supplied
### 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)

### What version of Kubernetes are you using?

1.33

### What is your Kubernetes environment?

Self-managed: kind (evaluation)

### How did you install the operator?

YAML manifest

### What happened?

Hello,

Thank you for building this operator! It helps a lot!

Just came across little bug. If I define cluster to be built based on `ImageCatalog` but actual CRD for `ImageCatalog` is missing, then no error message is generated and process sits indefinitely.

ArgoCD creates such event, yet one has to look quite deep into every element of such installation to find such information. ArgoCD "Out of Sync" tab also doesn't show the difference since cluster hasn't finished init process.

Simple generation of error message would help a lot in resolution of such unfortunate events.

Best regards,
Boris

### Cluster resource

```shell

```

### Relevant log output

```shell

```

### Code of Conduct

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

Hints from issue comments:
@dosubot: <!-- Answer -->
You're right—if the ImageCatalog CRD is missing, the operator does not currently generate an error message or log, and the cluster sits in an unresolved state. This gap in error handling and logging has been reported by other users as well, especially when clusters reference ImageCatalog or ClusterImageCatalog and the required CRDs or resources are absent. The operator does have a cluster phase for image catalog errors (`PhaseImageCatalogError`), but it doesn't seem to trigger or log anything when the CRD itself is missing, making troubleshooting difficult in these scenarios [example](https://github.com/cloudnative-pg/cloudnative-pg/issues/7928) [example](https://github.com/cloudnative-pg/cloudnative-pg/issues/6276).

There is no official workaround for this specific case, but specifying the `imageName` directly in your Cluster spec (instead of using `imageCatalogRef`) can help avoid some related CLI bugs—though it doesn't address the missing CRD detection [example](https://github.com/cloudnative-pg/cloudnative-pg/issues/8537). Improving error visibility for missing CRDs would be a valuable enhancement for the operator.


<!-- 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/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/9e06983e-8cae-4c81-87d9-a0d0063968a8?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/9016)@bmironov: Thank you @armru  very much!

Solution patch:
diff --git a/internal/controller/cluster_image.go b/internal/controller/cluster_image.go
index 21cbba1d..9c20c39e 100644
--- a/internal/controller/cluster_image.go
+++ b/internal/controller/cluster_image.go
@@ -162,6 +162,10 @@ func (r *ClusterReconciler) getRequestedImageInfo(
 			return apiv1.ImageInfo{}, fmt.Errorf("catalog %s/%s not found", catalogKind, catalogName)
 		}
 
+		r.Recorder.Eventf(cluster, "Warning", "DiscoverImage", "Error getting %v/%v: %v",
+			catalogKind, catalogName, err)
+		contextLogger.Error(err, "while getting imageCatalog",
+			"catalogKind", catalogKind, "catalogName", catalogName)
 		return apiv1.ImageInfo{}, err
 	}
 



Test patch:
diff --git a/internal/controller/cluster_image_test.go b/internal/controller/cluster_image_test.go
index 11123505..9d45e042 100644
--- a/internal/controller/cluster_image_test.go
+++ b/internal/controller/cluster_image_test.go
@@ -21,11 +21,15 @@ package controller
 
 import (
 	"context"
+	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/record"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
 	schemeBuilder "github.com/cloudnative-pg/cloudnative-pg/internal/scheme"
@@ -223,3 +227,58 @@ var _ = Describe("Cluster image detection", func() {
 		Expect(cluster.Status.PGDataImageInfo.MajorVersion).To(Equal(16))
 	})
 })
+
+var _ = Describe("Cluster image detection with errors", func() {
+	It("emits an event when ImageCatalog retrieval fails", func(ctx SpecContext) {
+		cluster := &apiv1.Cluster{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "cluster-example",
+				Namespace: "default",
+			},
+			Spec: apiv1.ClusterSpec{
+				ImageCatalogRef: &apiv1.ImageCatalogRef{
+					TypedLocalObjectReference: corev1.TypedLocalObjectReference{
+						Name:     "catalog",
+						Kind:     "ImageCatalog",
+						APIGroup: &apiv1.SchemeGroupVersion.Group,
+					},
+					Major: 15,
+				},
+			},
+		}
+
+		fakeClient := fake.NewClientBuilder().
+			WithScheme(schemeBuilder.BuildWithAllKnownScheme()).
+			WithRuntimeObjects(cluster).
+			WithStatusSubresource(cluster).
+			WithInterceptorFuncs(interceptor.Funcs{
+				Get: func(ctx context.Context, cl client.WithWatch, key client.ObjectKey, obj client.Object,
+					opts ...client.GetOption,
+				) error {
+					if _, ok := obj.(*apiv1.ImageCatalog); ok {
+						return fmt.Errorf("simulated error: no kind match")
+					}
+					return cl.Get(ctx, key, obj, opts...)
+				},
+			}).
+			Build()
+
+		recorder := record.NewFakeRecorder(10)
+		r := &ClusterReconciler{
+			Client:   fakeClient,
+			Recorder: recorder,
+		}
+
+		result, err := r.reconcileImage(ctx, cluster)
+
+		// reconcileImage handles the error by updating the status, so it returns nil error
+		Expect(err).ToNot(HaveOccurred())
+		Expect(result).To(Equal(&ctrl.Result{}))
+
+		// Check if the event was recorded
+		// The event string format is "Type Reason Message"
+		// We expect: Warning DiscoverImage Error getting ImageCatalog/catalog: ...
+		Eventually(recorder.Events).Should(Receive(ContainSubstring(
+			"Warning DiscoverImage Error getting ImageCatalog/catalog")))
+	})
+})
diff --git a/tests/e2e/publication_subscription_test.go b/tests/e2e/publication_subscription_test.go
index a3ea4166..437f5ac0 100644
--- a/tests/e2e/publication_subscription_test.go
+++ b/tests/e2e/publication_subscription_test.go
@@ -310,7 +310,25 @@ var _ = Describe("Publication and Subscription", Label(tests.LabelPublicationSub
 					DatabaseName: dbname,
 					TableName:    tableName,
 				}
-				AssertDataExpectedCount(env, tableLocator, 2)
+				Eventually(func(g Gomega) int {
+					row, err := postgres.RunQueryRowOverForward(
+						env.Ctx,
+						env.Client,
+						env.Interface,
+						env.RestClientConfig,
+						tableLocator.Namespace,
+						tableLocator.ClusterName,
+						tableLocator.DatabaseName,
+						apiv1.ApplicationUserSecretSuffix,
+						fmt.Sprintf("SELECT COUNT(*) FROM %s", tableLocator.TableName),
+					)
+					g.Expect(err).ToNot(HaveOccurred())
+
+					var nRows int
+					err = row.Scan(&nRows)
+					g.Expect(err).ToNot(HaveOccurred())
+					return nRows
+				}, 300).Should(BeEquivalentTo(2))
 			})
 
 			By("removing the objects", func() {