From c124069d30df4008b14166ef64d20659c6e7148e Mon Sep 17 00:00:00 2001 From: Abhishek Bongale Date: Tue, 19 May 2026 15:12:48 +0100 Subject: [PATCH] Basic CLI structure - Created main CLI entry point with Click - Defined all command signatures and arguments - Added helpful `--help` text for discoverability - Maked commands callable (even if they don't do anything yet) Closes: #3 Signed-off-by: Abhishek Bongale --- .github/PULL_REQUEST_TEMPLATE.md | 51 ------- pyproject.toml | 4 + stackbox/cli/__init__.py | 5 + stackbox/cli/__main__.py | 247 +++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 51 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 stackbox/cli/__main__.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index e613a94..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,51 +0,0 @@ -## Description - - -## Related Issue - -Fixes # - -## Type of Change - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update -- [ ] Refactoring (no functional changes) - -## Checklist - -- [ ] My code follows the style guidelines of this project (black, ruff) -- [ ] I have performed a self-review of my code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published - -## Testing - - -### Unit Tests -```bash -pytest tests/unit/ -v -``` - -### Code Quality -```bash -black --check stackbox/ tests/ -ruff check stackbox/ tests/ -mypy stackbox/core/compose.py -``` - -### Coverage -```bash -pytest --cov=stackbox --cov-report=term-missing -``` - -## Screenshots (if applicable) - - -## Additional Notes - diff --git a/pyproject.toml b/pyproject.toml index 256e87a..60028f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,10 @@ dependencies = [ "jinja2>=3.1.0", ] +# CLI entry points +[project.scripts] +sb = "stackbox.cli.__main__:main" + [tool.setuptools.dynamic] version = {attr = "stackbox.__version__"} diff --git a/stackbox/cli/__init__.py b/stackbox/cli/__init__.py index e69de29..b42cbe7 100644 --- a/stackbox/cli/__init__.py +++ b/stackbox/cli/__init__.py @@ -0,0 +1,5 @@ +"""StackBox CLI package.""" + +from stackbox.cli.__main__ import cli, main + +__all__ = ["cli", "main"] diff --git a/stackbox/cli/__main__.py b/stackbox/cli/__main__.py new file mode 100644 index 0000000..b2bb5ef --- /dev/null +++ b/stackbox/cli/__main__.py @@ -0,0 +1,247 @@ +"""StackBox CLI entry point - class-based architecture.""" + +import click + +from stackbox import __version__ + + +class StackBoxCLI: + """StackBox command-line interface. + + Provides commands for managing containerized Ironic development environments + with fast rebuild cycles. + """ + + def __init__(self) -> None: + """Initialize the CLI with all commands.""" + self.cli = click.Group( + name="sb", + help=self._get_main_help(), + context_settings={"help_option_names": ["-h", "--help"]}, + ) + self._register_commands() + + def _get_main_help(self) -> str: + """Get main CLI help text.""" + return """StackBox - Fast OpenStack CI job reproduction + + Reproduce Ironic CI jobs locally in containers with fast rebuild cycles. + + Quick start: + sb init ironic-basic --ironic-repo ~/code/ironic + sb test + sb rebuild ironic + + Get help on any command: + sb --help + """ + + def _register_commands(self) -> None: + """Register all CLI commands.""" + # Add version option to main group + self.cli = click.version_option( + version=__version__, prog_name="sb", message="%(prog)s version %(version)s" + )(self.cli) + + # Register command methods + self.cli.add_command(self._create_init_command()) + self.cli.add_command(self._create_rebuild_command()) + self.cli.add_command(self._create_test_command()) + self.cli.add_command(self._create_status_command()) + self.cli.add_command(self._create_logs_command()) + self.cli.add_command(self._create_down_command()) + + def _create_init_command(self) -> click.Command: + """Create the init command.""" + + @click.command(name="init") + @click.argument("job-name") + @click.option( + "--ironic-repo", + default="~/workspace/ironic", + type=click.Path(), + help="Path to local Ironic repository", + ) + @click.option( + "--config-dir", + default=".stackbox", + type=click.Path(), + help="Directory to store StackBox configuration", + ) + def init_cmd(job_name: str, ironic_repo: str, config_dir: str) -> None: + """Initialize environment for a CI job. + + Sets up infrastructure containers, builds Ironic, creates test nodes, + and configures Tempest. + + Example: + sb init ironic-basic --ironic-repo ~/code/ironic + """ + self.init(job_name, ironic_repo, config_dir) + + return init_cmd + + def _create_rebuild_command(self) -> click.Command: + """Create the rebuild command.""" + + @click.command(name="rebuild") + @click.argument("service", default="ironic") + @click.option("--no-cache", is_flag=True, help="Force rebuild without using cache") + def rebuild_cmd(service: str, no_cache: bool) -> None: + """Rebuild a service after code changes. + + Rebuilds the Docker image and restarts containers. + + Example: + sb rebuild ironic + sb rebuild ironic --no-cache + """ + self.rebuild(service, no_cache) + + return rebuild_cmd + + def _create_test_command(self) -> click.Command: + """Create the test command.""" + + @click.command(name="test") + @click.option("--regex", help="Test regex pattern to run (e.g., test_baremetal_basic_ops)") + @click.option("--verbose", "-v", is_flag=True, help="Show verbose output") + def test_cmd(regex: str | None, verbose: bool) -> None: + """Run Tempest tests. + + Runs ironic-tempest-plugin tests in the environment. + + Example: + sb test + sb test --regex test_baremetal_basic_ops + sb test --verbose + """ + self.test(regex, verbose) + + return test_cmd + + def _create_status_command(self) -> click.Command: + """Create the status command.""" + + @click.command(name="status") + @click.option("--json", is_flag=True, help="Output status as JSON") + def status_cmd(json: bool) -> None: + """Show environment status. + + Displays running containers, health status, and endpoints. + + Example: + sb status + sb status --json + """ + self.status(json) + + return status_cmd + + def _create_logs_command(self) -> click.Command: + """Create the logs command.""" + + @click.command(name="logs") + @click.argument("service") + @click.option("--follow", "-f", is_flag=True, help="Follow log output (like tail -f)") + @click.option( + "--tail", default=100, type=int, help="Number of lines to show from end of logs" + ) + def logs_cmd(service: str, follow: bool, tail: int) -> None: + """View service logs. + + Shows logs from a specific service container. + + Example: + sb logs ironic-conductor + sb logs ironic-conductor --follow --tail 50 + """ + self.logs(service, follow, tail) + + return logs_cmd + + def _create_down_command(self) -> click.Command: + """Create the down command.""" + + @click.command(name="down") + @click.option("--volumes", is_flag=True, help="Also remove volumes (deletes all data)") + @click.confirmation_option(prompt="Are you sure you want to stop the environment?") + def down_cmd(volumes: bool) -> None: + """Stop and remove environment. + + Stops all containers and optionally removes volumes. + + Example: + sb down + sb down --volumes # Also delete data + """ + self.down(volumes) + + return down_cmd + + # ======================================================================== + # Command Implementation Methods + # ======================================================================== + + def init(self, job_name: str, ironic_repo: str, config_dir: str) -> None: + """Initialize environment for a CI job.""" + click.echo(f"Initializing {job_name}...") + click.echo(f" Ironic repo: {ironic_repo}") + click.echo(f" Config dir: {config_dir}") + click.echo("\n⚠️ Not implemented yet (Issue #9)") + + def rebuild(self, service: str, no_cache: bool) -> None: + """Rebuild a service after code changes.""" + click.echo(f"Rebuilding {service}...") + if no_cache: + click.echo(" Using --no-cache") + click.echo("\n⚠️ Not implemented yet (Issue #16)") + + def test(self, regex: str | None, verbose: bool) -> None: + """Run Tempest tests.""" + if regex: + click.echo(f"Running tests matching: {regex}") + else: + click.echo("Running all tests...") + + if verbose: + click.echo(" Verbose mode enabled") + + click.echo("\n⚠️ Not implemented yet (Issue #13)") + + def status(self, json: bool) -> None: + """Show environment status.""" + click.echo("Checking environment status...") + if json: + click.echo('{"status": "not_implemented"}') + else: + click.echo("\n⚠️ Not implemented yet (Issue #20)") + + def logs(self, service: str, follow: bool, tail: int) -> None: + """View service logs.""" + click.echo(f"Showing logs for {service}") + click.echo(f" Tail: {tail} lines") + if follow: + click.echo(" Following logs...") + click.echo("\n⚠️ Not implemented yet (Issue #21)") + + def down(self, volumes: bool) -> None: + """Stop and remove environment.""" + click.echo("Stopping environment...") + if volumes: + click.echo(" Will also remove volumes") + click.echo("\n⚠️ Not implemented yet (Issue #22)") + + +# Module-level CLI instance +_cli_instance = StackBoxCLI() +cli = _cli_instance.cli + + +def main() -> None: + """Main entry point for the CLI.""" + cli() + + +if __name__ == "__main__": + main()