Skip to content

Commit f5aaa08

Browse files
authored
Merge pull request #42 from osodevops/fix/issue-41-cronjob-updates
fix: reconcile KafkaBackup resources into scheduled CronJobs
2 parents ca9cd40 + d5db997 commit f5aaa08

6 files changed

Lines changed: 59 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## 0.2.14 - 2026-07-11
6+
7+
### Fixed
8+
9+
- Force server-side apply for operator-owned scheduled backup CronJobs so `KafkaBackup` changes such as `spec.resources` converge even when another field manager previously claimed parts of the generated pod template. This prevents apply conflicts from leaving the CronJob stale until it is deleted and the backup is reconciled again. Fixes [#41](https://github.com/osodevops/strimzi-backup-operator/issues/41).
10+
511
## 0.2.13 - 2026-07-10
612

713
### Fixed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kafka-backup-operator"
3-
version = "0.2.13"
3+
version = "0.2.14"
44
edition = "2021"
55
rust-version = "1.88"
66
license = "Apache-2.0"

deploy/helm/strimzi-backup-operator/Chart.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ apiVersion: v2
22
name: strimzi-backup-operator
33
description: Kubernetes operator for Kafka backup and restore, integrated with Strimzi
44
type: application
5-
version: 0.2.13
6-
appVersion: "0.2.13"
5+
version: 0.2.14
6+
appVersion: "0.2.14"
77
home: https://github.com/osodevops/strimzi-backup-operator
88
sources:
99
- https://github.com/osodevops/strimzi-backup-operator

src/reconcilers/backup.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,11 @@ async fn apply_resource<
646646
let patch = serde_json::to_value(resource).map_err(Error::Serialization)?;
647647
api.patch(
648648
name,
649-
&PatchParams::apply("kafka-backup-operator"),
649+
// This resource is fully generated and owned by the KafkaBackup. A
650+
// mutating policy or manual edit may otherwise take ownership of pod
651+
// template fields and make every later reconciliation fail with a
652+
// server-side apply conflict until the resource is deleted.
653+
&PatchParams::apply("kafka-backup-operator").force(),
650654
&Patch::Apply(patch),
651655
)
652656
.await?;

tests/integration/reconcile_backup_test.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::sync::{Arc, Mutex};
1+
use std::{
2+
collections::BTreeMap,
3+
sync::{Arc, Mutex},
4+
};
25

36
use http::{Request, Response};
47
use http_body_util::BodyExt;
@@ -16,6 +19,7 @@ use tower_test::mock;
1619
struct RecordedRequest {
1720
method: String,
1821
path: String,
22+
query: Option<String>,
1923
body: serde_json::Value,
2024
}
2125

@@ -77,6 +81,7 @@ async fn reconcile_with_mock_api(backup: KafkaBackup) -> Vec<RecordedRequest> {
7781
while let Some((request, send)) = handle.next_request().await {
7882
let method = request.method().to_string();
7983
let path = request.uri().path().to_string();
84+
let query = request.uri().query().map(str::to_string);
8085
let bytes = request.into_body().collect().await.unwrap().to_bytes();
8186
let body: serde_json::Value = if bytes.is_empty() {
8287
serde_json::Value::Null
@@ -117,10 +122,12 @@ async fn reconcile_with_mock_api(backup: KafkaBackup) -> Vec<RecordedRequest> {
117122
(200, body.clone())
118123
};
119124

120-
recorded
121-
.lock()
122-
.unwrap()
123-
.push(RecordedRequest { method, path, body });
125+
recorded.lock().unwrap().push(RecordedRequest {
126+
method,
127+
path,
128+
query,
129+
body,
130+
});
124131

125132
let response = Response::builder()
126133
.status(status)
@@ -189,3 +196,35 @@ async fn test_reconcile_applies_active_cronjob_when_not_suspended() {
189196
json!("BackupScheduled")
190197
);
191198
}
199+
200+
#[tokio::test]
201+
async fn test_reconcile_force_applies_resource_updates_to_owned_cronjob() {
202+
let mut backup = scheduled_backup(false);
203+
backup.spec.resources = Some(ResourceRequirementsSpec {
204+
requests: BTreeMap::from([
205+
("cpu".to_string(), "250m".to_string()),
206+
("memory".to_string(), "256Mi".to_string()),
207+
]),
208+
limits: BTreeMap::from([
209+
("cpu".to_string(), "1".to_string()),
210+
("memory".to_string(), "1Gi".to_string()),
211+
]),
212+
});
213+
214+
let requests = reconcile_with_mock_api(backup).await;
215+
let cronjob_patch = find_cronjob_patch(&requests);
216+
let resources = &cronjob_patch.body["spec"]["jobTemplate"]["spec"]["template"]["spec"]
217+
["containers"][0]["resources"];
218+
219+
assert_eq!(resources["requests"]["cpu"], json!("250m"));
220+
assert_eq!(resources["requests"]["memory"], json!("256Mi"));
221+
assert_eq!(resources["limits"]["cpu"], json!("1"));
222+
assert_eq!(resources["limits"]["memory"], json!("1Gi"));
223+
assert!(
224+
cronjob_patch
225+
.query
226+
.as_deref()
227+
.is_some_and(|query| query.split('&').any(|part| part == "force=true")),
228+
"controller-owned CronJob fields must be force-applied so a stale field manager cannot block spec updates"
229+
);
230+
}

0 commit comments

Comments
 (0)