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
parent 49cde600b8
commit e4e686588e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 148 additions and 6 deletions

View File

@ -726,6 +726,18 @@ spec:
default: "30 00 * * *" default: "30 00 * * *"
logical_backup_cronjob_environment_secret: logical_backup_cronjob_environment_secret:
type: string type: string
logical_backup_failed_jobs_history_limit:
type: integer
minimum: 0
default: 3
logical_backup_successful_jobs_history_limit:
type: integer
minimum: 0
default: 3
logical_backup_ttl_seconds_after_finished:
type: integer
minimum: 0
default: 86400
debug: debug:
type: object type: object
properties: properties:

View File

@ -415,6 +415,12 @@ configLogicalBackup:
logical_backup_schedule: "30 00 * * *" logical_backup_schedule: "30 00 * * *"
# secret to be used as reference for env variables in cronjob # secret to be used as reference for env variables in cronjob
logical_backup_cronjob_environment_secret: "" logical_backup_cronjob_environment_secret: ""
# number of successful backup jobs to keep in cronjob history
logical_backup_successful_jobs_history_limit: 3
# number of failed backup jobs to keep in cronjob history
logical_backup_failed_jobs_history_limit: 3
# TTL in seconds after which finished backup jobs are automatically deleted
logical_backup_ttl_seconds_after_finished: 86400
# automate creation of human users with teams API service # automate creation of human users with teams API service
configTeamsApi: configTeamsApi:

View File

@ -904,6 +904,15 @@ grouped under the `logical_backup` key.
* **logical_backup_cronjob_environment_secret** * **logical_backup_cronjob_environment_secret**
Reference to a Kubernetes secret, which keys will be added as environment variables to the cronjob. Default: "" Reference to a Kubernetes secret, which keys will be added as environment variables to the cronjob. Default: ""
* **logical_backup_successful_jobs_history_limit**
number of successful backup jobs to keep in cronjob history. The default is `3`.
* **logical_backup_failed_jobs_history_limit**
number of failed backup jobs to keep in cronjob history. The default is `3`.
* **logical_backup_ttl_seconds_after_finished**
TTL in seconds after which finished backup jobs are automatically deleted. The default is `86400`.
The following environment variables can be passed to the logical backup The following environment variables can be passed to the logical backup
cronjob via `logical_backup_cronjob_environment_secret` to control cronjob via `logical_backup_cronjob_environment_secret` to control
connectivity checks before the backup starts: connectivity checks before the backup starts:

View File

@ -712,6 +712,18 @@ spec:
default: "30 00 * * *" default: "30 00 * * *"
logical_backup_cronjob_environment_secret: logical_backup_cronjob_environment_secret:
type: string type: string
logical_backup_failed_jobs_history_limit:
type: integer
minimum: 0
default: 3
logical_backup_successful_jobs_history_limit:
type: integer
minimum: 0
default: 3
logical_backup_ttl_seconds_after_finished:
type: integer
minimum: 0
default: 86400
debug: debug:
type: object type: object
properties: properties:

View File

