Skip to content

K8SPG-832: add .spec.backups.pgbackrest.repoHost.sidecars field #1235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

1,458 changes: 1,458 additions & 0 deletions build/crd/percona/generated/pgv2.percona.com_perconapgclusters.yaml

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions cmd/postgres-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ func main() {

features := feature.NewGate()
err = features.SetFromMap(map[string]bool{
string(feature.InstanceSidecars): true, // needed for PMM
string(feature.PGBouncerSidecars): true, // K8SPG-645
string(feature.TablespaceVolumes): true,
feature.InstanceSidecars: true, // needed for PMM
feature.PGBouncerSidecars: true, // K8SPG-645
feature.PGBackrestRepoHostSidecars: true, // K8SPG-832
feature.TablespaceVolumes: true,
})
assertNoError(err)

Expand Down
1,458 changes: 1,458 additions & 0 deletions config/crd/bases/pgv2.percona.com_perconapgclusters.yaml

Large diffs are not rendered by default.

1,458 changes: 1,458 additions & 0 deletions config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml

Large diffs are not rendered by default.

10,048 changes: 6,482 additions & 3,566 deletions deploy/bundle.yaml

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions deploy/cr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,13 @@ spec:
# repo3-path: /pgbackrest/postgres-operator/cluster1-multi-repo/repo3
# repo4-path: /pgbackrest/postgres-operator/cluster1-multi-repo/repo4
repoHost:
# sidecars:
# - name: testcontainer
# image: busybox:latest
# command: ["sleep", "30d"]
# securityContext: {}
# - name: testcontainer2
# image: mycontainer1:latest
# resources:
# limits:
# cpu: 200m
Expand Down
10,048 changes: 6,482 additions & 3,566 deletions deploy/crd.yaml

Large diffs are not rendered by default.

10,048 changes: 6,482 additions & 3,566 deletions deploy/cw-bundle.yaml

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion internal/controller/postgrescluster/pgbackrest.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,15 @@ func (r *Reconciler) generateRepoHostIntent(ctx context.Context, postgresCluster

// K8SPG-435
resources := corev1.ResourceRequirements{}
if postgresCluster.Spec.Backups.PGBackRest.RepoHost != nil {
if repoHost := postgresCluster.Spec.Backups.PGBackRest.RepoHost; repoHost != nil {
resources = postgresCluster.Spec.Backups.PGBackRest.RepoHost.Resources

// K8SPG-832
// If the PGBackrestRepoHostSidecars feature gate is enabled and instance sidecars are
// defined, add the defined container to the Pod.
if feature.Enabled(ctx, feature.PGBackrestRepoHostSidecars) && repoHost.Sidecars != nil {
repo.Spec.Template.Spec.Containers = append(repo.Spec.Template.Spec.Containers, repoHost.Sidecars...)
}
}
sizeLimit := getTMPSizeLimit(repo.Labels[naming.LabelVersion], resources)

Expand Down
22 changes: 13 additions & 9 deletions internal/feature/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ const (
// Support custom sidecars for pgBouncer Pods
PGBouncerSidecars = "PGBouncerSidecars"

// K8SPG-832: Support custom sidecars for pgbackrest repo-host Pods
PGBackrestRepoHostSidecars = "PGBackrestSidecars"

// Adjust PGUpgrade parallelism according to CPU resources
PGUpgradeCPUConcurrency = "PGUpgradeCPUConcurrency"

Expand All @@ -98,15 +101,16 @@ func NewGate() MutableGate {
gate := featuregate.NewFeatureGate()

if err := gate.Add(map[Feature]featuregate.FeatureSpec{
AppendCustomQueries: {Default: false, PreRelease: featuregate.Alpha},
AutoCreateUserSchema: {Default: true, PreRelease: featuregate.Beta},
AutoGrowVolumes: {Default: false, PreRelease: featuregate.Alpha},
BridgeIdentifiers: {Default: false, PreRelease: featuregate.Alpha},
InstanceSidecars: {Default: false, PreRelease: featuregate.Alpha},
PGBouncerSidecars: {Default: false, PreRelease: featuregate.Alpha},
PGUpgradeCPUConcurrency: {Default: false, PreRelease: featuregate.Alpha},
TablespaceVolumes: {Default: false, PreRelease: featuregate.Alpha},
VolumeSnapshots: {Default: false, PreRelease: featuregate.Alpha},
AppendCustomQueries: {Default: false, PreRelease: featuregate.Alpha},
AutoCreateUserSchema: {Default: true, PreRelease: featuregate.Beta},
AutoGrowVolumes: {Default: false, PreRelease: featuregate.Alpha},
BridgeIdentifiers: {Default: false, PreRelease: featuregate.Alpha},
InstanceSidecars: {Default: false, PreRelease: featuregate.Alpha},
PGBouncerSidecars: {Default: false, PreRelease: featuregate.Alpha},
PGBackrestRepoHostSidecars: {Default: false, PreRelease: featuregate.Alpha},
PGUpgradeCPUConcurrency: {Default: false, PreRelease: featuregate.Alpha},
TablespaceVolumes: {Default: false, PreRelease: featuregate.Alpha},
VolumeSnapshots: {Default: false, PreRelease: featuregate.Alpha},
}); err != nil {
panic(err)
}
Expand Down
1 change: 1 addition & 0 deletions internal/feature/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestDefaults(t *testing.T) {
assert.Assert(t, false == gate.Enabled(BridgeIdentifiers))
assert.Assert(t, false == gate.Enabled(InstanceSidecars))
assert.Assert(t, false == gate.Enabled(PGBouncerSidecars))
assert.Assert(t, false == gate.Enabled(PGBackrestRepoHostSidecars))
assert.Assert(t, false == gate.Enabled(PGUpgradeCPUConcurrency))
assert.Assert(t, false == gate.Enabled(TablespaceVolumes))
assert.Assert(t, false == gate.Enabled(VolumeSnapshots))
Expand Down
282 changes: 282 additions & 0 deletions percona/controller/pgcluster/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,288 @@ var _ = Describe("Security context", Ordered, func() {
})
})

var _ = Describe("Sidecars", Ordered, func() {
gate := feature.NewGate()
err := gate.SetFromMap(map[string]bool{
feature.InstanceSidecars: true,
feature.PGBouncerSidecars: true,
feature.PGBackrestRepoHostSidecars: true,
})
Expect(err).NotTo(HaveOccurred())

ctx := feature.NewContext(context.Background(), gate)

const crName = "sidecars"
const ns = crName
crNamespacedName := types.NamespacedName{Name: crName, Namespace: ns}

namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: ns,
},
}

BeforeAll(func() {
By("Creating the Namespace to perform the tests")
err := k8sClient.Create(ctx, namespace)
Expect(err).To(Not(HaveOccurred()))
})

AfterAll(func() {
By("Deleting the Namespace to perform the tests")
_ = k8sClient.Delete(ctx, namespace)
})

cr, err := readDefaultCR(crName, ns)
It("should read defautl cr.yaml", func() {
Expect(err).NotTo(HaveOccurred())
})

It("should create PerconaPGCluster", func() {
for i := range cr.Spec.InstanceSets {
i := &cr.Spec.InstanceSets[i]
i.Sidecars = []corev1.Container{
{
Name: "instance-sidecar",
Command: []string{"instance-cmd"},
Image: "instance-image",
},
}
}
cr.Spec.Proxy.PGBouncer.Sidecars = []corev1.Container{
{
Name: "pgbouncer-sidecar",
Command: []string{"pgbouncer-cmd"},
Image: "pgbouncer-image",
},
}
cr.Spec.Backups.PGBackRest.RepoHost.Sidecars = []corev1.Container{
{
Name: "repohost-sidecar",
Command: []string{"repohost-cmd"},
Image: "repohost-image",
},
}
status := cr.Status
Expect(k8sClient.Create(ctx, cr)).Should(Succeed())
cr.Status = status
Expect(k8sClient.Status().Update(ctx, cr)).Should(Succeed())
})

It("should reconcile", func() {
_, err := reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName})
Expect(err).NotTo(HaveOccurred())
_, err = crunchyReconciler().Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName})
Expect(err).NotTo(HaveOccurred())
})

