diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b4146..4ae0678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [1.6] - 2026-04-09 +## [1.6.1] - 2026-04-08 + +### Added + +- `--version` now includes Python version, Python executable path, and OS/platform alongside the Atlas version string +- `session prune --older-than DAYS` bulk-deletes uncaptured sessions (created but never captured) older than the specified number of days; supports `--dry-run` to preview and `--force` to skip confirmation + +### Changed + +- Fixed the licensing issues in a few files to reflect the GPL 3.0 license properly + +## [1.6] - 2026-04-08 ### Added diff --git a/README.md b/README.md index 1cafbe4..06d3b5d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # Platform Atlas -![Version](https://img.shields.io/badge/version-1.5-1B93D2?style=flat-square) ![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white) -![License](https://img.shields.io/badge/license-Apache%202.0-99CA3C?style=flat-square) -![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20RHEL%208%2B%20%7C%20Rocky%208%2B-lightgrey?style=flat-square) +![License](https://img.shields.io/badge/license-%20%20GNU%20GPLv3%20-green?style=flat-square) > Enterprise configuration auditing and compliance reporting for Itential Automation Platform @@ -738,20 +736,10 @@ For issues, questions, or feature requests, please contact: ## License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details. --- -**Version:** 1.5 +**Version:** 1.6.1 **Author:** Cody Rester **Last Updated:** April 2026 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6199f02..5511f03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [project] name = "platform-atlas" -version = "1.6" +version = "1.6.1" description = "Health Analysis for Platform" -license = "Apache-2.0" +license = "GPL-3.0-or-later" authors = [{ name = "Cody Rester", email = "cody.rester@itential.com" }] maintainers = [{ name = "Cody Rester", email = "cody.rester@itential.com" }] readme = "README.md" diff --git a/src/platform_atlas/core/_version.py b/src/platform_atlas/core/_version.py index c1f4473..eb5c305 100644 --- a/src/platform_atlas/core/_version.py +++ b/src/platform_atlas/core/_version.py @@ -1,2 +1,2 @@ """Platform Atlas Version""" -__version__ = "1.6" +__version__ = "1.6.1" diff --git a/src/platform_atlas/core/cli.py b/src/platform_atlas/core/cli.py index 0eab537..a776750 100644 --- a/src/platform_atlas/core/cli.py +++ b/src/platform_atlas/core/cli.py @@ -13,8 +13,10 @@ - preflight: Run preflight connectivity checks """ +import sys import json import argparse +import platform as _platform from pathlib import Path from rich_argparse import RichHelpFormatter @@ -23,6 +25,28 @@ theme = ui.theme +# ================================================= +# Version Action +# ================================================= + +class _VersionAction(argparse.Action): + """Custom --version action that includes system info.""" + + def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None): + super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0, help=help) + + def __call__(self, parser, namespace, values, option_string=None): + py_version = sys.version.split()[0] + py_path = sys.executable + os_name = _platform.system() + os_release = _platform.release() + machine = _platform.machine() + print(f"platform-atlas {__version__}") + print(f"Python {py_version} ({py_path})") + print(f"{os_name} {os_release} ({machine})") + parser.exit() + + # ================================================= # Custom Help Formatter # ================================================= @@ -59,8 +83,8 @@ def create_parser() -> argparse.ArgumentParser: parser.add_argument( '--version', - action='version', - version=f'%(prog)s {__version__}' + action=_VersionAction, + help='Show version and system information' ) parser.add_argument( @@ -434,6 +458,33 @@ def _add_session_commands(subparsers): help='Show what would change without writing anything' ) + # session prune + prune = session_subparsers.add_parser( + 'prune', + help='Delete uncaptured sessions older than N days', + formatter_class=AtlasHelpFormatter, + description='Bulk-delete sessions that were created but never captured, ' + 'older than the specified number of days.' + ) + prune.add_argument( + '--older-than', + dest='older_than', + type=int, + required=True, + metavar='DAYS', + help='Prune sessions created more than DAYS days ago (e.g. --older-than 90)' + ) + prune.add_argument( + '--dry-run', + action='store_true', + help='Show what would be deleted without removing anything' + ) + prune.add_argument( + '--force', + action='store_true', + help='Skip confirmation prompt' + ) + # ================================================= # RULESET Command Group # ================================================= diff --git a/src/platform_atlas/core/handlers/session.py b/src/platform_atlas/core/handlers/session.py index 82770c1..b12764a 100644 --- a/src/platform_atlas/core/handlers/session.py +++ b/src/platform_atlas/core/handlers/session.py @@ -1762,6 +1762,130 @@ def handle_session_repair(args: Namespace) -> int: return 1 +@registry.register("session", "prune", description="Delete uncaptured sessions older than N days") +def handle_session_prune(args: Namespace) -> int: + """ + Bulk-delete sessions that were created but never captured, + older than --older-than DAYS days. + + The active session is always skipped even if it qualifies. + Use --dry-run to preview without deleting. Use --force to skip confirmation. + """ + from datetime import datetime, timezone, timedelta + from rich.table import Table + from rich import box + + try: + manager = get_session_manager() + older_than: int = args.older_than + dry_run: bool = getattr(args, "dry_run", False) + force: bool = getattr(args, "force", False) + + cutoff = datetime.now(tz=timezone.utc) - timedelta(days=older_than) + active_name = manager.get_active_session_name() + + all_sessions = manager.list() + candidates = [] + skipped_active = False + + for session in all_sessions: + # Only sessions that were never captured + if session.metadata.capture_completed: + continue + # Age check against created_at + created = session.metadata.created_at + if created.tzinfo is None: + created = created.replace(tzinfo=timezone.utc) + if created >= cutoff: + continue + # Never touch the active session + if session.name == active_name: + skipped_active = True + continue + candidates.append(session) + + if not candidates: + console.print(f"\n [{theme.text_dim}]No uncaptured sessions older than {older_than} days.[/{theme.text_dim}]\n") + if skipped_active: + console.print( + f" [{theme.warning}]Note: active session '{active_name}' was skipped " + f"(deactivate it first to include it).[/{theme.warning}]\n" + ) + return 0 + + # Build preview table + table = Table( + title=f"{'[dim]Dry run — [/dim]' if dry_run else ''}Uncaptured sessions to prune ({len(candidates)})", + box=box.ROUNDED, + ) + table.add_column("Name", style="cyan") + table.add_column("Environment", style=theme.accent) + table.add_column("Created", style="dim") + table.add_column("Age (days)", justify="right", style=theme.warning) + + today = datetime.now(tz=timezone.utc) + for session in candidates: + created = session.metadata.created_at + if created.tzinfo is None: + created = created.replace(tzinfo=timezone.utc) + age_days = (today - created).days + env_display = session.metadata.environment or f"[{theme.text_ghost}]—[/{theme.text_ghost}]" + table.add_row( + session.name, + env_display, + created.strftime("%Y-%m-%d"), + str(age_days), + ) + + console.print() + console.print(table) + + if skipped_active: + console.print( + f"\n [{theme.warning}]Note: active session '{active_name}' was skipped.[/{theme.warning}]" + ) + + if dry_run: + console.print( + f"\n [{theme.text_dim}]Dry run — nothing deleted. " + f"Remove --dry-run to prune these {len(candidates)} session(s).[/{theme.text_dim}]\n" + ) + return 0 + + # Confirm unless --force + if not force: + console.print() + if not Confirm.ask( + f"Permanently delete {len(candidates)} session(s)?", default=False + ): + console.print(f" [{theme.text_dim}]Cancelled[/{theme.text_dim}]") + return 0 + + # Delete + deleted = 0 + failed = 0 + for session in candidates: + try: + manager.delete(session.name, force=True) + console.print(f" [{theme.success}]✓[/{theme.success}] Deleted: {session.name}") + deleted += 1 + except SessionError as e: + console.print(f" [red]✗[/red] {session.name}: {e.message}") + failed += 1 + + console.print() + console.print( + f" [{theme.success}]✓[/{theme.success}] {deleted} session(s) deleted" + + (f", {failed} failed" if failed else "") + ) + console.print() + return 0 if not failed else 1 + + except SessionError as e: + console.print(f"[red]✗[/red] {e.message}") + return 1 + + # ================================================= # Shared Helpers # ================================================= diff --git a/src/platform_atlas/core/whats_new.py b/src/platform_atlas/core/whats_new.py index 0fe5003..d24669b 100644 --- a/src/platform_atlas/core/whats_new.py +++ b/src/platform_atlas/core/whats_new.py @@ -5,12 +5,16 @@ 1. Prints a brief CLI summary with bullet points (always visible) 2. Opens a detailed HTML page in the default browser (if available) -The HTML template lives in reporting/assets/templates/whats-new-{version}.html. +The HTML template lives in reporting/assets/templates/whats-new-{series}.html, +where series is the major.minor version (e.g. "1.6" for 1.6, 1.6.1, 1.6.2, …). + Brand assets (logos, images) live in reporting/assets/images/ and are base64-encoded into the HTML at runtime so the output is fully self-contained. Tracking: - ~/.atlas/.seen_version stores the last version whose update was shown. + ~/.atlas/.seen_version stores the last minor series whose update was shown + (e.g. "1.6"). Any patch release in the same series won't re-show the page. + A user who jumps straight from 1.5 to 1.6.2 will still see the 1.6 page. """ from __future__ import annotations @@ -34,11 +38,12 @@ SEEN_VERSION_FILE = ATLAS_HOME / ".seen_version" ASSETS_IMAGES_DIR = PROJECT_TEMPLATES.parent / "images" -# Versions that have a What's New page -WHATS_NEW_VERSIONS = {"1.5"} +# Minor version series that have a What's New page. +# Key is "major.minor" — covers all patch releases in that series. +WHATS_NEW_VERSIONS = {"1.5", "1.6"} -# ── Version comparison ──────────────────────────────────────────── +# ── Version helpers ─────────────────────────────────────────────── def _parse_version(v: str) -> tuple[int, ...]: try: @@ -47,6 +52,14 @@ def _parse_version(v: str) -> tuple[int, ...]: return (0,) +def _get_minor_series(version: str) -> str: + """Return 'major.minor' from any version string (e.g. '1.6.1' → '1.6').""" + parts = version.strip().split(".") + if len(parts) >= 2: + return f"{parts[0]}.{parts[1]}" + return version.strip() + + def _get_seen_version() -> str | None: if not SEEN_VERSION_FILE.is_file(): return None @@ -57,8 +70,10 @@ def _get_seen_version() -> str | None: def _mark_seen(version: str) -> None: + """Store the minor series (e.g. '1.6') so any patch release is covered.""" + series = _get_minor_series(version) try: - SEEN_VERSION_FILE.write_text(version, encoding="utf-8") + SEEN_VERSION_FILE.write_text(series, encoding="utf-8") except OSError as e: logger.debug("Could not write seen version file: %s", e) @@ -66,12 +81,15 @@ def _mark_seen(version: str) -> None: def _should_show() -> bool: if not ATLAS_HOME.is_dir(): return False - if __version__ not in WHATS_NEW_VERSIONS: + current_series = _get_minor_series(__version__) + if current_series not in WHATS_NEW_VERSIONS: return False seen = _get_seen_version() if seen is None: return True - return _parse_version(__version__) > _parse_version(seen) + # Normalize seen value — old installs may have stored an exact version + seen_series = _get_minor_series(seen) + return seen_series != current_series # ── Asset helpers ───────────────────────────────────────────────── @@ -106,12 +124,13 @@ def _load_image_data_uri(filename: str) -> str: def _build_html(version: str) -> str | None: """ - Read the HTML template for a version and inject any asset placeholders. + Read the HTML template for the version's minor series and inject asset placeholders. Supported placeholders: {{ITENTIAL_LOGO}} — base64 data URI for itential-logo-dark.svg """ - template_path = PROJECT_TEMPLATES / f"whats-new-{version}.html" + series = _get_minor_series(version) + template_path = PROJECT_TEMPLATES / f"whats-new-{series}.html" if not template_path.is_file(): logger.debug("What's New template not found: %s", template_path) return None @@ -138,6 +157,13 @@ def _build_html(version: str) -> str | None: "Knowledge Base remediation steps shown by default in reports", "Run [bold]session repair[/bold] to backfill pre-1.5 session metadata", ], + "1.6": [ + "MongoDB logs now collected and analyzed alongside IAP logs", + "Kubernetes environment support — values.yaml + kubectl, no SSH required", + "[bold]--version[/bold] now shows Python version, path, and OS info", + "[bold]session prune --older-than DAYS[/bold] — bulk-delete uncaptured sessions", + "Use [bold]--dry-run[/bold] to preview which sessions would be pruned", + ], } @@ -146,7 +172,8 @@ def _show_cli_summary(version: str) -> None: theme = ui.theme console = Console() - bullets = _CLI_BULLETS.get(version, []) + series = _get_minor_series(version) + bullets = _CLI_BULLETS.get(series, []) if not bullets: return @@ -156,7 +183,7 @@ def _show_cli_summary(version: str) -> None: console.print(Panel( "\n".join(lines), - title=f"[bold {theme.primary}]🎉 What's New in v{version}[/bold {theme.primary}]", + title=f"[bold {theme.primary}]🎉 What's New in v{series}[/bold {theme.primary}]", title_align="left", border_style=theme.primary, box=box.ROUNDED, @@ -194,13 +221,13 @@ def _open_html_page(version: str) -> None: if html is None: return - # Write to ~/.atlas so the browser can read it reliably - page_path = ATLAS_HOME / f"whats-new-v{version}.html" + series = _get_minor_series(version) + page_path = ATLAS_HOME / f"whats-new-v{series}.html" try: page_path.write_text(html, encoding="utf-8") except OSError: tmp = tempfile.NamedTemporaryFile( - suffix=".html", prefix=f"atlas-whats-new-{version}-", + suffix=".html", prefix=f"atlas-whats-new-{series}-", delete=False, ) tmp.write(html.encode("utf-8")) @@ -221,11 +248,15 @@ def maybe_show_whats_new(*, force: bool = False) -> None: Prints CLI bullet points to the terminal (always visible, even over SSH), then opens the detailed HTML page in the browser (if available). + + The notice is keyed to the minor version series (major.minor). Any patch + release in the same series uses the same page and is only shown once. """ version = __version__ + series = _get_minor_series(version) if force: - if version in WHATS_NEW_VERSIONS: + if series in WHATS_NEW_VERSIONS: _show_cli_summary(version) _open_html_page(version) _mark_seen(version) diff --git a/src/platform_atlas/main.py b/src/platform_atlas/main.py index 11620e9..69cfb8b 100644 --- a/src/platform_atlas/main.py +++ b/src/platform_atlas/main.py @@ -33,7 +33,7 @@ __author__ = "Cody Rester" __contact__ = "cody.rester@itential.com" -__license__ = "Apache-2.0" +__license__ = "GPL-3.0-or-later" #----############## MAIN ##############----# @handle_errors(exit_on_error=True, show_traceback=False) diff --git a/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html b/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html new file mode 100644 index 0000000..ddaaa37 --- /dev/null +++ b/src/platform_atlas/reporting/assets/templates/whats-new-1.6.html @@ -0,0 +1,550 @@ + + + + + +What's New in Platform Atlas v1.6 + + + + + + + +
+ +

