Skip to content

Commit a85c858

Browse files
leonardocearmruNiccoloFeimnenciagbartolini
authored
feat(pooler): allow PgBouncer image management via image catalogs (cloudnative-pg#10568)
Users can now manage the PgBouncer image centrally by referencing an entry in an `ImageCatalog` or `ClusterImageCatalog` through the new `spec.pgbouncer.imageCatalogRef` field. Alternatively, a specific image can still be pinned via `spec.pgbouncer.image`. When a catalog entry is updated, all `Poolers` referencing it are automatically reconciled and the `Deployment` rolls out the new image without any change to the `Pooler` spec. The resolved image is reported in `status.image`. A new `status.phase` (`active`, `paused`, `inactive`, or `failed`) summarises the lifecycle, with `status.phaseReason` carrying a human-readable cause when something is wrong. `kubectl get pooler` also gains a `Phase` column for at-a-glance visibility. Upgrade note: after upgrading, each existing `Pooler`-managed `Deployment` receives a one-time annotation patch (`cnpg.io/poolerSpecHash`). The pod template is unchanged, so this patch alone does not roll pgbouncer pods. If the upgrade also bumps the default pgbouncer image, that bump (not the hash change) will trigger a normal rolling restart. Signed-off-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com> Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com> Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com> Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Signed-off-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com> Co-authored-by: Armando Ruocco <armando.ruocco@enterprisedb.com> Co-authored-by: Niccolò Fei <niccolo.fei@enterprisedb.com> Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Co-authored-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com>
1 parent 657878e commit a85c858

29 files changed

Lines changed: 1868 additions & 51 deletions

.wordlist-en-custom.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ CVE
7171
CVEs
7272
CannotReconcile
7373
Canovai
74+
CatalogComponentImage
7475
CatalogImage
7576
CatalogImages
7677
Cecchi
@@ -97,6 +98,7 @@ ClusterSpec
9798
ClusterStatus
9899
CodeQL
99100
ColumnName
101+
ComponentImages
100102
ConditionStatus
101103
ConfigMap
102104
ConfigMapKeySelector
@@ -215,6 +217,7 @@ IaC
215217
Ibryam
216218
IfNotPresent
217219
ImageCatalog
220+
ImageCatalogComponentRef
218221
ImageCatalogRef
219222
ImageCatalogSpec
220223
ImageInfo
@@ -369,6 +372,11 @@ PoolerIntegrations
369372
PoolerList
370373
PoolerMonitoringConfiguration
371374
PoolerMonitoringTLSConfiguration
375+
PoolerPhase
376+
PoolerPhaseActive
377+
PoolerPhaseFailed
378+
PoolerPhaseInactive
379+
PoolerPhasePaused
372380
PoolerSecrets
373381
PoolerSpec
374382
PoolerStatus
@@ -698,6 +706,7 @@ cGFzc
698706
caSecretVersion
699707
cannotReconcile
700708
catalogName
709+
catalogcomponentimage
701710
catalogimage
702711
cb
703712
cd
@@ -743,6 +752,7 @@ collationVersion
743752
columnValue
744753
commandError
745754
commandOutput
755+
componentImages
746756
conf
747757
config
748758
config's
@@ -963,6 +973,7 @@ imagePullPolicy
963973
imagePullSecrets
964974
imageVolume
965975
imagecatalog
976+
imagecatalogcomponentref
966977
imagecatalogref
967978
imagecatalogs
968979
imagecatalogspec
@@ -1251,6 +1262,7 @@ poolerName
12511262
poolerintegrations
12521263
poolermonitoringconfiguration
12531264
poolermonitoringtlsconfiguration
1265+
poolerphase
12541266
poolers
12551267
poolersecrets
12561268
poolerspec

api/v1/imagecatalog_funcs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,14 @@ func (spec *ImageCatalogSpec) FindExtensionsForMajor(major int) ([]ExtensionConf
6060

6161
return nil, false
6262
}
63+
64+
// FindComponentImageForKey finds the image string for a given component-image key.
65+
func (spec *ImageCatalogSpec) FindComponentImageForKey(key string) (string, bool) {
66+
for _, entry := range spec.ComponentImages {
67+
if entry.Key == key {
68+
return entry.Image, true
69+
}
70+
}
71+
72+
return "", false
73+
}

api/v1/imagecatalog_funcs_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,42 @@ var _ = Describe("image catalog", func() {
8888
Expect(extensions).To(BeNil())
8989
})
9090
})
91+
92+
var _ = Describe("image catalog component images", func() {
93+
catalogSpec := ImageCatalogSpec{
94+
Images: []CatalogImage{
95+
{Image: "test:16", Major: 16},
96+
},
97+
ComponentImages: []CatalogComponentImage{
98+
{Key: "pgbouncer", Image: "pgbouncer:1.24.0"},
99+
{Key: "other-tool", Image: "other-tool:1.0.0"},
100+
},
101+
}
102+
103+
It("looks up a component image by key", func() {
104+
image, ok := catalogSpec.FindComponentImageForKey("pgbouncer")
105+
Expect(ok).To(BeTrue())
106+
Expect(image).To(Equal("pgbouncer:1.24.0"))
107+
})
108+
109+
It("looks up a second component image by key", func() {
110+
image, ok := catalogSpec.FindComponentImageForKey("other-tool")
111+
Expect(ok).To(BeTrue())
112+
Expect(image).To(Equal("other-tool:1.0.0"))
113+
})
114+
115+
It("returns false when the key is not present", func() {
116+
image, ok := catalogSpec.FindComponentImageForKey("nonexistent")
117+
Expect(ok).To(BeFalse())
118+
Expect(image).To(BeEmpty())
119+
})
120+
121+
It("returns false when componentImages is empty", func() {
122+
emptySpec := ImageCatalogSpec{
123+
Images: []CatalogImage{{Image: "test:16", Major: 16}},
124+
}
125+
image, ok := emptySpec.FindComponentImageForKey("pgbouncer")
126+
Expect(ok).To(BeFalse())
127+
Expect(image).To(BeEmpty())
128+
})
129+
})

