[ 
https://issues.apache.org/jira/browse/RANGER-5655?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Ramachandran Krishnan updated RANGER-5655:
------------------------------------------
    Description: 
Implement a *dynamic unified ingestor registry* for Ranger audit-ingestor so 
operators can change Kafka partition routing and per-repo service allowlists at 
runtime — without restarting ingestor pods.

The registry is stored in a Kafka compacted topic 
({{{}ranger_audit_partition_plan{}}}) and managed via REST 
({{{}/api/audit/partition-plan{}}}). All ingestor replicas converge on the same 
versioned plan through a background watcher; {{AuditPartitioner}} routes audit 
records on the hot path from in-memory state only.

Feature flag (default off): 
{{ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled=false}}

 
----
h2. *Problem*

Today (static mode), audit-ingestor loads two kinds of configuration from XML 
at startup only:
||Job||Question||Static behavior||
|Service allowlist|May this Kerberos principal POST audits for repo 
{*}R{*}?|{{ranger.audit.ingestor.service.<repo>.allowed.users}} in site XML|
|Partition routing|After accept, which {{ranger_audits}} 
partition?|{{kafka.configured.plugins}} + per-plugin overrides in site XML|

Changing either requires editing XML and {*}restarting every ingestor 
replica{*}. Contiguous-range static allocation can also reshuffle later plugins 
when an early plugin's partition count changes.

*Goals:*
 * Onboard new plugins/repos and scale hot plugins without ingestor restart
 * Append-only partition growth (no reshuffle of existing plugin assignments)
 * One shared source of truth across all ingestor pods
 * No new infra (no Postgres / ZooKeeper for the registry)

----
h2. Solution

Introduce a *unified partition plan document* (versioned JSON) in Kafka topic 
{{ranger_audit_partition_plan}} (1 partition, compacted). One document holds:
 * {{plugins}} — dedicated partition IDs per plugin id (Kafka record key / 
agent id)
 * {{buffer}} — partition pool for not-yet-promoted plugins (sticky hash)
 * {{services}} — per-repo {{allowedUsers}} for {{POST /api/audit/access}}
 * {{topicPartitionCount}} — must match live {{ranger_audits}} partition count
 * {{version}} — optimistic locking for REST mutations

*Control plane:* REST + registry topic + {{PartitionPlanWatcher}}
*Data plane:* plugins POST {{/api/audit/access}} → allowlist check → 
{{AuditPartitioner}} → {{ranger_audits}}

Solr/HDFS dispatchers are unchanged; they consume all partitions of 
{{{}ranger_audits{}}}.
----
h2. Key deliverables
 * *Kafka registry* — {{{}KafkaPartitionPlanRegistry{}}}, bootstrap from XML on 
greenfield, brownfield pre-seed support
 * *REST API* — {{{}GET/PATCH /partition-plan{}}}, {{{}POST 
/partition-plan/plugins{}}}, {{{}POST /partition-plan/services{}}}, {{{}PATCH 
/partition-plan/plugins/{pluginId{}}}} with {{expectedVersion}} (409 on stale)
 * *Dynamic partitioner* — {{AuditPartitioner}} reads 
{{{}PartitionPlanHolder{}}}; round-robin for promoted plugins; buffer sticky 
hash for unknown plugins; post-scale routing uses {{max(cluster, plan)}} when 
metadata lags
 * *Unified allowlist* — {{services}} map in same registry; {{auth_to_local}} 
rules recomposed from allowlists in dynamic mode
 * *Topic grow* — grow {{ranger_audits}} before registry write when 
promoting/scaling
 * *E2E harness* — Docker Tier 3 scripts for partition-plan REST, plugin 
onboard/routing, auth_to_local, full plugin→Solr pipelines (HDFS, Ozone, Hive, 
etc.)
 * *Documentation* — implementation guide, ops runbook, brownfield migration, 
E2E test plan

----
h2. Configuration
{code:xml}
<property>
  <name>ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled</name>
  <value>true</value>
</property>
<property>
  <name>ranger.audit.ingestor.kafka.partition.plan.topic</name>
  <value>ranger_audit_partition_plan</value>
</property>
{code}
When dynamic mode is on and registry is empty, the first ingestor pod 
bootstraps an initial plan from existing XML properties 
({{{}kafka.configured.plugins{}}}, buffer/per-plugin counts, service 
allowlists).
----
h2. Example plan JSON
{code:json}
{
  "topic": "ranger_audits",
  "version": 12,
  "topicPartitionCount": 48,
  "plugins": {
    "hdfs":        { "partitions": [0, 1, 2, 3, 4, 5] },
    "hiveServer2": { "partitions": [6, 7, 8, 9, 10, 11] }
  },
  "buffer": { "partitions": [12, 13, "..."] },
  "services": {
    "dev_hive":  { "allowedUsers": ["hive"] },
    "dev_ozone": { "allowedUsers": ["om", "ozone"] },
    "dev_hdfs":  { "allowedUsers": ["hdfs", "nn"] }
  }
}
{code}
----
h2. Testing
h3. Validated scenarios
h4. 1. Static vs dynamic mode

What it proves: The feature flag works and existing deployments are not broken.
||Mode||Config||Expected behavior||
|Static (default)|{{kafka.partition.plan.dynamic.enabled=false}}|Routing and 
allowlists come from XML at startup only. {{GET /api/audit/partition-plan}} 
returns 503. Ingestor health is 200. HDFS/plugin audits still flow to Solr.|
|Dynamic|{{dynamic.enabled=true}}|Ingestor starts {{{}PartitionPlanWatcher{}}}, 
reads plan from Kafka topic {{{}ranger_audit_partition_plan{}}}, and routes 
audits from in-memory plan. {{GET /partition-plan}} returns JSON.|

 
----
h4. 2. Greenfield bootstrap

What it proves: On a new cluster with an empty plan topic, the first ingestor 
pod creates version 1 of the partition plan automatically — no manual REST call.

Flow:
 # Enable dynamic mode; restart ingestor.
 # Plan topic {{ranger_audit_partition_plan}} is created (1 partition, 
compacted).
 # Ingestor publishes v1 from existing XML ({{{}kafka.configured.plugins{}}}, 
