splent-cli 1.2.8__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {splent_cli-1.2.8/src/splent_cli.egg-info → splent_cli-1.4.0}/PKG-INFO +1 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/pyproject.toml +1 -1
- splent_cli-1.4.0/src/splent_cli/cli.py +149 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_versions.py +2 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_deps.py +63 -38
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_features.py +2 -2
- splent_cli-1.4.0/src/splent_cli/commands/check/check_infra.py +222 -0
- splent_cli-1.4.0/src/splent_cli/commands/check/check_product.py +330 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/clear_log.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/clear_uploads.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/coverage.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_console.py +3 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_dump.py +3 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_migrate.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_reset.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_restore.py +3 -0
- splent_cli-1.4.0/src/splent_cli/commands/database/db_rollback.py +195 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_seed.py +14 -6
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_status.py +32 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/database/db_upgrade.py +2 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/doctor.py +22 -12
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/env/env_set.py +1 -21
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/export_puml.py +11 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_add.py +20 -7
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_attach.py +11 -5
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_clone.py +19 -27
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_contract.py +77 -5
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_create.py +14 -2
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_diff.py +12 -2
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_discard.py +3 -2
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_drift.py +1 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_edit.py +100 -29
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_env.py +2 -2
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_fork.py +2 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_hook_add.py +3 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_hook_remove.py +3 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_hooks.py +3 -1
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_inject_config.py +215 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_order.py +98 -3
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_outdated.py +176 -0
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_pin.py +141 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_pull.py +3 -1
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_release.py +386 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_remove.py +22 -6
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_rename.py +3 -1
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_status.py +426 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_sync_template.py +1 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_test.py +32 -4
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_translate.py +189 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_upgrade.py +4 -8
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_versions.py +7 -10
- splent_cli-1.4.0/src/splent_cli/commands/feature/feature_xray.py +463 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature_compile.py +18 -27
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/locust.py +3 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_build.py +90 -35
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_clean.py +6 -9
- splent_cli-1.4.0/src/splent_cli/commands/product/product_commands.py +95 -0
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_sync.py → splent_cli-1.4.0/src/splent_cli/commands/product/product_complete.py +11 -16
- splent_cli-1.4.0/src/splent_cli/commands/product/product_config.py +173 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_configure.py +1053 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_console.py +108 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_create.py +56 -3
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_deploy.py +74 -8
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_derive.py +92 -77
- splent_cli-1.4.0/src/splent_cli/commands/product/product_deselect.py +35 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_down.py +110 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_env.py +45 -11
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_missing.py → splent_cli-1.4.0/src/splent_cli/commands/product/product_missing.py +9 -14
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_port.py +3 -6
- splent_cli-1.4.0/src/splent_cli/commands/product/product_release.py +117 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_restart.py +58 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_routes.py +121 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_select.py +0 -2
- splent_cli-1.4.0/src/splent_cli/commands/product/product_signals.py +64 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_status.py +49 -4
- splent_cli-1.4.0/src/splent_cli/commands/product/product_sync.py +256 -0
- splent_cli-1.4.0/src/splent_cli/commands/product/product_test.py +113 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_up.py +6 -9
- splent_cli-1.4.0/src/splent_cli/commands/product/product_validate.py +147 -0
- splent_cli-1.4.0/src/splent_cli/commands/release/release_core.py +46 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/selenium.py +1 -0
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_add_feature.py +217 -0
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_configs.py +147 -0
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_create.py +51 -0
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_deps.py → splent_cli-1.4.0/src/splent_cli/commands/spl/spl_deps.py +20 -30
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_features.py +41 -0
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_fetch.py +36 -0
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_fix.py → splent_cli-1.4.0/src/splent_cli/commands/spl/spl_fix.py +37 -54
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_info.py → splent_cli-1.4.0/src/splent_cli/commands/spl/spl_info.py +23 -18
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_list.py +55 -0
- splent_cli-1.4.0/src/splent_cli/commands/spl/spl_utils.py +93 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/uvl/uvl_utils.py +96 -13
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/version.py +7 -16
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/services/compose.py +4 -2
- splent_cli-1.4.0/src/splent_cli/services/context.py +89 -0
- splent_cli-1.4.0/src/splent_cli/services/preflight.py +88 -0
- splent_cli-1.4.0/src/splent_cli/services/release.py +403 -0
- splent_cli-1.4.0/src/splent_cli/utils/__init__.py +0 -0
- splent_cli-1.4.0/src/splent_cli/utils/contract_freshness.py +116 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/feature_utils.py +55 -0
- splent_cli-1.4.0/src/splent_cli/utils/git_url.py +60 -0
- splent_cli-1.4.0/src/splent_cli/utils/integrity.py +172 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/lifecycle.py +4 -3
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/manifest.py +4 -2
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/path_utils.py +12 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/template_drift.py +25 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli.egg-info/SOURCES.txt +34 -11
- splent_cli-1.2.8/src/splent_cli/cli.py +0 -88
- splent_cli-1.2.8/src/splent_cli/commands/database/db_rollback.py +0 -65
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_release.py +0 -483
- splent_cli-1.2.8/src/splent_cli/commands/feature/feature_status.py +0 -200
- splent_cli-1.2.8/src/splent_cli/commands/product/product_down.py +0 -65
- splent_cli-1.2.8/src/splent_cli/commands/product/product_release.py +0 -143
- splent_cli-1.2.8/src/splent_cli/commands/product/product_sync.py +0 -155
- splent_cli-1.2.8/src/splent_cli/commands/release/release_core.py +0 -244
- splent_cli-1.2.8/src/splent_cli/commands/route_list.py +0 -92
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_check.py +0 -156
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_configs.py +0 -58
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_features.py +0 -49
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_fetch.py +0 -55
- splent_cli-1.2.8/src/splent_cli/commands/uvl/uvl_valid.py +0 -81
- splent_cli-1.2.8/src/splent_cli/services/context.py +0 -32
- splent_cli-1.2.8/src/splent_cli/services/release.py +0 -207
- {splent_cli-1.2.8 → splent_cli-1.4.0}/LICENSE +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/README.md +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/setup.cfg +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/clear_cache.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/command_create.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/linter.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.2.8/src/splent_cli/services → splent_cli-1.4.0/src/splent_cli/commands/spl}/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/commands/tokens.py +0 -0
- {splent_cli-1.2.8/src/splent_cli/utils → splent_cli-1.4.0/src/splent_cli/services}/__init__.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.2.8 → splent_cli-1.4.0}/src/splent_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splent_cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
|
|
5
5
|
Author-email: DiversoLab <diversolab@us.es>
|
|
6
6
|
Project-URL: Homepage, https://github.com/diverso-lab/splent_cli
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from splent_cli.utils.dynamic_imports import get_app
|
|
7
|
+
from splent_cli.utils.command_loader import load_commands
|
|
8
|
+
from splent_cli.utils.db_utils import check_db_connection
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SPLENTCLI(click.Group):
|
|
14
|
+
"""
|
|
15
|
+
Main SPLENT CLI class.
|
|
16
|
+
|
|
17
|
+
- Automatically injects the Flask app context for commands marked with `requires_app = True`.
|
|
18
|
+
- Checks DB connectivity for commands marked with `requires_db = True`.
|
|
19
|
+
- Discovers CLI commands contributed by features via ``app.extensions["splent_feature_commands"]``.
|
|
20
|
+
- Displays commands grouped by category for a cleaner, more readable help output.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# ── Feature-contributed commands ──────────────────────────────
|
|
24
|
+
|
|
25
|
+
def _load_feature_commands(self) -> dict[str, click.BaseCommand]:
|
|
26
|
+
"""Return ``{"feature:<name>": click.Group, ...}`` from the Flask app.
|
|
27
|
+
|
|
28
|
+
Each feature that defines CLI commands gets a Click Group registered
|
|
29
|
+
as ``feature:<short_name>``. Individual commands become subcommands::
|
|
30
|
+
|
|
31
|
+
splent feature:mail config
|
|
32
|
+
splent feature:mail check --to user@example.com
|
|
33
|
+
|
|
34
|
+
Commands are only available when a product is active and the app can
|
|
35
|
+
be built. On any failure the result is an empty dict — built-in CLI
|
|
36
|
+
commands are never affected.
|
|
37
|
+
"""
|
|
38
|
+
if hasattr(self, "_feature_cmds_cache"):
|
|
39
|
+
return self._feature_cmds_cache
|
|
40
|
+
|
|
41
|
+
self._feature_cmds_cache: dict[str, click.BaseCommand] = {}
|
|
42
|
+
try:
|
|
43
|
+
app = get_app()
|
|
44
|
+
with app.app_context():
|
|
45
|
+
registry = app.extensions.get("splent_feature_commands", {})
|
|
46
|
+
for feature_short, commands in registry.items():
|
|
47
|
+
group = click.Group(
|
|
48
|
+
name=f"feature:{feature_short}",
|
|
49
|
+
help=f"Commands contributed by splent_feature_{feature_short}.",
|
|
50
|
+
)
|
|
51
|
+
group.requires_app = True # type: ignore[attr-defined]
|
|
52
|
+
for cmd in commands:
|
|
53
|
+
group.add_command(cmd)
|
|
54
|
+
self._feature_cmds_cache[group.name] = group
|
|
55
|
+
except Exception as e:
|
|
56
|
+
if os.getenv("SPLENT_DEBUG"):
|
|
57
|
+
click.secho(f" ⚠ Feature commands not loaded: {e}", fg="yellow", err=True)
|
|
58
|
+
return self._feature_cmds_cache
|
|
59
|
+
|
|
60
|
+
def get_command(self, ctx, cmd_name):
|
|
61
|
+
# Built-in commands take priority
|
|
62
|
+
cmd = super().get_command(ctx, cmd_name)
|
|
63
|
+
if cmd is not None:
|
|
64
|
+
return cmd
|
|
65
|
+
# Fall back to feature-contributed command groups
|
|
66
|
+
feat_cmds = self._load_feature_commands()
|
|
67
|
+
return feat_cmds.get(cmd_name)
|
|
68
|
+
|
|
69
|
+
def list_commands(self, ctx):
|
|
70
|
+
builtin = super().list_commands(ctx)
|
|
71
|
+
feat = sorted(self._load_feature_commands().keys())
|
|
72
|
+
return builtin + feat
|
|
73
|
+
|
|
74
|
+
# ── App context injection ─────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def invoke(self, ctx):
|
|
77
|
+
cmd_name = ctx.protected_args[0] if ctx.protected_args else None
|
|
78
|
+
command = self.get_command(ctx, cmd_name)
|
|
79
|
+
|
|
80
|
+
if command and getattr(command, "requires_app", False):
|
|
81
|
+
app = get_app()
|
|
82
|
+
if getattr(command, "requires_db", False):
|
|
83
|
+
if not check_db_connection(app):
|
|
84
|
+
raise SystemExit(1)
|
|
85
|
+
with app.app_context():
|
|
86
|
+
return super().invoke(ctx)
|
|
87
|
+
|
|
88
|
+
return super().invoke(ctx)
|
|
89
|
+
|
|
90
|
+
def format_commands(self, ctx, formatter):
|
|
91
|
+
"""Group SPLENT commands by category in the CLI help output."""
|
|
92
|
+
all_cmds = self.list_commands(ctx)
|
|
93
|
+
groups = {
|
|
94
|
+
"🌿 Feature Management": [
|
|
95
|
+
cmd for cmd in all_cmds
|
|
96
|
+
if cmd.startswith("feature:") and cmd not in self._load_feature_commands()
|
|
97
|
+
],
|
|
98
|
+
"🏗️ Product Management": [
|
|
99
|
+
cmd for cmd in all_cmds if cmd.startswith("product:")
|
|
100
|
+
],
|
|
101
|
+
"🧬 SPL & Variability": [
|
|
102
|
+
cmd for cmd in all_cmds if cmd.startswith(("spl:", "uvl:"))
|
|
103
|
+
],
|
|
104
|
+
"🧱 Database": [cmd for cmd in all_cmds if cmd.startswith("db:")],
|
|
105
|
+
"💾 Cache": [cmd for cmd in all_cmds if cmd.startswith("cache:")],
|
|
106
|
+
"🧰 Utilities": [
|
|
107
|
+
cmd
|
|
108
|
+
for cmd in all_cmds
|
|
109
|
+
if cmd.startswith(
|
|
110
|
+
("clear:", "env", "select", "info", "version", "doctor", "tokens")
|
|
111
|
+
)
|
|
112
|
+
],
|
|
113
|
+
"🐍 Development & QA": [
|
|
114
|
+
cmd
|
|
115
|
+
for cmd in all_cmds
|
|
116
|
+
if cmd.startswith(("linter", "test", "coverage", "locust"))
|
|
117
|
+
],
|
|
118
|
+
"🔌 Feature Commands": [
|
|
119
|
+
cmd for cmd in all_cmds
|
|
120
|
+
if cmd in self._load_feature_commands()
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
for title, cmds in groups.items():
|
|
124
|
+
if not cmds:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
with formatter.section(title):
|
|
128
|
+
rows = []
|
|
129
|
+
for cmd_name in sorted(cmds):
|
|
130
|
+
cmd = self.get_command(ctx, cmd_name)
|
|
131
|
+
if cmd is None or cmd.hidden:
|
|
132
|
+
continue
|
|
133
|
+
rows.append((cmd_name, cmd.get_short_help_str()))
|
|
134
|
+
if rows:
|
|
135
|
+
formatter.write_dl(rows)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@click.group(cls=SPLENTCLI)
|
|
139
|
+
def cli():
|
|
140
|
+
"""Command-line interface for managing SPLENT products, features, environments, and development workflows."""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Automatically load all command modules
|
|
145
|
+
load_commands(cli)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
cli()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from splent_cli.services import context
|
|
2
|
+
from splent_cli.utils.feature_utils import normalize_namespace
|
|
2
3
|
import click
|
|
3
4
|
|
|
4
5
|
|
|
@@ -17,7 +18,7 @@ def cache_versions(feature_ref: str):
|
|
|
17
18
|
raise SystemExit(1)
|
|
18
19
|
|
|
19
20
|
ns, name = feature_ref.split("/", 1)
|
|
20
|
-
ns_fs = ns
|
|
21
|
+
ns_fs = normalize_namespace(ns)
|
|
21
22
|
|
|
22
23
|
workspace = context.workspace()
|
|
23
24
|
ns_dir = workspace / ".splent_cache" / "features" / ns_fs
|
|
@@ -85,12 +85,15 @@ def _parse_uvl_deps(uvl_path: str) -> tuple[dict[str, str], dict[str, set[str]]]
|
|
|
85
85
|
|
|
86
86
|
def _scan_feature_imports(
|
|
87
87
|
feature_path: str, feature_name: str, all_packages: set[str]
|
|
88
|
-
) -> set[str]:
|
|
89
|
-
"""Scan
|
|
90
|
-
|
|
88
|
+
) -> tuple[set[str], set[str]]:
|
|
89
|
+
"""Scan a feature's source code and templates for cross-feature dependencies.
|
|
90
|
+
|
|
91
|
+
Returns (python_imports, template_deps) — both sets of package names.
|
|
92
|
+
"""
|
|
93
|
+
python_imports: set[str] = set()
|
|
94
|
+
template_deps: set[str] = set()
|
|
91
95
|
|
|
92
96
|
src_dir = None
|
|
93
|
-
# Find the src directory
|
|
94
97
|
for org_dir in os.listdir(os.path.join(feature_path, "src")):
|
|
95
98
|
candidate = os.path.join(feature_path, "src", org_dir, feature_name)
|
|
96
99
|
if os.path.isdir(candidate):
|
|
@@ -98,27 +101,46 @@ def _scan_feature_imports(
|
|
|
98
101
|
break
|
|
99
102
|
|
|
100
103
|
if not src_dir:
|
|
101
|
-
return
|
|
104
|
+
return python_imports, template_deps
|
|
105
|
+
|
|
106
|
+
# Map blueprint names to package names for template dependency detection
|
|
107
|
+
# Convention: blueprint name is the feature short name (e.g. "auth" for splent_feature_auth)
|
|
108
|
+
bp_to_pkg = {}
|
|
109
|
+
for pkg in all_packages:
|
|
110
|
+
short = pkg.replace("splent_feature_", "")
|
|
111
|
+
bp_to_pkg[short] = pkg
|
|
102
112
|
|
|
103
113
|
for root, _, files in os.walk(src_dir):
|
|
104
114
|
for f in files:
|
|
105
|
-
if not f.endswith(".py"):
|
|
106
|
-
continue
|
|
107
115
|
filepath = os.path.join(root, f)
|
|
108
|
-
try:
|
|
109
|
-
with open(filepath, "r", encoding="utf-8") as fh:
|
|
110
|
-
content = fh.read()
|
|
111
|
-
except (OSError, PermissionError):
|
|
112
|
-
continue
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
if f.endswith(".py"):
|
|
118
|
+
try:
|
|
119
|
+
with open(filepath, "r", encoding="utf-8") as fh:
|
|
120
|
+
content = fh.read()
|
|
121
|
+
except (OSError, PermissionError):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
for match in re.findall(
|
|
125
|
+
r"(?:from|import)\s+splent_io\.(splent_feature_\w+)", content
|
|
126
|
+
):
|
|
127
|
+
if match != feature_name and match in all_packages:
|
|
128
|
+
python_imports.add(match)
|
|
129
|
+
|
|
130
|
+
elif f.endswith(".html"):
|
|
131
|
+
try:
|
|
132
|
+
with open(filepath, "r", encoding="utf-8") as fh:
|
|
133
|
+
content = fh.read()
|
|
134
|
+
except (OSError, PermissionError):
|
|
135
|
+
continue
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
# Detect url_for('blueprint.endpoint', ...) references to other features
|
|
138
|
+
for bp_name in re.findall(r"url_for\s*\(\s*['\"](\w+)\.", content):
|
|
139
|
+
pkg = bp_to_pkg.get(bp_name)
|
|
140
|
+
if pkg and pkg != feature_name:
|
|
141
|
+
template_deps.add(pkg)
|
|
142
|
+
|
|
143
|
+
return python_imports, template_deps
|
|
122
144
|
|
|
123
145
|
|
|
124
146
|
# ---------------------------------------------------------------------------
|
|
@@ -174,20 +196,24 @@ def check_deps():
|
|
|
174
196
|
product = context.require_app()
|
|
175
197
|
product_dir = os.path.join(workspace, product)
|
|
176
198
|
|
|
177
|
-
# Read UVL
|
|
199
|
+
# Read UVL (catalog or legacy)
|
|
178
200
|
try:
|
|
179
201
|
reader = PyprojectReader.for_product(product_dir)
|
|
180
|
-
|
|
202
|
+
# 1. Catalog: [tool.splent].spl
|
|
203
|
+
spl_name = reader.splent_config.get("spl")
|
|
204
|
+
if spl_name:
|
|
205
|
+
uvl_path = os.path.join(workspace, "splent_catalog", spl_name, f"{spl_name}.uvl")
|
|
206
|
+
else:
|
|
207
|
+
# 2. Legacy: [tool.splent.uvl].file
|
|
208
|
+
uvl_file = reader.uvl_config.get("file")
|
|
209
|
+
if not uvl_file:
|
|
210
|
+
click.secho(" [✖] No UVL configured. Set [tool.splent].spl or [tool.splent.uvl].file.", fg="red")
|
|
211
|
+
raise SystemExit(1)
|
|
212
|
+
uvl_path = os.path.join(product_dir, "uvl", uvl_file)
|
|
181
213
|
except (FileNotFoundError, RuntimeError) as e:
|
|
182
214
|
click.secho(f" [✖] Cannot read pyproject.toml: {e}", fg="red")
|
|
183
215
|
raise SystemExit(1)
|
|
184
216
|
|
|
185
|
-
uvl_file = uvl_cfg.get("file")
|
|
186
|
-
if not uvl_file:
|
|
187
|
-
click.secho(" [✖] No UVL file configured.", fg="red")
|
|
188
|
-
raise SystemExit(1)
|
|
189
|
-
|
|
190
|
-
uvl_path = os.path.join(product_dir, "uvl", uvl_file)
|
|
191
217
|
if not os.path.isfile(uvl_path):
|
|
192
218
|
click.secho(f" [✖] UVL file not found: {uvl_path}", fg="red")
|
|
193
219
|
raise SystemExit(1)
|
|
@@ -217,40 +243,42 @@ def check_deps():
|
|
|
217
243
|
)
|
|
218
244
|
continue
|
|
219
245
|
|
|
220
|
-
|
|
246
|
+
py_imports, tpl_deps = _scan_feature_imports(fpath, pkg_name, all_packages)
|
|
221
247
|
allowed = allowed_deps.get(pkg_name, set())
|
|
248
|
+
all_deps = py_imports | tpl_deps
|
|
222
249
|
|
|
223
|
-
if not
|
|
250
|
+
if not all_deps:
|
|
224
251
|
click.echo(
|
|
225
252
|
click.style(" [✔] ", fg="green")
|
|
226
253
|
+ click.style(f"{short}", bold=True)
|
|
227
|
-
+ " — no cross-feature
|
|
254
|
+
+ " — no cross-feature dependencies"
|
|
228
255
|
)
|
|
229
256
|
ok += 1
|
|
230
257
|
continue
|
|
231
258
|
|
|
232
259
|
has_violation = False
|
|
233
|
-
|
|
260
|
+
|
|
261
|
+
for imp in sorted(all_deps):
|
|
234
262
|
imp_short = pkg_to_short.get(imp, imp)
|
|
263
|
+
source = "imports" if imp in py_imports else "references (template)"
|
|
235
264
|
|
|
236
265
|
if imp in allowed:
|
|
237
266
|
click.echo(
|
|
238
267
|
click.style(" [✔] ", fg="green")
|
|
239
268
|
+ click.style(f"{short}", bold=True)
|
|
240
|
-
+ f"
|
|
269
|
+
+ f" {source} {imp_short}"
|
|
241
270
|
+ click.style(
|
|
242
271
|
f" (allowed: {short} => {imp_short})", fg="bright_black"
|
|
243
272
|
)
|
|
244
273
|
)
|
|
245
274
|
ok += 1
|
|
246
275
|
else:
|
|
247
|
-
# Check if the reverse is declared (inverted dependency)
|
|
248
276
|
reverse_allowed = allowed_deps.get(imp, set())
|
|
249
277
|
if pkg_name in reverse_allowed:
|
|
250
278
|
click.echo(
|
|
251
279
|
click.style(" [✖] ", fg="red")
|
|
252
280
|
+ click.style(f"{short}", bold=True)
|
|
253
|
-
+ f"
|
|
281
|
+
+ f" {source} {imp_short}"
|
|
254
282
|
+ click.style(
|
|
255
283
|
f" INVERTED — UVL says {imp_short} => {short}, not the reverse",
|
|
256
284
|
fg="red",
|
|
@@ -260,7 +288,7 @@ def check_deps():
|
|
|
260
288
|
click.echo(
|
|
261
289
|
click.style(" [✖] ", fg="red")
|
|
262
290
|
+ click.style(f"{short}", bold=True)
|
|
263
|
-
+ f"
|
|
291
|
+
+ f" {source} {imp_short}"
|
|
264
292
|
+ click.style(
|
|
265
293
|
f" UNDECLARED — no UVL constraint between {short} and {imp_short}",
|
|
266
294
|
fg="red",
|
|
@@ -269,9 +297,6 @@ def check_deps():
|
|
|
269
297
|
violations += 1
|
|
270
298
|
has_violation = True
|
|
271
299
|
|
|
272
|
-
if not has_violation:
|
|
273
|
-
pass # all imports were OK, already printed
|
|
274
|
-
|
|
275
300
|
click.echo()
|
|
276
301
|
if violations:
|
|
277
302
|
click.secho(
|
|
@@ -10,7 +10,7 @@ import click
|
|
|
10
10
|
import tomllib
|
|
11
11
|
|
|
12
12
|
from splent_cli.services import context
|
|
13
|
-
from splent_cli.utils.feature_utils import read_features_from_data
|
|
13
|
+
from splent_cli.utils.feature_utils import normalize_namespace, read_features_from_data
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _pkg_installed(name: str) -> bool:
|
|
@@ -72,7 +72,7 @@ def check_features():
|
|
|
72
72
|
# Parse entry
|
|
73
73
|
if "/" in entry:
|
|
74
74
|
org_raw, rest = entry.split("/", 1)
|
|
75
|
-
org_safe = org_raw
|
|
75
|
+
org_safe = normalize_namespace(org_raw)
|
|
76
76
|
else:
|
|
77
77
|
org_safe = "splent_io"
|
|
78
78
|
rest = entry
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
check:infra — Validate Docker infrastructure declarations (ports, services, containers, networks).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import tomllib
|
|
11
|
+
|
|
12
|
+
from splent_cli.services import context, compose
|
|
13
|
+
from splent_cli.utils.feature_utils import read_features_from_data
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_compose_ports(compose_file: str) -> list[tuple[int, str, str]]:
|
|
17
|
+
"""Return [(host_port, service_name, source_label)] from a compose file."""
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["docker", "compose", "-f", compose_file, "config", "--format", "json"],
|
|
20
|
+
capture_output=True, text=True,
|
|
21
|
+
)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
return []
|
|
24
|
+
try:
|
|
25
|
+
config = json.loads(result.stdout)
|
|
26
|
+
except json.JSONDecodeError:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
ports = []
|
|
30
|
+
for svc_name, svc in config.get("services", {}).items():
|
|
31
|
+
for port in svc.get("ports", []):
|
|
32
|
+
published = port.get("published") if isinstance(port, dict) else None
|
|
33
|
+
if published:
|
|
34
|
+
try:
|
|
35
|
+
ports.append((int(published), svc_name, compose_file))
|
|
36
|
+
except (ValueError, TypeError):
|
|
37
|
+
pass
|
|
38
|
+
return ports
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_compose_services(compose_file: str) -> list[tuple[str, str, str]]:
|
|
42
|
+
"""Return [(service_name, container_name_or_None, source_label)]."""
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
["docker", "compose", "-f", compose_file, "config", "--format", "json"],
|
|
45
|
+
capture_output=True, text=True,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return []
|
|
49
|
+
try:
|
|
50
|
+
config = json.loads(result.stdout)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
services = []
|
|
55
|
+
for svc_name, svc in config.get("services", {}).items():
|
|
56
|
+
container_name = svc.get("container_name")
|
|
57
|
+
services.append((svc_name, container_name, compose_file))
|
|
58
|
+
return services
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.command("check:infra", short_help="Validate Docker infrastructure (ports, services, networks).")
|
|
62
|
+
def check_infra():
|
|
63
|
+
"""Check for port conflicts, duplicate services, container name collisions,
|
|
64
|
+
and network availability across all features and the product."""
|
|
65
|
+
workspace = str(context.workspace())
|
|
66
|
+
product = context.require_app()
|
|
67
|
+
product_path = os.path.join(workspace, product)
|
|
68
|
+
pyproject_path = os.path.join(product_path, "pyproject.toml")
|
|
69
|
+
|
|
70
|
+
ok = fail = warn = 0
|
|
71
|
+
|
|
72
|
+
def _ok(msg):
|
|
73
|
+
nonlocal ok
|
|
74
|
+
ok += 1
|
|
75
|
+
click.echo(click.style(" [OK] ", fg="green") + msg)
|
|
76
|
+
|
|
77
|
+
def _fail(msg):
|
|
78
|
+
nonlocal fail
|
|
79
|
+
fail += 1
|
|
80
|
+
click.echo(click.style(" [FAIL] ", fg="red") + msg)
|
|
81
|
+
|
|
82
|
+
def _warn(msg):
|
|
83
|
+
nonlocal warn
|
|
84
|
+
warn += 1
|
|
85
|
+
click.echo(click.style(" [WARN] ", fg="yellow") + msg)
|
|
86
|
+
|
|
87
|
+
click.echo()
|
|
88
|
+
click.echo(click.style(" Infrastructure check", bold=True))
|
|
89
|
+
click.echo()
|
|
90
|
+
|
|
91
|
+
if not os.path.exists(pyproject_path):
|
|
92
|
+
_fail("pyproject.toml not found")
|
|
93
|
+
raise SystemExit(1)
|
|
94
|
+
|
|
95
|
+
with open(pyproject_path, "rb") as f:
|
|
96
|
+
data = tomllib.load(f)
|
|
97
|
+
|
|
98
|
+
env = os.getenv("SPLENT_ENV", "dev")
|
|
99
|
+
features = read_features_from_data(data, env)
|
|
100
|
+
|
|
101
|
+
# Collect all compose files
|
|
102
|
+
compose_files: list[tuple[str, str]] = [] # (label, path)
|
|
103
|
+
|
|
104
|
+
for feat in features:
|
|
105
|
+
clean = compose.normalize_feature_ref(feat)
|
|
106
|
+
bare_name = clean.split("/")[-1] if "/" in clean else clean
|
|
107
|
+
feat_base = os.path.dirname(compose.feature_docker_dir(workspace, bare_name))
|
|
108
|
+
cf = compose.resolve_file(feat_base, env)
|
|
109
|
+
if cf:
|
|
110
|
+
compose_files.append((bare_name, cf))
|
|
111
|
+
|
|
112
|
+
cf = compose.resolve_file(product_path, env)
|
|
113
|
+
if cf:
|
|
114
|
+
compose_files.append((product, cf))
|
|
115
|
+
|
|
116
|
+
# --- Check 1: Port conflicts between declarations ---
|
|
117
|
+
click.echo(click.style(" Ports", bold=True))
|
|
118
|
+
all_ports: dict[int, list[str]] = {} # port -> [labels]
|
|
119
|
+
for label, cf in compose_files:
|
|
120
|
+
for host_port, svc_name, _ in _parse_compose_ports(cf):
|
|
121
|
+
all_ports.setdefault(host_port, []).append(f"{label}/{svc_name}")
|
|
122
|
+
|
|
123
|
+
port_conflicts = {p: srcs for p, srcs in all_ports.items() if len(srcs) > 1}
|
|
124
|
+
if port_conflicts:
|
|
125
|
+
for port, sources in sorted(port_conflicts.items()):
|
|
126
|
+
_fail(f"Port {port} declared by multiple services: {', '.join(sources)}")
|
|
127
|
+
else:
|
|
128
|
+
_ok(f"No port conflicts ({len(all_ports)} ports declared)")
|
|
129
|
+
|
|
130
|
+
# Check against running containers
|
|
131
|
+
running_conflicts = []
|
|
132
|
+
for port in all_ports:
|
|
133
|
+
result = subprocess.run(
|
|
134
|
+
["docker", "ps", "--format", "{{.ID}}\t{{.Names}}\t{{.Ports}}"],
|
|
135
|
+
capture_output=True, text=True,
|
|
136
|
+
)
|
|
137
|
+
for line in result.stdout.splitlines():
|
|
138
|
+
parts = line.split("\t", 2)
|
|
139
|
+
if len(parts) < 3:
|
|
140
|
+
continue
|
|
141
|
+
cid, name, ports_str = parts
|
|
142
|
+
if f":{port}->" in ports_str:
|
|
143
|
+
running_conflicts.append((port, name))
|
|
144
|
+
|
|
145
|
+
if running_conflicts:
|
|
146
|
+
for port, cname in running_conflicts:
|
|
147
|
+
_warn(f"Port {port} already in use by running container: {cname}")
|
|
148
|
+
else:
|
|
149
|
+
_ok("No conflicts with running containers")
|
|
150
|
+
|
|
151
|
+
# --- Check 2: Service name collisions ---
|
|
152
|
+
click.echo()
|
|
153
|
+
click.echo(click.style(" Services", bold=True))
|
|
154
|
+
all_services: dict[str, list[str]] = {} # svc_name -> [labels]
|
|
155
|
+
all_container_names: dict[str, list[str]] = {} # container_name -> [labels]
|
|
156
|
+
|
|
157
|
+
for label, cf in compose_files:
|
|
158
|
+
for svc_name, container_name, _ in _parse_compose_services(cf):
|
|
159
|
+
all_services.setdefault(svc_name, []).append(label)
|
|
160
|
+
if container_name:
|
|
161
|
+
all_container_names.setdefault(container_name, []).append(label)
|
|
162
|
+
|
|
163
|
+
svc_conflicts = {s: srcs for s, srcs in all_services.items() if len(srcs) > 1}
|
|
164
|
+
if svc_conflicts:
|
|
165
|
+
for svc, sources in sorted(svc_conflicts.items()):
|
|
166
|
+
_warn(f"Service '{svc}' defined by multiple features: {', '.join(sources)}")
|
|
167
|
+
else:
|
|
168
|
+
_ok(f"No service name collisions ({len(all_services)} services)")
|
|
169
|
+
|
|
170
|
+
cn_conflicts = {c: srcs for c, srcs in all_container_names.items() if len(srcs) > 1}
|
|
171
|
+
if cn_conflicts:
|
|
172
|
+
for cn, sources in sorted(cn_conflicts.items()):
|
|
173
|
+
_fail(f"Container name '{cn}' used by multiple features: {', '.join(sources)}")
|
|
174
|
+
else:
|
|
175
|
+
_ok(f"No container name collisions ({len(all_container_names)} named containers)")
|
|
176
|
+
|
|
177
|
+
# --- Check 3: Network availability ---
|
|
178
|
+
click.echo()
|
|
179
|
+
click.echo(click.style(" Networks", bold=True))
|
|
180
|
+
required_networks: set[str] = set()
|
|
181
|
+
for label, cf in compose_files:
|
|
182
|
+
result = subprocess.run(
|
|
183
|
+
["docker", "compose", "-f", cf, "config", "--format", "json"],
|
|
184
|
+
capture_output=True, text=True,
|
|
185
|
+
)
|
|
186
|
+
if result.returncode != 0:
|
|
187
|
+
continue
|
|
188
|
+
try:
|
|
189
|
+
config = json.loads(result.stdout)
|
|
190
|
+
except json.JSONDecodeError:
|
|
191
|
+
continue
|
|
192
|
+
for net_name, net_def in config.get("networks", {}).items():
|
|
193
|
+
if isinstance(net_def, dict) and net_def.get("external"):
|
|
194
|
+
required_networks.add(net_name)
|
|
195
|
+
|
|
196
|
+
if required_networks:
|
|
197
|
+
existing_networks = subprocess.run(
|
|
198
|
+
["docker", "network", "ls", "--format", "{{.Name}}"],
|
|
199
|
+
capture_output=True, text=True,
|
|
200
|
+
).stdout.splitlines()
|
|
201
|
+
for net in sorted(required_networks):
|
|
202
|
+
if net in existing_networks:
|
|
203
|
+
_ok(f"Network '{net}' exists")
|
|
204
|
+
else:
|
|
205
|
+
_fail(f"External network '{net}' does not exist (run: docker network create {net})")
|
|
206
|
+
else:
|
|
207
|
+
_ok("No external networks required")
|
|
208
|
+
|
|
209
|
+
# --- Summary ---
|
|
210
|
+
click.echo()
|
|
211
|
+
total = ok + fail + warn
|
|
212
|
+
if fail:
|
|
213
|
+
click.secho(f" {fail} check(s) failed, {warn} warning(s), {ok} passed.", fg="red")
|
|
214
|
+
raise SystemExit(1)
|
|
215
|
+
elif warn:
|
|
216
|
+
click.secho(f" All passed with {warn} warning(s) ({ok} checks OK).", fg="yellow")
|
|
217
|
+
else:
|
|
218
|
+
click.secho(f" All {ok} checks passed.", fg="green")
|
|
219
|
+
click.echo()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
cli_command = check_infra
|