This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new 78720dd5f RANGER-5821: Align functional-tests/run-tests.sh with docker
setup changes (#1288)
78720dd5f is described below
commit 78720dd5fe7f5b3cc2c725ee2ffb63880a6f60b9
Author: Chinmay Hegde <[email protected]>
AuthorDate: Wed Oct 7 16:20:28 2026 +0530
RANGER-5821: Align functional-tests/run-tests.sh with docker setup changes
(#1288)
* RANGER-5821: Align run-tests.sh with docker setup changes
* RANGER-5821: Error fixes and improvements in hdfs and kms module
* RANGER-5821: Update functional-tests/readme.md with latest changes
---
functional-tests/hdfs/conftest.py | 10 +--
functional-tests/hdfs/test_encryption.py | 35 +++++---
functional-tests/hdfs/test_encryption02.py | 19 ++--
functional-tests/hdfs/utils.py | 86 ++++++++++++------
functional-tests/kms/conftest.py | 5 ++
functional-tests/kms/test_keyDetails.py | 15 ++--
functional-tests/kms/test_keyOps_policy.py | 27 +++++-
functional-tests/kms/utils.py | 104 +++++++++++++---------
functional-tests/readme.md | 135 ++++++++++++++++++++---------
functional-tests/run-tests.sh | 88 ++++++++++++++++---
10 files changed, 366 insertions(+), 158 deletions(-)
diff --git a/functional-tests/hdfs/conftest.py
b/functional-tests/hdfs/conftest.py
index f14f42e4c..deb0ea4cc 100644
--- a/functional-tests/hdfs/conftest.py
+++ b/functional-tests/hdfs/conftest.py
@@ -114,6 +114,7 @@ def setup_environment(hadoop_container):
ensure_user_exists(hadoop_container, "hive")
ensure_user_exists(hadoop_container, "hbase")
ensure_hadoop_user_keytab("hive")
+ ensure_hadoop_user_keytab("hbase")
# safemode leave with kerberos (hdfs principal already has keytab in
container)
hadoop_container.exec_run(
@@ -124,20 +125,13 @@ def setup_environment(hadoop_container):
)
kms_container = client.containers.get(KMS_CONTAINER)
- if ensure_kms_kerberos_rules(kms_container):
- time.sleep(5)
+ ensure_kms_kerberos_rules(kms_container) # already sleeps 30s internally
when restart is needed
ensure_keyadmin_keytab()
ensure_ticket()
ensure_kms_hdfs_policy()
- ensure_ticket()
cleanup_test_artifacts(hadoop_container, TEST_KMS_KEYS, TEST_EZ_PATHS)
yield
cleanup_test_artifacts(hadoop_container, TEST_KMS_KEYS, TEST_EZ_PATHS)
[email protected](scope="session", autouse=True)
-def setup_kerberos():
- ensure_keyadmin_keytab()
- ensure_ticket()
-
diff --git a/functional-tests/hdfs/test_encryption.py
b/functional-tests/hdfs/test_encryption.py
index ebac6a7a7..57cd0c168 100644
--- a/functional-tests/hdfs/test_encryption.py
+++ b/functional-tests/hdfs/test_encryption.py
@@ -77,30 +77,39 @@ def test_hive_user_write_read(hadoop_container):
# Negative Test - Unauthorized User Cannot Write i.e 'HBASE' in this
case-------------
def test_unauthorized_write(hadoop_container):
- filename2="hdfs-test-file2" #writing new file into EZ
- failure_detected = False
+ filename2 = "hdfs-test-file2"
- unauth_write_cmd=
UNAUTHORIZED_WRITE_COMMAND.format(filename=filename2,user=HBASE_USER,ez_name=ez_name)
- output,exit_code=
run_command(hadoop_container,unauth_write_cmd,HBASE_USER,fail_on_error=False,return_exit_code=True)
+ # Create a local file for hbase to attempt uploading — needed so the
failure is
+ # HDFS authorization (Permission denied on EZ), not a missing-local-file
error.
+ create_file_cmd = CREATE_FILE_COMMAND[0].format(
+ filename=filename2, filecontent="unauthorized write attempt",
user=HBASE_USER
+ )
+ run_command(hadoop_container, ["bash", "-c", create_file_cmd], HBASE_USER)
- print(f"Command Output:\n{output}")
+ unauth_write_cmd = UNAUTHORIZED_WRITE_COMMAND.format(filename=filename2,
user=HBASE_USER, ez_name=ez_name)
+ output, exit_code = run_command(hadoop_container, unauth_write_cmd,
HBASE_USER, fail_on_error=False, return_exit_code=True)
- # Check for known failure indicators in output
- if exit_code != 0:
- failure_detected = True
+ print(f"Command Output:\n{output}")
- #assert that failure was detected as expected
- assert failure_detected, "Expected failure due to no permission on EZ, but
command succeeded."
+ assert exit_code != 0, "Expected failure: hbase should not have write
permission on EZ, but command succeeded."
+ # Verify the failure is HDFS authorization (Permission denied), NOT a
Kerberos auth failure.
+ # If this assert fails it means hbase is not properly authenticated and
the test is a false positive.
+ assert any(p in output.lower() for p in ["permission denied", "access
denied", "accesscontrolexception"]), \
+ f"Expected HDFS Permission denied error but got: {output}"
# Negative Test - Unauthorized User 'HBASE' Cannot Read
------------------------------
def test_unauthorized_read(hadoop_container):
- unauth_read= UNAUTHORIZED_READ_COMMAND.format(filename=filename,
ez_name=ez_name, user=HBASE_USER)
- output,exit_code =
run_command(hadoop_container,unauth_read,HBASE_USER,fail_on_error=False,return_exit_code=True)
+ unauth_read = UNAUTHORIZED_READ_COMMAND.format(filename=filename,
ez_name=ez_name, user=HBASE_USER)
+ output, exit_code = run_command(hadoop_container, unauth_read, HBASE_USER,
fail_on_error=False, return_exit_code=True)
print(f"Command Output:\n{output}")
- assert exit_code != 0, "Expected failure due to no permission on EZ, but
command succeeded."
+ assert exit_code != 0, "Expected failure: hbase should not have read
permission on EZ, but command succeeded."
+ # Verify the failure is HDFS authorization (Permission denied), NOT a
Kerberos auth failure.
+ # If this assert fails it means hbase is not properly authenticated and
the test is a false positive.
+ assert any(p in output.lower() for p in ["permission denied", "access
denied", "accesscontrolexception"]), \
+ f"Expected HDFS Permission denied error but got: {output}"
# Clean Up - Remove Test file and EZ
-------------------------------------------------
diff --git a/functional-tests/hdfs/test_encryption02.py
b/functional-tests/hdfs/test_encryption02.py
index a76ae265d..70f0c787c 100644
--- a/functional-tests/hdfs/test_encryption02.py
+++ b/functional-tests/hdfs/test_encryption02.py
@@ -116,7 +116,7 @@ def
test_writeAndRead_Newfile_after_rollover(hadoop_container):
#read-write using 'hive' user-------
read_write_cmd= [cmd.format(filename=filename, ez_name=ez_name,
user=HIVE_USER) for cmd in ACTIONS_COMMANDS]
for cmd in read_write_cmd:
- output=run_command(hadoop_container,cmd,HIVE_USER)
+ output = run_kerberos_command(hadoop_container, cmd, HIVE_USER,
HIVE_PRINCIPAL, HIVE_KEYTAB)
print(output)
#roll-over of key---------
@@ -135,13 +135,15 @@ def
test_writeAndRead_Newfile_after_rollover(hadoop_container):
#read-write new file now
read_write_cmd= [cmd.format(filename=filename2, ez_name=ez_name,
user=HIVE_USER) for cmd in ACTIONS_COMMANDS]
for cmd in read_write_cmd:
- output=run_command(hadoop_container,cmd,HIVE_USER)
+ output = run_kerberos_command(hadoop_container, cmd, HIVE_USER,
HIVE_PRINCIPAL, HIVE_KEYTAB)
print(output)
#cleanup EZ and EZ file--------
+ # fail_on_error=False for all but the last cmd — partial file-removal
failures are OK,
+ # but the final recursive EZ delete must succeed (also removes filename2 /
testfile3).
cleanup_cmd=[cmd.format(filename=filename, ez_name=ez_name) for cmd in
CLEANUP_COMMANDS]
- for cmd in cleanup_cmd:
- run_command(hadoop_container,cmd,HDFS_USER)
+ for i, cmd in enumerate(cleanup_cmd):
+ run_command(hadoop_container, cmd, HDFS_USER, fail_on_error=(i ==
len(cleanup_cmd) - 1))
#delete EZ key ----------
delete_kms_key(key_name)
@@ -181,7 +183,7 @@ def test_Readfile_after_keyDeletion(hadoop_container):
#read-write using 'hive' user-------
read_write_cmd= [cmd.format(filename=filename, ez_name=ez_name,
user=HIVE_USER) for cmd in ACTIONS_COMMANDS]
for cmd in read_write_cmd:
- output=run_command(hadoop_container,cmd,HIVE_USER)
+ output = run_kerberos_command(hadoop_container, cmd, HIVE_USER,
HIVE_PRINCIPAL, HIVE_KEYTAB)
print(output)
@@ -194,11 +196,12 @@ def test_Readfile_after_keyDeletion(hadoop_container):
failure_detected = False
for cmd in read_write_cmd:
- output = run_command(hadoop_container, cmd, HIVE_USER,
fail_on_error=False)
+ # Use return_exit_code=True so stderr (where HDFS error messages live)
is included
+ # in output — required after demux=True split in run_command.
+ output, exit_code = run_command(hadoop_container, cmd, HIVE_USER,
fail_on_error=False, return_exit_code=True)
print(f"Command Output:\n{output}")
- # Check for known failure indicators in output
- if any(err in output.lower() for err in ["error", "exception",
"failed", "not found"]):
+ if exit_code != 0:
failure_detected = True
#assert that failure was detected as expected
diff --git a/functional-tests/hdfs/utils.py b/functional-tests/hdfs/utils.py
index 73fa8a940..3d22817d1 100644
--- a/functional-tests/hdfs/utils.py
+++ b/functional-tests/hdfs/utils.py
@@ -18,7 +18,7 @@
import docker
import requests
from hdfs.test_config import (KMS_CONTAINER, HADOOP_NAMENODE_LOG_PATH,
KMS_LOG_PATH)
-from kms.utils import krb_requests, ensure_ticket, BASE_URL, PARAMS
+from kms.utils import krb_requests, ensure_ticket, BASE_URL, PARAMS, container
as kms_container
from hdfs.test_config import HDFS_USER
import subprocess
import tempfile
@@ -51,19 +51,40 @@
HDFS_PRINCIPAL = "hdfs/[email protected]"
HDFS_KEYTAB = "/etc/keytabs/hdfs.keytab"
+HBASE_PRINCIPAL = "hbase/[email protected]"
+HBASE_KEYTAB = "/etc/keytabs/hbase.keytab"
+
+# Map each OS user to their Kerberos principal and keytab.
+# Defined here (near the constants) so run_command can reference it clearly.
+_KERBEROS_CREDENTIALS = {
+ "hdfs": (HDFS_PRINCIPAL, HDFS_KEYTAB),
+ "hive": (HIVE_PRINCIPAL, HIVE_KEYTAB),
+ "hbase": (HBASE_PRINCIPAL, HBASE_KEYTAB),
+}
+
#to run all HDFS commands
-def run_command(container, cmd, user,
fail_on_error=True,return_exit_code=False):
+def run_command(container, cmd, user, fail_on_error=True,
return_exit_code=False):
# For string commands on kerberos-capable users, wrap with kinit
automatically.
+ # klist -s checks for an existing valid ticket first — skips kinit when
one is present,
+ # avoiding redundant re-authentication on every command call.
# List commands (e.g. from run_kerberos_command) are passed through
unchanged.
if isinstance(cmd, str) and user in _KERBEROS_CREDENTIALS:
principal, keytab = _KERBEROS_CREDENTIALS[user]
actual_cmd = ["bash", "-c",
- f"kinit -kt {keytab} {principal} 2>/dev/null; {cmd}"]
+ f"klist -s 2>/dev/null || kinit -kt {keytab} {principal}
2>/dev/null; {cmd}"]
else:
actual_cmd = cmd
- exit_code, output = container.exec_run(actual_cmd, user=user)
- output_response = output.decode()
+ # demux=True separates stdout and stderr — prevents hadoop usage text
(stderr)
+ # from leaking into the captured output returned to callers and printed in
tests.
+ exit_code, (stdout, stderr) = container.exec_run(actual_cmd, user=user,
demux=True)
+ stdout_text = (stdout or b"").decode()
+ stderr_text = (stderr or b"").decode()
+
+ # Negative/failure-detection tests (return_exit_code=True) need stderr to
validate
+ # the error type (e.g. Permission denied vs auth failure). Positive tests
get stdout
+ # only, which keeps hadoop usage dumps from polluting the console.
+ output_response = stdout_text if not return_exit_code else stdout_text +
stderr_text
if exit_code != 0 and fail_on_error:
kms_container = client.containers.get(KMS_CONTAINER)
@@ -72,8 +93,11 @@ def run_command(container, cmd, user,
fail_on_error=True,return_exit_code=False)
Command failed: {cmd}
Exit Code: {exit_code}
- Output:
- {output_response}
+ Stdout:
+ {stdout_text}
+
+ Stderr:
+ {stderr_text}
Hadoop Container Logs:
{hadoop_logs}
@@ -117,7 +141,9 @@ def _ensure_kdc_service_principal(username: str, service:
str = "ranger-hadoop")
exit_code, output = kdc.exec_run(f'kadmin.local -q "getprinc
{principal}"', user="root")
if exit_code != 0 or b"does not exist" in output:
- kdc.exec_run(f'kadmin.local -q "addprinc -randkey {principal}"',
user="root")
+ ec, out = kdc.exec_run(f'kadmin.local -q "addprinc -randkey
{principal}"', user="root")
+ if ec != 0:
+ raise RuntimeError(f"addprinc failed for {principal}:
{out.decode()}")
exit_code, output = kdc.exec_run(
f'kadmin.local -q "xst -k {kdc_tmp} {principal}"', user="root"
@@ -138,8 +164,9 @@ def ensure_hadoop_user_keytab(username: str) -> None:
_ensure_kdc_service_principal(username)
def run_kerberos_command(container, cmd, user, principal, keytab, **kwargs):
+ # Use klist -s to skip kinit when a valid ticket already exists for this
user.
shell = (
- f"kinit -kt {keytab} {principal} && "
+ f"klist -s 2>/dev/null || kinit -kt {keytab} {principal} 2>/dev/null; "
f'HADOOP_OPTS="-Dhadoop.security.authentication=kerberos" {cmd}'
)
return run_command(container, ["bash", "-c", shell], user, **kwargs)
@@ -185,8 +212,8 @@ def ensure_kms_kerberos_rules(kms_container) -> bool:
return False
rules = [
+ # [ndj]n already covers nn, dn, jn — no separate dn rule needed
"RULE:[2:$1/$2@$0]([ndj]n/ranger-hadoop\\.rangernw@EXAMPLE\\.COM)s/.*/keyadmin/",
-
"RULE:[2:$1/$2@$0](dn/ranger-hadoop\\.rangernw@EXAMPLE\\.COM)s/.*/keyadmin/",
"RULE:[2:$1/$2@$0](hive/ranger-hadoop\\.rangernw@EXAMPLE\\.COM)s/.*/hive/",
]
for rule in rules:
@@ -201,7 +228,12 @@ def ensure_kms_kerberos_rules(kms_container) -> bool:
def ensure_kms_hdfs_policy() -> None:
- # Grant HDFS principals KMS access by updating the default wildcard policy
+ # Grant HDFS principals KMS access by updating the default wildcard policy.
+ # Ensure every user in HDFS_KMS_USERS exists in Ranger first — service
accounts
+ # like 'nn' (NameNode) and 'dn' (DataNode) are not auto-created in Ranger
admin
+ # and will cause a 400 if added to a policy without being registered first.
+ for username in HDFS_KMS_USERS:
+ ensure_ranger_user(username)
resp = requests.get(RANGER_SERVICE_POLICY_URL, auth=RANGER_AUTH,
timeout=30)
@@ -263,7 +295,11 @@ def _kms_key_missing(resp) -> bool:
def delete_kms_key(key_name: str) -> None:
# Delete KMS key if present. Ignores missing-key responses.
- ensure_ticket()
+ # Only refresh the keyadmin ticket when no valid ticket exists — avoids a
full
+ # kdestroy+kinit on every key deletion (e.g. 6 calls in
cleanup_test_artifacts).
+ ec, _ = kms_container.exec_run("klist -s 2>/dev/null", user="root")
+ if ec != 0:
+ ensure_ticket()
resp = krb_requests.delete(f"{BASE_URL}/key/{key_name}", params=PARAMS)
if resp.status_code in (200, 404):
return
@@ -285,12 +321,19 @@ def ensure_kms_key(key_name: str,
cipher="AES/CTR/NoPadding", length=128) -> Non
assert resp.status_code == 201, f"Key creation failed: {resp.text}"
def delete_hdfs_path(container, path: str) -> None:
- # Remove HDFS path if present. Kinit as hdfs first (kerberos mode)
- container.exec_run(
- ["bash", "-c",
- f"kinit -kt {HDFS_KEYTAB} {HDFS_PRINCIPAL} 2>/dev/null; hdfs dfs -rm
-R -f {path}"],
- user=HDFS_USER,
+ # Remove HDFS path if present. Uses klist -s check before kinit.
+ # demux=True so errors appear in stderr (not mixed into stdout).
+ shell = (
+ f"klist -s 2>/dev/null || kinit -kt {HDFS_KEYTAB} {HDFS_PRINCIPAL}
2>/dev/null; "
+ f"hdfs dfs -rm -R -f {path}"
)
+ exit_code, (stdout, stderr) = container.exec_run(
+ ["bash", "-c", shell], user=HDFS_USER, demux=True
+ )
+ if exit_code != 0:
+ stderr_text = (stderr or b"").decode()
+ stdout_text = (stdout or b"").decode()
+ print(f"WARN: delete_hdfs_path {path} failed (exit {exit_code}):
{stderr_text or stdout_text}")
def cleanup_encryption_zone(container, ez_path: str) -> None:
# Remove EZ directory (Must run before deleting bound KMS key)
@@ -309,11 +352,4 @@ def create_encryption_zone(container, ez_name: str,
key_name: str) -> None:
cleanup_encryption_zone(container, ez_path)
run_command(container, f"hdfs dfs -mkdir {ez_path}", HDFS_USER)
- run_command(container, f"hdfs crypto -createZone -keyName {key_name} -path
{ez_path}", HDFS_USER)
-
-#to run all HDFS commands
-# Map each OS user to their kerberos principal and keytab (pre-existing in
container)
-_KERBEROS_CREDENTIALS = {
- "hdfs": ("hdfs/[email protected]",
"/etc/keytabs/hdfs.keytab"),
- "hive": ("hive/[email protected]",
"/etc/keytabs/hive.keytab"),
-}
\ No newline at end of file
+ run_command(container, f"hdfs crypto -createZone -keyName {key_name} -path
{ez_path}", HDFS_USER)
\ No newline at end of file
diff --git a/functional-tests/kms/conftest.py b/functional-tests/kms/conftest.py
index 30b20b316..8cabb4481 100644
--- a/functional-tests/kms/conftest.py
+++ b/functional-tests/kms/conftest.py
@@ -29,6 +29,11 @@
TEST_USER = "keyadmin"
HEADERS = {"Content-Type": "application/json"}
[email protected](scope="session", autouse=True)
+def setup_kerberos_ticket():
+ """Ensure keyadmin has a valid Kerberos ticket before any test runs."""
+ ensure_keyadmin_ticket()
+
@pytest.fixture(scope="session")
def headers():
return HEADERS
diff --git a/functional-tests/kms/test_keyDetails.py
b/functional-tests/kms/test_keyDetails.py
index 55a6e6beb..4afcfe5d5 100644
--- a/functional-tests/kms/test_keyDetails.py
+++ b/functional-tests/kms/test_keyDetails.py
@@ -51,8 +51,9 @@ def test_get_key_names(self):
def test_get_key_metadata(self, headers, key_name, expected_status,
expected_response):
response = krb_requests.get(f"{BASE_URL}/key/{key_name}/_metadata",
headers=headers, params=PARAMS)
- logs = fetch_logs() # log check
- assert response.status_code == expected_status, f"Get key metadata
operation failed. API Response: {response.text}\nLogs:\n{logs}"
+ if response.status_code != expected_status:
+ logs = fetch_logs()
+ pytest.fail(f"Get key metadata operation failed. API Response:
{response.text}\nLogs:\n{logs}")
if expected_response == "invalid":
assert response.text.strip() in ["", "{}", "{ }", "[]", "[ ]"],
f"Expected blank response for non-existent key, got: {response.text}"
@@ -70,8 +71,9 @@ def test_get_key_metadata(self, headers, key_name,
expected_status, expected_res
def test_get_key_versions(self, headers, key_name,
expected_status,expected_response):
response = krb_requests.get(f"{BASE_URL}/key/{key_name}/_versions",
headers=headers, params=PARAMS)
- logs = fetch_logs() # log check
- assert response.status_code == expected_status, f"Get key version
operation failed. API Response: {response.text}\nLogs:\n{logs}"
+ if response.status_code != expected_status:
+ logs = fetch_logs()
+ pytest.fail(f"Get key version operation failed. API Response:
{response.text}\nLogs:\n{logs}")
if expected_response == "invalid":
assert response.text.strip() in ["", "{}", "{ }", "[]", "[ ]"],
f"Expected blank response for non-existent key, got: {response.text}"
@@ -133,8 +135,9 @@ def test_get_keys_metadata(self, headers):
def test_get_key_version(self, headers, version_name, expected_status,
expected_valid):
response = krb_requests.get(f"{BASE_URL}/keyversion/{version_name}",
headers=headers, params=PARAMS)
- logs = fetch_logs()
- assert response.status_code == expected_status, f"Get key version
failed. Response: {response.text}\nLogs:\n{logs}"
+ if response.status_code != expected_status:
+ logs = fetch_logs()
+ pytest.fail(f"Get key version failed. Response:
{response.text}\nLogs:\n{logs}")
if expected_valid:
try:
diff --git a/functional-tests/kms/test_keyOps_policy.py
b/functional-tests/kms/test_keyOps_policy.py
index a0be38298..845049f11 100644
--- a/functional-tests/kms/test_keyOps_policy.py
+++ b/functional-tests/kms/test_keyOps_policy.py
@@ -22,7 +22,8 @@
ensure_testuser_keytab, ensure_testuser_ticket, ensure_keyadmin_ticket,
TESTUSER_PARAMS, ensure_test_user_exists, delete_test_user,
BASE_URL_RANGER, BASE_URL_RANGER_USERS, BASE_URL_RANGER_USERS_BY_NAME,
- RANGER_ADMIN_AUTH, RANGER_KMS_AUTH, KMS_SERVICE_NAME, TEST_USER
+ RANGER_ADMIN_AUTH, RANGER_KMS_AUTH, KMS_SERVICE_NAME, TEST_USER,
+ container, KEYADMIN_KEYTAB, KEYADMIN_PRINCIPAL, TESTUSER_KEYTAB,
TESTUSER_PRINCIPAL
)
@@ -35,6 +36,13 @@ def test_user_lifecycle():
delete_test_user(TEST_USER)
[email protected](scope="session", autouse=True)
+def setup_keytabs():
+ """Create keytabs for both principals once per session — keytab creation
is expensive."""
+ ensure_testuser_keytab()
+ # keyadmin keytab is handled by the global setup_kerberos_ticket in
conftest.py
+
+
# create base policy
------------------------------------------------------------------
@pytest.fixture(scope="function", autouse=True)
def create_initial_kms_policy():
@@ -77,9 +85,22 @@ def create_initial_kms_policy():
@pytest.fixture(autouse=True)
def kms_as_testuser():
- ensure_testuser_ticket()
+ """Switch to testuser ticket for the test body; restore keyadmin ticket in
teardown.
+ Keytab creation is handled once by setup_keytabs (session-scoped), so this
fixture
+ only does the cheap kinit/kdestroy calls."""
+ container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
+ exit_code, output = container.exec_run(
+ f"kinit -kt {TESTUSER_KEYTAB} {TESTUSER_PRINCIPAL}", user="root"
+ )
+ if exit_code != 0:
+ raise RuntimeError(f"testuser kinit failed: {output.decode()}")
yield
- ensure_keyadmin_ticket()
+ container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
+ exit_code, output = container.exec_run(
+ f"kinit -kt {KEYADMIN_KEYTAB} {KEYADMIN_PRINCIPAL}", user="root"
+ )
+ if exit_code != 0:
+ raise RuntimeError(f"keyadmin kinit restore failed: {output.decode()}")
# method to update
policy---------------------------------------------------------------
def update_kms_policy(policy_id, username, accesses):
diff --git a/functional-tests/kms/utils.py b/functional-tests/kms/utils.py
index dd6b44435..27560e597 100644
--- a/functional-tests/kms/utils.py
+++ b/functional-tests/kms/utils.py
@@ -17,6 +17,7 @@
import json
import io
import tarfile
+import time
import docker
import tempfile, os
import xml.etree.ElementTree as ET
@@ -88,21 +89,41 @@ def _curl(self, method, url, json_body=None, params=None):
items = params # list of tuples — allows duplicate keys e.g.
[("key","k1"),("key","k2")]
qs = "&".join(f"{k}={v}" for k, v in items)
full_url = f"{url}?{qs}"
- cmd = [
+
+ # Build curl args as a shell-safe string so we can append the body read
+ # in the same exec_run call — avoids a second docker exec for "cat".
+ curl_args = [
"curl", "-s", "-o", "/tmp/curl_body.txt", "-w", "%{http_code}",
"--negotiate", "-u", ":",
"-X", method.upper(),
"-H", "Content-Type: application/json",
]
if json_body is not None:
- cmd += ["-d", json.dumps(json_body)]
- cmd.append(full_url)
+ curl_args += ["-d", json.dumps(json_body)]
+ curl_args.append(full_url)
+
+ # Escape each argument for a POSIX shell one-liner
+ def _sh_quote(s):
+ return "'" + s.replace("'", "'\\''") + "'"
+
+ curl_cmd = " ".join(_sh_quote(a) for a in curl_args)
+ # Single exec: run curl then print a fixed separator then the body.
+ # Output format: <status_code>\n---BODY---\n<body text>
+ shell_cmd = f'{curl_cmd}; echo "\\n---BODY---"; cat /tmp/curl_body.txt'
- exit_code, output = container.exec_run(cmd, user="root")
- status_code = int(output.decode().strip()) if output else 0
+ exit_code, output = container.exec_run(["sh", "-c", shell_cmd],
user="root")
+ raw = output.decode() if output else ""
- _, body_out = container.exec_run("cat /tmp/curl_body.txt", user="root")
- body = body_out.decode() if body_out else ""
+ separator = "\n---BODY---\n"
+ if separator in raw:
+ status_part, body = raw.split(separator, 1)
+ else:
+ status_part, body = raw.strip(), ""
+
+ try:
+ status_code = int(status_part.strip())
+ except ValueError:
+ status_code = 0
return _FakeResponse(status_code, body)
@@ -141,7 +162,9 @@ def _ensure_kdc_principal_keytab(principal: str,
dest_container_name: str, dest_
exit_code, output = kdc.exec_run(f'kadmin.local -q "getprinc
{principal}"', user="root")
if exit_code != 0 or b"does not exist" in output:
- kdc.exec_run(f'kadmin.local -q "addprinc -randkey {principal}"',
user="root")
+ ec, out = kdc.exec_run(f'kadmin.local -q "addprinc -randkey
{principal}"', user="root")
+ if ec != 0:
+ raise RuntimeError(f"addprinc failed for {principal}:
{out.decode()}")
exit_code, output = kdc.exec_run(
f'kadmin.local -q "xst -k {kdc_tmp_path} {principal}"', user="root"
@@ -159,20 +182,41 @@ def ensure_keyadmin_keytab():
# creation of [email protected] & install keytab in ranger-kms
_ensure_kdc_principal_keytab(KEYADMIN_PRINCIPAL, KMS_CONTAINER_NAME,
KEYADMIN_KEYTAB,"/tmp/keyadmin.keytab",)
-def ensure_ticket():
- # Always kinit as keyadmin — do not reuse a stale rangerkms ticket from
cache.
- exit_code, _ = container.exec_run(f"test -f {KEYADMIN_KEYTAB}",
user="root")
+def _kinit_with_retry(principal: str, keytab: str, ensure_keytab_fn,
max_attempts: int = 5, wait: int = 10) -> None:
+ """Ensure the keytab exists then kinit, retrying to absorb KDC warm-up
delays."""
+ exit_code, _ = container.exec_run(f"test -f {keytab}", user="root")
if exit_code != 0:
- ensure_keyadmin_keytab()
+ ensure_keytab_fn()
container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
- exit_code, output = container.exec_run(
- f"kinit -kt {KEYADMIN_KEYTAB} {KEYADMIN_PRINCIPAL}", user="root"
+ for _ in range(max_attempts):
+ exit_code, output = container.exec_run(
+ f"kinit -kt {keytab} {principal}", user="root"
+ )
+ if exit_code == 0:
+ return
+ time.sleep(wait)
+ raise RuntimeError(f"kinit failed for {principal} after {max_attempts}
attempts: {output.decode()}")
+
+def ensure_ticket():
+ """Obtain a keyadmin Kerberos ticket (backward-compat alias for
ensure_keyadmin_ticket)."""
+ _kinit_with_retry(KEYADMIN_PRINCIPAL, KEYADMIN_KEYTAB,
ensure_keyadmin_keytab)
+
+def ensure_keyadmin_ticket():
+ """Obtain a keyadmin Kerberos ticket."""
+ _kinit_with_retry(KEYADMIN_PRINCIPAL, KEYADMIN_KEYTAB,
ensure_keyadmin_keytab)
+
+def ensure_testuser_keytab():
+ _ensure_kdc_principal_keytab(
+ TESTUSER_PRINCIPAL,
+ KMS_CONTAINER_NAME,
+ TESTUSER_KEYTAB,
+ f"/tmp/{TESTUSER}.keytab",
)
- if exit_code != 0:
- raise RuntimeError(f"kinit failed: {output.decode()}")
-# Blacklist helpers
+def ensure_testuser_ticket():
+ """Obtain a testuser Kerberos ticket."""
+ _kinit_with_retry(TESTUSER_PRINCIPAL, TESTUSER_KEYTAB,
ensure_testuser_keytab)
def modify_blacklist_property(operation, users, action="add"):
dbks_site_path = (
@@ -233,32 +277,6 @@ def blacklist_op_users(operation, users=[]):
def unblacklist_op_users(operation, users=[]):
modify_blacklist_property(operation, users, action="remove")
-def ensure_testuser_keytab():
- _ensure_kdc_principal_keytab(
- TESTUSER_PRINCIPAL,
- KMS_CONTAINER_NAME,
- TESTUSER_KEYTAB,
- f"/tmp/{TESTUSER}.keytab",
- )
-
-def ensure_testuser_ticket():
- ensure_testuser_keytab()
- container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
- exit_code, output = container.exec_run(
- f"kinit -kt {TESTUSER_KEYTAB} {TESTUSER_PRINCIPAL}", user="root"
- )
- if exit_code != 0:
- raise RuntimeError(f"testuser kinit failed: {output.decode()}")
-
-def ensure_keyadmin_ticket():
- ensure_keyadmin_keytab()
- container.exec_run("kdestroy -A 2>/dev/null || true", user="root")
- exit_code, output = container.exec_run(
- f"kinit -kt {KEYADMIN_KEYTAB} {KEYADMIN_PRINCIPAL}", user="root"
- )
- if exit_code != 0:
- raise RuntimeError(f"keyadmin kinit failed: {output.decode()}")
-
def ensure_test_user_exists(username: str) -> None:
payload = {
"name": username,
diff --git a/functional-tests/readme.md b/functional-tests/readme.md
index dd6b5e3e0..4fd0ec485 100644
--- a/functional-tests/readme.md
+++ b/functional-tests/readme.md
@@ -18,7 +18,7 @@ limitations under the License.
## Pytest Functional Test Suite
-This test suite validates REST API endpoints for Apache Ranger services,Admin
(rolerest, xuserrest, servicerest), KMS (Key Management Service), and tests
HDFS encryption functionalities including key management and file operations
within encryption zones.
+This test suite validates REST API endpoints for Apache Ranger services —
Admin (rolerest, xuserrest, servicerest,tagrest), KMS (Key Management Service),
and HDFS encryption functionalities including key management and file
operations within encryption zones.
### Available Test Suites
@@ -29,6 +29,7 @@ This test suite validates REST API endpoints for Apache
Ranger services,Admin (r
| **xuserrest** | Test cases for Ranger Admin User/Group REST APIs |
| **rolerest** | Test cases for Ranger Admin Role REST APIs |
| **servicerest** | Test cases for Ranger Admin Service REST APIs |
+| **tagrest** | Test cases for Ranger Admin Tag REST APIs |
---
@@ -41,113 +42,169 @@ functional-tests/
├── xuserrest/ # Tests on Ranger User/Group/Role REST APIs
├── rolerest/ # Tests on Ranger Role REST APIs
├── servicerest/ # Tests on Ranger Service REST APIs
-│
+├── tagrest/ # Tests on Ranger Tag REST APIs
+│
├── pytest.ini # Registers custom pytest markers
├── run-tests.sh # Script to automate setup and test execution
├── requirements.txt # Python dependencies
└── readme.md # This documentation
-
```
-> **Note:** A Python virtual environment folder named `myenv` will be
automatically
-> generated upon running the tests for the first time.
+> **Note:** A Python virtual environment folder named `myenv` is created
automatically
+> on the first run and **reused on all subsequent runs** — no reinstallation
overhead.
+
+---
## Prerequisites
+
1. Docker & Docker Compose installed and running
2. Python 3.10 or higher
-3. Change the working directory to functional-tests
+3. Change the working directory to `functional-tests/`
```text
cd functional-tests/
```
4. Make the shell script executable
-
```text
chmod +x run-tests.sh
```
+---
## Environment Variables
-Configure container behavior before running the script using the following
environment variables:
+These can be exported before running, or passed inline. All have sensible
defaults
+
+| Variable | Default | Description |
+|---|---|---|
+| `CLEAN_CONTAINERS` | `0` | Set to `1` to wipe all containers and force a
full rebuild from scratch |
+| `RUN_TESTS` | `1` | Set to `0` to bring up infrastructure only without
running any tests |
+| `AUDIT_INDEX_STORE` | `opensearch` | Audit backend: `opensearch`, `solr`, or
`none` to skip the audit pipeline entirely |
+
+### `CLEAN_CONTAINERS`
-1. Fresh Setup & Cleanup:
+By default containers **persist between runs** for fast re-execution. Use
`CLEAN_CONTAINERS=1` only when you want a completely clean rebuild (e.g. for
rebuilding the Docker image via Maven against your local source):
-Force a clean environment & helps building binaries with local changes:
```text
export CLEAN_CONTAINERS=1
./run-tests.sh
```
-After initial setup, disable fresh container creation to speed up subsequent
runs (default behavior):
-
+On all subsequent re-runs (no rebuild needed):
```text
-export CLEAN_CONTAINERS=0
./run-tests.sh
```
-2. Infrastructure Only (Skip Tests)
+### `RUN_TESTS`
-Start Docker infrastructure without executing Pytest suites:
+Bring up the full infrastructure without executing any tests. Useful when
containers are still initializing or you want to inspect the environment first:
```text
export RUN_TESTS=0
./run-tests.sh
```
-This is useful when tests fail due to slow container startup. Once all
containers are healthy, re-enable tests (default behavior):
+
+Once containers are healthy, run tests normally (default):
+```text
+./run-tests.sh
+```
+
+### `AUDIT_INDEX_STORE`
+
+Controls whether the audit pipeline (Kafka + ingestor + OpenSearch/Solr) is
started. Defaults to `opensearch`. Allowed values for export AUDIT_INDEX_STORE=
(solr/opensearch/none)
```text
-export RUN_TESTS=1
+export AUDIT_INDEX_STORE=none
./run-tests.sh
```
+---
## Running Tests
-The run-tests.sh script manages Docker container setup, dependency
installation, and test execution. It supports both interactive and
argument-based modes.
-1. Interactive Mode:
+The `run-tests.sh` script manages Docker container setup, dependency
installation, and test execution. It supports both interactive and
argument-based modes.
+
+### 1. Interactive Mode
-Run the script without arguments to be prompted for inputs:
+Run the script without arguments to be prompted for each input:
```text
./run-tests.sh
```
-> DB Type: Enter one of postgres, mysql, oracle, mssql. Defaults to postgres.
-> Test Suites: Enter space-separated suite names. Defaults to ALL suites.
+You will be asked three questions in sequence:
-example:
```text
-
Available DB types: postgres, mysql, oracle
Enter DB type (press Enter to default to postgres): postgres
-Available test suites: xuserrest servicerest hdfs kms
+Available audit stores: opensearch, solr, none
+Enter audit store (press Enter to default to opensearch): none
+
+Available test suites: rolerest xuserrest servicerest tagrest hdfs kms
Enter test suites space-separated (press Enter to run ALL): kms hdfs
```
-2. Command-Line Arguments Mode:
+> Press **Enter** at any prompt to accept the default value.
+
+### 2. Command-Line Arguments Mode
-Pass arguments directly to skip prompts:
+Pass `db-type` and `test-suites` directly to skip prompts. Use env vars for
the remaining options:
```text
./run-tests.sh [db-type] [test-suites...]
```
-db-type — Must be the first argument. Valid values: postgres, mysql, oracle.
-test-suites — Space-separated list: hdfs, kms, xuserrest, servicerest.
+- `db-type` — First argument. Valid values: `postgres`, `mysql`, `oracle`.
+- `test-suites` — Space-separated list: `hdfs`, `kms`, `rolerest`,
`xuserrest`, `servicerest`, `tagrest`.
Examples:
```text
+# Run kms and hdfs tests with postgres, no audit pipeline
+export AUDIT_INDEX_STORE=none
./run-tests.sh postgres kms hdfs
- ```
+
+# Run all suites with mysql and opensearch audit
+./run-tests.sh mysql
+
+# Full clean rebuild with all suites
+CLEAN_CONTAINERS=1 ./run-tests.sh postgres
+```
+
+---
+
+## Containers Brought Up
+
+### Always (Base Services)
+
+Regardless of which test suites you choose, these containers always start:
+
+| Container | Role |
+|---|---|
+| `ranger` | Ranger Admin (policies, users, REST APIs) |
+| `ranger-kdc` | Kerberos KDC |
+| `ranger-<db>` | Database (`ranger-postgres`, `ranger-mysql`,
`ranger-oracle`) |
+| `ranger-zk` | ZooKeeper |
+| `ranger-kms` | Ranger KMS |
+
+### Audit Pipeline (when `AUDIT_INDEX_STORE != none`)
+
+| Container | Role |
+|---|---|
+| `ranger-kafka` | Kafka broker |
+| `ranger-audit-ingestor` | Reads from Kafka, writes to audit store |
+| `ranger-opensearch` / `ranger-solr` | Audit index store |
+| `ranger-audit-dispatcher-<store>` | Dispatches audit events |
+
+### Suite-Specific
+
+| Suite | Extra Container |
+|---|---|
+| `hdfs` | `ranger-hadoop` |
+| `kms` | _(already in base)_ |
+| `rolerest`, `xuserrest`, `servicerest`, `tagrest`| _(none — use Ranger Admin
API only)_ |
+
+---
## Test Reports
-After execution, HTML reports are automatically generated for each suite. Open
the corresponding file in any browser to view detailed results:
-
-| Suite | Report File |
-|:------------|:------------------------|
-| hdfs | report_hdfs.html |
-| kms | report_kms.html |
-| xuserrest | report_xuserrest.html |
-| rolerest | report_rolerest.html |
-| servicerest | report_servicerest.html |
\ No newline at end of file
+
+After execution, an HTML report is automatically generated for each suite in
the `functional-tests/` directory:
diff --git a/functional-tests/run-tests.sh b/functional-tests/run-tests.sh
index 652ee5cf1..758681911 100755
--- a/functional-tests/run-tests.sh
+++ b/functional-tests/run-tests.sh
@@ -16,13 +16,13 @@
#!/bin/bash
# All available test suites (pytest folders)
-ALL_TEST_SUITES=(rolerest xuserrest servicerest hdfs kms)
+ALL_TEST_SUITES=(rolerest xuserrest servicerest tagrest hdfs kms)
# Suites that have actual docker-compose services
DOCKER_SERVICES=(hdfs kms)
# Test-only suites (no docker-compose file needed)
-TEST_ONLY_SUITES=(rolerest xuserrest servicerest)
+TEST_ONLY_SUITES=(rolerest xuserrest servicerest tagrest)
#handle input
DB_TYPE="${1:-}"
@@ -37,6 +37,31 @@ if [[ -z "${DB_TYPE}" ]]; then
DB_TYPE="${input_db:-postgres}"
fi
+# Prompt for AUDIT_INDEX_STORE if not provided
+AUDIT_INDEX_STORE="${AUDIT_INDEX_STORE:-}"
+if [[ -z "${AUDIT_INDEX_STORE}" ]]; then
+ echo ""
+ echo "Available audit stores: opensearch, solr, none"
+ read -rp "Enter audit store (press Enter to default to opensearch): "
input_audit
+ AUDIT_INDEX_STORE="${input_audit:-opensearch}"
+fi
+
+# Set AUDIT_DESTINATIONS based on AUDIT_INDEX_STORE
+if [[ "${AUDIT_INDEX_STORE}" == "none" ]]; then
+ AUDIT_DESTINATIONS=""
+else
+ AUDIT_DESTINATIONS="audit-store-${AUDIT_INDEX_STORE}"
+fi
+
+# Export environment variables
+export RANGER_DB_TYPE="${DB_TYPE}"
+export AUDIT_INDEX_STORE="${AUDIT_INDEX_STORE}"
+export AUDIT_DESTINATIONS="${AUDIT_DESTINATIONS}"
+
+# Default to 0 — containers persist after each run.
+# Pass CLEAN_CONTAINERS=1 explicitly to wipe and rebuild everything.
+CLEAN_CONTAINERS="${CLEAN_CONTAINERS:-0}"
+
# Prompt for EXTRA_SERVICES / TEST SUITES if not provided
if [[ "${#EXTRA_SERVICES[@]}" -eq 0 ]]; then
echo ""
@@ -51,8 +76,10 @@ if [[ "${#EXTRA_SERVICES[@]}" -eq 0 ]]; then
fi
echo ""
-echo "DB Type : ${DB_TYPE}"
-echo "Test Suites : ${EXTRA_SERVICES[*]}"
+echo "DB Type : ${DB_TYPE}"
+echo "Audit Store : ${AUDIT_INDEX_STORE}"
+echo "Audit Destinations: ${AUDIT_DESTINATIONS}"
+echo "Test Suites : ${EXTRA_SERVICES[*]}"
echo ""
echo "CLEAN_CONTAINERS=${CLEAN_CONTAINERS}"
@@ -83,6 +110,9 @@ for service in "${EXTRA_SERVICES[@]}"; do
# 'hadoop' arg alone does NOT download tez
DOCKER_BACKED+=("hive")
;;
+ kms)
+ : # KMS needs no archive download — it is always in BASE_SERVICES as
a container
+ ;;
*)
DOCKER_BACKED+=("$service")
;;
@@ -91,8 +121,11 @@ for service in "${EXTRA_SERVICES[@]}"; do
done
done
-# Deduplicatec
-DOCKER_BACKED+=("kms")
+# Kafka archive is needed whenever the audit pipeline is up (ranger-kafka
build)
+if [[ "${AUDIT_INDEX_STORE}" != "none" ]]; then
+ DOCKER_BACKED+=("kafka")
+fi
+
DOCKER_BACKED=($(printf '%s\n' "${DOCKER_BACKED[@]}" | sort -u))
if [[ "${#DOCKER_BACKED[@]}" -gt 0 ]]; then
@@ -148,6 +181,11 @@ DOCKER_FILES=(
"-f" "docker-compose.ranger-kms.yml"
)
+# Add audit service if audit store is not "none"
+if [[ "${AUDIT_INDEX_STORE}" != "none" ]]; then
+ DOCKER_FILES+=("-f" "docker-compose.ranger-audit-service.yml")
+fi
+
# Add compose files based on requested suites
for service in "${EXTRA_SERVICES[@]}"; do
case "$service" in
@@ -161,8 +199,19 @@ for service in "${EXTRA_SERVICES[@]}"; do
done
# Build ALL_SERVICES list — only docker-backed services get container checks
-BASE_SERVICES=(ranger ranger-${RANGER_DB_TYPE} ranger-zk ranger-solr
ranger-kms)
+BASE_SERVICES=(ranger ranger-kdc ranger-${RANGER_DB_TYPE} ranger-zk ranger-kms)
ALL_SERVICES=("${BASE_SERVICES[@]}")
+
+# Audit pipeline containers brought up by
docker-compose.ranger-audit-service.yml
+if [[ "${AUDIT_INDEX_STORE}" != "none" ]]; then
+ ALL_SERVICES+=(
+ ranger-kafka
+ ranger-audit-ingestor
+ "ranger-${AUDIT_INDEX_STORE}"
+ "ranger-audit-dispatcher-${AUDIT_INDEX_STORE}"
+ )
+fi
+
for service in "${EXTRA_SERVICES[@]}"; do
case "$service" in
hdfs)
@@ -186,10 +235,18 @@ done
if [[ "${missing}" == "true" ]]; then
echo "Some containers are missing. Creating services..."
- docker compose "${DOCKER_FILES[@]}" up -d --build
+ if [[ -n "${AUDIT_DESTINATIONS}" ]]; then
+ docker compose --profile "${AUDIT_DESTINATIONS}" "${DOCKER_FILES[@]}" up
-d --build
+ else
+ docker compose "${DOCKER_FILES[@]}" up -d --build
+ fi
else
echo "All containers already exist. Starting without rebuild..."
- docker compose "${DOCKER_FILES[@]}" up -d
+ if [[ -n "${AUDIT_DESTINATIONS}" ]]; then
+ docker compose --profile "${AUDIT_DESTINATIONS}" "${DOCKER_FILES[@]}" up -d
+ else
+ docker compose "${DOCKER_FILES[@]}" up -d
+ fi
fi
echo "Waiting for containers to start..."
@@ -247,10 +304,15 @@ if [[ $flag == true ]]; then
echo "All required containers are up. Running test cases..."
cd "$TESTS_PATH" || exit 1 # Switch to the tests directory
- python3 -m venv myenv || { echo "Failed to create venv"; exit 1; }
# Create a new virtual environment
- source myenv/bin/activate || { echo "Failed to activate venv"; exit 1; }
# Activate it
- pip install --upgrade pip
- pip install -r requirements.txt || { echo "Failed to install
requirements"; exit 1; } # Install dependencies
+ # Only create the venv (and upgrade pip) on first run — skip on re-runs.
+ if [[ ! -d "myenv" ]]; then
+ python3 -m venv myenv || { echo "Failed to create venv"; exit 1; }
+ source myenv/bin/activate || { echo "Failed to activate venv"; exit 1; }
+ pip install --upgrade pip # upgrade pip once, when the venv is
fresh
+ else
+ source myenv/bin/activate || { echo "Failed to activate venv"; exit 1; }
+ fi
+ pip install -q -r requirements.txt || { echo "Failed to install
requirements"; exit 1; } # Install/sync dependencies
echo ""
echo "Running tests for suites: ${EXTRA_SERVICES[*]}"