buffer size, per-plugin overrides, service allowlists).
 # Plan’s {{topicPartitionCount}} matches live {{ranger_audits}} partition 
count (e.g. 48 = 13 plugins × 3 + 9 buffer on full lab layout).

Success: {{GET /partition-plan}} shows v1, correct plugin list, partition count 
aligned with Kafka. Audits still work after enable.
----
h4. 3. REST promote / scale (200 / 409 / 400)

What it proves: Operators can change routing at runtime via REST without 
restarting ingestor. The API rejects bad requests.
||Action||REST||Success||Failure cases||
|Promote new plugin (e.g. {{{}storm{}}}) from buffer → dedicated 
partitions|{{POST /partition-plan/plugins}}|200, {{version}} increments, plugin 
in {{plugins}} map|400 if plugin already promoted (e.g. {{hdfs}} twice)|
|Scale hot plugin (+N partitions)|{{{}PATCH 
/partition-plan/plugins/{pluginId{}}}}|200, new partition IDs appended at tail; 
{{ranger_audits}} grown first if needed|400 if scaling a buffer-only plugin|
|Stale edit|Any mutation with wrong {{expectedVersion}}|—|409 + current plan 
body (forces refresh and retry)|
|Invalid plan|Overlapping partitions, reshuffle existing IDs, {{partitionCount: 
0}}|—|400|

Key property: Changes are append-only — existing plugin partition lists are 
never reshuffled.
----
h4. 4. Multi-pod convergence

What it proves: In a real cluster with multiple ingestor replicas behind a load 
balancer, all pods see the same plan after an admin change.

Flow:
 # Primary ingestor on :7081, second replica on :7082 (same Kafka plan topic, 
different Kerberos identity).
 # Both pods report the same {{version}} at startup.
 # Admin promotes a plugin on primary only.
 # Within one watcher cycle (~30s), replica on :7082 shows the same new version 
without any REST call on the replica.

Success: No drift between pods; routing is consistent cluster-wide.
----
h4. 5. Brownfield pre-seed

What it proves: Existing production clusters can cut over to dynamic mode 
safely by writing the plan into Kafka before enabling the feature — ingestor 
must not overwrite it with a fresh XML bootstrap.

Flow (Path A migration):
 # Capture current plan JSON while dynamic is briefly on.
 # Turn dynamic off; delete plan topic (simulate “still on static”).
 # Operator pre-seeds plan to Kafka with {{version=1}} and marker 
{{{}updatedBy=brownfield-e2e-seed{}}}.
 # Enable dynamic; restart ingestor.
 # Ingestor adopts pre-seeded plan — not auto-{{{}bootstrap{}}} from XML.
 # Rollback: turn dynamic off → partition-plan API 503, health 200, audits 
still OK in static mode.

----
h4. 6. Plugin onboard + Kafka partition routing

What it proves: A real plugin can be onboarded via REST and its audit events 
land on the correct Kafka partitions defined in the plan.

Flow:
 # Enable dynamic mode (greenfield buffer-only layout).
 # For each running plugin container (HDFS, Ozone, Hive, etc.): {{POST 
/partition-plan/services}} with {{{}serviceName{}}}, {{{}pluginId{}}}, 
{{{}partitionCount{}}}, {{{}allowedUsers{}}}.
 # Plugin authenticates with Kerberos and {{{}POST /api/audit/access{}}}.
 # Read Kafka record for that event → partition number must be in the plugin’s 
assigned list in the plan (not buffer, after promote).

----
h4. 7. auth_to_local recomposition

What it proves: The unified registry {{services}} map controls who may POST 
audits, and Kerberos principals are mapped to short usernames correctly.

Flow (per plugin repo):
 # Plugin calls {{POST /access}} with Kerberos principal (e.g. 
{{{}hdfs/[email protected]{}}}) → ingestor maps via 
{{auth_to_local}} → short name {{hdfs}} → 200 if in 
{{{}services[dev_hdfs].allowedUsers{}}}.
 # Remove user from allowlist via {{PATCH /partition-plan}} (services delta) → 
same principal gets 403.
 # Re-add allowlist → 200 again.
 # Cross-repo denial: HDFS principal posting to {{dev_kms}} repo → 403.