getContainer := func(containers []corev1.Container, name string) *corev1.Container {
for _, c := range containers {
if c.Name == name {
return &c
}
}
return nil
}

It("Instances should have sidecar", func() {
stsList := &appsv1.StatefulSetList{}
labels := map[string]string{
"postgres-operator.crunchydata.com/data": "postgres",
"postgres-operator.crunchydata.com/cluster": crName,
}
err = k8sClient.List(ctx, stsList, client.InNamespace(cr.Namespace), client.MatchingLabels(labels))
Expect(err).NotTo(HaveOccurred())
Expect(stsList.Items).NotTo(BeEmpty())

for _, sts := range stsList.Items {
sidecar := getContainer(sts.Spec.Template.Spec.Containers, "instance-sidecar")
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Command).To(Equal([]string{"instance-cmd"}))
Expect(sidecar.Image).To(Equal("instance-image"))
}
})

It("PgBouncer should have sidecar", func() {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: crName + "-pgbouncer",
Namespace: cr.Namespace,
},
}
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment)
Expect(err).NotTo(HaveOccurred())
sidecar := getContainer(deployment.Spec.Template.Spec.Containers, "pgbouncer-sidecar")
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Command).To(Equal([]string{"pgbouncer-cmd"}))
Expect(sidecar.Image).To(Equal("pgbouncer-image"))
})

