diff --git a/internal/cnpgi/operator/config/config.go b/internal/cnpgi/operator/config/config.go index db9ca41d..1d3c7037 100644 --- a/internal/cnpgi/operator/config/config.go +++ b/internal/cnpgi/operator/config/config.go @@ -20,6 +20,7 @@ SPDX-License-Identifier: Apache-2.0 package config import ( + "cmp" "strings" cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" @@ -71,6 +72,10 @@ type PluginConfiguration struct { BarmanObjectName string ServerName string + // SidecarImage overrides the default sidecar image. It is the first set + // of: spec.plugins[], the recovery source, the replica source. + SidecarImage string + RecoveryBarmanObjectName string RecoveryServerName string @@ -186,6 +191,11 @@ func NewFromCluster(cluster *cnpgv1.Cluster) *PluginConfiguration { // used for the backup/archive BarmanObjectName: helper.Parameters["barmanObjectName"], ServerName: serverName, + SidecarImage: cmp.Or( + helper.Parameters["sidecarImage"], + getRecoveryParameters(cluster)["sidecarImage"], + getReplicaSourceParameters(cluster)["sidecarImage"], + ), // used for restore and wal_restore during backup recovery RecoveryServerName: recoveryServerName, RecoveryBarmanObjectName: recoveryBarmanObjectName, diff --git a/internal/cnpgi/operator/config/config_test.go b/internal/cnpgi/operator/config/config_test.go index aaec9fa0..f0be2b2e 100644 --- a/internal/cnpgi/operator/config/config_test.go +++ b/internal/cnpgi/operator/config/config_test.go @@ -124,3 +124,73 @@ var _ = Describe("NewFromCluster", func() { Expect(cfg.Validate()).NotTo(Succeed()) }) }) + +var _ = Describe("sidecar image resolution", func() { + newCluster := func(pluginImage, recoveryImage string) *cnpgv1.Cluster { + cluster := &cnpgv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "test-ns"}, + Spec: cnpgv1.ClusterSpec{ + Bootstrap: &cnpgv1.BootstrapConfiguration{ + Recovery: &cnpgv1.BootstrapRecovery{Source: "source"}, + }, + ExternalClusters: []cnpgv1.ExternalCluster{{ + Name: "source", + PluginConfiguration: &cnpgv1.PluginConfiguration{ + Name: metadata.PluginName, + Parameters: map[string]string{ + "barmanObjectName": "object-store", + "sidecarImage": recoveryImage, + }, + }, + }}, + }, + } + if pluginImage != "" { + cluster.Spec.Plugins = []cnpgv1.PluginConfiguration{{ + Name: metadata.PluginName, + Parameters: map[string]string{"sidecarImage": pluginImage}, + }} + } + return cluster + } + + It("is empty when no image is requested", func() { + cfg := NewFromCluster(newCluster("", "")) + Expect(cfg.SidecarImage).To(BeEmpty()) + }) + + It("uses the spec.plugins[] image", func() { + cfg := NewFromCluster(newCluster("plugin:1", "")) + Expect(cfg.SidecarImage).To(Equal("plugin:1")) + }) + + It("falls back to the recovery source image", func() { + cfg := NewFromCluster(newCluster("", "recovery:1")) + Expect(cfg.SidecarImage).To(Equal("recovery:1")) + }) + + It("prefers the spec.plugins[] image over the recovery source image", func() { + cfg := NewFromCluster(newCluster("plugin:1", "recovery:1")) + Expect(cfg.SidecarImage).To(Equal("plugin:1")) + }) + + It("falls back to the replica source image", func() { + cluster := &cnpgv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "test-ns"}, + Spec: cnpgv1.ClusterSpec{ + ReplicaCluster: &cnpgv1.ReplicaClusterConfiguration{Source: "source"}, + ExternalClusters: []cnpgv1.ExternalCluster{{ + Name: "source", + PluginConfiguration: &cnpgv1.PluginConfiguration{ + Name: metadata.PluginName, + Parameters: map[string]string{ + "barmanObjectName": "object-store", + "sidecarImage": "replica:1", + }, + }, + }}, + }, + } + Expect(NewFromCluster(cluster).SidecarImage).To(Equal("replica:1")) + }) +}) diff --git a/internal/cnpgi/operator/lifecycle.go b/internal/cnpgi/operator/lifecycle.go index 18730fd1..3b01fcc2 100644 --- a/internal/cnpgi/operator/lifecycle.go +++ b/internal/cnpgi/operator/lifecycle.go @@ -20,6 +20,7 @@ SPDX-License-Identifier: Apache-2.0 package operator import ( + "cmp" "context" "errors" "fmt" @@ -151,6 +152,7 @@ func (impl LifecycleImplementation) reconcileJob( } return reconcileJob(ctx, cluster, request, sidecarConfiguration{ + image: cmp.Or(pluginConfiguration.SidecarImage, viper.GetString("sidecar-image")), env: env, certificates: certificates, resources: resources, @@ -158,6 +160,7 @@ func (impl LifecycleImplementation) reconcileJob( } type sidecarConfiguration struct { + image string env []corev1.EnvVar certificates []corev1.VolumeProjection resources corev1.ResourceRequirements @@ -249,6 +252,7 @@ func (impl LifecycleImplementation) reconcilePod( } return reconcileInstancePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{ + image: cmp.Or(pluginConfiguration.SidecarImage, viper.GetString("sidecar-image")), env: env, certificates: certificates, resources: resources, @@ -450,7 +454,7 @@ func reconcilePodSpec( // fixed values sidecarTemplate.Name = "plugin-barman-cloud" - sidecarTemplate.Image = viper.GetString("sidecar-image") + sidecarTemplate.Image = config.image sidecarTemplate.ImagePullPolicy = cluster.Spec.ImagePullPolicy sidecarTemplate.StartupProbe = baseProbe.DeepCopy() sidecarTemplate.SecurityContext = &corev1.SecurityContext{ diff --git a/internal/cnpgi/operator/lifecycle_test.go b/internal/cnpgi/operator/lifecycle_test.go index 76d658ad..02bf8146 100644 --- a/internal/cnpgi/operator/lifecycle_test.go +++ b/internal/cnpgi/operator/lifecycle_test.go @@ -157,6 +157,25 @@ var _ = Describe("LifecycleImplementation", func() { Expect(response.JsonPatch).NotTo(BeEmpty()) }) + It("uses the requested sidecar image", func(ctx SpecContext) { + job := &batchv1.Job{ + TypeMeta: jobTypeMeta, + ObjectMeta: metav1.ObjectMeta{Name: "test-job", Labels: map[string]string{}}, + Spec: batchv1.JobSpec{Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{utils.JobRoleLabelName: fullRecoveryJobName}, + }, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: fullRecoveryJobName}}}, + }}, + } + jobJSON, _ := json.Marshal(job) + request := &lifecycle.OperatorLifecycleRequest{ObjectDefinition: jobJSON} + + response, err := reconcileJob(ctx, cluster, request, sidecarConfiguration{image: "custom:1"}) + Expect(err).NotTo(HaveOccurred()) + Expect(string(response.JsonPatch)).To(ContainSubstring(`"image":"custom:1"`)) + }) + It("skips non-recovery jobs", func(ctx SpecContext) { job := &batchv1.Job{ TypeMeta: jobTypeMeta, @@ -242,6 +261,21 @@ var _ = Describe("LifecycleImplementation", func() { HaveKey("value"))) }) + It("uses the requested sidecar image", func(ctx SpecContext) { + pod := &corev1.Pod{ + TypeMeta: podTypeMeta, + ObjectMeta: metav1.ObjectMeta{Name: "test-pod"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "postgres"}}}, + } + podJSON, _ := json.Marshal(pod) + request := &lifecycle.OperatorLifecycleRequest{ObjectDefinition: podJSON} + + response, err := reconcileInstancePod( + ctx, cluster, request, pluginConfiguration, sidecarConfiguration{image: "custom:1"}) + Expect(err).NotTo(HaveOccurred()) + Expect(string(response.JsonPatch)).To(ContainSubstring(`"image":"custom:1"`)) + }) + It("injects the sidecar for a recovery-only cluster", func(ctx SpecContext) { recoveryOnlyConfig := &config.PluginConfiguration{ RecoveryBarmanObjectName: "object-store-recovery", diff --git a/web/docs/images.md b/web/docs/images.md index f6c32d34..652b51ba 100644 --- a/web/docs/images.md +++ b/web/docs/images.md @@ -35,3 +35,12 @@ built from the These sidecar images are designed to work seamlessly with the [`minimal` PostgreSQL container images](https://github.com/cloudnative-pg/postgres-containers?tab=readme-ov-file#minimal-images) maintained by the CloudNativePG Community. + +The image can be overridden per cluster with the +[`sidecarImage` parameter](parameters.md). + +:::warning +This override is meant for debugging and testing. Running a sidecar whose +version differs from the operator's is not recommended. Leave it unset in +production. +::: diff --git a/web/docs/parameters.md b/web/docs/parameters.md index ca0cd2ba..759e121c 100644 --- a/web/docs/parameters.md +++ b/web/docs/parameters.md @@ -11,6 +11,15 @@ The following parameters are available for the Barman Cloud Plugin: - `barmanObjectName`: references the `ObjectStore` resource to be used by the plugin. - `serverName`: Specifies the server name in the object store. +- `sidecarImage`: overrides the [sidecar container image](images.md#sidecar-container-image) + injected into the instance pods and the recovery job pods of the cluster. + It can also be set on a recovery or replica source in `externalClusters[]`. + +:::warning +`sidecarImage` is meant for debugging and testing. Running a sidecar whose +version differs from the operator's is not recommended. Leave it unset in +production. +::: :::important The `serverName` parameter in the `ObjectStore` resource is retained solely for