Why it matters: In dynamic mode, allowlists live in the registry (not XML 
restart). {{auth_to_local}} rules must stay in sync with 
{{{}services[].allowedUsers{}}}.
----
h4. 8. HDFS / Ozone / Hive full audit pipelines

What it proves: Dynamic partition plan changes do not break the end-to-end 
audit path: plugin → ingestor → Kafka → dispatcher → Solr (and Admin Audit UI 
when {{{}audit_store=solr{}}}).
||Plugin||What is exercised||
|HDFS|Real NameNode operation → plugin audit → ingestor → correct partition → 
Solr doc|
|Ozone|OM principal ({{{}om{}}}, {{{}ozone{}}}) → {{dev_ozone}} repo → full 
pipeline|
|Hive|HS2 principal ({{{}hive{}}}) → {{dev_hive}} repo → full pipeline|

Validated after: promote/scale (partition plan changed live), not only at 
bootstrap.

Verify Solr:
curl -s 
'http://localhost:8983/solr/ranger_audits/select?q=repo:dev_hdfs&rows=3&wt=json'
----
h3. Summary table (for Jira)
||Scenario||Plain question||How verified||Pass criteria||
|Static vs dynamic|Does default-off behavior still work?|Feature flag + REST 
503/200|Static unchanged; dynamic enables plan API|
|Greenfield bootstrap|Who creates v1 on empty topic?|First ingestor start|Plan 
v1 in Kafka; counts match|
|REST promote/scale|Can ops change routing live?|REST mutations|200 on valid; 
409 stale; 400 illegal|
|Multi-pod convergence|Do all replicas agree?|2 ingestors, promote on one|Same 
version ≤35s on both|
|Brownfield pre-seed|Safe production cutover?|Pre-write plan, then 
enable|Pre-seed preserved; rollback OK|
|Plugin onboard + routing|Do audits hit right partitions?|POST access + Kafka 
inspect|Partition ∈ plan list|
|auth_to_local|Does allowlist enforcement work?|Allow/deny via services 
map|200/403 per principal+repo|
|Full pipelines|Is audit delivery intact?|HDFS/Ozone/Hive → Solr|Docs in 
{{ranger_audits}} Solr collection|
h2. Acceptance criteria
 # With {{{}dynamic.enabled=false{}}}, behavior matches existing static XML 
partitioning; {{GET /partition-plan}} returns 503
 # With {{{}dynamic.enabled=true{}}}, registry topic created (1 partition, 
compacted); bootstrap plan published on greenfield
 # REST promote/scale updates plan version; stale {{expectedVersion}} returns 
409
 # All ingestor replicas converge to same plan within watcher refresh interval
 # Audits for promoted plugin land only on assigned {{ranger_audits}} partitions
 # {{POST /partition-plan/services}} updates allowlist; unauthorized principal 
returns 403
 # Post-scale routing works when Kafka metadata lags plan 
({{{}AuditPartitioner{}}} bound logic)

  was:
Implement a *dynamic unified ingestor registry* for Ranger audit-ingestor so 
operators can change Kafka partition routing and per-repo service allowlists at 
runtime — without restarting ingestor pods.

The registry is stored in a Kafka compacted topic 
({{{}ranger_audit_partition_plan{}}}) and managed via REST 
({{{}/api/audit/partition-plan{}}}). All ingestor replicas converge on the same 
versioned plan through a background watcher; {{AuditPartitioner}} routes audit 
records on the hot path from in-memory state only.

Feature flag (default off): 
{{ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled=false}}

 
----
h2. *Problem*

Today (static mode), audit-ingestor loads two kinds of configuration from XML 
at startup only:
||Job||Question||Static behavior||
|Service allowlist|May this Kerberos principal POST audits for repo 
{*}R{*}?|{{ranger.audit.ingestor.service.<repo>.allowed.users}} in site XML|
|Partition routing|After accept, which {{ranger_audits}} 
partition?|{{kafka.configured.plugins}} + per-plugin overrides in site XML|

Changing either requires editing XML and {*}restarting every ingestor 
replica{*}. Contiguous-range static allocation can also reshuffle later plugins 
when an early plugin's partition count changes.

*Goals:*
 * Onboard new plugins/repos and scale hot plugins without ingestor restart
 * Append-only partition growth (no reshuffle of existing plugin assignments)
 * One shared source of truth across all ingestor pods
 * No new infra (no Postgres / ZooKeeper for the registry)

----
h2. Solution

Introduce a *unified partition plan document* (versioned JSON) in Kafka topic 
{{ranger_audit_partition_plan}} (1 partition, compacted). One document holds:
 * {{plugins}} — dedicated partition IDs per plugin id (Kafka record key / 
agent id)
 * {{buffer}} — partition pool for not-yet-promoted plugins (sticky hash)
 * {{services}} — per-repo {{allowedUsers}} for {{POST /api/audit/access}}
 * {{topicPartitionCount}} — must match live {{ranger_audits}} partition count
 * {{version}} — optimistic locking for REST mutations

*Control plane:* REST + registry topic + {{PartitionPlanWatcher}}
*Data plane:* plugins POST {{/api/audit/access}} → allowlist check → 
{{AuditPartitioner}} → {{ranger_audits}}

Solr/HDFS dispatchers are unchanged; they consume all partitions of 
{{{}ranger_audits{}}}.
----
h2. Key deliverables
 * *Kafka registry* — {{{}KafkaPartitionPlanRegistry{}}}, bootstrap from XML on 
