Repository: cloudnative-pg/cloudnative-pg

Issue:
[Bug]: pooler bootstrap-controller setting resources to {}
### 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

chris.gooch@hotmail.co.uk

### Version

1.27 (latest patch)

### What version of Kubernetes are you using?

1.31

### What is your Kubernetes environment?

Cloud: Amazon EKS

### How did you install the operator?

Helm

### What happened?

As per #7822 and #7922 this issue should be fixed.
However, in our environment it has caused a regression, and the generated deployment is now setting the pooler bootstrap-controller resources to {} causing excessive resources to be wasted when the pods schedule.
My earlier workaround of forcing the bootstrap-controller resources in the pooler manifest also no longer works.
 
pooler manifest example:
```
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pooler-ro
spec:
  cluster:
    name: cluster
  instances: 2
  type: ro
  serviceTemplate:
    spec:
      type: LoadBalancer
      loadBalancerClass: service.k8s.aws/nlb
  template:
    spec:
      initContainers:
        - name: bootstrap-controller
          resources:
            limits:
              cpu: "1"
              memory: 100Mi
            requests:
              cpu: 2m
              memory: 30Mi
      containers:
        - name: pgbouncer
          resources:
            requests:
              cpu: 2m
              memory: 30Mi
            limits:
              cpu: "1"
              memory: 100Mi
```
 
v1.27.0 generated deployment example:
```
initContainers:
- command:
  - /manager
  - bootstrap
  - /controller/manager
  image: ghcr.io/cloudnative-pg/cloudnative-pg:1.27.0
  imagePullPolicy: IfNotPresent
  name: bootstrap-controller
  resources: {}
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
      - ALL
    privileged: false
    readOnlyRootFilesystem: true
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
```
 
v1.26.0 generated deployment example:
```
initContainers:
- command:
  - /manager
  - bootstrap
  - /controller/manager
  image: ghcr.io/cloudnative-pg/cloudnative-pg:1.26.0
  imagePullPolicy: IfNotPresent
  name: bootstrap-controller
  resources:
   limits:
    cpu: "1"
    memory: 100Mi
   requests:
    cpu: 2m
    memory: 30Mi
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
      - ALL
    privileged: false
    readOnlyRootFilesystem: true
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
```

### Cluster resource

```shell

```

### Relevant log output

```shell

```

### Code of Conduct

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

Discussion:
@zerkms: I confirm it's in 1.27.0 too.

