Skip to content

feat(otdf-local): add CLI for local test environment management#406

Draft
dmihalcik-virtru wants to merge 4 commits intoopentdf:mainfrom
dmihalcik-virtru:feat/otdf-local
Draft

feat(otdf-local): add CLI for local test environment management#406
dmihalcik-virtru wants to merge 4 commits intoopentdf:mainfrom
dmihalcik-virtru:feat/otdf-local

Conversation

@dmihalcik-virtru
Copy link
Member

Summary

  • Add new otdf-local Python package for managing Docker services, platform, KAS instances, health checks, log viewing, and environment export
  • Add "Lint otdf-local" step to CI check workflow
  • Add /xtest/logs/ to .gitignore
  • Add Environment Management section to AGENTS.md

Parent PRs

Test plan

  • cd otdf-local && uv sync && uv run ruff check . && uv run ruff format --check . && uv run pyright
  • cd otdf-local && uv run pytest tests/
  • CI lint workflow passes

🤖 Generated with Claude Code

Part of stacked PR series decomposing chore/the-claudiest-day-tmux

dmihalcik-virtru and others added 4 commits February 12, 2026 19:58
setup-uv already handles Python installation, making the separate
setup-python action redundant. This also removes the pip install
step in resolve-versions, which will use uv instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: David Mihalcik <dmihalcik@virtru.com>
Rewrite AGENTS.md as comprehensive agent guide with test framework
overview, key concepts, debugging workflow, and best practices.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: David Mihalcik <dmihalcik@virtru.com>
Add new Python package for managing Docker services, platform,
KAS instances, health checks, log viewing, and environment export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: David Mihalcik <dmihalcik@virtru.com>
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dmihalcik-virtru, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a dedicated Python CLI tool, otdf-local, to simplify the management of the local OpenTDF test environment. This initiative aims to enhance developer experience by providing a centralized and consistent way to interact with various services, replacing ad-hoc scripts with a structured command-line interface. Concurrently, the documentation has been thoroughly revised to guide users through the new environment management practices and debugging procedures, ensuring a smoother development and testing workflow.