It("PgBackrest Repo should have sidecar", func() {
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: crName + "-repo-host",
Namespace: cr.Namespace,
},
}
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(sts), sts)
Expect(err).NotTo(HaveOccurred())
sidecar := getContainer(sts.Spec.Template.Spec.Containers, "repohost-sidecar")
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Command).To(Equal([]string{"repohost-cmd"}))
Expect(sidecar.Image).To(Equal("repohost-image"))
})

It("should update PerconaPGCluster with multiple sidecars", func() {
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cr), cr)).Should(Succeed())

for i := range cr.Spec.InstanceSets {
i := &cr.Spec.InstanceSets[i]
i.Sidecars = []corev1.Container{
{
Name: "instance-sidecar-2",
Command: []string{"instance-cmd-2"},
Image: "instance-image-2",
},
{
Name: "instance-sidecar",
Command: []string{"instance-cmd"},
Image: "instance-image",
},
}
}
cr.Spec.Proxy.PGBouncer.Sidecars = []corev1.Container{
{
Name: "pgbouncer-sidecar",
Command: []string{"pgbouncer-cmd"},
Image: "pgbouncer-image",
},
{
Name: "pgbouncer-sidecar-2",
Command: []string{"pgbouncer-cmd-2"},
Image: "pgbouncer-image-2",
},
{
Name: "pgbouncer-sidecar-3",
Command: []string{"pgbouncer-cmd-3"},
Image: "pgbouncer-image-3",
},
}
cr.Spec.Backups.PGBackRest.RepoHost.Sidecars = []corev1.Container{
{
Name: "repohost-sidecar-2",
Command: []string{"repohost-cmd-2"},
Image: "repohost-image-2",
},
{
Name: "repohost-sidecar",
Command: []string{"repohost-cmd"},
Image: "repohost-image",
},
{
Name: "repohost-sidecar-3",
Command: []string{"repohost-cmd-3"},
Image: "repohost-image-3",
},
}
Expect(k8sClient.Update(ctx, cr)).Should(Succeed())
})

It("should reconcile", func() {
_, err := reconciler(cr).Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName})
Expect(err).NotTo(HaveOccurred())
_, err = crunchyReconciler().Reconcile(ctx, ctrl.Request{NamespacedName: crNamespacedName})
Expect(err).NotTo(HaveOccurred())
})