Solution patch:
diff --git a/pkg/specs/pgbouncer/deployments.go b/pkg/specs/pgbouncer/deployments.go
index 2df9f7d7..612f3e42 100644
--- a/pkg/specs/pgbouncer/deployments.go
+++ b/pkg/specs/pgbouncer/deployments.go
@@ -94,7 +94,7 @@ func Deployment(pooler *apiv1.Pooler, cluster *apiv1.Cluster) (*appsv1.Deploymen
 		WithInitContainerCommand(specs.BootstrapControllerContainerName,
 			[]string{"/manager", "bootstrap", "/controller/manager"},
 			true).
-		WithInitContainerResources(specs.BootstrapControllerContainerName, pooler.GetResourcesRequirements(), true).
+		WithInitContainerResources(specs.BootstrapControllerContainerName, pooler.GetResourcesRequirements(), false).
 		WithInitContainerSecurityContext(specs.BootstrapControllerContainerName,
 			specs.CreateContainerSecurityContext(cluster.GetSeccompProfile()),
 			true).



Test patch:
diff --git a/internal/controller/pooler_status_test.go b/internal/controller/pooler_status_test.go
index 9bbba948..955520af 100644
--- a/internal/controller/pooler_status_test.go
+++ b/internal/controller/pooler_status_test.go
@@ -23,11 +23,10 @@ import (
 	"context"
 
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 
-	v1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
+	apiv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
 	"github.com/cloudnative-pg/cloudnative-pg/pkg/specs/pgbouncer"
 
 	. "github.com/onsi/ginkgo/v2"
@@ -40,12 +39,12 @@ var _ = Describe("pooler_status unit tests", func() {
 		env = buildTestEnvironment()
 	})
 
-	assertClusterInheritedStatus := func(pooler *v1.Pooler, cluster *v1.Cluster) {
+	assertClusterInheritedStatus := func(pooler *apiv1.Pooler, cluster *apiv1.Cluster) {
 		Expect(pooler.Status.Secrets.ServerCA.Name).To(Equal(cluster.GetServerCASecretName()))
 		Expect(pooler.Status.Secrets.ServerTLS.Name).To(Equal(cluster.GetServerTLSSecretName()))
 		Expect(pooler.Status.Secrets.ClientCA.Name).To(Equal(cluster.GetClientCASecretName()))
 	}
-	assertAuthUserStatus := func(pooler *v1.Pooler, authUserSecret *corev1.Secret) {
+	assertAuthUserStatus := func(pooler *apiv1.Pooler, authUserSecret *corev1.Secret) {
 		Expect(pooler.Status.Secrets.PgBouncerSecrets.AuthQuery.Name).To(Equal(authUserSecret.Name))
 		Expect(pooler.Status.Secrets.PgBouncerSecrets.AuthQuery.Version).To(Equal(authUserSecret.ResourceVersion))
 	}
@@ -96,39 +95,6 @@ var _ = Describe("pooler_status unit tests", func() {
 		Expect(pooler.Status.Instances).To(Equal(dep.Status.Replicas))
 	})
 
-	It("should correctly set pod resources to the bootstrap init container", func() {
-		cluster := newFakeCNPGCluster(env.client, "test-namespace")
-
-		pooler := &v1.Pooler{
-			Spec: v1.PoolerSpec{
-				Template: &v1.PodTemplateSpec{
-					Spec: corev1.PodSpec{
-						Resources: &corev1.ResourceRequirements{
-							Requests: corev1.ResourceList{
-								corev1.ResourceCPU:    resource.MustParse("100m"),
-								corev1.ResourceMemory: resource.MustParse("128Mi"),
-							},
-							Limits: corev1.ResourceList{
-								corev1.ResourceCPU:    resource.MustParse("200m"),
-								corev1.ResourceMemory: resource.MustParse("256Mi"),
-							},
-						},
-					},
-				},
-			},
-		}
-
-		dep, err := pgbouncer.Deployment(pooler, cluster)
-		Expect(err).ToNot(HaveOccurred())
-		// check that the init container has the correct resources
-		Expect(dep.Spec.Template.Spec.InitContainers).To(HaveLen(1))
-		initResources := dep.Spec.Template.Spec.InitContainers[0].Resources
-		Expect(initResources.Requests).To(HaveKeyWithValue(corev1.ResourceCPU, resource.MustParse("100m")))
-		Expect(initResources.Requests).To(HaveKeyWithValue(corev1.ResourceMemory, resource.MustParse("128Mi")))
-		Expect(initResources.Limits).To(HaveKeyWithValue(corev1.ResourceCPU, resource.MustParse("200m")))
-		Expect(initResources.Limits).To(HaveKeyWithValue(corev1.ResourceMemory, resource.MustParse("256Mi")))
-	})
-
 	It("should correctly interact with the api server", func() {
 		ctx := context.Background()
 		namespace := newFakeNamespace(env.client)
@@ -148,14 +114,14 @@ var _ = Describe("pooler_status unit tests", func() {
 		res := &poolerManagedResources{AuthUserSecret: authUserSecret, Cluster: cluster, Deployment: dep}
 
 		By("making sure it updates the remote stored status when there are changes", func() {
-			poolerBefore := &v1.Pooler{}
+			poolerBefore := &apiv1.Pooler{}
 			err := env.client.Get(ctx, poolerQuery, poolerBefore)
 			Expect(err).ToNot(HaveOccurred())
 
 			err = env.poolerReconciler.updatePoolerStatus(ctx, pooler, res)
 			Expect(err).ToNot(HaveOccurred())
 
-			poolerAfter := &v1.Pooler{}
+			poolerAfter := &apiv1.Pooler{}
 			err = env.client.Get(ctx, poolerQuery, poolerAfter)
 			Expect(err).ToNot(HaveOccurred())
 
@@ -166,14 +132,14 @@ var _ = Describe("pooler_status unit tests", func() {
 		})
 
 		By("making sure it doesn't update the remote stored status when there aren't changes", func() {
-			poolerBefore := &v1.Pooler{}
+			poolerBefore := &apiv1.Pooler{}
 			err := env.client.Get(ctx, poolerQuery, poolerBefore)
 			Expect(err).ToNot(HaveOccurred())
 
 			err = env.poolerReconciler.updatePoolerStatus(ctx, pooler, res)
 			Expect(err).ToNot(HaveOccurred())
 
-			poolerAfter := &v1.Pooler{}
+			poolerAfter := &apiv1.Pooler{}
 			err = env.client.Get(ctx, poolerQuery, poolerAfter)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(poolerBefore.Status).To(BeEquivalentTo(poolerAfter.Status))
diff --git a/pkg/specs/pgbouncer/deployments_test.go b/pkg/specs/pgbouncer/deployments_test.go
index 31af3a07..b18bd852 100644
--- a/pkg/specs/pgbouncer/deployments_test.go
+++ b/pkg/specs/pgbouncer/deployments_test.go
@@ -21,6 +21,8 @@ package pgbouncer
 
 import (
 	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/intstr"
 	"k8s.io/utils/ptr"
@@ -159,4 +161,70 @@ var _ = Describe("Deployment", func() {
 		Expect(deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.TCPSocket.Port).
 			To(Equal(intstr.FromInt32(pgBouncerConfig.PgBouncerPort)))
 	})
+
+	It("should correctly set pod resources to the bootstrap init container", func() {
+		pooler := &apiv1.Pooler{
+			Spec: apiv1.PoolerSpec{
+				Template: &apiv1.PodTemplateSpec{
+					Spec: corev1.PodSpec{
+						Resources: &corev1.ResourceRequirements{
+							Requests: corev1.ResourceList{
+								corev1.ResourceCPU:    resource.MustParse("100m"),
+								corev1.ResourceMemory: resource.MustParse("128Mi"),
+							},
+							Limits: corev1.ResourceList{
+								corev1.ResourceCPU:    resource.MustParse("200m"),
+								corev1.ResourceMemory: resource.MustParse("256Mi"),
+							},
+						},
+					},
+				},
+			},
+		}
+
+		dep, err := Deployment(pooler, cluster)
+		Expect(err).ToNot(HaveOccurred())
+		// check that the init container has the correct resources
+		Expect(dep.Spec.Template.Spec.InitContainers).To(HaveLen(1))
+		initResources := dep.Spec.Template.Spec.InitContainers[0].Resources
+		Expect(initResources.Requests).To(HaveKeyWithValue(corev1.ResourceCPU, resource.MustParse("100m")))
+		Expect(initResources.Requests).To(HaveKeyWithValue(corev1.ResourceMemory, resource.MustParse("128Mi")))
+		Expect(initResources.Limits).To(HaveKeyWithValue(corev1.ResourceCPU, resource.MustParse("200m")))
+		Expect(initResources.Limits).To(HaveKeyWithValue(corev1.ResourceMemory, resource.MustParse("256Mi")))
+	})
+
+	It("retains user-defined bootstrap-controller resources", func() {
+		pooler.Spec.Template = &apiv1.PodTemplateSpec{
+			Spec: corev1.PodSpec{
+				InitContainers: []corev1.Container{{
+					Name: specs.BootstrapControllerContainerName,
+					Resources: corev1.ResourceRequirements{
+						Requests: corev1.ResourceList{
+							corev1.ResourceCPU:    resource.MustParse("2m"),
+							corev1.ResourceMemory: resource.MustParse("30Mi"),
+						},
+						Limits: corev1.ResourceList{
+							corev1.ResourceCPU:    resource.MustParse("1"),
+							corev1.ResourceMemory: resource.MustParse("100Mi"),
+						},
+					},
+				}},
+			},
+		}
+
+		deployment, err := Deployment(pooler, cluster)
+		Expect(err).ShouldNot(HaveOccurred())
+		var found *corev1.Container
+		for i := range deployment.Spec.Template.Spec.InitContainers {
+			if deployment.Spec.Template.Spec.InitContainers[i].Name == specs.BootstrapControllerContainerName {
+				found = &deployment.Spec.Template.Spec.InitContainers[i]
+				break
+			}
+		}
+		Expect(found).ToNot(BeNil())
+		Expect(found.Resources.Requests.Cpu().String()).To(Equal("2m"))
+		Expect(found.Resources.Requests.Memory().String()).To(Equal("30Mi"))
+		Expect(found.Resources.Limits.Cpu().String()).To(Equal("1"))
+		Expect(found.Resources.Limits.Memory().String()).To(Equal("100Mi"))
+	})
 })