What's New

+
Version 1.6 Series · April 2026
+
+
+ +
+ + +
+
+ Log Analysis +

MongoDB logs in the mix

+

+ Atlas now captures and analyzes MongoDB logs alongside IAP and webserver logs. + Run top-N frequency ranking to surface the most repeated messages, or switch + to heuristic keyword matching to catch errors and warnings that matter most. +

+

+ Control log collection with + --log-mode top or + --log-mode heuristics, + tune depth with --log-top-n, + and filter by level with --log-levels error warn. + Skip log collection entirely with --skip-logs. +

+
+
+ + + + + + + + + MONGODB + mongod.log + + + + + + ATLAS + Log Engine + + + + + + TOP MESSAGES + + + + ×412 + + + ×218 + + + ×97 + + + ×44 + + + ×21 + + + + top + + heuristics + +
+
+ + +
+
+ Environments +

Kubernetes environments

+

+ When creating an environment, you can now select Kubernetes as the deployment type. + Atlas collects configuration by parsing values.yaml + and running kubectl commands — no SSH access required. +

+

+ IAP deployments on Kubernetes have different paths and configuration patterns. + Atlas handles them natively so you don't have to adapt your workflow. +

+

+ env create — select Kubernetes when prompted for deployment type. +

+
+
+ + + + DEPLOYMENT TYPE + + + + standalone + + + ha2 + + + + + kubernetes + + + custom + + + ✓ No SSH required + + + + + + + + VALUES.YAML + topology · credentials · config + + + KUBECTL + runtime data · pod status + +
+
+ + +
+
+ Session Management +

