Fix/logical backup job cleanup (#3111)

* feat(logical-backup): add configurable job history limits and TTL

Adds three new configuration options for logical backup cronjobs:
- logical_backup_successful_jobs_history_limit (default: 3)
- logical_backup_failed_jobs_history_limit (default: 3)
- logical_backup_ttl_seconds_after_finished (default: 86400)

These options control how many completed/failed backup jobs are
retained by Kubernetes and when finished jobs are automatically
deleted. This prevents accumulation of old backup jobs and pods
in namespaces with many PostgreSQL clusters.

Also updates the CronJob comparison logic to detect changes in
these new fields and trigger reconciliation when needed.

Closes zalando/postgres-operator#1092

* add added the 3 new fieldson crd

* updated gen api

---------

Co-authored-by: Jairo Llopis <jairo@moduon.team>
Co-authored-by: Felix Kunde <felix-kunde@gmx.de>
This commit is contained in:
Jociele Padilha
2026-06-19 12:12:58 +02:00
committed by GitHub
co-authored by Jairo Llopis Felix Kunde
parent 49cde600b8
commit e4e686588e
13 changed files with 148 additions and 6 deletions
+9
View File
@@ -899,6 +899,15 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"logical_backup_cronjob_environment_secret": {
Type: "string",
},
"logical_backup_successful_jobs_history_limit": {
Type: "integer",
},
"logical_backup_failed_jobs_history_limit": {
Type: "integer",
},
"logical_backup_ttl_seconds_after_finished": {
Type: "integer",
},
},
},
"debug": {
@@ -252,6 +252,9 @@ type OperatorLogicalBackupConfiguration struct {
MemoryRequest string `json:"logical_backup_memory_request,omitempty"`
CPULimit string `json:"logical_backup_cpu_limit,omitempty"`
MemoryLimit string `json:"logical_backup_memory_limit,omitempty"`
SuccessfulJobsHistoryLimit *int32 `json:"logical_backup_successful_jobs_history_limit,omitempty"`
FailedJobsHistoryLimit *int32 `json:"logical_backup_failed_jobs_history_limit,omitempty"`
TTLSecondsAfterFinished *int32 `json:"logical_backup_ttl_seconds_after_finished,omitempty"`
}
// PatroniConfiguration defines configuration for Patroni
@@ -480,7 +480,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
in.TeamsAPI.DeepCopyInto(&out.TeamsAPI)
out.LoggingRESTAPI = in.LoggingRESTAPI
out.Scalyr = in.Scalyr
out.LogicalBackup = in.LogicalBackup
in.LogicalBackup.DeepCopyInto(&out.LogicalBackup)
in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler)
in.Patroni.DeepCopyInto(&out.Patroni)
return
@@ -558,6 +558,21 @@ func (in *OperatorDebugConfiguration) DeepCopy() *OperatorDebugConfiguration {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OperatorLogicalBackupConfiguration) DeepCopyInto(out *OperatorLogicalBackupConfiguration) {
*out = *in
if in.SuccessfulJobsHistoryLimit != nil {
in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit
*out = new(int32)
**out = **in
}
if in.FailedJobsHistoryLimit != nil {
in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit
*out = new(int32)
**out = **in
}
if in.TTLSecondsAfterFinished != nil {
in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished
*out = new(int32)
**out = **in
}
return
}
+15
View File
@@ -919,6 +919,21 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog
reasons = append(reasons, fmt.Sprintf("logical backup container specs do not match: %v", strings.Join(contReasons, `', '`)))
}
if !reflect.DeepEqual(cur.Spec.SuccessfulJobsHistoryLimit, new.Spec.SuccessfulJobsHistoryLimit) {
match = false
reasons = append(reasons, fmt.Sprintf("new job's successfulJobsHistoryLimit %v does not match the current one %v", new.Spec.SuccessfulJobsHistoryLimit, cur.Spec.SuccessfulJobsHistoryLimit))
}
if !reflect.DeepEqual(cur.Spec.FailedJobsHistoryLimit, new.Spec.FailedJobsHistoryLimit) {
match = false
reasons = append(reasons, fmt.Sprintf("new job's failedJobsHistoryLimit %v does not match the current one %v", new.Spec.FailedJobsHistoryLimit, cur.Spec.FailedJobsHistoryLimit))
}
if !reflect.DeepEqual(cur.Spec.JobTemplate.Spec.TTLSecondsAfterFinished, new.Spec.JobTemplate.Spec.TTLSecondsAfterFinished) {
match = false
reasons = append(reasons, fmt.Sprintf("new job's TTLSecondsAfterFinished %v does not match the current one %v", new.Spec.JobTemplate.Spec.TTLSecondsAfterFinished, cur.Spec.JobTemplate.Spec.TTLSecondsAfterFinished))
}
return &compareLogicalBackupJobResult{match: match, reasons: reasons, deletedPodAnnotations: deletedPodAnnotations}
}
+10 -1
View File
@@ -1567,12 +1567,21 @@ func TestCompareServices(t *testing.T) {
}
}
var (
defaultSuccessfulJobsHistoryLimit = int32(3)
defaultFailedJobsHistoryLimit = int32(3)
defaultTTLSecondsAfterFinished = int32(86400)
)
func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMount) *batchv1.CronJob {
cron := &batchv1.CronJob{
Spec: batchv1.CronJobSpec{
Schedule: schedule,
Schedule: schedule,
SuccessfulJobsHistoryLimit: &defaultSuccessfulJobsHistoryLimit,
FailedJobsHistoryLimit: &defaultFailedJobsHistoryLimit,
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &defaultTTLSecondsAfterFinished,
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
+24 -4
View File
@@ -2452,7 +2452,13 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
// configure a batch job
jobSpec := batchv1.JobSpec{
Template: *podTemplate,
Template: *podTemplate,
TTLSecondsAfterFinished: c.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished,
}
if jobSpec.TTLSecondsAfterFinished == nil {
defaultTTL := int32(86400)
jobSpec.TTLSecondsAfterFinished = &defaultTTL
}
// configure a cron job
@@ -2470,6 +2476,18 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
schedule = c.OpConfig.LogicalBackupSchedule
}
successfulJobsHistoryLimit := c.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit
if successfulJobsHistoryLimit == nil {
defaultLimit := int32(3)
successfulJobsHistoryLimit = &defaultLimit
}
failedJobsHistoryLimit := c.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit
if failedJobsHistoryLimit == nil {
defaultLimit := int32(3)
failedJobsHistoryLimit = &defaultLimit
}
cronJob := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: c.getLogicalBackupJobName(),
@@ -2479,9 +2497,11 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
OwnerReferences: c.ownerReferences(),
},
Spec: batchv1.CronJobSpec{
Schedule: schedule,
JobTemplate: jobTemplateSpec,
ConcurrencyPolicy: batchv1.ForbidConcurrent,
Schedule: schedule,
JobTemplate: jobTemplateSpec,
ConcurrencyPolicy: batchv1.ForbidConcurrent,
SuccessfulJobsHistoryLimit: successfulJobsHistoryLimit,
FailedJobsHistoryLimit: failedJobsHistoryLimit,
},
}
+26
View File
@@ -4229,6 +4229,32 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
if !reflect.DeepEqual(tt.expectedResources, clusterResources) {
t.Errorf("%s - %s: expected resources %#v, got %#v", t.Name(), tt.subTest, tt.expectedResources, clusterResources)
}
expectedSuccessfulJobsHistoryLimit := int32(3)
if cluster.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit != nil {
expectedSuccessfulJobsHistoryLimit = *cluster.OpConfig.LogicalBackup.LogicalBackupSuccessfulJobsHistoryLimit
}
if *cronJob.Spec.SuccessfulJobsHistoryLimit != expectedSuccessfulJobsHistoryLimit {
t.Errorf("%s - %s: expected successfulJobsHistoryLimit %d, got %d", t.Name(), tt.subTest, expectedSuccessfulJobsHistoryLimit, *cronJob.Spec.SuccessfulJobsHistoryLimit)
}
expectedFailedJobsHistoryLimit := int32(3)
if cluster.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit != nil {
expectedFailedJobsHistoryLimit = *cluster.OpConfig.LogicalBackup.LogicalBackupFailedJobsHistoryLimit
}
if *cronJob.Spec.FailedJobsHistoryLimit != expectedFailedJobsHistoryLimit {
t.Errorf("%s - %s: expected failedJobsHistoryLimit %d, got %d", t.Name(), tt.subTest, expectedFailedJobsHistoryLimit, *cronJob.Spec.FailedJobsHistoryLimit)
}
expectedTTL := int32(86400)
if cluster.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished != nil {
expectedTTL = *cluster.OpConfig.LogicalBackup.LogicalBackupTTLSecondsAfterFinished
}
if cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished == nil {
t.Errorf("%s - %s: expected TTLSecondsAfterFinished to be set", t.Name(), tt.subTest)
} else if *cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished != expectedTTL {
t.Errorf("%s - %s: expected TTLSecondsAfterFinished %d, got %d", t.Name(), tt.subTest, expectedTTL, *cronJob.Spec.JobTemplate.Spec.TTLSecondsAfterFinished)
}
}
}
+3
View File
@@ -217,6 +217,9 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.LogicalBackupMemoryRequest = fromCRD.LogicalBackup.MemoryRequest
result.LogicalBackupCPULimit = fromCRD.LogicalBackup.CPULimit
result.LogicalBackupMemoryLimit = fromCRD.LogicalBackup.MemoryLimit
result.LogicalBackupSuccessfulJobsHistoryLimit = util.CoalesceInt32(fromCRD.LogicalBackup.SuccessfulJobsHistoryLimit, k8sutil.Int32ToPointer(3))
result.LogicalBackupFailedJobsHistoryLimit = util.CoalesceInt32(fromCRD.LogicalBackup.FailedJobsHistoryLimit, k8sutil.Int32ToPointer(3))
result.LogicalBackupTTLSecondsAfterFinished = fromCRD.LogicalBackup.TTLSecondsAfterFinished
// debug config
result.DebugLogging = *util.CoalesceBool(fromCRD.OperatorDebug.DebugLogging, util.True())
+3
View File
@@ -149,6 +149,9 @@ type LogicalBackup struct {
LogicalBackupMemoryRequest string `name:"logical_backup_memory_request"`
LogicalBackupCPULimit string `name:"logical_backup_cpu_limit"`
LogicalBackupMemoryLimit string `name:"logical_backup_memory_limit"`
LogicalBackupSuccessfulJobsHistoryLimit *int32 `name:"logical_backup_successful_jobs_history_limit" default:"3"`
LogicalBackupFailedJobsHistoryLimit *int32 `name:"logical_backup_failed_jobs_history_limit" default:"3"`
LogicalBackupTTLSecondsAfterFinished *int32 `name:"logical_backup_ttl_seconds_after_finished" default:"86400"`
}
// Operator options for connection pooler