It("Instances should have multiple sidecars", func() {
stsList := &appsv1.StatefulSetList{}
labels := map[string]string{
"postgres-operator.crunchydata.com/data": "postgres",
"postgres-operator.crunchydata.com/cluster": crName,
}
err = k8sClient.List(ctx, stsList, client.InNamespace(cr.Namespace), client.MatchingLabels(labels))
Expect(err).NotTo(HaveOccurred())
Expect(stsList.Items).NotTo(BeEmpty())

for _, sts := range stsList.Items {
l := len(sts.Spec.Template.Spec.Containers)
sidecar := sts.Spec.Template.Spec.Containers[l-4]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("instance-sidecar-2"))
Expect(sidecar.Command).To(Equal([]string{"instance-cmd-2"}))
Expect(sidecar.Image).To(Equal("instance-image-2"))

sidecar = sts.Spec.Template.Spec.Containers[l-3]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("instance-sidecar"))
Expect(sidecar.Command).To(Equal([]string{"instance-cmd"}))
Expect(sidecar.Image).To(Equal("instance-image"))
}
})

It("PgBouncer should have multiple sidecars", func() {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: crName + "-pgbouncer",
Namespace: cr.Namespace,
},
}
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment)
Expect(err).NotTo(HaveOccurred())

l := len(deployment.Spec.Template.Spec.Containers)
sidecar := deployment.Spec.Template.Spec.Containers[l-3]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("pgbouncer-sidecar"))
Expect(sidecar.Command).To(Equal([]string{"pgbouncer-cmd"}))
Expect(sidecar.Image).To(Equal("pgbouncer-image"))

sidecar = deployment.Spec.Template.Spec.Containers[l-2]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("pgbouncer-sidecar-2"))
Expect(sidecar.Command).To(Equal([]string{"pgbouncer-cmd-2"}))
Expect(sidecar.Image).To(Equal("pgbouncer-image-2"))

sidecar = deployment.Spec.Template.Spec.Containers[l-1]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("pgbouncer-sidecar-3"))
Expect(sidecar.Command).To(Equal([]string{"pgbouncer-cmd-3"}))
Expect(sidecar.Image).To(Equal("pgbouncer-image-3"))
})

It("PgBackrest Repo should have multiple sidecars", func() {
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: crName + "-repo-host",
Namespace: cr.Namespace,
},
}
err = k8sClient.Get(ctx, client.ObjectKeyFromObject(sts), sts)
Expect(err).NotTo(HaveOccurred())

l := len(sts.Spec.Template.Spec.Containers)
sidecar := sts.Spec.Template.Spec.Containers[l-3]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("repohost-sidecar-2"))
Expect(sidecar.Command).To(Equal([]string{"repohost-cmd-2"}))
Expect(sidecar.Image).To(Equal("repohost-image-2"))

sidecar = sts.Spec.Template.Spec.Containers[l-2]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("repohost-sidecar"))
Expect(sidecar.Command).To(Equal([]string{"repohost-cmd"}))
Expect(sidecar.Image).To(Equal("repohost-image"))

sidecar = sts.Spec.Template.Spec.Containers[l-1]
Expect(sidecar).NotTo(BeNil())
Expect(sidecar.Name).To(Equal("repohost-sidecar-3"))
Expect(sidecar.Command).To(Equal([]string{"repohost-cmd-3"}))
Expect(sidecar.Image).To(Equal("repohost-image-3"))
})
})

var _ = Describe("Operator-created sidecar container resources", Ordered, func() {
ctx := context.Background()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@ type PGBackRestRepoHost struct {
// SecurityContext defines the security settings for PGBackRest pod.
// +optional
SecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"`

// Custom sidecars for PostgreSQL instance pods. Changing this value causes
// PostgreSQL to restart.
// +optional
Sidecars []corev1.Container `json:"sidecars,omitempty"`
}

// PGBackRestRestore defines an in-place restore for the PostgresCluster.
Expand Down
Loading
Loading