@ -899,6 +899,15 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"logical_backup_cronjob_environment_secret": { "logical_backup_cronjob_environment_secret": {
Type: "string", 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": { "debug": {

View File

@ -252,6 +252,9 @@ type OperatorLogicalBackupConfiguration struct {
MemoryRequest string `json:"logical_backup_memory_request,omitempty"` MemoryRequest string `json:"logical_backup_memory_request,omitempty"`
CPULimit string `json:"logical_backup_cpu_limit,omitempty"` CPULimit string `json:"logical_backup_cpu_limit,omitempty"`
MemoryLimit string `json:"logical_backup_memory_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 // PatroniConfiguration defines configuration for Patroni

View File

@ -480,7 +480,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
in.TeamsAPI.DeepCopyInto(&out.TeamsAPI) in.TeamsAPI.DeepCopyInto(&out.TeamsAPI)
out.LoggingRESTAPI = in.LoggingRESTAPI out.LoggingRESTAPI = in.LoggingRESTAPI
out.Scalyr = in.Scalyr out.Scalyr = in.Scalyr
out.LogicalBackup = in.LogicalBackup in.LogicalBackup.DeepCopyInto(&out.LogicalBackup)
in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler) in.ConnectionPooler.DeepCopyInto(&out.ConnectionPooler)
in.Patroni.DeepCopyInto(&out.Patroni) in.Patroni.DeepCopyInto(&out.Patroni)
return 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OperatorLogicalBackupConfiguration) DeepCopyInto(out *OperatorLogicalBackupConfiguration) { func (in *OperatorLogicalBackupConfiguration) DeepCopyInto(out *OperatorLogicalBackupConfiguration) {
*out = *in *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 return
} }

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, `', '`))) 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} return &compareLogicalBackupJobResult{match: match, reasons: reasons, deletedPodAnnotations: deletedPodAnnotations}
} }

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 { func newCronJob(image, schedule string, vars []v1.EnvVar, mounts []v1.VolumeMount) *batchv1.CronJob {
cron := &batchv1.CronJob{ cron := &batchv1.CronJob{
Spec: batchv1.CronJobSpec{ Spec: batchv1.CronJobSpec{
Schedule: schedule, Schedule: schedule,
SuccessfulJobsHistoryLimit: &defaultSuccessfulJobsHistoryLimit,
FailedJobsHistoryLimit: &defaultFailedJobsHistoryLimit,
JobTemplate: batchv1.JobTemplateSpec{ JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{ Spec: batchv1.JobSpec{
TTLSecondsAfterFinished: &defaultTTLSecondsAfterFinished,
Template: v1.PodTemplateSpec{ Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{ Spec: v1.PodSpec{
Containers: []v1.Container{ Containers: []v1.Container{

View File

@ -2452,7 +2452,13 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
// configure a batch job // configure a batch job
jobSpec := batchv1.JobSpec{ 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 // configure a cron job
@ -2470,6 +2476,18 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
schedule = c.OpConfig.LogicalBackupSchedule 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{ cronJob := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: c.getLogicalBackupJobName(), Name: c.getLogicalBackupJobName(),
@ -2479,9 +2497,11 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
OwnerReferences: c.ownerReferences(), OwnerReferences: c.ownerReferences(),
}, },
Spec: batchv1.CronJobSpec{ Spec: batchv1.CronJobSpec{
Schedule: schedule, Schedule: schedule,
JobTemplate: jobTemplateSpec, JobTemplate: jobTemplateSpec,
ConcurrencyPolicy: batchv1.ForbidConcurrent, ConcurrencyPolicy: batchv1.ForbidConcurrent,
SuccessfulJobsHistoryLimit: successfulJobsHistoryLimit,
FailedJobsHistoryLimit: failedJobsHistoryLimit,
}, },
} }

View File

@ -4229,6 +4229,32 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
if !reflect.DeepEqual(tt.expectedResources, clusterResources) { if !reflect.DeepEqual(tt.expectedResources, clusterResources) {
t.Errorf("%s - %s: expected resources %#v, got %#v", t.Name(), tt.subTest, 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)
}
} }
} }

View File

@ -217,6 +217,9 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.LogicalBackupMemoryRequest = fromCRD.LogicalBackup.MemoryRequest result.LogicalBackupMemoryRequest = fromCRD.LogicalBackup.MemoryRequest
result.LogicalBackupCPULimit = fromCRD.LogicalBackup.CPULimit result.LogicalBackupCPULimit = fromCRD.LogicalBackup.CPULimit
result.LogicalBackupMemoryLimit = fromCRD.LogicalBackup.MemoryLimit 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 // debug config
result.DebugLogging = *util.CoalesceBool(fromCRD.OperatorDebug.DebugLogging, util.True()) result.DebugLogging = *util.CoalesceBool(fromCRD.OperatorDebug.DebugLogging, util.True())

View File

@ -149,6 +149,9 @@ type LogicalBackup struct {
LogicalBackupMemoryRequest string `name:"logical_backup_memory_request"` LogicalBackupMemoryRequest string `name:"logical_backup_memory_request"`
LogicalBackupCPULimit string `name:"logical_backup_cpu_limit"` LogicalBackupCPULimit string `name:"logical_backup_cpu_limit"`
LogicalBackupMemoryLimit string `name:"logical_backup_memory_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 // Operator options for connection pooler