greenfield, brownfield pre-seed support
 * *REST API* — {{{}GET/PATCH /partition-plan{}}}, {{{}POST 
/partition-plan/plugins{}}}, {{{}POST /partition-plan/services{}}}, {{{}PATCH 
/partition-plan/plugins/{pluginId{}}}} with {{expectedVersion}} (409 on stale)
 * *Dynamic partitioner* — {{AuditPartitioner}} reads 
{{{}PartitionPlanHolder{}}}; round-robin for promoted plugins; buffer sticky 
hash for unknown plugins; post-scale routing uses {{max(cluster, plan)}} when 
metadata lags
 * *Unified allowlist* — {{services}} map in same registry; {{auth_to_local}} 
rules recomposed from allowlists in dynamic mode
 * *Topic grow* — grow {{ranger_audits}} before registry write when 
promoting/scaling
 * *E2E harness* — Docker Tier 3 scripts for partition-plan REST, plugin 
onboard/routing, auth_to_local, full plugin→Solr pipelines (HDFS, Ozone, Hive, 
etc.)
 * *Documentation* — implementation guide, ops runbook, brownfield migration, 
E2E test plan

----
h2. Configuration
{code:xml}
<property>
  <name>ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled</name>
  <value>true</value>
</property>
<property>
  <name>ranger.audit.ingestor.kafka.partition.plan.topic</name>
  <value>ranger_audit_partition_plan</value>
</property>
{code}
When dynamic mode is on and registry is empty, the first ingestor pod 
bootstraps an initial plan from existing XML properties 
({{{}kafka.configured.plugins{}}}, buffer/per-plugin counts, service 
allowlists).
----
h2. Example plan JSON
{code:json}
{
  "topic": "ranger_audits",
  "version": 12,
  "topicPartitionCount": 48,
  "plugins": {
    "hdfs":        { "partitions": [0, 1, 2, 3, 4, 5] },
    "hiveServer2": { "partitions": [6, 7, 8, 9, 10, 11] }
  },
  "buffer": { "partitions": [12, 13, "..."] },
  "services": {
    "dev_hive":  { "allowedUsers": ["hive"] },
    "dev_ozone": { "allowedUsers": ["om", "ozone"] },
    "dev_hdfs":  { "allowedUsers": ["hdfs", "nn"] }
  }
}
{code}
----
h2. Testing

 
h3. Validated scenarios
h4. 1. Static vs dynamic mode

What it proves: The feature flag works and existing deployments are not broken.
||Mode||Config||Expected behavior||
|Static (default)|{{kafka.partition.plan.dynamic.enabled=false}}|Routing and 
allowlists come from XML at startup only. {{GET /api/audit/partition-plan}} 
returns 503. Ingestor health is 200. HDFS/plugin audits still flow to Solr.|
|Dynamic|{{dynamic.enabled=true}}|Ingestor starts {{{}PartitionPlanWatcher{}}}, 
reads plan from Kafka topic {{{}ranger_audit_partition_plan{}}}, and routes 
audits from in-memory plan. {{GET /partition-plan}} returns JSON.|

Script: {{verify-partition-plan-e2e.sh --static-only}} then {{--dynamic 
--restore-static}}
----
h4. 2. Greenfield bootstrap

What it proves: On a new cluster with an empty plan topic, the first ingestor 
pod creates version 1 of the partition plan automatically — no manual REST call.

Flow:
 # Enable dynamic mode; restart ingestor.
 # Plan topic {{ranger_audit_partition_plan}} is created (1 partition, 
compacted).
 # Ingestor publishes v1 from existing XML ({{{}kafka.configured.plugins{}}}, 
buffer size, per-plugin overrides, service allowlists).
 # Plan’s {{topicPartitionCount}} matches live {{ranger_audits}} partition 
count (e.g. 48 = 13 plugins × 3 + 9 buffer on full lab layout).

Success: {{GET /partition-plan}} shows v1, correct plugin list, partition count 
aligned with Kafka. Audits still work after enable.
----
h4. 3. REST promote / scale (200 / 409 / 400)

What it proves: Operators can change routing at runtime via REST without 
restarting ingestor. The API rejects bad requests.
||Action||REST||Success||Failure cases||
|Promote new plugin (e.g. {{{}storm{}}}) from buffer → dedicated 
partitions|{{POST /partition-plan/plugins}}|200, {{version}} increments, plugin 
in {{plugins}} map|400 if plugin already promoted (e.g. {{hdfs}} twice)|
|Scale hot plugin (+N partitions)|{{PATCH 
/partition-plan/plugins/\{pluginId}}}|200, new partition IDs appended at tail; 
{{ranger_audits}} grown first if needed|400 if scaling a buffer-only plugin|
|Stale edit|Any mutation with wrong {{expectedVersion}}|—|409 + current plan 
body (forces refresh and retry)|
|Invalid plan|Overlapping partitions, reshuffle existing IDs, {{partitionCount: 
0}}|—|400|

Key property: Changes are append-only — existing plugin partition lists are 
never reshuffled.

Script: {{verify-partition-plan-e2e.sh --dynamic}}
----
h4. 4. Multi-pod convergence

