-
Notifications
You must be signed in to change notification settings - Fork 2
DSPX 2655 gemini simplified workflow #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3567fd3
0ea2e5d
7c20d73
cd0aec5
1f02700
9b9c9d7
0c1b428
bf739dc
edb5e3a
8f783c3
1e5fc53
821c02c
2901544
7e99e2d
13385f5
bfe1119
9cfc673
5e8ab2d
ca607a1
0cac095
dd8b417
00dc688
19524cd
374bd6b
2f1957a
c332a6e
2bdf2a9
cc5e12d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| """CI-specific commands for otdf-local. | ||
|
|
||
| These commands adapt the local environment management for GitHub Actions CI, | ||
| where the platform is already started by an external action and we only need | ||
| to start KAS instances as background processes. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Annotated | ||
|
|
||
| import typer | ||
|
|
||
| from otdf_local.config.ports import Ports | ||
| from otdf_local.config.settings import Settings | ||
| from otdf_local.health.waits import WaitTimeoutError, wait_for_health | ||
| from otdf_local.services import get_kas_manager | ||
| from otdf_local.utils.console import ( | ||
| print_error, | ||
| print_info, | ||
| print_success, | ||
| print_warning, | ||
| ) | ||
| from otdf_local.utils.yaml import load_yaml, save_yaml, set_nested | ||
|
|
||
| ci_app = typer.Typer( | ||
| name="ci", | ||
| help="CI-specific commands for GitHub Actions workflows.", | ||
| no_args_is_help=True, | ||
| ) | ||
|
|
||
|
|
||
| def _emit_github_output(key: str, value: str) -> None: | ||
| """Write a key=value pair to $GITHUB_OUTPUT if available, else print to stdout.""" | ||
| github_output = os.environ.get("GITHUB_OUTPUT") | ||
| if github_output: | ||
| with open(github_output, "a") as f: | ||
| f.write(f"{key}={value}\n") | ||
| else: | ||
| # Fallback for local testing | ||
| print(f"{key}={value}", file=sys.stdout) | ||
|
|
||
|
|
||
| def _prepare_kas_template( | ||
| settings: Settings, root_key: str | None, ec_tdf_enabled: bool | ||
| ) -> None: | ||
| """Ensure the KAS template config has the right root key and EC TDF settings. | ||
|
|
||
| In CI, the platform config may have a root_key that differs from what | ||
| we want for additional KAS instances. This updates the platform config | ||
| in-place so that KASService._generate_config reads the correct root_key. | ||
| """ | ||
| if root_key: | ||
| config = load_yaml(settings.platform_config) | ||
| set_nested(config, "services.kas.root_key", root_key) | ||
| if ec_tdf_enabled: | ||
| set_nested(config, "services.kas.preview.ec_tdf_enabled", True) | ||
| save_yaml(settings.platform_config, config) | ||
|
|
||
|
|
||
| @ci_app.command("start-kas") | ||
| def start_kas( | ||
| platform_dir: Annotated[ | ||
| Path, | ||
| typer.Option( | ||
| "--platform-dir", | ||
| help="Path to the platform checkout (must contain opentdf-kas-mode.yaml)", | ||
| envvar="OTDF_LOCAL_PLATFORM_DIR", | ||
| ), | ||
| ], | ||
| root_key: Annotated[ | ||
| str | None, | ||
| typer.Option( | ||
| "--root-key", | ||
| help="Root key for KAS instances (overrides platform config value)", | ||
| envvar="OT_ROOT_KEY", | ||
| ), | ||
| ] = None, | ||
| ec_tdf_enabled: Annotated[ | ||
| bool, | ||
| typer.Option( | ||
| "--ec-tdf-enabled/--no-ec-tdf", | ||
| help="Enable EC TDF support", | ||
| ), | ||
| ] = True, | ||
| key_management: Annotated[ | ||
| bool, | ||
| typer.Option( | ||
| "--key-management/--no-key-management", | ||
| help="Enable key management on km1/km2 instances", | ||
| ), | ||
| ] = False, | ||
| log_type: Annotated[ | ||
| str, | ||
| typer.Option( | ||
| "--log-type", | ||
| help="Log format type (json, text)", | ||
| ), | ||
| ] = "json", | ||
| health_timeout: Annotated[ | ||
| int, | ||
| typer.Option( | ||
| "--health-timeout", | ||
| help="Seconds to wait for each KAS instance to become healthy", | ||
| ), | ||
| ] = 60, | ||
| instances: Annotated[ | ||
| str | None, | ||
| typer.Option( | ||
| "--instances", | ||
| help="Comma-separated KAS instance names (default: all)", | ||
| ), | ||
| ] = None, | ||
| ) -> None: | ||
| """Start KAS instances for CI and emit GitHub Actions outputs. | ||
|
|
||
| Expects the platform to already be running (started by start-up-with-containers). | ||
| Starts all 6 KAS instances (alpha, beta, gamma, delta, km1, km2) as background | ||
| processes, waits for each to pass health checks, and emits log file paths as | ||
| GitHub Actions step outputs. | ||
|
|
||
| Output keys (written to $GITHUB_OUTPUT): | ||
| kas-alpha-log-file, kas-beta-log-file, kas-gamma-log-file, | ||
| kas-delta-log-file, kas-km1-log-file, kas-km2-log-file | ||
| """ | ||
| platform_dir = platform_dir.resolve() | ||
| if not platform_dir.is_dir(): | ||
| print_error(f"Platform directory does not exist: {platform_dir}") | ||
| raise typer.Exit(1) | ||
|
|
||
| # Check for required template files | ||
| kas_template = platform_dir / "opentdf-kas-mode.yaml" | ||
| platform_config = platform_dir / "opentdf-dev.yaml" | ||
| if not kas_template.exists(): | ||
| # Fall back to opentdf.yaml if opentdf-kas-mode.yaml doesn't exist | ||
| kas_template_alt = platform_dir / "opentdf.yaml" | ||
| if kas_template_alt.exists(): | ||
| print_info( | ||
| f"Using {kas_template_alt} as KAS template (opentdf-kas-mode.yaml not found)" | ||
| ) | ||
| else: | ||
| print_error( | ||
| f"Neither opentdf-kas-mode.yaml nor opentdf.yaml found in {platform_dir}" | ||
| ) | ||
| raise typer.Exit(1) | ||
|
|
||
| if not platform_config.exists(): | ||
| # Try opentdf.yaml as fallback | ||
| platform_config_alt = platform_dir / "opentdf.yaml" | ||
| if platform_config_alt.exists(): | ||
| platform_config = platform_config_alt | ||
|
|
||
| # Build settings with CI-specific overrides | ||
| # We use a fresh xtest_root derived from this package's location | ||
| settings = Settings( | ||
| platform_dir=platform_dir, | ||
| ) | ||
| settings.ensure_directories() | ||
|
|
||
| # Update root key in platform config if provided | ||
| if root_key: | ||
| _prepare_kas_template(settings, root_key, ec_tdf_enabled) | ||
|
|
||
| # Determine which instances to start | ||
| if instances: | ||
| kas_names = [n.strip() for n in instances.split(",")] | ||
| for name in kas_names: | ||
| if name not in Ports.all_kas_names(): | ||
| print_error(f"Unknown KAS instance: {name}") | ||
| raise typer.Exit(1) | ||
| else: | ||
| kas_names = Ports.all_kas_names() | ||
|
|
||
| # Start KAS instances | ||
| print_info(f"Starting KAS instances: {', '.join(kas_names)}...") | ||
| kas_manager = get_kas_manager(settings) | ||
|
|
||
| failed = [] | ||
| for name in kas_names: | ||
| kas = kas_manager.get(name) | ||
| if kas is None: | ||
| print_error(f"KAS instance {name} not found in manager") | ||
| failed.append(name) | ||
| continue | ||
| if not kas.start(): | ||
| print_error(f"Failed to start KAS {name}") | ||
| failed.append(name) | ||
|
|
||
| if failed: | ||
| print_error(f"Failed to start: {', '.join(failed)}") | ||
| raise typer.Exit(1) | ||
|
|
||
| # Wait for health | ||
| print_info("Waiting for KAS health checks...") | ||
| unhealthy = [] | ||
| for name in kas_names: | ||
| port = Ports.get_kas_port(name) | ||
| try: | ||
| wait_for_health( | ||
| f"http://localhost:{port}/healthz", | ||
| timeout=health_timeout, | ||
| service_name=f"KAS {name}", | ||
| ) | ||
| except WaitTimeoutError as e: | ||
| print_warning(str(e)) | ||
| unhealthy.append(name) | ||
|
|
||
| if unhealthy: | ||
| print_error(f"KAS instances failed health check: {', '.join(unhealthy)}") | ||
| raise typer.Exit(1) | ||
|
|
||
| print_success(f"All {len(kas_names)} KAS instances are healthy") | ||
|
|
||
| # Emit outputs | ||
| for name in kas_names: | ||
| log_path = settings.get_kas_log_path(name) | ||
| output_key = f"kas-{name}-log-file" | ||
| _emit_github_output(output_key, str(log_path)) | ||
|
|
||
| print_success("CI KAS startup complete") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -54,29 +54,76 @@ | |
| def _find_platform_dir(xtest_root: Path) -> Path: | ||
| """Find the platform directory by searching for a sibling of an ancestor. | ||
|
|
||
| Searches up the directory tree from xtest_root looking for a 'platform' directory | ||
| that has the expected shape (contains docker-compose.yaml and opentdf-dev.yaml). | ||
| Searches for a 'platform' directory in: | ||
| 1. Ancestor siblings (for local development checkouts) | ||
| 2. The otdf-sdk-mgr managed location (xtest/sdk/platform/src/main) | ||
| 3. Last resort: attempts to checkout 'main' via otdf-sdk-mgr | ||
|
|
||
| Raises: | ||
| FileNotFoundError: If platform directory is not found with expected shape. | ||
| """ | ||
| # Start from xtest_root and walk up | ||
| # 1. Search for sibling 'platform' checkouts | ||
| current = xtest_root | ||
| while current != current.parent: | ||
| # Check siblings at this level | ||
| platform_candidate = current.parent / "platform" | ||
| if platform_candidate.exists() and platform_candidate.is_dir(): | ||
| # Verify it has the expected shape | ||
| has_compose = (platform_candidate / "docker-compose.yaml").exists() | ||
| has_config = (platform_candidate / "opentdf-dev.yaml").exists() | ||
| has_config = (platform_candidate / "opentdf-dev.yaml").exists() or ( | ||
| platform_candidate / "opentdf.yaml" | ||
| ).exists() | ||
| if has_compose and has_config: | ||
| return platform_candidate | ||
| current = current.parent | ||
|
|
||
| # 2. Search in otdf-sdk-mgr managed location | ||
| managed_main = xtest_root / "sdk" / "platform" / "src" / "main" | ||
| if managed_main.exists() and managed_main.is_dir(): | ||
| if (managed_main / "docker-compose.yaml").exists() and ( | ||
|
Check warning on line 83 in otdf-local/src/otdf_local/config/settings.py
|
||
| (managed_main / "opentdf-dev.yaml").exists() | ||
| or (managed_main / "opentdf.yaml").exists() | ||
| ): | ||
| return managed_main | ||
|
|
||
| # 3. Last resort: checkout 'main' via otdf-sdk-mgr | ||
| sdk_mgr_dir = xtest_root.parent / "otdf-sdk-mgr" | ||
| if sdk_mgr_dir.exists(): | ||
| import subprocess | ||
|
|
||
| try: | ||
| # We use plain print to avoid circular imports or dependency on rich here | ||
| print( | ||
| "Platform not found. Attempting to checkout 'main' via otdf-sdk-mgr..." | ||
| ) | ||
| subprocess.check_call( | ||
| [ | ||
| "uv", | ||
| "run", | ||
| "--project", | ||
| str(sdk_mgr_dir), | ||
| "otdf-sdk-mgr", | ||
| "checkout", | ||
| "platform", | ||
| "main", | ||
| ], | ||
| cwd=sdk_mgr_dir, | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL, | ||
| ) | ||
| if managed_main.exists() and managed_main.is_dir(): | ||
| if (managed_main / "docker-compose.yaml").exists() and ( | ||
|
Check warning on line 115 in otdf-local/src/otdf_local/config/settings.py
|
||
| managed_main / "opentdf-dev.yaml" | ||
| ).exists(): | ||
| return managed_main | ||
| except Exception: | ||
| # Fall through to FileNotFoundError | ||
| pass | ||
|
|
||
| # If we get here, we didn't find it | ||
| raise FileNotFoundError( | ||
| f"Could not find platform directory with expected shape " | ||
| f"(docker-compose.yaml and opentdf-dev.yaml) searching from {xtest_root}" | ||
| f"(docker-compose.yaml and opentdf.yaml or opentdf-dev.yaml) searching from {xtest_root}" | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -91,9 +138,18 @@ | |
|
|
||
| # Directory paths - computed from xtest_root | ||
| xtest_root: Path = Field(default_factory=_find_xtest_root) | ||
| platform_dir: Path = Field( | ||
| default_factory=lambda: _find_platform_dir(_find_xtest_root()) | ||
| ) | ||
| _platform_dir: Path | None = None | ||
|
|
||
| @property | ||
| def platform_dir(self) -> Path: | ||
| """Platform directory path.""" | ||
| if self._platform_dir: | ||
| return self._platform_dir | ||
| return _find_platform_dir(self.xtest_root) | ||
|
|
||
| @platform_dir.setter | ||
| def platform_dir(self, value: Path) -> None: | ||
| self._platform_dir = value | ||
|
Comment on lines
+141
to
+152
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing |
||
|
|
||
| @property | ||
| def logs_dir(self) -> Path: | ||
|
|
@@ -113,12 +169,12 @@ | |
| @property | ||
| def platform_config(self) -> Path: | ||
| """Platform config file path.""" | ||
| return self.platform_dir / "opentdf-dev.yaml" | ||
| return self.platform_dir / "opentdf.yaml" | ||
|
|
||
| @property | ||
| def platform_template_config(self) -> Path: | ||
| """Platform config template path.""" | ||
| return self.platform_dir / "opentdf.yaml" | ||
| return self.platform_dir / "opentdf-dev.yaml" | ||
|
|
||
| @property | ||
| def kas_template_config(self) -> Path: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback logic for
kas_templateandplatform_configupdates local variables that are never used or passed to theSettingsobject. This means the discovered paths are ignored, and theSettingsinstance will use its own default paths, potentially leading to errors if the expected files are missing or named differently (e.g.,opentdf.yamlvsopentdf-dev.yaml).