This is an automated email from the ASF dual-hosted git repository.

ramackri pushed a commit to branch ranger-kafka-dynamic-partition-plan
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 4bf951fb46d3e99cfd11bd6f2f73556ea3c9a6d5
Author: ramk <[email protected]>
AuthorDate: Tue Jun 23 12:15:35 2026 +0530

    Fix post-scale partition routing and HDFS dynamic partition E2E.
    
    Route planned tail partitions using plan.topicPartitionCount when Kafka 
metadata lags after scale, harden Solr Kerberos relogin, and add a green HDFS 
dynamic-partition E2E harness with Kafka marker verification fixes.
    
    Co-authored-by: Cursor <[email protected]>
---
 .../ranger/audit/utils/AbstractKerberosUser.java   |  12 +-
 .../audit/producer/kafka/AuditPartitioner.java     |  30 +++-
 .../resources/conf/ranger-audit-ingestor-site.xml  |   2 +-
 .../kafka/AuditPartitionerDynamicTest.java         |  28 +++
 .../ranger-audit-dispatcher-solr-site.xml          |   4 +-
 .../audit/dynamic-partition-plugin-e2e-lib.sh      | 128 ++++++++++++--
 .../scripts/audit/partition-plan-e2e-lib.sh        |  77 ++++++++-
 .../audit/verify-hdfs-dynamic-partition-e2e.sh     | 189 +++++++++++++++++++++
 8 files changed, 435 insertions(+), 35 deletions(-)