Highlights

  • New CLI Tool for Local Environment Management: Introduced a new Python package, otdf-local, which provides a command-line interface for managing the local OpenTDF test environment. This tool streamlines operations such as starting/stopping Docker services, platform components, KAS instances, performing health checks, viewing logs, and exporting environment variables.
  • Enhanced Documentation for Test Environment: Significantly updated AGENTS.md to include a comprehensive 'Agent Guide' for working with OpenTDF tests and debugging. This new section details test framework overview, running tests, environment management using otdf-local, key TDF concepts, common test failures, debugging workflows, and best practices for code modification.
  • CI Integration for otdf-local: Added a new 'Lint otdf-local' step to the continuous integration workflow to ensure code quality and consistency for the newly introduced otdf-local Python package.
  • Improved Log Management: Added /xtest/logs/ to the .gitignore file to prevent local log files generated by the test environment from being committed to the repository.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • .gitignore
    • Added '/xtest/logs/' to ignore generated log files.
  • AGENTS.md
    • Renamed document title to 'Agent Guide: Working with OpenTDF Tests and Debugging'.
    • Restructured and expanded content to include a 'Test Framework Overview', 'Environment Management' (introducing otdf-local), 'Key Concepts' (TDF Wrapping Algorithms), 'Common Test Failures and Debugging', 'Debugging Workflow', 'Code Modification Best Practices', 'Test File Organization', 'Common Pitfalls', and 'Quick Reference'.
    • Removed outdated sections on 'Repository Guidelines', 'Project Structure & Module Organization', 'Build, Test, and Development Commands', and 'Coding Style & Naming Conventions'.
  • otdf-local/CLAUDE.md
    • Added a new operational guide for otdf-local, detailing environment setup, service ports, restart procedures (full, service-specific, manual), log viewing, golden key auto-configuration, troubleshooting, and extending otdf-local.
  • otdf-local/README.md
    • Added a new README file for the otdf-local Python CLI, covering installation, global installation with tab completion, quick start, detailed command reference (up, down, ls, status, logs, restart, provision, env, clean), tmux session integration, managed services, configuration, development guidelines, and project structure.
  • otdf-local/pyproject.toml
    • Added a new pyproject.toml file, defining the otdf-local Python package, its version, description, Python requirements, core dependencies (docker, httpx, pydantic-settings, rich, ruamel.yaml, typer), development dependencies (pyright, pytest, ruff), and the otdf-local CLI entry point.
  • otdf-local/src/otdf_local/init.py
    • Added the package initialization file, setting the otdf-local version.
  • otdf-local/src/otdf_local/main.py
    • Added the main entry point to allow otdf-local to be run as a Python module.
  • otdf-local/src/otdf_local/cli.py
    • Added the core CLI implementation using typer, defining commands for managing the test environment: up (start services), down (stop services), ls (list services), status (show detailed status), logs (view service logs), clean (cleanup generated files), provision (run provisioning steps), and restart (restart specific services).
  • otdf-local/src/otdf_local/config/init.py
    • Added the package initialization file for the config module.
  • otdf-local/src/otdf_local/config/features.py
    • Added a module to detect platform features based on its version, including parsing semantic versioning and computing supported features like EC wrapping, hexless format, and logger output options.
  • otdf-local/src/otdf_local/config/ports.py
    • Added a module defining port constants for various services, including Keycloak, PostgreSQL, the OpenTDF Platform, and multiple KAS instances (alpha, beta, gamma, delta, km1, km2).
  • otdf-local/src/otdf_local/config/settings.py
    • Added a module for managing application settings using Pydantic, including automatic discovery of the xtest root and platform directories, defining paths for logs, keys, and configurations, and setting default service ports and URLs.
  • otdf-local/src/otdf_local/health/init.py
    • Added the package initialization file for the health module.
  • otdf-local/src/otdf_local/health/checks.py
    • Added utilities for performing port availability checks and HTTP health checks, including a function to determine overall service status.
  • otdf-local/src/otdf_local/health/waits.py
    • Added utilities for waiting until a service or condition is ready, including wait_for_port, wait_for_health, wait_for_condition, and wait_for_multiple, along with a WaitTimeoutError exception.
  • otdf-local/src/otdf_local/process/init.py
    • Added the package initialization file for the process module.
  • otdf-local/src/otdf_local/process/logs.py
    • Added utilities for managing and aggregating log files, including LogEntry and LogReader classes, and a LogAggregator for combining logs from multiple services with filtering and following capabilities.
  • otdf-local/src/otdf_local/process/manager.py
    • Added a module for managing external subprocesses, including ManagedProcess and ProcessManager classes for starting, stopping, and monitoring processes, along with utilities to kill processes by port or name.
  • otdf-local/src/otdf_local/services/init.py
    • Added the package initialization file for the services module.
  • otdf-local/src/otdf_local/services/base.py
    • Added an abstract base class Service and a ServiceInfo dataclass, defining a common interface for managing different types of services (Docker, subprocess) with properties for name, port, type, running status, and health checks.
  • otdf-local/src/otdf_local/services/docker.py
    • Added a DockerService class to manage Docker Compose services (Keycloak, PostgreSQL), providing methods to start, stop, check running status, and retrieve detailed container information.
  • otdf-local/src/otdf_local/services/kas.py
    • Added a KASService class to manage individual Key Access Server (KAS) instances and a KASManager to oversee all KAS instances, including dynamic configuration generation based on platform features and root keys.
  • otdf-local/src/otdf_local/services/platform.py
    • Added a PlatformService class to manage the OpenTDF platform, including generating its configuration from a template, setting up golden keys for legacy TDF decryption, and managing its lifecycle as a subprocess.
  • otdf-local/src/otdf_local/services/provisioner.py
    • Added a Provisioner class to handle provisioning tasks for Keycloak and test fixtures, executing Go-based provisioning scripts and returning detailed results.
  • otdf-local/src/otdf_local/utils/init.py
    • Added the package initialization file for the utils module.
  • otdf-local/src/otdf_local/utils/console.py
    • Added Rich console helpers for formatted output, including functions for printing success, error, warning, and info messages, creating service status tables, and managing status spinners.
  • otdf-local/src/otdf_local/utils/keys.py
    • Added utilities for cryptographic key management, including generating root keys, RSA and EC keypairs, ensuring key existence, and setting up golden keys for legacy TDF decryption.
  • otdf-local/src/otdf_local/utils/yaml.py
    • Added utilities for loading, saving, and manipulating YAML files using ruamel.yaml, including functions for getting/setting nested values, updating files with dot-notation paths, and appending items to lists.
  • otdf-local/tests/init.py
    • Added the package initialization file for the tests module.
  • otdf-local/tests/test_health.py
    • Added unit tests for the health check utilities, covering port checks, HTTP health checks, service status determination, and various wait-for-condition scenarios.
  • otdf-local/tests/test_integration.py
    • Added integration tests for the otdf-local CLI, verifying basic commands like version and help, and testing the full up/down lifecycle of Docker services.
  • otdf-local/uv.lock
    • Added a uv.lock file to precisely manage and lock the versions of all project dependencies for otdf-local.