Bulk session cleanup

+

+ Accumulate unused sessions over time? session prune + deletes sessions that were created but never captured, older than a threshold you set. +

+

+ Run with --dry-run first to see exactly what would + be removed — a table shows each session's name, environment, creation date, and age in days. + The active session is always protected even if it qualifies. +

+

+ session prune --older-than 90 --dry-run +

+
+
+ + + + + + + SESSION + ENV + CREATED + AGE + + + + + + acme-jan-draft + acme-prod + 2025-12-10 + 120d + + + + + + beta-test-old + beta-staging + 2026-01-04 + 95d + + + + + + q2-prep + acme-prod + 2026-02-22 + 45d + + + + + + prod-audit-q1 + acme-prod + 2025-11-01 + active + + + + Dry run — 2 sessions would be deleted · 2 skipped + +
+
+ + +
+
+
🖥️
+

--version system info 1.6.1

+

+ platform-atlas --version now shows your Python version and + executable path alongside the Atlas version, and the OS and architecture. + Useful for diagnosing environment issues without hunting through system settings. +

+
+
+
📋
+

Log collection flags 1.6

+

+ Fine-tune log collection per capture run: + --log-mode, + --log-top-n, + --log-levels, and + --skip-logs. + Apply to session run capture or + session run all. +

+
+
+ +
+ + + + + +