This is an automated email from the ASF dual-hosted git repository.
ayushtkn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git
The following commit(s) were added to refs/heads/master by this push:
new 828f668ee1a HIVE-29999: HiveCluster CR string fields should be
validated before being injected into k8s operator generated bash commands
(#6802)
828f668ee1a is described below
commit 828f668ee1a3c98e74356a9a1fdb66c33a1d9847
Author: Tanishq Chugh <[email protected]>
AuthorDate: Mon Sep 21 10:50:36 2026 +0530
HIVE-29999: HiveCluster CR string fields should be validated before being
injected into k8s operator generated bash commands (#6802)
---
.../crds/hiveclusters.hive.apache.org-v1.yml | 1 +
.../operator/dependent/HiveDependentResource.java | 103 +++++++++++++++++++++
.../dependent/HiveServer2DeploymentDependent.java | 1 +
.../operator/model/spec/DatabaseConfig.java | 2 +
.../operator/reconciler/HiveClusterReconciler.java | 47 +++++++++-
5 files changed, 151 insertions(+), 3 deletions(-)
diff --git
a/packaging/src/kubernetes/helm/hive-operator/crds/hiveclusters.hive.apache.org-v1.yml
b/packaging/src/kubernetes/helm/hive-operator/crds/hiveclusters.hive.apache.org-v1.yml
index 516e4309d28..1f5bd05e2ce 100644
---
a/packaging/src/kubernetes/helm/hive-operator/crds/hiveclusters.hive.apache.org-v1.yml
+++
b/packaging/src/kubernetes/helm/hive-operator/crds/hiveclusters.hive.apache.org-v1.yml
@@ -648,6 +648,7 @@ spec:
default: derby
description: "Database type: derby, mysql, postgres,
mssql,\
\ or oracle"
+ pattern: ^(derby|mysql|postgres|mssql|oracle)$
type: string
url:
description: JDBC connection URL
diff --git
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
index bb031b50ac2..a64440acab8 100644
---
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
+++
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveDependentResource.java
@@ -19,6 +19,10 @@
package org.apache.hive.kubernetes.operator.dependent;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
@@ -26,6 +30,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.regex.Pattern;
+
import io.fabric8.kubernetes.api.model.AffinityBuilder;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerBuilder;
@@ -49,7 +55,9 @@
import org.apache.hive.kubernetes.operator.autoscaling.HiveClusterAutoscaler;
import org.apache.hive.kubernetes.operator.model.HiveCluster;
import org.apache.hive.kubernetes.operator.model.spec.AutoscalingSpec;
+import org.apache.hive.kubernetes.operator.model.HiveClusterSpec;
import org.apache.hive.kubernetes.operator.model.spec.DatabaseConfig;
+import org.apache.hive.kubernetes.operator.model.spec.LlapSpec;
import org.apache.hive.kubernetes.operator.model.spec.SecretKeyRef;
import org.apache.hive.kubernetes.operator.model.spec.ProbeSpec;
@@ -74,6 +82,9 @@ public abstract class HiveDependentResource<R extends
HasMetadata,
private static final Logger LOG =
LoggerFactory.getLogger(HiveDependentResource.class);
+ /** Closed set of valid schematool -dbType values. */
+ private static final Pattern DB_TYPE_PATTERN =
Pattern.compile("derby|mysql|postgres|mssql|oracle");
+
protected static final String CONF_MOUNT_PATH = "/etc/hive/conf";
protected static final String HIVE_CONF_DIR = "/opt/hive/conf";
protected static final String EXT_JARS_PATH = "/tmp/ext-jars";
@@ -360,6 +371,96 @@ protected static String sha256(String... inputs) {
}
}
+ /**
+ * Validates CR-provided database fields before they are embedded into
+ * shell command lines (schematool -dbType in the schema-init Job) and
+ * into SERVICE_OPTS, which the image entrypoint expands into JVM arguments.
+ */
+ protected static void validateDatabaseConfig(DatabaseConfig db) {
+ if (!DB_TYPE_PATTERN.matcher(db.type()).matches()) {
+ throw new IllegalArgumentException(
+ "spec.metastore.database.type must be one of derby, mysql, postgres,
mssql, oracle");
+ }
+ validateOptValue("spec.metastore.database.url", db.url());
+ validateOptValue("spec.metastore.database.driver", db.driver());
+ validateOptValue("spec.metastore.database.username", db.username());
+ }
+
+ /**
+ * Validates CR-provided string fields before they are embedded into
+ * HiveServer2 SERVICE_OPTS and related env vars.
+ */
+ protected static void validateHiveServer2EmbeddedValues(HiveClusterSpec
spec) {
+ if (!spec.metastore().isEnabled()) {
+ validateOptValue("spec.metastore.externalUri",
spec.metastore().externalUri());
+ }
+ if (spec.tezAm().isEnabled()) {
+ validateOptValue("spec.zookeeper.quorum", spec.zookeeper().quorum());
+ }
+ spec.llapClusters().stream()
+ .filter(LlapSpec::isEnabled)
+ .forEach(llap -> validateOptValue("spec.llapClusters.serviceHosts",
llap.serviceHosts()));
+ }
+
+ /**
+ * Validates CR-provided string fields before they are embedded into
+ * LLAP / TezAM env vars of llap cluster.
+ */
+ public static void validateLlapEmbeddedValues(HiveClusterSpec spec, LlapSpec
llap) {
+ validateOptValue("spec.zookeeper.quorum", spec.zookeeper().quorum());
+ validateOptValue("spec.llapClusters.serviceHosts", llap.serviceHosts());
+ }
+
+ private static void validateOptValue(String field, String value) {
+ if (containsUnsafeShellChars(value) || (value != null &&
value.contains("$("))) {
+ throw new IllegalArgumentException(field + " must not contain
whitespace, quotes, backslashes, "
+ + "control characters or Kubernetes variable references");
+ }
+ }
+
+ /**
+ * Validates a CR-provided external JAR location before it is embedded
+ * into a bash download command.
+ */
+ protected static void validateJarUrl(String jarUrl) {
+ validateOptValue("external JAR location", jarUrl);
+
+ if (jarUrl != null && (jarUrl.startsWith("http://") ||
jarUrl.startsWith("https://"))) {
+ // Normalizing parse: rejects malformed URLs early instead of
+ // letting wget interpret them, preventing the init containers
+ // from crashing and the spun-up pods from entering CrashLoopBackOff.
+ try {
+ URL url = new URI(jarUrl).toURL();
+
+ String host = url.getHost();
+ if (host == null || host.isEmpty()) {
+ throw new IllegalArgumentException("HTTP/HTTPS external JAR URL must
specify a host");
+ }
+
+ int port = url.getPort();
+ if (port != -1 && (port < 1 || port > 65535)) {
+ throw new IllegalArgumentException("HTTP/HTTPS external JAR URL has
invalid port");
+ }
+
+ } catch (MalformedURLException | URISyntaxException e) {
+ throw new IllegalArgumentException("Malformed HTTP/HTTPS external JAR
URL");
+ }
+ }
+ }
+
+ private static boolean containsUnsafeShellChars(String value) {
+ if (value == null) {
+ return false;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (Character.isWhitespace(c) || c == '\'' || c == '"' || c == '\\' || c
== '`' || Character.isISOControl(c)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Builds the database connection env vars: DB_DRIVER, DBPASSWORD
* (from SecretKeyRef), and SERVICE_OPTS with javax.jdo connection
@@ -367,6 +468,7 @@ protected static String sha256(String... inputs) {
* SchemaInitJobDependent.
*/
protected static List<EnvVar> buildDbEnvVars(DatabaseConfig db) {
+ validateDatabaseConfig(db);
List<EnvVar> envVars = new ArrayList<>();
envVars.add(new EnvVar("DB_DRIVER", db.type(), null));
@@ -511,6 +613,7 @@ protected static Container buildExternalJarsInitContainer(
cmd.append("export HADOOP_CONF_DIR=").append(CONF_MOUNT_PATH).append(" &&
");
for (String jarUrl : externalJars) {
+ validateJarUrl(jarUrl);
if (jarUrl.startsWith("http://") || jarUrl.startsWith("https://")) {
cmd.append("wget -q --tries=3 --waitretry=5 -P ").append(targetDir)
.append(" '").append(jarUrl).append("' && ");
diff --git
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
index aab6c890897..6bc6291fd1f 100644
---
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
+++
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/dependent/HiveServer2DeploymentDependent.java
@@ -72,6 +72,7 @@ protected Deployment desired(HiveCluster hiveCluster,
Context<HiveCluster> context) {
HiveClusterSpec spec = hiveCluster.getSpec();
HiveServer2Spec hs2 = spec.hiveServer2();
+ validateHiveServer2EmbeddedValues(spec);
Map<String, String> selectorLabels =
Labels.selectorForComponent(hiveCluster, COMPONENT);
diff --git
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/model/spec/DatabaseConfig.java
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/model/spec/DatabaseConfig.java
index cd60f80f857..9e0a79d80d1 100644
---
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/model/spec/DatabaseConfig.java
+++
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/model/spec/DatabaseConfig.java
@@ -21,11 +21,13 @@
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.generator.annotation.Default;
+import io.fabric8.generator.annotation.Pattern;
/** JDBC database connection configuration for the Hive Metastore backend. */
public record DatabaseConfig(
@JsonPropertyDescription("Database type: derby, mysql, postgres, mssql, or
oracle")
@Default("derby")
+ @Pattern("^(derby|mysql|postgres|mssql|oracle)$")
String type,
@JsonPropertyDescription("JDBC connection URL")
String url,
diff --git
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
index ec37d6820ad..557ebe99d54 100644
---
a/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
+++
b/packaging/src/kubernetes/src/java/org/apache/hive/kubernetes/operator/reconciler/HiveClusterReconciler.java
@@ -49,6 +49,7 @@
import org.apache.hive.kubernetes.operator.autoscaling.MetricsCache;
import org.apache.hive.kubernetes.operator.autoscaling.MetricsScraper;
import org.apache.hive.kubernetes.operator.autoscaling.PodMetrics;
+import org.apache.hive.kubernetes.operator.dependent.HiveDependentResource;
import org.apache.hive.kubernetes.operator.dependent.LlapResourceBuilder;
import org.apache.hive.kubernetes.operator.model.HiveCluster;
import org.apache.hive.kubernetes.operator.model.HiveClusterSpec;
@@ -72,6 +73,8 @@ public class HiveClusterReconciler
private static final Logger LOG =
LoggerFactory.getLogger(HiveClusterReconciler.class);
+ private static final String CONDITION_READY_LITERAL = "Ready";
+ private static final String RECONCILIATION_ERROR_LITERAL =
"ReconciliationError";
private volatile HiveClusterAutoscaler autoscaler;
private volatile BackgroundMetricsScraper bgScraper;
@@ -178,9 +181,11 @@ public UpdateControl<HiveCluster> reconcile(HiveCluster
resource, Context<HiveCl
? 2 : getMinScrapeInterval(resource.getSpec());
}
+ boolean workflowError = applyWorkflowDependentErrors(resource, context,
newStatus, existingStatus);
+
// --- Single exit point for status update ---
boolean statusNowChanged = !statusEqualsIgnoringTimestamps(existingStatus,
newStatus);
- if (!statusNowChanged && rescheduleSeconds == 0) {
+ if (!statusNowChanged && rescheduleSeconds == 0 && !workflowError) {
return UpdateControl.noUpdate();
}
resource.setStatus(newStatus);
@@ -217,7 +222,7 @@ public ErrorStatusUpdateControl<HiveCluster>
updateErrorStatus(HiveCluster resou
status.getConditions() != null ? status.getConditions() :
Collections.emptyList();
status.setConditions(List.of(
- buildCondition("Ready", "False", "ReconciliationError",
+ buildCondition(CONDITION_READY_LITERAL, "False",
RECONCILIATION_ERROR_LITERAL,
e.getMessage(), existingConditions)
));
status.setObservedGeneration(resource.getMetadata().getGeneration());
@@ -324,7 +329,7 @@ private HiveClusterStatus buildStatus(HiveCluster resource,
// Overall Ready condition
boolean allReady = schemaReady && metastoreReady && hs2Ready;
- conditions.add(buildCondition("Ready", allReady ? "True" : "False",
+ conditions.add(buildCondition(CONDITION_READY_LITERAL, allReady ? "True" :
"False",
allReady ? "AllComponentsReady" : "ComponentsNotReady",
allReady ? "All Hive components are ready" : "One or more components
are not ready",
existingConditions));
@@ -387,6 +392,41 @@ private <T extends HasMetadata> ComponentStatus
buildComponentStatus(
return cs;
}
+ /**
+ * When the managed workflow dependents incur failures, update the Ready
+ * condition with the incurred error, while preserving component conditions.
+ */
+ private boolean applyWorkflowDependentErrors(HiveCluster resource,
Context<HiveCluster> context,
+ HiveClusterStatus newStatus, HiveClusterStatus existingStatus) {
+ var workflowResult =
context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult();
+ if (workflowResult.isEmpty() ||
!workflowResult.get().erroredDependentsExist()) {
+ return false;
+ }
+
+ Exception error =
workflowResult.get().getErroredDependents().values().iterator().next();
+ String errorMessage = error.getMessage();
+ LOG.error("Error reconciling HiveCluster: {}/{} - {}",
resource.getMetadata().getNamespace(),
+ resource.getMetadata().getName(), errorMessage, error);
+
+ List<Condition> existingConditions = existingStatus != null &&
existingStatus.getConditions() != null
+ ? existingStatus.getConditions() : Collections.emptyList();
+ boolean alreadyReported = existingConditions.stream()
+ .anyMatch(c -> CONDITION_READY_LITERAL.equals(c.getType())
+ && "False".equals(c.getStatus())
+ && RECONCILIATION_ERROR_LITERAL.equals(c.getReason())
+ && Objects.equals(errorMessage, c.getMessage()));
+
+ List<Condition> conditions = newStatus.getConditions();
+ if (conditions == null) {
+ conditions = new ArrayList<>();
+ newStatus.setConditions(conditions);
+ }
+ conditions.removeIf(c -> CONDITION_READY_LITERAL.equals(c.getType()));
+ conditions.add(buildCondition(CONDITION_READY_LITERAL, "False",
RECONCILIATION_ERROR_LITERAL,
+ errorMessage, existingConditions));
+ return !alreadyReported;
+ }
+
private Condition buildCondition(String type, String conditionStatus,
String reason, String message, List<Condition> existingConditions) {
@@ -612,6 +652,7 @@ private void reconcileLlapClusters(HiveCluster resource,
KubernetesClient client
if (!llapSpec.isEnabled()) {
continue;
}
+ HiveDependentResource.validateLlapEmbeddedValues(resource.getSpec(),
llapSpec);
desiredNames.add(llapSpec.name());
int replicas = resolveLlapReplicaCount(resource, llapSpec, ns,
clusterName);