From cd79ec617d5dd576771c118cb7d6e17dc81f710f Mon Sep 17 00:00:00 2001 From: "ahmed.toumi-ext" Date: Thu, 2 Oct 2025 16:02:37 +0200 Subject: [PATCH 1/3] #111: Add support for extending Java CLASSPATH via CUB_CLASSPATH_DIRS environment variable --- README.md | 18 +++++++++++++ confluent/docker_utils/cub.py | 49 ++++++++++++++++++++++++++++++++++- docs/CHANGELOG.rst | 7 +++++ test/test_classpath.py | 45 ++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 test/test_classpath.py diff --git a/README.md b/README.md index a28e9cf..f4ca834 100644 --- a/README.md +++ b/README.md @@ -7,5 +7,23 @@ This project includes common logic for testing Confluent's Docker images. For more information, see: https://docs.confluent.io/platform/current/installation/docker/development.html#docker-utility-belt-dub +## Extending the Java CLASSPATH + +The `cub` utility now supports extending the Java CLASSPATH at runtime via environment variables: + +- `CUB_CLASSPATH` (existing): overrides the entire base classpath when set. Keep quotes if you pass it as a single value. +- `CUB_CLASSPATH_DIRS` (new): append one or more directories to the base classpath. + - Accepts multiple entries separated by `:`, `;` or `,`. + - Each directory is normalized to include all jars within it (a trailing `/*` is added if missing). + - The final CLASSPATH is kept quoted to avoid shell expansion issues. + - The final classpath separator is always `:` (Linux/JVM convention in Confluent images). +- `CUB_EXTRA_CLASSPATH` (legacy fallback): used only if `CUB_CLASSPATH_DIRS` is not set. + +Examples: +- Linux: set `CUB_CLASSPATH_DIRS=/opt/libs:/opt/plugins,/usr/share/java/custom` +- Windows host (executing inside Linux containers): set `CUB_CLASSPATH_DIRS=C:\\libs;C:\\plugins` — entries are parsed but the resulting CLASSPATH uses `:` between segments. + +If neither `CUB_CLASSPATH_DIRS` nor `CUB_EXTRA_CLASSPATH` is provided, the default base classpath is used. + ## License [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fconfluentinc%2Fconfluent-docker-utils.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fconfluentinc%2Fconfluent-docker-utils?ref=badge_large) diff --git a/confluent/docker_utils/cub.py b/confluent/docker_utils/cub.py index 05d9a32..4e5522b 100755 --- a/confluent/docker_utils/cub.py +++ b/confluent/docker_utils/cub.py @@ -46,7 +46,54 @@ import time from requests.auth import HTTPBasicAuth -CLASSPATH = os.environ.get("CUB_CLASSPATH", '"/usr/share/java/cp-base/*:/usr/share/java/cp-base-new/*"') +# Build CLASSPATH with optional extra directories from env var CUB_CLASSPATH_DIRS (or fallback CUB_EXTRA_CLASSPATH) +# - Accepts one or more directories separated by ':' ';' or ',' +# - For each directory, if no wildcard/jar is provided, appends '/*' to include all jars within +# - Uses ':' as final classpath separator (Linux/JVM convention) +DEFAULT_BASE_CLASSPATH = '"/usr/share/java/cp-base/*:/usr/share/java/cp-base-new/*"' + +def _strip_outer_quotes(s): + s = s.strip() + if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")): + return s[1:-1] + return s + +def _normalize_extra_classpath(extra_value): + if not extra_value: + return [] + parts = re.split(r'[;:,]', extra_value) + normalized = [] + for p in parts: + p = p.strip() + if not p: + continue + # If user already provided wildcard or a specific jar/file, keep as is + if '*' in p or p.endswith('.jar') or p.endswith('/') or p.endswith('\\'): + # If it ends with a path separator but no wildcard, append '*' + if (p.endswith('/') or p.endswith('\\')) and '*' not in p and not p.endswith('/*'): + p = p.rstrip('/\\') + '/*' + normalized.append(p) + else: + # Append wildcard to include jars under the directory + normalized.append(p.rstrip('/\\') + '/*') + return normalized + +def _build_classpath(): + base = os.environ.get("CUB_CLASSPATH", DEFAULT_BASE_CLASSPATH) + extra = os.environ.get("CUB_CLASSPATH_DIRS") or os.environ.get("CUB_EXTRA_CLASSPATH") or "" + + base_unquoted = _strip_outer_quotes(base) + extras = _normalize_extra_classpath(extra) + + if not extras: + # Keep original quoting + return base if base.strip() else DEFAULT_BASE_CLASSPATH + + sep = ":" # Always use JVM/Linux classpath separator + full = sep.join([p for p in [base_unquoted] + extras if p]) + return f'"{full}"' + +CLASSPATH = _build_classpath() LOG4J_FILE_NAME = "log4j.properties" DEFAULT_LOG4J_FILE = f"/etc/cp-base-new/{LOG4J_FILE_NAME}" LOG4J2_FILE_NAME = "log4j2.yaml" diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index ef76782..85d0d7f 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -1,3 +1,10 @@ +Unreleased +-------------------------------------------------------------------------------- + +* Add `CUB_CLASSPATH_DIRS` environment variable to append one or more directories to the Java CLASSPATH (fallback to `CUB_EXTRA_CLASSPATH`). + Accepts `:`, `;` or `,` separators and normalizes each directory to include `/*` when missing. + + Version 0.0.35 -------------------------------------------------------------------------------- diff --git a/test/test_classpath.py b/test/test_classpath.py new file mode 100644 index 0000000..112ea96 --- /dev/null +++ b/test/test_classpath.py @@ -0,0 +1,45 @@ +import os +import sys +import importlib +from mock import patch + + +def _load_cub_with_env(env): + with patch.dict('os.environ', env, clear=True): + # Ensure a fresh import to recompute CLASSPATH + if 'confluent.docker_utils.cub' in sys.modules: + del sys.modules['confluent.docker_utils.cub'] + mod = importlib.import_module('confluent.docker_utils.cub') + return mod + + +def test_classpath_default_kept_when_no_extra(): + cub = _load_cub_with_env({}) + assert cub.CLASSPATH == cub.DEFAULT_BASE_CLASSPATH + + +def test_classpath_with_single_dir_via_CUB_CLASSPATH_DIRS(): + cub = _load_cub_with_env({'CUB_CLASSPATH_DIRS': '/opt/libs'}) + base_unquoted = cub.DEFAULT_BASE_CLASSPATH[1:-1] + expected = '"' + base_unquoted + ':' + '/opt/libs/*' + '"' + assert cub.CLASSPATH == expected + + +def test_classpath_with_multiple_dirs_and_delimiters(): + cub = _load_cub_with_env({'CUB_CLASSPATH_DIRS': '/opt/a, /opt/b;/opt/c: /opt/d/*'}) + base_unquoted = cub.DEFAULT_BASE_CLASSPATH[1:-1] + extras = ['/opt/a/*', '/opt/b/*', '/opt/c/*', '/opt/d/*'] + expected = '"' + ':'.join([base_unquoted] + extras) + '"' + assert cub.CLASSPATH == expected + + +def test_classpath_with_fallback_CUB_EXTRA_CLASSPATH(): + cub = _load_cub_with_env({'CUB_EXTRA_CLASSPATH': '/ext/libs/'}) + base_unquoted = cub.DEFAULT_BASE_CLASSPATH[1:-1] + expected = '"' + base_unquoted + ':' + '/ext/libs/*' + '"' + assert cub.CLASSPATH == expected + + +def test_classpath_respects_explicit_CUB_CLASSPATH_when_no_extra(): + cub = _load_cub_with_env({'CUB_CLASSPATH': '"/custom/base1/*:/custom/base2/*"'}) + assert cub.CLASSPATH == '"/custom/base1/*:/custom/base2/*"' From d5609a567dd38ada0101e6cc8dd94583e6163c55 Mon Sep 17 00:00:00 2001 From: "ahmed.toumi-ext" Date: Fri, 3 Oct 2025 00:02:53 +0200 Subject: [PATCH 2/3] #111: Add support for extending Java CLASSPATH via CUB_CLASSPATH_DIRS environment variable --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 59d621d..c324044 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ java/.settings/ .tox/ .eggs/ *.egg-info/ +/.venv/ From 5047cf7134e707037d22b3f12f1689bba3db8134 Mon Sep 17 00:00:00 2001 From: Ahmed Toumi Date: Fri, 3 Oct 2025 00:26:22 +0200 Subject: [PATCH 3/3] Update confluent/docker_utils/cub.py check only linux file separator Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- confluent/docker_utils/cub.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/confluent/docker_utils/cub.py b/confluent/docker_utils/cub.py index 4e5522b..c48cbe9 100755 --- a/confluent/docker_utils/cub.py +++ b/confluent/docker_utils/cub.py @@ -68,14 +68,14 @@ def _normalize_extra_classpath(extra_value): if not p: continue # If user already provided wildcard or a specific jar/file, keep as is - if '*' in p or p.endswith('.jar') or p.endswith('/') or p.endswith('\\'): + if '*' in p or p.endswith('.jar') or p.endswith('/'): # If it ends with a path separator but no wildcard, append '*' - if (p.endswith('/') or p.endswith('\\')) and '*' not in p and not p.endswith('/*'): - p = p.rstrip('/\\') + '/*' + if p.endswith('/') and '*' not in p and not p.endswith('/*'): + p = p.rstrip('/') + '/*' normalized.append(p) else: # Append wildcard to include jars under the directory - normalized.append(p.rstrip('/\\') + '/*') + normalized.append(p.rstrip('/') + '/*') return normalized def _build_classpath():