Ignored Files
  • Ignored by pattern: .github/workflows/** (2)
    • .github/workflows/check.yml
    • .github/workflows/xtest.yml
Activity
  • The pull request was generated using Claude Code.
  • This PR is part of a larger stacked PR series, decomposing a larger change related to chore/the-claudiest-day-tmux.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the otdf-local Python package, a CLI for managing the local test environment, significantly improving upon shell script usage. While the code is generally well-structured, a potential path traversal vulnerability has been identified when setting up golden keys, and sensitive directories/files are created with world-readable default permissions. The review also addresses improving overall robustness, fixing a critical dependency issue, and ensuring consistency across documentation, configuration, and implementation, including corrections to file paths, improved path resolution, and suggestions for performance and portability.

Comment on lines +598 to +613
try:
import requests

platform = get_platform_service(settings)
if platform.is_running():
resp = requests.get(
f"{settings.platform_url}/.well-known/opentdf-configuration",
timeout=5,
)
if resp.ok:
config = resp.json()
if "version" in config:
env_vars["PLATFORM_VERSION"] = config["version"]
except Exception:
# If we can't get the version, that's okay
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The requests library is imported and used here, but it is not listed as a dependency in pyproject.toml. This will cause a runtime error if the library is not installed. The httpx library is already a dependency, so it should be used here instead for consistency.

Please add import httpx to the top of the file and use it here. Also, it's better to catch more specific exceptions than the broad Exception.

Suggested change
try:
import requests
platform = get_platform_service(settings)
if platform.is_running():
resp = requests.get(
f"{settings.platform_url}/.well-known/opentdf-configuration",
timeout=5,
)
if resp.ok:
config = resp.json()
if "version" in config:
env_vars["PLATFORM_VERSION"] = config["version"]
except Exception:
# If we can't get the version, that's okay
pass
# Try to get platform version from API
try:
platform = get_platform_service(settings)
if platform.is_running():
resp = httpx.get(
f"{settings.platform_url}/.well-known/opentdf-configuration",
timeout=5,
)
resp.raise_for_status()
config = resp.json()
if "version" in config:
env_vars["PLATFORM_VERSION"] = config["version"]
except (httpx.RequestError, json.JSONDecodeError):
# If we can't get the version, that's okay
pass

Comment on lines +14 to +31
def _find_xtest_root() -> Path:
"""Find the xtest/tests root directory by walking up from this file."""
current = Path(__file__).resolve()
while current != current.parent:
# Check if we're in the otdf_local package within tests
if (current / "otdf_local").exists() and current.name == "tests":
return current
# Legacy: check for xtest directory
if (current / "conftest.py").exists() and current.name == "xtest":
return current
# Also check if we're in the otdf_local package within xtest
if (current / "otdf_local").exists() and (
current.parent / "conftest.py"
).exists():
return current.parent
current = current.parent
# Fallback to assuming we're in tests/otdf-local
return Path(__file__).resolve().parent.parent.parent.parent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic in _find_xtest_root seems fragile and might not work correctly depending on where the code is checked out. It makes assumptions about the directory structure (e.g., that otdf-local is inside a tests or xtest directory) which don't seem to hold given the file structure in this PR. The fallback logic is also likely incorrect, as it resolves to the otdf-local directory itself, not the repository root. This will lead to incorrect paths for logs and other resources.

A more robust approach would be to find the project root based on a marker file like .git or by looking for the repo root, and then construct paths from there.

## Environment Setup for pytest

```bash
cd tests/otdf-local
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The path to the otdf-local package seems to be incorrect. Based on the file structure in this pull request, the package is located at otdf-local/, not tests/otdf-local/. This should be corrected throughout the documentation to avoid confusion.

Suggested change
cd tests/otdf-local
cd otdf-local

Comment on lines +183 to +184
private_path = platform_dir / f"{kid}-private.pem"
cert_path = platform_dir / f"{kid}-cert.pem"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The setup_golden_keys function uses the kid field from extra-keys.json to construct file paths for writing private keys and certificates without proper sanitization. A malicious kid value containing path traversal sequences (e.g., ../../) could allow an attacker to write files to arbitrary locations on the filesystem, potentially overwriting sensitive system files if the tool is run with sufficient privileges.

Comment on lines +106 to +108
tail -f tests/xtest/logs/platform.log
tail -f tests/xtest/logs/kas-alpha.log
tail -f tests/xtest/logs/kas-km1.log
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The log file paths mentioned here appear to be inconsistent with the implementation and other documentation. The settings.py file defines the log directory as xtest_root / "tmp" / "logs", and the .gitignore is updated for /xtest/logs/. Please ensure all paths are consistent across the implementation and documentation for clarity.

Comment on lines +148 to +150
self.logs_dir.mkdir(parents=True, exist_ok=True)
self.config_dir.mkdir(parents=True, exist_ok=True)
self.keys_dir.mkdir(parents=True, exist_ok=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The ensure_directories method creates directories for storing sensitive information, such as private keys and configuration files containing the root_key, using default system permissions. On many systems, this results in these directories and the files within them being world-readable, which could allow other users on the same system to access sensitive cryptographic material.

Comment on lines +153 to +158
@lru_cache
def get_settings() -> Settings:
"""Get cached settings instance."""
platform_url = os.environ.get("OTDF_LOCAL_PLATFORM_URL", "http://localhost:8080")
keycloak_url = os.environ.get("OTDF_LOCAL_KEYCLOAK_URL", "http://localhost:8888")
return Settings(platform_url=platform_url, keycloak_url=keycloak_url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The get_settings function manually reads environment variables, which is redundant as pydantic-settings is designed to handle this automatically. You can simplify this by letting Pydantic manage the environment variable loading.

In addition to this change, you can simplify the field definitions in the Settings class (lines 124-125) to:

    platform_url: str = "http://localhost:8080"
    keycloak_url: str = "http://localhost:8888"

pydantic-settings will automatically look for OTDF_LOCAL_PLATFORM_URL and OTDF_LOCAL_KEYCLOAK_URL and fall back to these defaults if the environment variables are not set. This makes the code cleaner and relies on the library's intended usage.

Suggested change
@lru_cache
def get_settings() -> Settings:
"""Get cached settings instance."""
platform_url = os.environ.get("OTDF_LOCAL_PLATFORM_URL", "http://localhost:8080")
keycloak_url = os.environ.get("OTDF_LOCAL_KEYCLOAK_URL", "http://localhost:8888")
return Settings(platform_url=platform_url, keycloak_url=keycloak_url)
@lru_cache
def get_settings() -> Settings:
"""Get cached settings instance."""
return Settings()

Comment on lines +40 to +49
def read_tail(self, n: int = 50) -> list[LogEntry]:
"""Read the last n lines from the log file."""
if not self.log_file.exists():
return []

# Simple tail implementation
with open(self.log_file) as f:
lines = f.readlines()

return [self._parse_line(line) for line in lines[-n:]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The read_tail implementation reads the entire file into memory to get the last n lines. This can be inefficient for large log files. For better performance and memory usage, consider a more efficient implementation that seeks near the end of the file and reads backwards.

Comment on lines +177 to +205
def kill_process_on_port(port: int) -> bool:
"""Kill any process listening on the specified port.

Args:
port: Port number

Returns:
True if a process was killed
"""
try:
# Use lsof to find process on port
result = subprocess.run(
["lsof", "-ti", f":{port}"],
capture_output=True,
text=True,
)
if result.returncode != 0 or not result.stdout.strip():
return False

pids = result.stdout.strip().split("\n")
for pid in pids:
try:
os.kill(int(pid), signal.SIGKILL)
except (ProcessLookupError, ValueError):
pass
return True
except FileNotFoundError:
# lsof not available
return False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The kill_process_on_port function relies on lsof, which is not available on Windows. This limits the portability of this feature. While FileNotFoundError is handled, you might consider adding a platform-specific implementation for Windows (e.g., using netstat -ano and taskkill) if Windows support is a goal for this tool. The same applies to find_pid_by_name which uses pgrep.

capture_output=True,
text=True,
timeout=timeout,
cwd=str(__file__).rsplit("/tests/", 1)[0],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cwd path is constructed with str(__file__).rsplit("/tests/", 1)[0]. This is a bit fragile as it assumes a specific directory structure. It would be more robust to determine the project root dynamically, for example by searching upwards for the pyproject.toml file. This would make the tests less dependent on the exact location of the test file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant