Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ java/.settings/
.tox/
.eggs/
*.egg-info/
/.venv/
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
49 changes: 48 additions & 1 deletion confluent/docker_utils/cub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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
--------------------------------------------------------------------------------

Expand Down
45 changes: 45 additions & 0 deletions test/test_classpath.py
Original file line number Diff line number Diff line change
@@ -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/*"'