api/v1/imagecatalog_types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,26 @@ type ImageCatalogSpec struct {
3030
// +kubebuilder:validation:MaxItems=8
3131
// +kubebuilder:validation:XValidation:rule="self.all(e, self.filter(f, f.major==e.major).size() == 1)",message=Images must have unique major versions
3232
Images []CatalogImage `json:"images"`
33+
34+
// ComponentImages is a list of named images for components other than PostgreSQL
35+
// (e.g. pgbouncer). Keys must be unique within a catalog.
36+
// +optional
37+
// +listType=map
38+
// +listMapKey=key
39+
// +kubebuilder:validation:MaxItems=32
40+
// +kubebuilder:validation:XValidation:rule="self.all(e, self.filter(f, f.key==e.key).size() == 1)",message="Component image keys must be unique"
41+
ComponentImages []CatalogComponentImage `json:"componentImages,omitempty"`
42+
}
43+
44+
// CatalogComponentImage is a named image entry for a non-PostgreSQL component.
45+
type CatalogComponentImage struct {
46+
// Key is the unique identifier for this image within the catalog.
47+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
48+
// +kubebuilder:validation:MaxLength=63
49+
Key string `json:"key"`
50+
51+
// Image is the container image reference.
52+
Image string `json:"image"`
3353
}
3454

3555
// CatalogImage defines the image and major version

api/v1/pooler_types.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ import (
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
)
2828

29+
// PoolerPhase represents the lifecycle phase of a Pooler.
30+
// +kubebuilder:validation:Enum=active;paused;inactive;failed
31+
type PoolerPhase string
32+
33+
const (
34+
// PoolerPhaseActive means the pooler is running normally and serving traffic.
35+
PoolerPhaseActive PoolerPhase = "active"
36+
37+
// PoolerPhasePaused means PgBouncer is up and running but holding new client
38+
// connections in the queue because spec.pgbouncer.paused is true. The Deployment
39+
// keeps reconciling; lifting the pause transitions back to Active.
40+
PoolerPhasePaused PoolerPhase = "paused"
41+
42+
// PoolerPhaseInactive means the pooler cannot make progress because a
43+
// prerequisite resource is missing (cluster, secret, certificate). The
44+
// controller retries periodically until the prerequisite shows up. Check
45+
// status.phaseReason for the specific cause.
46+
PoolerPhaseInactive PoolerPhase = "inactive"
47+
48+
// PoolerPhaseFailed means the pooler cannot be reconciled due to a
49+
// configuration error. Check status.phaseReason for details.
50+
PoolerPhaseFailed PoolerPhase = "failed"
51+
)
52+
2953
// PoolerType is the type of the connection pool, meaning the service
3054
// we are targeting. Allowed values are `rw` and `ro`.
3155
// +kubebuilder:validation:Enum=rw;ro;r
@@ -188,7 +212,21 @@ type ServiceTemplateSpec struct {
188212
Spec corev1.ServiceSpec `json:"spec,omitempty"`
189213
}
190214

