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/ 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..c48cbe9 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('/'): + # If it ends with a path separator but no wildcard, append '*' + 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('/') + '/*') + 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/*"'