What it proves: In a real cluster with multiple ingestor replicas behind a load 
balancer, all pods see the same plan after an admin change.

Flow:
 # Primary ingestor on :7081, second replica on :7082 (same Kafka plan topic, 
different Kerberos identity).
 # Both pods report the same {{version}} at startup.
 # Admin promotes a plugin on primary only.
 # Within one watcher cycle (~30s), replica on :7082 shows the same new version 
without any REST call on the replica.

Success: No drift between pods; routing is consistent cluster-wide.

Script: {{verify-partition-plan-multipod-e2e.sh}}
----
h4. 5. Brownfield pre-seed

What it proves: Existing production clusters can cut over to dynamic mode 
safely by writing the plan into Kafka before enabling the feature — ingestor 
must not overwrite it with a fresh XML bootstrap.

Flow (Path A migration):
 # Capture current plan JSON while dynamic is briefly on.
 # Turn dynamic off; delete plan topic (simulate “still on static”).
 # Operator pre-seeds plan to Kafka with {{version=1}} and marker 
{{{}updatedBy=brownfield-e2e-seed{}}}.
 # Enable dynamic; restart ingestor.
 # Ingestor adopts pre-seeded plan — not auto-{{{}bootstrap{}}} from XML.
 # Rollback: turn dynamic off → partition-plan API 503, health 200, audits 
still OK in static mode.

Script: {{verify-partition-plan-brownfield-e2e.sh --restore-static}}

Doc: {{README-KAFKA-PARTITION-PLAN-BROWNFIELD-MIGRATION.md}}
----
h4. 6. Plugin onboard + Kafka partition routing

What it proves: A real plugin can be onboarded via REST and its audit events 
land on the correct Kafka partitions defined in the plan.

Flow:
 # Enable dynamic mode (greenfield buffer-only layout).
 # For each running plugin container (HDFS, Ozone, Hive, etc.): {{POST 
/partition-plan/services}} with {{{}serviceName{}}}, {{{}pluginId{}}}, 
{{{}partitionCount{}}}, {{{}allowedUsers{}}}.
 # Plugin authenticates with Kerberos and {{{}POST /api/audit/access{}}}.
 # Read Kafka record for that event → partition number must be in the plugin’s 
assigned list in the plan (not buffer, after promote).

Script: {{verify-dynamic-partition-plugin-e2e.sh}}
HDFS-focused variant: {{verify-hdfs-dynamic-partition-e2e.sh --fresh-plan 
--with-hdfs-trigger}}
----
h4. 7. auth_to_local recomposition

What it proves: The unified registry {{services}} map controls who may POST 
audits, and Kerberos principals are mapped to short usernames correctly.

Flow (per plugin repo):
 # Plugin calls {{POST /access}} with Kerberos principal (e.g. 
{{{}hdfs/[email protected]{}}}) → ingestor maps via 
{{auth_to_local}} → short name {{hdfs}} → 200 if in 
{{{}services[dev_hdfs].allowedUsers{}}}.
 # Remove user from allowlist via {{PATCH /partition-plan}} (services delta) → 
same principal gets 403.
 # Re-add allowlist → 200 again.
 # Cross-repo denial: HDFS principal posting to {{dev_kms}} repo → 403.

Why it matters: In dynamic mode, allowlists live in the registry (not XML 
restart). {{auth_to_local}} rules must stay in sync with 
{{{}services[].allowedUsers{}}}.

Script: {{verify-dynamic-auth-to-local-e2e.sh}}
----
h4. 8. HDFS / Ozone / Hive full audit pipelines

What it proves: Dynamic partition plan changes do not break the end-to-end 
audit path: plugin → ingestor → Kafka → dispatcher → Solr (and Admin Audit UI 
when {{{}audit_store=solr{}}}).
||Plugin||What is exercised||
|HDFS|Real NameNode operation → plugin audit → ingestor → correct partition → 
Solr doc|
|Ozone|OM principal ({{{}om{}}}, {{{}ozone{}}}) → {{dev_ozone}} repo → full 
pipeline|
|Hive|HS2 principal ({{{}hive{}}}) → {{dev_hive}} repo → full pipeline|

Validated after: promote/scale (partition plan changed live), not only at 
bootstrap.

Scripts: {{{}verify-audit-tier3-e2e.sh{}}}, 
{{{}verify-hdfs-dynamic-partition-e2e.sh --with-hdfs-trigger{}}}, plugin matrix 
in {{verify-dynamic-partition-plugin-e2e.sh --with-harness-triggers}}

Verify Solr:
curl -s 
'http://localhost:8983/solr/ranger_audits/select?q=repo:dev_hdfs&rows=3&wt=json'
----
h3. Summary table (for Jira)
||Scenario||Plain question||How verified||Pass criteria||
|Static vs dynamic|Does default-off behavior still work?|Feature flag + REST 
503/200|Static unchanged; dynamic enables plan API|
|Greenfield bootstrap|Who creates v1 on empty topic?|First ingestor start|Plan 
v1 in Kafka; counts match|
|REST promote/scale|Can ops change routing live?|REST mutations|200 on valid; 
409 stale; 400 illegal|
|Multi-pod convergence|Do all replicas agree?|2 ingestors, promote on one|Same 
version ≤35s on both|
|Brownfield pre-seed|Safe production cutover?|Pre-write plan, then 
enable|Pre-seed preserved; rollback OK|
|Plugin onboard + routing|Do audits hit right partitions?|POST access + Kafka 
inspect|Partition ∈ plan list|
|auth_to_local|Does allowlist enforcement work?|Allow/deny via services 
map|200/403 per principal+repo|
|Full pipelines|Is audit delivery intact?|HDFS/Ozone/Hive → Solr|Docs in 
{{ranger_audits}} Solr collection|
h2. Acceptance criteria
 # With {{{}dynamic.enabled=false{}}}, behavior matches existing static XML 
