Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.openmetadata.operator.model.CronOMJobStatus;
import org.openmetadata.operator.model.OMJobResource;
import org.openmetadata.operator.model.OMJobSpec;
import org.openmetadata.operator.util.KubernetesNameBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -51,6 +52,7 @@ public class CronOMJobReconciler
implements Reconciler<CronOMJobResource>, ErrorStatusHandler<CronOMJobResource> {

private static final Logger LOG = LoggerFactory.getLogger(CronOMJobReconciler.class);
private static final int MAX_OMJOB_NAME_LENGTH = 63;
private final Duration defaultRequeue;
private final OperatorConfig config;
Comment thread
Darshan3690 marked this conversation as resolved.

Expand Down Expand Up @@ -166,28 +168,27 @@ public UpdateControl<CronOMJobResource> reconcile(

private OMJobResource buildOMJob(CronOMJobResource cronOMJob, Instant scheduledTime) {
String baseName = cronOMJob.getMetadata().getName();
String name = baseName + "-" + scheduledTime.getEpochSecond();
if (name.length() > 253) {
name = name.substring(0, 253);
}
String name = buildScheduledOMJobName(baseName, scheduledTime);

final String finalName = name; // Make final for use in lambda

// Generate unique run ID for this execution
String runId = UUID.randomUUID().toString();
HashMap<String, String> labels =
cronOMJob.getMetadata().getLabels() != null
? new HashMap<>(cronOMJob.getMetadata().getLabels())
: new HashMap<>();

ObjectMeta metadata =
new ObjectMetaBuilder()
.withName(name)
.withNamespace(cronOMJob.getMetadata().getNamespace())
.withLabels(cronOMJob.getMetadata().getLabels())
.withLabels(labels)
.withAnnotations(cronOMJob.getMetadata().getAnnotations())
.build();

// Add run ID to labels for tracking
if (metadata.getLabels() != null) {
metadata.getLabels().put("app.kubernetes.io/run-id", runId);
}
metadata.getLabels().put("app.kubernetes.io/run-id", runId);

OMJobResource omJob = new OMJobResource();
omJob.setMetadata(metadata);
Expand Down Expand Up @@ -239,6 +240,17 @@ private OMJobResource buildOMJob(CronOMJobResource cronOMJob, Instant scheduledT
return omJob;
}

private String buildScheduledOMJobName(String baseName, Instant scheduledTime) {
String suffix = "-" + scheduledTime.getEpochSecond();
int maxBaseLength = MAX_OMJOB_NAME_LENGTH - suffix.length();

if (maxBaseLength < 1) {
throw new IllegalArgumentException("Scheduled OMJob suffix leaves no room for base name");
}

return KubernetesNameBuilder.fitNameWithHash(baseName, maxBaseLength, "omjob") + suffix;
}
Comment thread
Darshan3690 marked this conversation as resolved.

private OMJobSpec deepCopyOMJobSpec(OMJobSpec source, String runId, String jobName) {
if (source == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.openmetadata.operator.model.OMJobSpec;
import org.openmetadata.operator.model.OMJobStatus;
import org.openmetadata.operator.util.EnvVarUtils;
import org.openmetadata.operator.util.KubernetesNameBuilder;
import org.openmetadata.operator.util.LabelBuilder;
import org.openmetadata.schema.entity.services.ingestionPipelines.PipelineStatusType;
import org.slf4j.Logger;
Expand All @@ -51,6 +52,7 @@ public class PodManager {

private static final Logger LOG = LoggerFactory.getLogger(PodManager.class);
private static final String PIPELINE_STATUS = "pipelineStatus";
private static final int MAX_POD_NAME_LENGTH = 253;
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_POD_NAME_LENGTH is introduced but not used (pod names are currently capped using MAX_LABEL_LENGTH). Consider removing it to avoid dead code, or apply it where appropriate if you still intend to enforce the 253-char DNS subdomain limit elsewhere.

Suggested change
private static final int MAX_POD_NAME_LENGTH = 253;

Copilot uses AI. Check for mistakes.

Comment thread
pmbrull marked this conversation as resolved.
Comment thread
Darshan3690 marked this conversation as resolved.
private final KubernetesClient client;

Expand Down Expand Up @@ -425,11 +427,35 @@ private Optional<Pod> findPod(OMJobResource omJob, Map<String, String> selector)
}

private String generateMainPodName(OMJobResource omJob) {
return omJob.getMetadata().getName() + "-main";
return buildPodName(omJob, "-main");
}

private String generateExitHandlerPodName(OMJobResource omJob) {
return omJob.getMetadata().getName() + "-exit";
return buildPodName(omJob, "-exit");
}

private String buildPodName(OMJobResource omJob, String suffix) {
// Kubernetes pod names can be up to 253 characters (DNS subdomain limit).
// The OMJob name label is derived from omJob.getMetadata().getName()
// and sanitized by LabelBuilder to satisfy the separate 63-character label limit.
// It is not derived from the generated pod name.
String baseName = omJob.getMetadata().getName();

// Account for suffix length when calculating max base name length.
int maxBaseLength = MAX_POD_NAME_LENGTH - suffix.length();
String podName =
KubernetesNameBuilder.fitNameWithHash(baseName, maxBaseLength, "omjob") + suffix;

// Log a warning if the original name was truncated to fit the DNS subdomain limit.
if (!podName.equals(omJob.getMetadata().getName() + suffix)) {
LOG.warn(
"Pod name for OMJob {} was truncated from {} to {} to comply with Kubernetes DNS name length limits",
omJob.getMetadata().getName(),
omJob.getMetadata().getName() + suffix,
podName);
}

return podName;
Comment on lines +437 to +458
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildPodName truncates the OMJob name when it would exceed the 253-char DNS limit, but truncation is prefix-only and can cause pod-name collisions between different long OMJob names that only differ near the end. That can break the idempotency checks (withName(podName).get()) and/or cause create conflicts. Consider using a hash-based truncation scheme when truncation is required (similar to LabelBuilder.sanitizeLabelValue) so the generated pod name remains unique within the length limit.

Copilot uses AI. Check for mistakes.
}

private String determinePipelineStatus(OMJobResource omJob) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2021 Collate.
Comment thread
Darshan3690 marked this conversation as resolved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.openmetadata.operator.util;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class HashUtils {
private static final int HASH_LENGTH = 6;

private HashUtils() {
// Utility class
}

/**
* Generates a 6-character SHA-256 hash of the input string for deterministic name
* truncation.
*
* @param input the string to hash
* @return a 6-character hex string
* @throws IllegalArgumentException if SHA-256 is not available
*/
public static String hash(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encoded = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte value : encoded) {
hex.append(String.format("%02x", value & 0xff));
}
return hex.substring(0, HASH_LENGTH);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("SHA-256 algorithm not available", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.openmetadata.operator.util;

/**
* Utility for building deterministic Kubernetes resource names within a length limit.
*/
public final class KubernetesNameBuilder {

private static final int HASH_LENGTH = 6;

private KubernetesNameBuilder() {
// Utility class
}

public static String fitNameWithHash(
String baseName, int maxBaseLength, String fallbackBaseName) {
Comment thread
gitar-bot[bot] marked this conversation as resolved.
if (maxBaseLength < 1) {
throw new IllegalArgumentException(
"Kubernetes resource name must allow at least one character");
}

if (fallbackBaseName == null || fallbackBaseName.isEmpty()) {
throw new IllegalArgumentException("fallbackBaseName must not be null or empty");
}

String candidate = baseName == null ? "" : baseName;
if (candidate.length() > maxBaseLength) {
String hash = HashUtils.hash(candidate).substring(0, Math.min(HASH_LENGTH, maxBaseLength));
int maxPrefixLength = maxBaseLength - hash.length() - 1;
Comment thread
pmbrull marked this conversation as resolved.

if (maxPrefixLength > 0) {
String prefix = trimTrailingSeparators(candidate.substring(0, maxPrefixLength));
if (!prefix.isEmpty()) {
return prefix + "-" + hash;
}

String fallbackPrefix =
trimTrailingSeparators(
fallbackBaseName.substring(
0, Math.min(fallbackBaseName.length(), maxPrefixLength)));
if (!fallbackPrefix.isEmpty()) {
return fallbackPrefix + "-" + hash;
}
}

return hash;
}

if (candidate.isEmpty()) {
candidate = fallbackBaseName;
}

if (candidate.length() > maxBaseLength) {
throw new IllegalStateException(
"Fallback name exceeds max base length (" + maxBaseLength + ")");
}

return candidate;
}

private static String trimTrailingSeparators(String value) {
return value.replaceAll("[^a-z0-9]+$", "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
*/
public class LabelBuilder {

private static final int MAX_LABEL_VALUE_LENGTH = 63;

// Standard Kubernetes labels
public static final String LABEL_APP_NAME = "app.kubernetes.io/name";
public static final String LABEL_APP_COMPONENT = "app.kubernetes.io/component";
Expand Down Expand Up @@ -56,7 +58,7 @@ public static Map<String, String> buildBaseLabels(OMJobResource omJob) {
labels.put(LABEL_APP_NAME, APP_NAME);
labels.put(LABEL_APP_COMPONENT, COMPONENT_INGESTION);
labels.put(LABEL_APP_MANAGED_BY, MANAGED_BY_OMJOB_OPERATOR);
labels.put(LABEL_OMJOB_NAME, omJob.getMetadata().getName());
labels.put(LABEL_OMJOB_NAME, buildOMJobNameLabelValue(omJob));

// Copy pipeline and run-id from OMJob labels
String pipelineName = omJob.getPipelineName();
Expand Down Expand Up @@ -113,10 +115,18 @@ public static Map<String, String> buildExitHandlerLabels(OMJobResource omJob) {
*/
public static Map<String, String> buildPodSelector(OMJobResource omJob) {
Map<String, String> selector = new HashMap<>();
selector.put(LABEL_OMJOB_NAME, omJob.getMetadata().getName());
selector.put(LABEL_OMJOB_NAME, buildOMJobNameLabelValue(omJob));
selector.put(LABEL_APP_MANAGED_BY, MANAGED_BY_OMJOB_OPERATOR);
return selector;
Comment thread
Darshan3690 marked this conversation as resolved.
}

/**
* Build the OMJob name label value used by operator-managed pods.
*/
public static String buildOMJobNameLabelValue(OMJobResource omJob) {
return sanitizeLabelValue(omJob.getMetadata().getName());
}

/**
* Build selector for finding main pod
*/
Expand All @@ -143,10 +153,51 @@ public static String sanitizeLabelValue(String value) {
return "";
}

// Replace invalid characters with hyphens and truncate to 63 chars
String sanitized =
value.replaceAll("[^a-zA-Z0-9\\-_.]", "-").replaceAll("-+", "-").replaceAll("^-|-$", "");
// Replace invalid characters with hyphens and collapse repeated hyphens.
// We intentionally keep dots and underscores, as they are allowed in label values.
String sanitized = value.replaceAll("[^a-zA-Z0-9\\-_.]", "-").replaceAll("-+", "-");

if (sanitized.length() <= MAX_LABEL_VALUE_LENGTH) {
// Even for short values, ensure we respect the Kubernetes rule that
// label values must start and end with an alphanumeric character.
return ensureValidLabelValue(sanitized, value);
}

// For long values, preserve uniqueness by appending a short hash while
// keeping the overall value within the 63-character limit.
String hash = HashUtils.hash(value);
int maxPrefixLength = MAX_LABEL_VALUE_LENGTH - hash.length() - 1;
String prefix = sanitized.substring(0, maxPrefixLength);

String candidate = prefix + "-" + hash;
return ensureValidLabelValue(candidate, value);
}

/**
* Ensure the label value starts and ends with an alphanumeric character.
* If trimming non-alphanumeric characters from the edges results in an
* empty string, fall back to a hash-based value derived from the original
* input so that the label remains valid and stable.
*/
private static String ensureValidLabelValue(String candidate, String original) {
String result = candidate.replaceAll("^[^a-zA-Z0-9]+", "").replaceAll("[^a-zA-Z0-9]+$", "");

if (!result.isEmpty() && result.length() <= MAX_LABEL_VALUE_LENGTH) {
return result;
}

// Fallback: build a short, deterministic value based on a hash of the
// original input. This guarantees the output is non-empty, starts/ends
// with an alphanumeric character, and fits within the label limit.
int maxHashLength = Math.max(1, MAX_LABEL_VALUE_LENGTH - 3); // Reserve for "om-"
String fullHash = HashUtils.hash(original);
String hash = fullHash.substring(0, Math.min(maxHashLength, fullHash.length()));
String fallback = "om-" + hash;

if (fallback.length() > MAX_LABEL_VALUE_LENGTH) {
fallback = fallback.substring(0, MAX_LABEL_VALUE_LENGTH);
}

return sanitized.length() > 63 ? sanitized.substring(0, 63) : sanitized;
return fallback;
}
}
Loading
Loading