diff --git 
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
 
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
index 47d16f31a..1f990ec7b 100644
--- 
a/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
+++ 
b/agents-audit/core/src/main/java/org/apache/ranger/audit/utils/AbstractKerberosUser.java
@@ -172,8 +172,16 @@ public synchronized boolean checkTGTAndRelogin() throws 
LoginException {
 
         LOG.debug("Performing relogin for {}", getPrincipal());
 
-        logout();
-        login();
+        try {
+            logout();
+            login();
+        } catch (LoginException e) {
+            LOG.warn("Relogin failed for {}, recreating JAAS login context: 
{}", getPrincipal(), e.getMessage());
+            loginContext = null;
+            subject        = new Subject();
+            loggedIn.set(false);
+            login();
+        }
 
         return true;
     }
diff --git 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditPartitioner.java
 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditPartitioner.java
index f6df42a3b..af825e14f 100644
--- 
a/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditPartitioner.java
+++ 
b/audit-server/audit-ingestor/src/main/java/org/apache/ranger/audit/producer/kafka/AuditPartitioner.java
@@ -175,24 +175,27 @@ public void close() {
     }
 
     /** Picks a partition from the in-memory plan: round-robin for known 
plugins, hash for buffer. */
-    private int partitionFromPlan(String appId, int numPartitions) {
+    private int partitionFromPlan(String appId, int clusterPartitionCount) {
         PartitionPlan plan = PartitionPlanHolder.getInstance().getPlan();
         if (plan == null) {
             LOG.error("Dynamic partition plan is not loaded; falling back to 
hash routing for appId '{}'", appId);
-            return hashAppIdToPartitionIndex(appId, numPartitions);
+            return hashAppIdToPartitionIndex(appId, clusterPartitionCount);
         }
+        // After scale, producer cluster metadata can lag behind 
plan.topicPartitionCount; trust the plan
+        // so planned tail ids (e.g. 12) are not clamped into buffer (e.g. 11).
+        int routingPartitionCount = Math.max(clusterPartitionCount, 
plan.getTopicPartitionCount());
         PluginPartitionAssignment assignment = findPluginAssignment(plan, 
appId);
         if (assignment != null && !assignment.getPartitions().isEmpty()) {
             List<Integer> partitionIds = assignment.getPartitions();
             int roundRobinIndex = nextRoundRobinIndex(appId, 
partitionIds.size());
-            return boundPartitionToTopic(partitionIds.get(roundRobinIndex), 
numPartitions);
+            return boundPartitionToTopic(partitionIds.get(roundRobinIndex), 
routingPartitionCount);
         }
         List<Integer> bufferIds = plan.getBuffer().getPartitions();
         if (bufferIds.isEmpty()) {
-            return hashAppIdToPartitionIndex(appId, numPartitions);
+            return hashAppIdToPartitionIndex(appId, routingPartitionCount);
         }
         int bufferIndex = hashAppIdToPartitionIndex(appId, bufferIds.size());
-        return boundPartitionToTopic(bufferIds.get(bufferIndex), 
numPartitions);
+        return boundPartitionToTopic(bufferIds.get(bufferIndex), 
routingPartitionCount);
     }
 
     /** Sticky hash: maps {@code appId} to an index in {@code [0, 
partitionCount)}. */
@@ -228,12 +231,21 @@ private static int resolveTopicPartitionCount(Cluster 
cluster, String topic) {
         return 1;
     }
 
-    /** Bounds a planned partition id to the topic's current live partition 
range. */
-    private static int boundPartitionToTopic(int plannedPartitionId, int 
topicPartitionCount) {
-        if (topicPartitionCount <= 0) {
+    /**
+     * Returns a valid partition id for routing. Planned ids below {@code 
routingPartitionCount} are returned
+     * as-is; never clamp a tail id into the previous last partition when 
cluster metadata is stale.
+     */
+    private static int boundPartitionToTopic(int plannedPartitionId, int 
routingPartitionCount) {
+        if (routingPartitionCount <= 0) {
             return 0;
         }
-        return Math.min(Math.max(0, plannedPartitionId), topicPartitionCount - 
1);
+        if (plannedPartitionId < 0) {
+            return 0;
+        }
+        if (plannedPartitionId < routingPartitionCount) {
+            return plannedPartitionId;
+        }
+        return plannedPartitionId;
     }
 
     /** Logs the dynamic plan snapshot installed in {@link 
PartitionPlanHolder} at startup. */
diff --git 
a/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
 
b/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
index d29f03a2f..6adba2ca1 100644
--- 
a/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
+++ 
b/audit-server/audit-ingestor/src/main/resources/conf/ranger-audit-ingestor-site.xml
@@ -184,7 +184,7 @@
 
     <property>
         <name>ranger.audit.ingestor.service.dev_hdfs.allowed.users</name>
-        <value>hdfs</value>
+        <value>hdfs,nn</value>
         <description>Allowed users for dev_hdfs (Policy Manager service name; 
from policy.download.auth.users)</description>
     </property>
 
diff --git 
a/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditPartitionerDynamicTest.java
 
b/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditPartitionerDynamicTest.java
index 75431b395..7cad59f43 100644
--- 
a/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditPartitionerDynamicTest.java
+++ 
b/audit-server/audit-ingestor/src/test/java/org/apache/ranger/audit/producer/kafka/AuditPartitionerDynamicTest.java
@@ -24,6 +24,7 @@
 import 
org.apache.ranger.audit.producer.kafka.partition.PartitionPlanBootstrapConfig;
 import org.apache.ranger.audit.producer.kafka.partition.PartitionPlanHolder;
 import org.apache.ranger.audit.producer.kafka.partition.model.PartitionPlan;
+import 
org.apache.ranger.audit.producer.kafka.partition.model.PluginPartitionAssignment;
 import org.apache.ranger.audit.server.AuditServerConstants;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
@@ -33,6 +34,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -95,6 +97,32 @@ public void testDynamicModeRoutesUnknownPluginToBuffer() {
         assertTrue(partition >= 3 && partition <= 5);
     }
 
+    @Test
+    public void 
testDynamicModeUsesPlannedTailPartitionWhenClusterMetadataLagsAfterScale() {
+        PartitionPlan planV1 = PartitionPlanBootstrap.createInitialPlan(
+                PartitionPlanBootstrapConfig.create(TOPIC, new String[] 
{"hdfs"}, 3, 9));
+        PartitionPlanHolder.getInstance().install(planV1, 12);
+
+        AuditPartitioner partitioner = new AuditPartitioner();
+        partitioner.configure(dynamicProducerConfig());
+
+        // Simulate scale: plan now assigns tail partitions 12,13 but producer 
metadata still shows 12 partitions.
+        PartitionPlan planV2 = planV1.toBuilder()
+                .version(2)
+                .topicPartitionCount(14)
+                .plugins(Map.of("hdfs", PluginPartitionAssignment.of(0, 1, 2, 
12, 13)))
+                .buffer(PluginPartitionAssignment.ofRange(3, 11))
+                .updatedBy("test")
+                .build();
+        PartitionPlanHolder.getInstance().install(planV2, 14);
+
+        // Round-robin index 3 -> planned partition 12; must not clamp to 11 
when cluster still reports 12 partitions.
+        for (int i = 0; i < 3; i++) {
+            partitioner.partition(TOPIC, "hdfs", null, null, null, 
cluster(12));
+        }
+        assertEquals(12, partitioner.partition(TOPIC, "hdfs", null, null, 
null, cluster(12)));
+    }
+
     @Test
     public void testConcurrentPartitionWhilePlanSwaps() throws Exception {
         PartitionPlan planV1 = 
PartitionPlanBootstrap.createInitialPlan(PartitionPlanBootstrapConfig.create(TOPIC,
 new String[] {"hdfs"}, 2, 2));
diff --git 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
index 3f19c69a9..39e5d820d 100644
--- 
a/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
+++ 
b/dev-support/ranger-docker/scripts/audit-dispatcher/ranger-audit-dispatcher-solr-site.xml
@@ -157,8 +157,8 @@
 
     <property>
         <name>xasecure.audit.jaas.Client.option.useTicketCache</name>
-        <value>true</value>
-        <description>Allow use of cached Kerberos tickets for Solr 
authentication</description>
+        <value>false</value>
+        <description>Use keytab for Solr auth (ticket cache breaks relogin 
with "No key to store")</description>
     </property>
 
     <property>
diff --git 
a/dev-support/ranger-docker/scripts/audit/dynamic-partition-plugin-e2e-lib.sh 
b/dev-support/ranger-docker/scripts/audit/dynamic-partition-plugin-e2e-lib.sh
index e59dd4f8e..e264b357c 100755
--- 
a/dev-support/ranger-docker/scripts/audit/dynamic-partition-plugin-e2e-lib.sh
+++ 
b/dev-support/ranger-docker/scripts/audit/dynamic-partition-plugin-e2e-lib.sh
@@ -67,6 +67,28 @@ dpp_plan_plugin_partitions() {
 print(','.join(str(x) for x in (p or {}).get('partitions') or []))" 
"${plugin_id}" 2>/dev/null || echo ""
 }
 
+dpp_allowed_users_for_repo() {
+  local repo="$1"
+  local short_name="$2"
+  if [[ "${repo}" == "dev_ozone" ]]; then
+    printf '%s' "om,ozone"
+  elif [[ "${repo}" == "dev_hdfs" ]]; then
+    printf '%s' "hdfs,nn"
+  else
+    printf '%s' "${short_name}"
+  fi
+}
+
+dpp_plan_service_has_users() {
+  local repo="$1"
+  local users_csv="$2"
+  local plan_json="$3"
+  printf '%s' "${plan_json}" | python3 -c \
+    'import sys,json; d=json.load(sys.stdin); repo=sys.argv[1]; 
want=set(u.strip() for u in sys.argv[2].split(",") if u.strip()); \
+entry=(d.get("services") or {}).get(repo) or {}; 
have=set(entry.get("allowedUsers") or []); sys.exit(0 if want.issubset(have) 
else 1)' \
+    "${repo}" "${users_csv}" 2>/dev/null
+}
+
 dpp_onboard_repo_multi() {
   local repo="$1"
   local plugin_id="$2"
@@ -104,16 +126,18 @@ dpp_ensure_plugin_onboarded() {
     return 1
   fi
 
+  users_csv="$(dpp_allowed_users_for_repo "${repo}" "${short_name}")"
+
   if dpp_plan_has_plugin "${plugin_id}" "${HTTP_BODY}"; then
-    pp_record_pass "${plugin_id} already in partition plan"
-    return 0
+    if dpp_plan_service_has_users "${repo}" "${users_csv}" "${HTTP_BODY}"; then
+      pp_record_pass "${plugin_id} already in partition plan (allowlist OK)"
+      return 0
+    fi
+    pp_record_fail "${plugin_id} in plan but allowlist missing [${users_csv}] 
— rerun with --fresh-plan"
+    return 1
   fi
 
   version="$(pp_json_field "${HTTP_BODY}" version)"
-  users_csv="${short_name}"
-  if [[ "${repo}" == "dev_ozone" ]]; then
-    users_csv="om,ozone"
-  fi
 
   echo "  Onboard ${repo} pluginId=${plugin_id} partitions=${part_count} 
allowedUsers=[${users_csv}]..."
   if ! dpp_onboard_repo_multi "${repo}" "${plugin_id}" "${part_count}" 
"${users_csv}" "${version}"; then
@@ -167,10 +191,44 @@ curl -s -o ${outfile} -w '%{http_code}' --negotiate -u : 
-X POST \
 dpp_kafka_find_partition_for_marker() {
   local record_key="$1"
   local marker="$2"
-  local max_messages="${3:-800}"
-  local out
+  local partitions_csv="${3:-}"
+  local max_messages="${4:-800}"
+  local out found partition_id end_offset start_offset
+
+  if [[ -n "${partitions_csv}" ]]; then
+    for partition_id in $(printf '%s' "${partitions_csv}" | tr ',' ' '); do
+      [[ -n "${partition_id}" ]] || continue
+      end_offset="$(pp_kafka_run "/opt/kafka/bin/kafka-get-offsets.sh \
+        --bootstrap-server ranger-kafka.rangernw:9092 --topic '${AUDIT_TOPIC}' 
--partitions ${partition_id} --time -1" \
+        | awk -F: '{print $NF}' | tail -1)"
+      [[ -n "${end_offset}" && "${end_offset}" =~ ^[0-9]+$ ]] || continue
+      [[ "${end_offset}" -gt 0 ]] || continue
+      start_offset=$((end_offset > 120 ? end_offset - 120 : 0))
+      out="$(pp_kafka_consumer_run "timeout 25 
/opt/kafka/bin/kafka-console-consumer.sh \
+        --bootstrap-server ranger-kafka.rangernw:9092 \
+        --topic '${AUDIT_TOPIC}' \
+        --partition ${partition_id} \
+        --offset ${start_offset} \
+        --max-messages 120 \
+        --property print.key=true \
+        --timeout-ms 15000")"
+      found="$(printf '%s' "${out}" | python3 -c "
+import sys
+key, marker, pid = sys.argv[1:4]
+for line in sys.stdin:
+    if key in line and marker in line:
+        print(pid)
+        break
+" "${record_key}" "${marker}" "${partition_id}" 2>/dev/null || true)"
+      if [[ -n "${found}" ]]; then
+        echo "${found}"
+        return 0
+      fi
+    done
+    return 1
+  fi
 
-  out="$(pp_kafka_run "timeout 25 /opt/kafka/bin/kafka-console-consumer.sh \
+  out="$(pp_kafka_consumer_run "timeout 25 
/opt/kafka/bin/kafka-console-consumer.sh \
     --bootstrap-server ranger-kafka.rangernw:9092 \
     --topic '${AUDIT_TOPIC}' \
     --from-beginning \
@@ -179,13 +237,18 @@ dpp_kafka_find_partition_for_marker() {
     --property print.key=true \
     --timeout-ms 15000")"
 
-  printf '%s' "${out}" | python3 -c \
-    "import sys,re; key,marker=sys.argv[1],sys.argv[2]; \
-for line in sys.stdin: \
-  if key not in line or marker not in line: continue; \
-  m=re.search(r'Partition[:\\s]+(\\d+)', line, re.I); \
-  if m: print(m.group(1)); break" \
-    "${record_key}" "${marker}" 2>/dev/null || echo ""
+  printf '%s' "${out}" | python3 -c "
+import re
+import sys
+key, marker = sys.argv[1:3]
+for line in sys.stdin:
+    if key not in line or marker not in line:
+        continue
+    match = re.search(r'Partition[:\\s]+(\\d+)', line, re.I)
+    if match:
+        print(match.group(1))
+        break
+" "${record_key}" "${marker}" 2>/dev/null || true
 }
 
 dpp_verify_plugin_partition_routing() {
@@ -220,8 +283,12 @@ dpp_verify_plugin_partition_routing() {
   fi
   pp_record_pass "${plugin_id} POST /access HTTP ${DAEL_ACCESS_CODE}"
 
-  sleep 3
-  found="$(dpp_kafka_find_partition_for_marker "${plugin_id}" "${marker}")"
+  found=""
+  for _attempt in 1 2 3 4 5; do
+    sleep 4
+    found="$(dpp_kafka_find_partition_for_marker "${plugin_id}" "${marker}" 
"${assigned}")"
+    [[ -n "${found}" ]] && break
+  done
   if [[ -z "${found}" ]]; then
     pp_record_fail "${plugin_id} audit not found on Kafka (key=${plugin_id}, 
marker=${marker}) — check ACLs or dispatcher lag"
     return 1
@@ -256,3 +323,28 @@ dpp_optional_harness_triggers() {
   dpp_run_harness_plugin_smoke 
"${docker_dir}/scripts/knox/trigger-knox-plugin-audit-e2e.sh" "Knox"
   dpp_run_harness_plugin_smoke 
"${docker_dir}/scripts/hbase/trigger-hbase-plugin-audit-e2e.sh" "HBase"
 }
+
+# Solr dispatcher Kerberos can break after ingestor/topic restarts; restart 
before HDFS pipeline smoke.
+dpp_ensure_solr_dispatcher_ready() {
+  local container="ranger-audit-dispatcher-solr"
+  local 
site="/opt/ranger/audit-dispatcher/conf/ranger-audit-dispatcher-solr-site.xml"
+  local keytab="/etc/keytabs/rangerauditserver.keytab"
+  local 
principal="rangerauditserver/[email protected]"
+
+  if ! docker ps --filter "name=^${container}$" --filter status=running -q | 
grep -q .; then
+    pp_record_fail "Solr dispatcher not running"
+    return 1
+  fi
+  if docker exec "${container}" test -f "${site}" 2>/dev/null; then
+    pp_patch_site_prop "${container}" "${site}" 
"xasecure.audit.jaas.Client.option.useTicketCache" "false" || true
+  fi
+  echo "  restarting ${container} (Solr Kerberos + consumer reset)..."
+  docker restart "${container}" >/dev/null
+  sleep 30
+  if docker exec "${container}" bash -c "kinit -kt '${keytab}' '${principal}'" 
>/dev/null 2>&1 \
+    && curl -sf "http://localhost:7091/api/health/ping"; >/dev/null 2>&1; then
+    return 0
+  fi
+  pp_record_fail "Solr dispatcher not healthy after restart"
+  return 1
+}
diff --git a/dev-support/ranger-docker/scripts/audit/partition-plan-e2e-lib.sh 
b/dev-support/ranger-docker/scripts/audit/partition-plan-e2e-lib.sh
index ab3035a17..30ce6f059 100755
--- a/dev-support/ranger-docker/scripts/audit/partition-plan-e2e-lib.sh
+++ b/dev-support/ranger-docker/scripts/audit/partition-plan-e2e-lib.sh
@@ -199,7 +199,7 @@ pp_patch_site_prop() {
   local site="$2"
   local prop="$3"
   local value="$4"
-  docker exec -u root "${container}" python3 - "${site}" "${prop}" "${value}" 
<<'PY'
+  docker exec -i -u root "${container}" python3 - "${site}" "${prop}" 
"${value}" <<'PY'
 import sys
 import xml.etree.ElementTree as ET
 
@@ -226,6 +226,21 @@ tree.write(path, encoding="unicode", xml_declaration=False)
 PY
 }
 
+pp_ensure_audit_partitioner_for_dynamic() {
+  local container="${1:-${CONTAINER}}"
+  local seed_plugin="${2:-${PP_DYNAMIC_PRODUCER_PLUGIN:-hdfs}}"
+  local plugins
+  plugins="$(pp_read_site_prop "${container}" 
"ranger.audit.ingestor.kafka.configured.plugins")"
+  if [[ -n "${plugins}" ]]; then
+    return 0
+  fi
+  for site in "${SITE_XMLS[@]}"; do
+    if docker exec "${container}" test -f "${site}" 2>/dev/null; then
+      pp_patch_site_prop "${container}" "${site}" 
"ranger.audit.ingestor.kafka.configured.plugins" "${seed_plugin}"
+    fi
+  done
+}
+
 pp_set_dynamic_enabled() {
   local container="$1"
   local value="$2"
@@ -238,6 +253,9 @@ pp_set_dynamic_enabled() {
     pp_patch_site_prop "${container}" "${site}" "${PROP_DYNAMIC}" "${value}"
     changed=true
   done
+  if [[ "${value}" == "true" ]]; then
+    pp_ensure_audit_partitioner_for_dynamic "${container}"
+  fi
   if [[ "${changed}" == "true" && "${restart}" == "true" ]]; then
     echo "  Restarting ${container} (dynamic=${value})..."
     PP_RESTART_SINCE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
@@ -298,6 +316,51 @@ ${script} --command-config 
/tmp/pp-kafka-client.properties" \
     2>/dev/null || true
 }
 
+# kafka-console-consumer uses --consumer.config (not --command-config).
+pp_kafka_consumer_run() {
+  local script="$1"
+  docker exec "${KAFKA_CONTAINER}" bash -c \
+    "cat > /tmp/pp-kafka-client.properties <<'EOF'
+security.protocol=SASL_PLAINTEXT
+sasl.kerberos.service.name=kafka
+EOF
+cat > /tmp/pp-kafka-client-jaas.conf <<EOF
+KafkaClient {
+  com.sun.security.auth.module.Krb5LoginModule required
+  useKeyTab=true
+  storeKey=true
+  keyTab=\"${KAFKA_KEYTAB}\"
+  principal=\"${KAFKA_PRINCIPAL}\";
+};
+EOF
+export 
KAFKA_OPTS=\"-Djava.security.auth.login.config=/tmp/pp-kafka-client-jaas.conf\"
+${script} --consumer.config /tmp/pp-kafka-client.properties" \
+    2>/dev/null || true
+}
+
+
+pp_stop_audit_stack_consumers() {
+  docker stop ranger-audit-ingestor ranger-audit-dispatcher-solr 
ranger-audit-dispatcher-hdfs 2>/dev/null || true
+}
+
+pp_start_audit_stack_consumers() {
+  docker start ranger-audit-ingestor ranger-audit-dispatcher-solr 
ranger-audit-dispatcher-hdfs 2>/dev/null || true
+}
+
+pp_kafka_wait_topic_absent() {
+  local topic="$1"
+  local timeout="${2:-180}"
+  local deadline=$((SECONDS + timeout))
+  while (( SECONDS < deadline )); do
+    if ! pp_kafka_run "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 
ranger-kafka.rangernw:9092 --list" | grep -qx "${topic}"; then
+      return 0
+    fi
+    sleep 5
+  done
+  echo "ERROR: Kafka topic '${topic}' still present after ${timeout}s" >&2
+  return 1
+}
+
 pp_kafka_topic_partition_count() {
   local topic="$1"
   local out
@@ -306,8 +369,11 @@ pp_kafka_topic_partition_count() {
 }
 
 pp_delete_plan_topic() {
+  pp_stop_audit_stack_consumers
   pp_kafka_run "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 
ranger-kafka.rangernw:9092 --delete --topic '${PLAN_TOPIC}'"
-  sleep 3
+  pp_kafka_wait_topic_absent "${PLAN_TOPIC}" 180 || true
+  pp_start_audit_stack_consumers
+  sleep 10
 }
 
 # Greenfield bootstrap requires plan.topicPartitionCount == Kafka 
ranger_audits partitions.
@@ -321,10 +387,15 @@ pp_prepare_audit_topic_for_greenfield() {
     return 0
   fi
   echo "  Recreating ${AUDIT_TOPIC} (${actual} partitions -> ${expected} for 
site XML layout)..."
+  pp_stop_audit_stack_consumers
   pp_kafka_run "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 
ranger-kafka.rangernw:9092 --delete --topic '${AUDIT_TOPIC}'"
-  sleep 5
+  pp_kafka_run "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 
ranger-kafka.rangernw:9092 --delete --topic '${PLAN_TOPIC}'" || true
+  pp_kafka_wait_topic_absent "${AUDIT_TOPIC}" 180
+  pp_kafka_wait_topic_absent "${PLAN_TOPIC}" 180 || true
   pp_kafka_run "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 
ranger-kafka.rangernw:9092 --create --topic '${AUDIT_TOPIC}' --partitions 
${expected} --replication-factor 1"
   sleep 3
+  pp_start_audit_stack_consumers
+  sleep 10
 }
 
 pp_seed_plan_json() {
diff --git 
a/dev-support/ranger-docker/scripts/audit/verify-hdfs-dynamic-partition-e2e.sh 
b/dev-support/ranger-docker/scripts/audit/verify-hdfs-dynamic-partition-e2e.sh
new file mode 100755
index 000000000..6c2a1538f
--- /dev/null
+++ 
b/dev-support/ranger-docker/scripts/audit/verify-hdfs-dynamic-partition-e2e.sh
@@ -0,0 +1,189 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+# HDFS-focused dynamic partition E2E:
+#   1. Enable dynamic mode (greenfield buffer-only plan)
+#   2. POST /partition-plan/services — dev_hdfs + hdfs plugin + allowlist
+#   3. Verify POST /access + Kafka partition routing
+#   4. PATCH /partition-plan/plugins/hdfs — expand partition count
+#   5. Verify routing again after scale
+#
+# Usage:
+#   cd dev-support/ranger-docker
+#   ./scripts/audit/verify-hdfs-dynamic-partition-e2e.sh
+#   ./scripts/audit/verify-hdfs-dynamic-partition-e2e.sh --with-hdfs-trigger
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "${SCRIPT_DIR}"
+
+# shellcheck source=partition-plan-e2e-lib.sh
+source "${SCRIPT_DIR}/scripts/audit/partition-plan-e2e-lib.sh"
+# shellcheck source=dynamic-auth-to-local-e2e-lib.sh
+source "${SCRIPT_DIR}/scripts/audit/dynamic-auth-to-local-e2e-lib.sh"
+# shellcheck source=dynamic-partition-plugin-e2e-lib.sh
+source "${SCRIPT_DIR}/scripts/audit/dynamic-partition-plugin-e2e-lib.sh"
+
+readonly 
HDFS_SPEC="dev_hdfs|hdfs|hdfs|2|ranger-hadoop|/etc/keytabs/hdfs.keytab|hdfs/[email protected]"
+SCALE_ADDITIONAL="${SCALE_ADDITIONAL:-2}"
+TIMEOUT=300
+FRESH_PLAN=false
+WITH_HDFS_TRIGGER=false
+
+while [[ $# -gt 0 ]]; do
+  case "$1" in
+    --fresh-plan) FRESH_PLAN=true; shift ;;
+    --with-hdfs-trigger) WITH_HDFS_TRIGGER=true; shift ;;
+    --scale-additional) SCALE_ADDITIONAL="${2:?}"; shift 2 ;;
+    --timeout) TIMEOUT="${2:?}"; shift 2 ;;
+    -h|--help)
+      sed -n '26,29p' "$0"
+      exit 0
+      ;;
+    *)
+      echo "Unknown option: $1" >&2
+      exit 1
+      ;;
+  esac
+done
+
+hdfs_scale_plugin() {
+  local plugin_id="$1"
+  local additional="$2"
+  local version="$3"
+  local url body before after attempt
+
+  pp_ingestor_request "${CONTAINER}" GET "${PLAN_URL}"
+  before="$(dpp_plan_plugin_partitions "${plugin_id}" "${HTTP_BODY}")"
+
+  url="${PLAN_URL}/plugins/${plugin_id}"
+  
body="{\"additionalPartitions\":${additional},\"expectedVersion\":${version}}"
+  for attempt in 1 2 3; do
+    echo "  PATCH ${url} (+${additional} partitions, 
expectedVersion=${version}, attempt ${attempt})..."
+    pp_ingestor_request "${CONTAINER}" PATCH "${url}" "${body}"
+    if [[ "${HTTP_CODE}" == "200" ]]; then
+      after="$(dpp_plan_plugin_partitions "${plugin_id}" "${HTTP_BODY}")"
+      pp_record_pass "scale ${plugin_id} partitions [${before}] -> [${after}] 
plan v$(pp_json_field "${HTTP_BODY}" version)"
+      if [[ "$(echo "${before}" | tr ',' '\n' | wc -l | tr -d ' ')" -ge 
"$(echo "${after}" | tr ',' '\n' | wc -l | tr -d ' ')" ]]; then
+        pp_record_fail "scale ${plugin_id} did not increase partition count"
+        return 1
+      fi
+      return 0
+    fi
+    if [[ "${HTTP_CODE}" == "503" && "${attempt}" -lt 3 ]]; then
+      echo "  scale returned 503 (Kafka grow timeout?) — retrying in 15s..."
+      sleep 15
+      version="$(dpp_plan_version)"
+      
body="{\"additionalPartitions\":${additional},\"expectedVersion\":${version}}"
+      continue
+    fi
+    pp_record_fail "scale ${plugin_id} expected 200, got ${HTTP_CODE}: 
${HTTP_BODY}"
+    return 1
+  done
+  return 1
+}
+
+echo "=== HDFS dynamic partition REST + routing E2E ==="
+if [[ -x "${SCRIPT_DIR}/scripts/audit/ensure-audit-ingestor-plugin-users.sh" 
]]; then
+  "${SCRIPT_DIR}/scripts/audit/ensure-audit-ingestor-plugin-users.sh" || true
+fi
+for site in "${SITE_XMLS[@]}"; do
+  if docker exec "${CONTAINER}" test -f "${site}" 2>/dev/null; then
+    pp_patch_site_prop "${CONTAINER}" "${site}" 
"ranger.audit.ingestor.service.dev_hdfs.allowed.users" "hdfs,nn"
+  fi
+done
+pp_preflight_tier3 "${TIMEOUT}"
+
+if ! dael_container_running "ranger-hadoop"; then
+  echo "ERROR: ranger-hadoop is not running. Start HDFS stack first, e.g.:" >&2
+  echo "  ./setup-audit-e2e.sh up --hdfs-only --no-verify" >&2
+  exit 1
+fi
+
+PLAN_URL="$(pp_ingestor_plan_url "${CONTAINER}")"
+
+echo ""
+echo "=== Step 1: Enable dynamic partition plan ==="
+if [[ "${FRESH_PLAN}" == "true" ]]; then
+  echo "  Fresh plan requested — resetting plan topic and greenfield audit 
topic layout..."
+  pp_set_dynamic_enabled "${CONTAINER}" "false" "false" || true
+  pp_prepare_audit_topic_for_greenfield "${CONTAINER}"
+  pp_delete_plan_topic
+fi
+if ! pp_dynamic_enabled "${CONTAINER}" "${SITE_XMLS[0]}"; then
+  pp_prepare_audit_topic_for_greenfield "${CONTAINER}"
+  if [[ "${FRESH_PLAN}" == "true" ]]; then
+    pp_delete_plan_topic
+  fi
+  pp_set_dynamic_enabled "${CONTAINER}" "true" || { pp_record_fail "enable 
dynamic"; pp_print_results; exit 1; }
+  pp_wait_health 7081 "Ingestor after enable" "${TIMEOUT}" || { pp_record_fail 
"health"; pp_print_results; exit 1; }
+else
+  pp_record_pass "dynamic mode already enabled"
+fi
+
+pp_wait_watcher "${CONTAINER}" "${TIMEOUT}" || { pp_record_fail "watcher"; 
pp_print_results; exit 1; }
+pp_record_pass "PartitionPlanWatcher ready"
+
+echo ""
+echo "=== Step 2: POST /partition-plan/services (dev_hdfs + allowlist) ==="
+dpp_ensure_plugin_onboarded "${HDFS_SPEC}" || { pp_print_results; exit 1; }
+
+echo ""
+echo "=== Step 3: E2E after onboard — POST /access + Kafka partition routing 
==="
+dpp_verify_plugin_partition_routing "${HDFS_SPEC}" || true
+
+if [[ "${WITH_HDFS_TRIGGER}" == "true" && -x 
"${SCRIPT_DIR}/setup-audit-e2e.sh" ]]; then
+  echo ""
+  echo "=== Step 3b: Full HDFS audit pipeline (dfs smoke) ==="
+  dpp_ensure_solr_dispatcher_ready || true
+  if "${SCRIPT_DIR}/setup-audit-e2e.sh" trigger-hdfs-audit --no-verify 
2>/dev/null; then
+    pp_record_pass "trigger-hdfs-audit pipeline"
+  else
+    pp_record_fail "trigger-hdfs-audit pipeline"
+  fi
+fi
+
+echo ""
+echo "=== Step 4: PATCH /partition-plan/plugins/hdfs (expand partitions) ==="
+version="$(dpp_plan_version)"
+if [[ -z "${version}" ]]; then
+  pp_record_fail "could not read plan version before scale"
+  pp_print_results
+  exit 1
+fi
+hdfs_scale_plugin "hdfs" "${SCALE_ADDITIONAL}" "${version}" || true
+dael_wait_auth_to_local_applied "${CONTAINER}" 45 && pp_record_pass 
"auth_to_local after scale" || pp_record_fail "auth_to_local after scale"
+echo "  waiting 10s for ingestor partitioner metadata after scale..."
+sleep 10
+
+echo ""
+echo "=== Step 5: E2E after scale — POST /access + Kafka partition routing ==="
+dpp_verify_plugin_partition_routing "${HDFS_SPEC}" || true
+
+if [[ "${WITH_HDFS_TRIGGER}" == "true" && -x 
"${SCRIPT_DIR}/setup-audit-e2e.sh" ]]; then
+  echo ""
+  echo "=== Step 5b: Full HDFS audit pipeline after scale ==="
+  dpp_ensure_solr_dispatcher_ready || true
+  if "${SCRIPT_DIR}/setup-audit-e2e.sh" trigger-hdfs-audit --no-verify 
2>/dev/null; then
+    pp_record_pass "trigger-hdfs-audit after scale"
+  else
+    pp_record_fail "trigger-hdfs-audit after scale"
+  fi
+fi
+
+pp_print_results
+[[ "${PP_FAIL}" -eq 0 ]]

Reply via email to