|
| 1 | +use k8s_openapi::api::batch::v1::Job; |
| 2 | + |
| 3 | +/// Aggregate state of the Jobs spawned for a backup/restore CR. |
| 4 | +/// |
| 5 | +/// Derived from the full list of Jobs matching the CR's label selector so |
| 6 | +/// the reconciler can decide whether another Job may be created. A completed |
| 7 | +/// or failed Job must block re-creation just like a running one: backup and |
| 8 | +/// restore runs are one-shot, and pod-level retries are owned by the Job's |
| 9 | +/// `backoffLimit`, not the reconciler. |
| 10 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 11 | +pub enum JobsState { |
| 12 | + /// No Jobs exist for this resource. |
| 13 | + NoJobs, |
| 14 | + /// At least one Job is running or has not reached a terminal state yet. |
| 15 | + InProgress, |
| 16 | + /// A Job completed successfully. |
| 17 | + Succeeded { job_name: String }, |
| 18 | + /// A Job failed terminally (exhausted its backoffLimit). |
| 19 | + Failed { job_name: String }, |
| 20 | +} |
| 21 | + |
| 22 | +/// Classify the Jobs belonging to one CR into an aggregate state. |
| 23 | +/// |
| 24 | +/// Precedence: Succeeded > InProgress > Failed. A succeeded Job means the |
| 25 | +/// operation completed even if a stray duplicate is still running; a failed |
| 26 | +/// Job with a newer active run (manual re-trigger) reports running. |
| 27 | +pub fn classify_jobs(jobs: &[Job]) -> JobsState { |
| 28 | + if let Some(job) = jobs.iter().find(|j| job_succeeded(j)) { |
| 29 | + return JobsState::Succeeded { |
| 30 | + job_name: job.metadata.name.clone().unwrap_or_default(), |
| 31 | + }; |
| 32 | + } |
| 33 | + // Any non-terminal Job counts as in progress, including a just-created |
| 34 | + // Job whose status the Job controller has not populated yet. |
| 35 | + if jobs.iter().any(|j| !job_failed(j)) { |
| 36 | + return JobsState::InProgress; |
| 37 | + } |
| 38 | + if let Some(job) = jobs.iter().find(|j| job_failed(j)) { |
| 39 | + return JobsState::Failed { |
| 40 | + job_name: job.metadata.name.clone().unwrap_or_default(), |
| 41 | + }; |
| 42 | + } |
| 43 | + JobsState::NoJobs |
| 44 | +} |
| 45 | + |
| 46 | +/// A Job succeeded if it reports a `Complete=True` condition or any |
| 47 | +/// succeeded pods. |
| 48 | +pub fn job_succeeded(job: &Job) -> bool { |
| 49 | + let Some(status) = job.status.as_ref() else { |
| 50 | + return false; |
| 51 | + }; |
| 52 | + status.succeeded.unwrap_or(0) > 0 || has_condition(job, "Complete") |
| 53 | +} |
| 54 | + |
| 55 | +/// A Job failed terminally only when it reports a `Failed=True` condition |
| 56 | +/// (backoffLimit exhausted or deadline exceeded). `status.failed > 0` alone |
| 57 | +/// counts pod retries still owned by the Job controller and is not terminal. |
| 58 | +pub fn job_failed(job: &Job) -> bool { |
| 59 | + has_condition(job, "Failed") |
| 60 | +} |
| 61 | + |
| 62 | +fn has_condition(job: &Job, condition_type: &str) -> bool { |
| 63 | + job.status |
| 64 | + .as_ref() |
| 65 | + .and_then(|s| s.conditions.as_ref()) |
| 66 | + .is_some_and(|conds| { |
| 67 | + conds |
| 68 | + .iter() |
| 69 | + .any(|c| c.type_ == condition_type && c.status == "True") |
| 70 | + }) |
| 71 | +} |
| 72 | + |
| 73 | +/// Whether a restore Job may be created. Restores are strictly one-shot: |
| 74 | +/// any existing Job — running, succeeded, or failed — suppresses creation. |
| 75 | +pub fn should_create_restore_job(state: &JobsState) -> bool { |
| 76 | + matches!(state, JobsState::NoJobs) |
| 77 | +} |
| 78 | + |
| 79 | +/// Whether a one-shot backup Job may be created. A manual trigger |
| 80 | +/// (`kafkabackup.com/trigger=now`) requests a fresh run, so it bypasses the |
| 81 | +/// "a terminal Job already exists" gate — but never stacks onto an active Job. |
| 82 | +pub fn should_create_backup_job(state: &JobsState, manually_triggered: bool) -> bool { |
| 83 | + match state { |
| 84 | + JobsState::InProgress => false, |
| 85 | + JobsState::NoJobs => true, |
| 86 | + JobsState::Succeeded { .. } | JobsState::Failed { .. } => manually_triggered, |
| 87 | + } |
| 88 | +} |
0 commit comments