feat: Implement console session management with tenant and profile handling
- Add ConsoleSessionStore for managing console session state including tenants, profile, and token information. - Create OperatorContextService to manage operator context for orchestrator actions. - Implement OperatorMetadataInterceptor to enrich HTTP requests with operator context metadata. - Develop ConsoleProfileComponent to display user profile and session details, including tenant information and access tokens. - Add corresponding HTML and SCSS for ConsoleProfileComponent to enhance UI presentation. - Write unit tests for ConsoleProfileComponent to ensure correct rendering and functionality.
This commit is contained in:
		| @@ -23,11 +23,13 @@ import pathlib | ||||
| import re | ||||
| import shlex | ||||
| import shutil | ||||
| import stat | ||||
| import subprocess | ||||
| import sys | ||||
| import tarfile | ||||
| import tempfile | ||||
| import uuid | ||||
| import zipfile | ||||
| from collections import OrderedDict | ||||
| from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple | ||||
|  | ||||
| @@ -190,6 +192,8 @@ class ReleaseBuilder: | ||||
|         self.metadata_dir = ensure_directory(self.artifacts_dir / "metadata") | ||||
|         self.debug_dir = ensure_directory(self.output_dir / "debug") | ||||
|         self.debug_store_dir = ensure_directory(self.debug_dir / ".build-id") | ||||
|         self.cli_config = config.get("cli") | ||||
|         self.cli_output_dir = ensure_directory(self.output_dir / "cli") if self.cli_config else None | ||||
|         self.temp_dir = pathlib.Path(tempfile.mkdtemp(prefix="stellaops-release-")) | ||||
|         self.skip_signing = skip_signing | ||||
|         self.tlog_upload = tlog_upload | ||||
| @@ -220,7 +224,8 @@ class ReleaseBuilder: | ||||
|         helm_meta = self._package_helm() | ||||
|         compose_meta = self._digest_compose_files() | ||||
|         debug_meta = self._collect_debug_store(components_result) | ||||
|         manifest = self._compose_manifest(components_result, helm_meta, compose_meta, debug_meta) | ||||
|         cli_meta = self._build_cli_artifacts() | ||||
|         manifest = self._compose_manifest(components_result, helm_meta, compose_meta, debug_meta, cli_meta) | ||||
|         return manifest | ||||
|  | ||||
|     def _prime_buildx_plugin(self) -> None: | ||||
| @@ -262,6 +267,12 @@ class ReleaseBuilder: | ||||
|     def _component_ref(self, repo: str, digest: str) -> str: | ||||
|         return f"{self.registry}/{repo}@{digest}" | ||||
|  | ||||
|     def _relative_path(self, path: pathlib.Path) -> str: | ||||
|         try: | ||||
|             return str(path.relative_to(self.output_dir.parent)) | ||||
|         except ValueError: | ||||
|             return str(path) | ||||
|  | ||||
|     def _build_component(self, component: Mapping[str, Any]) -> Mapping[str, Any]: | ||||
|         name = component["name"] | ||||
|         repo = component.get("repository", name) | ||||
| @@ -601,6 +612,165 @@ class ReleaseBuilder: | ||||
|             ("directory", store_rel), | ||||
|         )) | ||||
|  | ||||
|     # ---------------- | ||||
|     # CLI packaging | ||||
|     # ---------------- | ||||
|     def _build_cli_artifacts(self) -> List[Mapping[str, Any]]: | ||||
|         if not self.cli_config or self.dry_run: | ||||
|             return [] | ||||
|         project_rel = self.cli_config.get("project") | ||||
|         if not project_rel: | ||||
|             return [] | ||||
|         project_path = (self.repo_root / project_rel).resolve() | ||||
|         if not project_path.exists(): | ||||
|             raise FileNotFoundError(f"CLI project not found at {project_path}") | ||||
|         runtimes: Sequence[str] = self.cli_config.get("runtimes", []) | ||||
|         if not runtimes: | ||||
|             runtimes = ("linux-x64",) | ||||
|         package_prefix = self.cli_config.get("packagePrefix", "stella") | ||||
|         ensure_directory(self.cli_output_dir or (self.output_dir / "cli")) | ||||
|  | ||||
|         cli_entries: List[Mapping[str, Any]] = [] | ||||
|         for runtime in runtimes: | ||||
|             entry = self._build_cli_for_runtime(project_path, runtime, package_prefix) | ||||
|             cli_entries.append(entry) | ||||
|         return cli_entries | ||||
|  | ||||
|     def _build_cli_for_runtime( | ||||
|         self, | ||||
|         project_path: pathlib.Path, | ||||
|         runtime: str, | ||||
|         package_prefix: str, | ||||
|     ) -> Mapping[str, Any]: | ||||
|         publish_dir = ensure_directory(self.temp_dir / f"cli-publish-{runtime}") | ||||
|         publish_cmd = [ | ||||
|             "dotnet", | ||||
|             "publish", | ||||
|             str(project_path), | ||||
|             "--configuration", | ||||
|             "Release", | ||||
|             "--runtime", | ||||
|             runtime, | ||||
|             "--self-contained", | ||||
|             "true", | ||||
|             "/p:PublishSingleFile=true", | ||||
|             "/p:IncludeNativeLibrariesForSelfExtract=true", | ||||
|             "/p:EnableCompressionInSingleFile=true", | ||||
|             "/p:InvariantGlobalization=true", | ||||
|             "--output", | ||||
|             str(publish_dir), | ||||
|         ] | ||||
|         run(publish_cmd, cwd=self.repo_root) | ||||
|  | ||||
|         original_name = "StellaOps.Cli" | ||||
|         if runtime.startswith("win"): | ||||
|             source = publish_dir / f"{original_name}.exe" | ||||
|             target = publish_dir / "stella.exe" | ||||
|         else: | ||||
|             source = publish_dir / original_name | ||||
|             target = publish_dir / "stella" | ||||
|         if source.exists(): | ||||
|             if target.exists(): | ||||
|                 target.unlink() | ||||
|             source.rename(target) | ||||
|             if not runtime.startswith("win"): | ||||
|                 target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | ||||
|  | ||||
|         package_dir = self.cli_output_dir or (self.output_dir / "cli") | ||||
|         ensure_directory(package_dir) | ||||
|         archive_name = f"{package_prefix}-{self.version}-{runtime}" | ||||
|         if runtime.startswith("win"): | ||||
|             package_path = package_dir / f"{archive_name}.zip" | ||||
|             self._archive_zip(publish_dir, package_path) | ||||
|         else: | ||||
|             package_path = package_dir / f"{archive_name}.tar.gz" | ||||
|             self._archive_tar(publish_dir, package_path) | ||||
|  | ||||
|         digest = compute_sha256(package_path) | ||||
|         sha_path = package_path.with_suffix(package_path.suffix + ".sha256") | ||||
|         sha_path.write_text(f"{digest}  {package_path.name}\n", encoding="utf-8") | ||||
|  | ||||
|         archive_info = OrderedDict(( | ||||
|             ("path", self._relative_path(package_path)), | ||||
|             ("sha256", digest), | ||||
|         )) | ||||
|         signature_info = self._sign_file(package_path) | ||||
|         if signature_info: | ||||
|             archive_info["signature"] = signature_info | ||||
|  | ||||
|         sbom_info = self._generate_cli_sbom(runtime, publish_dir) | ||||
|  | ||||
|         entry = OrderedDict(( | ||||
|             ("runtime", runtime), | ||||
|             ("archive", archive_info), | ||||
|         )) | ||||
|         if sbom_info: | ||||
|             entry["sbom"] = sbom_info | ||||
|         return entry | ||||
|  | ||||
|     def _archive_tar(self, source_dir: pathlib.Path, archive_path: pathlib.Path) -> None: | ||||
|         with tarfile.open(archive_path, "w:gz") as tar: | ||||
|             for item in sorted(source_dir.rglob("*")): | ||||
|                 arcname = item.relative_to(source_dir) | ||||
|                 tar.add(item, arcname=arcname) | ||||
|  | ||||
|     def _archive_zip(self, source_dir: pathlib.Path, archive_path: pathlib.Path) -> None: | ||||
|         with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as zipf: | ||||
|             for item in sorted(source_dir.rglob("*")): | ||||
|                 if item.is_dir(): | ||||
|                     continue | ||||
|                 arcname = item.relative_to(source_dir).as_posix() | ||||
|                 zip_info = zipfile.ZipInfo(arcname) | ||||
|                 zip_info.external_attr = (item.stat().st_mode & 0xFFFF) << 16 | ||||
|                 with item.open("rb") as handle: | ||||
|                     zipf.writestr(zip_info, handle.read()) | ||||
|  | ||||
|     def _generate_cli_sbom(self, runtime: str, publish_dir: pathlib.Path) -> Optional[Mapping[str, Any]]: | ||||
|         if self.dry_run: | ||||
|             return None | ||||
|         sbom_dir = ensure_directory(self.sboms_dir / "cli") | ||||
|         sbom_path = sbom_dir / f"cli-{runtime}.cyclonedx.json" | ||||
|         run([ | ||||
|             "syft", | ||||
|             f"dir:{publish_dir}", | ||||
|             "--output", | ||||
|             f"cyclonedx-json={sbom_path}", | ||||
|         ]) | ||||
|         entry = OrderedDict(( | ||||
|             ("path", self._relative_path(sbom_path)), | ||||
|             ("sha256", compute_sha256(sbom_path)), | ||||
|         )) | ||||
|         signature_info = self._sign_file(sbom_path) | ||||
|         if signature_info: | ||||
|             entry["signature"] = signature_info | ||||
|         return entry | ||||
|  | ||||
|     def _sign_file(self, path: pathlib.Path) -> Optional[Mapping[str, Any]]: | ||||
|         if self.skip_signing: | ||||
|             return None | ||||
|         if not (self.cosign_key_ref or self.cosign_identity_token): | ||||
|             raise ValueError( | ||||
|                 "Signing requested but no cosign key or identity token provided. Use --skip-signing to bypass." | ||||
|             ) | ||||
|         signature_path = path.with_suffix(path.suffix + ".sig") | ||||
|         sha_path = path.with_suffix(path.suffix + ".sha256") | ||||
|         digest = compute_sha256(path) | ||||
|         sha_path.write_text(f"{digest}  {path.name}\n", encoding="utf-8") | ||||
|         cmd = ["cosign", "sign-blob", "--yes", str(path)] | ||||
|         if self.cosign_key_ref: | ||||
|             cmd.extend(["--key", self.cosign_key_ref]) | ||||
|         if self.cosign_identity_token: | ||||
|             cmd.extend(["--identity-token", self.cosign_identity_token]) | ||||
|         if not self.tlog_upload: | ||||
|             cmd.append("--tlog-upload=false") | ||||
|         signature_data = run(cmd, env=self.cosign_env).strip() | ||||
|         signature_path.write_text(signature_data + "\n", encoding="utf-8") | ||||
|         return OrderedDict(( | ||||
|             ("path", self._relative_path(signature_path)), | ||||
|             ("sha256", compute_sha256(signature_path)), | ||||
|             ("tlogUploaded", self.tlog_upload), | ||||
|         )) | ||||
|  | ||||
|     def _extract_debug_entries(self, component_name: str, image_ref: str) -> List[OrderedDict[str, Any]]: | ||||
|         if self.dry_run: | ||||
|             return [] | ||||
| @@ -832,6 +1002,7 @@ class ReleaseBuilder: | ||||
|         helm_meta: Optional[Mapping[str, Any]], | ||||
|         compose_meta: List[Mapping[str, Any]], | ||||
|         debug_meta: Optional[Mapping[str, Any]], | ||||
|         cli_meta: Sequence[Mapping[str, Any]], | ||||
|     ) -> Dict[str, Any]: | ||||
|         manifest = OrderedDict() | ||||
|         manifest["release"] = OrderedDict(( | ||||
| @@ -847,6 +1018,8 @@ class ReleaseBuilder: | ||||
|             manifest["compose"] = compose_meta | ||||
|         if debug_meta: | ||||
|             manifest["debugStore"] = debug_meta | ||||
|         if cli_meta: | ||||
|             manifest["cli"] = list(cli_meta) | ||||
|         return manifest | ||||
|  | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user