partitioning; {{GET /partition-plan}} returns 503
 # With {{{}dynamic.enabled=true{}}}, registry topic created (1 partition, 
compacted); bootstrap plan published on greenfield
 # REST promote/scale updates plan version; stale {{expectedVersion}} returns 
409
 # All ingestor replicas converge to same plan within watcher refresh interval
 # Audits for promoted plugin land only on assigned {{ranger_audits}} partitions
 # {{POST /partition-plan/services}} updates allowlist; unauthorized principal 
returns 403
 # Post-scale routing works when Kafka metadata lags plan 
({{{}AuditPartitioner{}}} bound logic)


> Implement dynamic unified ingestor registry for audit-ingestor: runtime Kafka 
> partition routing and per-repo service allowlists via compacted topic + REST, 
> without ingestor restarts. Feature flag default off.
> ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: RANGER-5655
>                 URL: https://issues.apache.org/jira/browse/RANGER-5655
>             Project: Ranger
>          Issue Type: Task
>          Components: Ranger
>            Reporter: Ramachandran Krishnan
>            Assignee: Ramachandran Krishnan
>            Priority: Major
>             Fix For: 3.0.0
>
>
> Implement a *dynamic unified ingestor registry* for Ranger audit-ingestor so 
> operators can change Kafka partition routing and per-repo service allowlists 
> at runtime — without restarting ingestor pods.
> The registry is stored in a Kafka compacted topic 
> ({{{}ranger_audit_partition_plan{}}}) and managed via REST 
> ({{{}/api/audit/partition-plan{}}}). All ingestor replicas converge on the 
> same versioned plan through a background watcher; {{AuditPartitioner}} routes 
> audit records on the hot path from in-memory state only.
> Feature flag (default off): 
> {{ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled=false}}
>  
> ----
> h2. *Problem*
> Today (static mode), audit-ingestor loads two kinds of configuration from XML 
> at startup only:
> ||Job||Question||Static behavior||
> |Service allowlist|May this Kerberos principal POST audits for repo 
> {*}R{*}?|{{ranger.audit.ingestor.service.<repo>.allowed.users}} in site XML|
> |Partition routing|After accept, which {{ranger_audits}} 
> partition?|{{kafka.configured.plugins}} + per-plugin overrides in site XML|
> Changing either requires editing XML and {*}restarting every ingestor 
> replica{*}. Contiguous-range static allocation can also reshuffle later 
> plugins when an early plugin's partition count changes.
> *Goals:*
>  * Onboard new plugins/repos and scale hot plugins without ingestor restart
>  * Append-only partition growth (no reshuffle of existing plugin assignments)
>  * One shared source of truth across all ingestor pods
>  * No new infra (no Postgres / ZooKeeper for the registry)
> ----
> h2. Solution
> Introduce a *unified partition plan document* (versioned JSON) in Kafka topic 
> {{ranger_audit_partition_plan}} (1 partition, compacted). One document holds:
>  * {{plugins}} — dedicated partition IDs per plugin id (Kafka record key / 
> agent id)
>  * {{buffer}} — partition pool for not-yet-promoted plugins (sticky hash)
>  * {{services}} — per-repo {{allowedUsers}} for {{POST /api/audit/access}}
>  * {{topicPartitionCount}} — must match live {{ranger_audits}} partition count
>  * {{version}} — optimistic locking for REST mutations
> *Control plane:* REST + registry topic + {{PartitionPlanWatcher}}
> *Data plane:* plugins POST {{/api/audit/access}} → allowlist check → 
> {{AuditPartitioner}} → {{ranger_audits}}
> Solr/HDFS dispatchers are unchanged; they consume all partitions of 
> {{{}ranger_audits{}}}.
> ----
> h2. Key deliverables
>  * *Kafka registry* — {{{}KafkaPartitionPlanRegistry{}}}, bootstrap from XML 
> on greenfield, brownfield pre-seed support
>  * *REST API* — {{{}GET/PATCH /partition-plan{}}}, {{{}POST 
> /partition-plan/plugins{}}}, {{{}POST /partition-plan/services{}}}, {{{}PATCH 
> /partition-plan/plugins/{pluginId{}}}} with {{expectedVersion}} (409 on stale)
>  * *Dynamic partitioner* — {{AuditPartitioner}} reads 
> {{{}PartitionPlanHolder{}}}; round-robin for promoted plugins; buffer sticky 
> hash for unknown plugins; post-scale routing uses {{max(cluster, plan)}} when 
> metadata lags
>  * *Unified allowlist* — {{services}} map in same registry; {{auth_to_local}} 
> rules recomposed from allowlists in dynamic mode
>  * *Topic grow* — grow {{ranger_audits}} before registry write when 
> promoting/scaling
>  * *E2E harness* — Docker Tier 3 scripts for partition-plan REST, plugin 
> onboard/routing, auth_to_local, full plugin→Solr pipelines (HDFS, Ozone, 
> Hive, etc.)
>  * *Documentation* — implementation guide, ops runbook, brownfield migration, 
> E2E test plan
> ----
> h2. Configuration
> {code:xml}
> <property>
>   <name>ranger.audit.ingestor.kafka.partition.plan.dynamic.enabled</name>
>   <value>true</value>
> </property>
> <property>
>   <name>ranger.audit.ingestor.kafka.partition.plan.topic</name>
>   <value>ranger_audit_partition_plan</value>
> </property>
> {code}
> When dynamic mode is on and registry is empty, the first ingestor pod 
> bootstraps an initial plan from existing XML properties 
> ({{{}kafka.configured.plugins{}}}, buffer/per-plugin counts, service 
> allowlists).
> ----
> h2. Example plan JSON
> {code:json}
> {
>   "topic": "ranger_audits",
>   "version": 12,
>   "topicPartitionCount": 48,
>   "plugins": {
>     "hdfs":        { "partitions": [0, 1, 2, 3, 4, 5] },
>     "hiveServer2": { "partitions": [6, 7, 8, 9, 10, 11] }
>   },
>   "buffer": { "partitions": [12, 13, "..."] },
>   "services": {
>     "dev_hive":  { "allowedUsers": ["hive"] },
>     "dev_ozone": { "allowedUsers": ["om", "ozone"] },
>     "dev_hdfs":  { "allowedUsers": ["hdfs", "nn"] }
>   }
> }
> {code}
> ----
> h2. Testing
> h3. Validated scenarios
> h4. 1. Static vs dynamic mode
> What it proves: The feature flag works and existing deployments are not 
> broken.
> ||Mode||Config||Expected behavior||
> |Static (default)|{{kafka.partition.plan.dynamic.enabled=false}}|Routing and 
> allowlists come from XML at startup only. {{GET /api/audit/partition-plan}} 
> returns 503. Ingestor health is 200. HDFS/plugin audits still flow to Solr.|
> |Dynamic|{{dynamic.enabled=true}}|Ingestor starts 
> {{{}PartitionPlanWatcher{}}}, reads plan from Kafka topic 
> {{{}ranger_audit_partition_plan{}}}, and routes audits from in-memory plan. 
> {{GET /partition-plan}} returns JSON.|
>  
> ----
> h4. 2. Greenfield bootstrap
> What it proves: On a new cluster with an empty plan topic, the first ingestor 
> pod creates version 1 of the partition plan automatically — no manual REST 
> call.
> Flow:
>  # Enable dynamic mode; restart ingestor.
>  # Plan topic {{ranger_audit_partition_plan}} is created (1 partition, 
> compacted).
>  # Ingestor publishes v1 from existing XML ({{{}kafka.configured.plugins{}}}, 
> buffer size, per-plugin overrides, service allowlists).
>  # Plan’s {{topicPartitionCount}} matches live {{ranger_audits}} partition 
> count (e.g. 48 = 13 plugins × 3 + 9 buffer on full lab layout).
> Success: {{GET /partition-plan}} shows v1, correct plugin list, partition 
> count aligned with Kafka. Audits still work after enable.
> ----
> h4. 3. REST promote / scale (200 / 409 / 400)
> What it proves: Operators can change routing at runtime via REST without 
> restarting ingestor. The API rejects bad requests.
> ||Action||REST||Success||Failure cases||
> |Promote new plugin (e.g. {{{}storm{}}}) from buffer → dedicated 
> partitions|{{POST /partition-plan/plugins}}|200, {{version}} increments, 
> plugin in {{plugins}} map|400 if plugin already promoted (e.g. {{hdfs}} 
> twice)|
> |Scale hot plugin (+N partitions)|{{{}PATCH 
> /partition-plan/plugins/{pluginId{}}}}|200, new partition IDs appended at 
> tail; {{ranger_audits}} grown first if needed|400 if scaling a buffer-only 
> plugin|
> |Stale edit|Any mutation with wrong {{expectedVersion}}|—|409 + current plan 
> body (forces refresh and retry)|
> |Invalid plan|Overlapping partitions, reshuffle existing IDs, 
> {{partitionCount: 0}}|—|400|
> Key property: Changes are append-only — existing plugin partition lists are 
> never reshuffled.
> ----
> h4. 4. Multi-pod convergence
> What it proves: In a real cluster with multiple ingestor replicas behind a 
> load balancer, all pods see the same plan after an admin change.
> Flow:
>  # Primary ingestor on :7081, second replica on :7082 (same Kafka plan topic, 
> different Kerberos identity).
>  # Both pods report the same {{version}} at startup.
>  # Admin promotes a plugin on primary only.
>  # Within one watcher cycle (~30s), replica on :7082 shows the same new 
> version without any REST call on the replica.
> Success: No drift between pods; routing is consistent cluster-wide.
> ----
> h4. 5. Brownfield pre-seed
> What it proves: Existing production clusters can cut over to dynamic mode 
> safely by writing the plan into Kafka before enabling the feature — ingestor 
> must not overwrite it with a fresh XML bootstrap.
> Flow (Path A migration):
>  # Capture current plan JSON while dynamic is briefly on.
>  # Turn dynamic off; delete plan topic (simulate “still on static”).
>  # Operator pre-seeds plan to Kafka with {{version=1}} and marker 
> {{{}updatedBy=brownfield-e2e-seed{}}}.
>  # Enable dynamic; restart ingestor.
>  # Ingestor adopts pre-seeded plan — not auto-{{{}bootstrap{}}} from XML.
>  # Rollback: turn dynamic off → partition-plan API 503, health 200, audits 
> still OK in static mode.
> ----
> h4. 6. Plugin onboard + Kafka partition routing
> What it proves: A real plugin can be onboarded via REST and its audit events 
> land on the correct Kafka partitions defined in the plan.
> Flow:
>  # Enable dynamic mode (greenfield buffer-only layout).
>  # For each running plugin container (HDFS, Ozone, Hive, etc.): {{POST 
> /partition-plan/services}} with {{{}serviceName{}}}, {{{}pluginId{}}}, 
> {{{}partitionCount{}}}, {{{}allowedUsers{}}}.
>  # Plugin authenticates with Kerberos and {{{}POST /api/audit/access{}}}.
>  # Read Kafka record for that event → partition number must be in the 
> plugin’s assigned list in the plan (not buffer, after promote).
> ----
> h4. 7. auth_to_local recomposition
> What it proves: The unified registry {{services}} map controls who may POST 
> audits, and Kerberos principals are mapped to short usernames correctly.
> Flow (per plugin repo):
>  # Plugin calls {{POST /access}} with Kerberos principal (e.g. 
> {{{}hdfs/[email protected]{}}}) → ingestor maps via 
> {{auth_to_local}} → short name {{hdfs}} → 200 if in 
> {{{}services[dev_hdfs].allowedUsers{}}}.
>  # Remove user from allowlist via {{PATCH /partition-plan}} (services delta) 
> → same principal gets 403.
>  # Re-add allowlist → 200 again.
>  # Cross-repo denial: HDFS principal posting to {{dev_kms}} repo → 403.
> Why it matters: In dynamic mode, allowlists live in the registry (not XML 
> restart). {{auth_to_local}} rules must stay in sync with 
> {{{}services[].allowedUsers{}}}.
> ----
> h4. 8. HDFS / Ozone / Hive full audit pipelines
> What it proves: Dynamic partition plan changes do not break the end-to-end 
> audit path: plugin → ingestor → Kafka → dispatcher → Solr (and Admin Audit UI 
> when {{{}audit_store=solr{}}}).
> ||Plugin||What is exercised||
> |HDFS|Real NameNode operation → plugin audit → ingestor → correct partition → 
> Solr doc|
> |Ozone|OM principal ({{{}om{}}}, {{{}ozone{}}}) → {{dev_ozone}} repo → full 
> pipeline|
> |Hive|HS2 principal ({{{}hive{}}}) → {{dev_hive}} repo → full pipeline|
> Validated after: promote/scale (partition plan changed live), not only at 
> bootstrap.
> Verify Solr:
> curl -s 
> 'http://localhost:8983/solr/ranger_audits/select?q=repo:dev_hdfs&rows=3&wt=json'
> ----
> h3. Summary table (for Jira)
> ||Scenario||Plain question||How verified||Pass criteria||
> |Static vs dynamic|Does default-off behavior still work?|Feature flag + REST 
> 503/200|Static unchanged; dynamic enables plan API|
> |Greenfield bootstrap|Who creates v1 on empty topic?|First ingestor 
> start|Plan v1 in Kafka; counts match|
> |REST promote/scale|Can ops change routing live?|REST mutations|200 on valid; 
> 409 stale; 400 illegal|
> |Multi-pod convergence|Do all replicas agree?|2 ingestors, promote on 
> one|Same version ≤35s on both|
> |Brownfield pre-seed|Safe production cutover?|Pre-write plan, then 
> enable|Pre-seed preserved; rollback OK|
> |Plugin onboard + routing|Do audits hit right partitions?|POST access + Kafka 
> inspect|Partition ∈ plan list|
> |auth_to_local|Does allowlist enforcement work?|Allow/deny via services 
> map|200/403 per principal+repo|
> |Full pipelines|Is audit delivery intact?|HDFS/Ozone/Hive → Solr|Docs in 
> {{ranger_audits}} Solr collection|
> h2. Acceptance criteria
>  # With {{{}dynamic.enabled=false{}}}, behavior matches existing static XML 
> partitioning; {{GET /partition-plan}} returns 503
>  # With {{{}dynamic.enabled=true{}}}, registry topic created (1 partition, 
> compacted); bootstrap plan published on greenfield
>  # REST promote/scale updates plan version; stale {{expectedVersion}} returns 
> 409
>  # All ingestor replicas converge to same plan within watcher refresh interval
>  # Audits for promoted plugin land only on assigned {{ranger_audits}} 
> partitions
>  # {{POST /partition-plan/services}} updates allowlist; unauthorized 
> principal returns 403
>  # Post-scale routing works when Kafka metadata lags plan 
> ({{{}AuditPartitioner{}}} bound logic)



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to