Skip to content

Commit 54ae549

Browse files
pmbrullclaude
andauthored
Fixes open-metadata#27852: propagate tolerations from CronOMJob to scheduled OMJob (open-metadata#27955)
CronOMJobReconciler.deepCopyPodSpec was copying nodeSelector but silently dropping tolerations when generating an OMJob from a CronOMJob template. Manual runs worked because they go straight through K8sPipelineClient.buildOMJob, but scheduled runs went through this deep-copy and lost the field, leaving pods Pending on tainted nodes. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 80375a7 commit 54ae549

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

openmetadata-k8s-operator/src/main/java/org/openmetadata/operator/controller/CronOMJobReconciler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ private OMJobSpec.OMJobPodSpec deepCopyPodSpec(
352352
copy.setResources(source.getResources());
353353
copy.setNodeSelector(
354354
source.getNodeSelector() != null ? new HashMap<>(source.getNodeSelector()) : null);
355+
copy.setTolerations(
356+
source.getTolerations() != null ? new ArrayList<>(source.getTolerations()) : null);
355357
copy.setSecurityContext(source.getSecurityContext());
356358
copy.setLabels(source.getLabels() != null ? new HashMap<>(source.getLabels()) : null);
357359
copy.setAnnotations(

openmetadata-k8s-operator/src/test/java/org/openmetadata/operator/unit/CronOMJobReconcilerTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,28 @@
1717
import static org.mockito.Mockito.*;
1818

1919
import io.fabric8.kubernetes.api.model.ObjectMeta;
20+
import io.fabric8.kubernetes.api.model.Toleration;
21+
import io.fabric8.kubernetes.api.model.TolerationBuilder;
2022
import io.fabric8.kubernetes.client.KubernetesClient;
2123
import io.fabric8.kubernetes.client.dsl.MixedOperation;
2224
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
2325
import io.fabric8.kubernetes.client.dsl.Resource;
2426
import io.javaoperatorsdk.operator.api.reconciler.Context;
2527
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
2628
import java.time.Instant;
29+
import java.util.List;
2730
import java.util.Map;
2831
import org.junit.jupiter.api.BeforeEach;
2932
import org.junit.jupiter.api.Test;
3033
import org.junit.jupiter.api.extension.ExtendWith;
34+
import org.mockito.ArgumentCaptor;
3135
import org.mockito.Mock;
3236
import org.mockito.junit.jupiter.MockitoExtension;
3337
import org.openmetadata.operator.controller.CronOMJobReconciler;
3438
import org.openmetadata.operator.model.CronOMJobResource;
3539
import org.openmetadata.operator.model.CronOMJobSpec;
3640
import org.openmetadata.operator.model.CronOMJobStatus;
41+
import org.openmetadata.operator.model.OMJobResource;
3742
import org.openmetadata.operator.model.OMJobSpec;
3843

3944
@ExtendWith(MockitoExtension.class)
@@ -158,6 +163,79 @@ void testReconcileWithNullSpec() {
158163
assertEquals("Missing schedule", cronOMJob.getStatus().getMessage());
159164
}
160165

166+
@Test
167+
@SuppressWarnings("unchecked")
168+
void testReconcilePropagatesTolerationsFromCronOMJobToScheduledOMJob() {
169+
List<Toleration> tolerations =
170+
List.of(
171+
new TolerationBuilder()
172+
.withKey("dedicated")
173+
.withOperator("Equal")
174+
.withValue("ingestion")
175+
.withEffect("NoSchedule")
176+
.build(),
177+
new TolerationBuilder()
178+
.withKey("gpu")
179+
.withOperator("Exists")
180+
.withEffect("NoExecute")
181+
.withTolerationSeconds(300L)
182+
.build());
183+
184+
cronOMJob.getSpec().getOmJobSpec().getMainPodSpec().setTolerations(tolerations);
185+
cronOMJob.getSpec().getOmJobSpec().getExitHandlerSpec().setTolerations(tolerations);
186+
187+
cronOMJob.getSpec().setSchedule("* * * * *");
188+
cronOMJob.getSpec().setStartingDeadlineSeconds(86400);
189+
190+
reconciler.reconcile(cronOMJob, context);
191+
192+
ArgumentCaptor<OMJobResource> captor = ArgumentCaptor.forClass(OMJobResource.class);
193+
verify(mixedOp).resource(captor.capture());
194+
OMJobResource scheduled = captor.getValue();
195+
196+
assertNotNull(scheduled.getSpec(), "Scheduled OMJob spec must not be null");
197+
assertNotNull(
198+
scheduled.getSpec().getMainPodSpec().getTolerations(),
199+
"Main pod tolerations must be propagated from CronOMJob template");
200+
assertEquals(2, scheduled.getSpec().getMainPodSpec().getTolerations().size());
201+
assertEquals(
202+
"dedicated", scheduled.getSpec().getMainPodSpec().getTolerations().get(0).getKey());
203+
assertEquals(
204+
"NoSchedule", scheduled.getSpec().getMainPodSpec().getTolerations().get(0).getEffect());
205+
assertEquals("gpu", scheduled.getSpec().getMainPodSpec().getTolerations().get(1).getKey());
206+
assertEquals(
207+
Long.valueOf(300L),
208+
scheduled.getSpec().getMainPodSpec().getTolerations().get(1).getTolerationSeconds());
209+
210+
assertNotNull(
211+
scheduled.getSpec().getExitHandlerSpec().getTolerations(),
212+
"Exit handler tolerations must be propagated from CronOMJob template");
213+
assertEquals(2, scheduled.getSpec().getExitHandlerSpec().getTolerations().size());
214+
}
215+
216+
@Test
217+
@SuppressWarnings("unchecked")
218+
void testReconcileLeavesTolerationsNullWhenSourceHasNone() {
219+
cronOMJob.getSpec().getOmJobSpec().getMainPodSpec().setTolerations(null);
220+
cronOMJob.getSpec().getOmJobSpec().getExitHandlerSpec().setTolerations(null);
221+
222+
cronOMJob.getSpec().setSchedule("* * * * *");
223+
cronOMJob.getSpec().setStartingDeadlineSeconds(86400);
224+
225+
reconciler.reconcile(cronOMJob, context);
226+
227+
ArgumentCaptor<OMJobResource> captor = ArgumentCaptor.forClass(OMJobResource.class);
228+
verify(mixedOp).resource(captor.capture());
229+
OMJobResource scheduled = captor.getValue();
230+
231+
assertNull(
232+
scheduled.getSpec().getMainPodSpec().getTolerations(),
233+
"Main pod tolerations must remain null when CronOMJob has none");
234+
assertNull(
235+
scheduled.getSpec().getExitHandlerSpec().getTolerations(),
236+
"Exit handler tolerations must remain null when CronOMJob has none");
237+
}
238+
161239
private CronOMJobResource createTestCronOMJob(String name, String schedule) {
162240
CronOMJobResource cronOMJob = new CronOMJobResource();
163241

0 commit comments

Comments
 (0)