215+
// ImageCatalogComponentRef identifies a named image within the componentImages list of an
216+
// ImageCatalog or ClusterImageCatalog.
217+
type ImageCatalogComponentRef struct {
218+
// +kubebuilder:validation:XValidation:rule="self.kind == 'ImageCatalog' || self.kind == 'ClusterImageCatalog'",message="Only ImageCatalog and ClusterImageCatalog are supported"
219+
// +kubebuilder:validation:XValidation:rule="self.apiGroup == 'postgresql.cnpg.io'",message="apiGroup must be postgresql.cnpg.io"
220+
corev1.TypedLocalObjectReference `json:",inline"`
221+
222+
// Key identifies the entry within the catalog's componentImages list.
223+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
224+
// +kubebuilder:validation:MaxLength=63
225+
Key string `json:"key"`
226+
}
227+
191228
// PgBouncerSpec defines how to configure PgBouncer
229+
// +kubebuilder:validation:XValidation:rule="!(has(self.image) && has(self.imageCatalogRef))",message="image and imageCatalogRef are mutually exclusive"
192230
type PgBouncerSpec struct {
193231
// The pool mode. Default: `session`.
194232
// +kubebuilder:default:=session
@@ -250,16 +288,45 @@ type PgBouncerSpec struct {
250288
// +kubebuilder:default:=false
251289
// +optional
252290
Paused *bool `json:"paused,omitempty"`
291+
292+
// Image is the pgbouncer container image to use. When set, it takes
293+
// precedence over ImageCatalogRef and the operator default, but is
294+
// overridden by an explicit image set in the pod template.
295+
// +optional
296+
Image string `json:"image,omitempty"`
297+
298+
// ImageCatalogRef points to an entry in an ImageCatalog or ClusterImageCatalog.
299+
// Mutually exclusive with Image.
300+
// +optional
301+
ImageCatalogRef *ImageCatalogComponentRef `json:"imageCatalogRef,omitempty"`
253302
}
254303

255304
// PoolerStatus defines the observed state of Pooler
256305
type PoolerStatus struct {
257306
// The resource version of the config object
258307
// +optional
259308
Secrets *PoolerSecrets `json:"secrets,omitempty"`
309+
260310
// The number of pods trying to be scheduled
261311
// +optional
262312
Instances int32 `json:"instances,omitempty"`
313+
314+
// Phase summarizes the overall lifecycle state of the Pooler.
315+
// +optional
316+
Phase PoolerPhase `json:"phase,omitempty"`
317+
318+
// PhaseReason is a human-readable explanation of the current Phase.
319+
// +optional
320+
PhaseReason string `json:"phaseReason,omitempty"`
321+
322+
// Image is the resolved pgbouncer container image that the operator is
323+
// using for this Pooler, including any override coming from spec.template.
324+
// While Phase is Active or Paused this field reflects what the Deployment
325+
// actually runs; while Phase is Inactive or Failed it may carry the last
326+
// successfully resolved value (or be empty if the Pooler has never reconciled
327+
// successfully).
328+
// +optional
329+
Image string `json:"image,omitempty"`
263330
}
264331

265332
// PoolerSecrets contains the versions of all the secrets used
@@ -310,6 +377,7 @@ type SecretVersion struct {
310377
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
311378
// +kubebuilder:printcolumn:name="Cluster",type="string",JSONPath=".spec.cluster.name"
312379
// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type"
380+
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
313381
// +kubebuilder:subresource:scale:specpath=.spec.instances,statuspath=.status.instances
314382

315383
// Pooler is the Schema for the poolers API

api/v1/zz_generated.deepcopy.go

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/postgresql.cnpg.io_clusterimagecatalogs.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,35 @@ spec:
4646
Specification of the desired behavior of the ClusterImageCatalog.
4747
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
4848
properties:
49+
componentImages:
50+
description: |-
51+
ComponentImages is a list of named images for components other than PostgreSQL
52+
(e.g. pgbouncer). Keys must be unique within a catalog.
53+
items:
54+
description: CatalogComponentImage is a named image entry for a
55+
non-PostgreSQL component.
56+
properties:
57+
image:
58+
description: Image is the container image reference.
59+
type: string
60+
key:
61+
description: Key is the unique identifier for this image within
62+
the catalog.
63+
maxLength: 63
64+
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
65+
type: string
66+
required:
67+
- image
68+
- key
69+
type: object
70+
maxItems: 32
71+
type: array
72+
x-kubernetes-list-map-keys:
73+
- key
74+
x-kubernetes-list-type: map
75+
x-kubernetes-validations:
76+
- message: Component image keys must be unique
77+
rule: self.all(e, self.filter(f, f.key==e.key).size() == 1)
4978
images:
5079
description: List of CatalogImages available in the catalog
5180
items:

0 commit comments

Comments
 (0)