splent-cli 1.28.1__tar.gz → 1.29.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.28.1/src/splent_cli.egg-info → splent_cli-1.29.0}/PKG-INFO +1 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0}/pyproject.toml +1 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_build.py +94 -9
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_create.py +12 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_deploy.py +238 -53
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_derive.py +10 -2
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_env.py +26 -5
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_release.py +83 -47
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_sync_template.py +11 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/release/release_resume.py +16 -9
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/tokens_setup.py +4 -1
- splent_cli-1.29.0/src/splent_cli/services/image_registry.py +173 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/registry.py +100 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/release.py +10 -6
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/release_gate.py +107 -18
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/git_url.py +21 -3
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/template_drift.py +25 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli.egg-info/SOURCES.txt +1 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/LICENSE +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/README.md +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/setup.cfg +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/__main__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/cli.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_deps.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_docker.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_env.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_features.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_github.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_infra.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_product.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/check/check_releases.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/clear/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/command/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/command/command_create.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/coverage.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_console.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_dump.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_reset.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_restore.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_seed.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_status.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/doctor.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_list.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_set.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/env/env_show.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/export/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/export/export_puml.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_compile.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_release.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/lint.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/locust.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/login.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_clean.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_commands.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_config.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_configure.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_console.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_containers.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_down.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_drift.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_list.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_logs.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_missing.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_port.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_restart.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_routes.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_run.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_select.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_shell.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_signals.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_test.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_up.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/product/product_validate.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/release/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/release/release_core.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/selenium.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/commands/version.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/compose.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/context.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/credentials.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/index_refresh.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/marketplace.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/marketplace_api.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/marketplace_url.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/preflight.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/pypi_quota.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/release_dry_run.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/services/spl_store.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/__init__.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/archetype.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/cache_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/command_loader.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/contract_freshness.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/db_client.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/db_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/decorators.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/feature_installer.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/feature_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/forge.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/integrity.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/io_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/lifecycle.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/manifest.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/path_utils.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/proc.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/redact.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/refinement_facts.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/toml_edit.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.0}/src/splent_cli.egg-info/requires.txt +0 -0
- {splent_cli-1.28.1 → splent_cli-1.29.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.29.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
|
|
@@ -88,6 +88,16 @@ OPTIONAL_DEPLOY_SETTINGS: tuple[tuple[str, str, tuple[str, ...]], ...] = (
|
|
|
88
88
|
"trusted proxy. true forces it on every cookie, false never sets it.",
|
|
89
89
|
),
|
|
90
90
|
),
|
|
91
|
+
(
|
|
92
|
+
"SPLENT_IMAGE",
|
|
93
|
+
"git.splent.io/<org>/<product>:stable",
|
|
94
|
+
(
|
|
95
|
+
"Image the web service runs. Unset means the image built on this",
|
|
96
|
+
"host. Set it to the image a release publishes, tagged stable, to",
|
|
97
|
+
"run releases without building here. Every deploy pulls it, and the",
|
|
98
|
+
"autodeploy feature follows it.",
|
|
99
|
+
),
|
|
100
|
+
),
|
|
91
101
|
(
|
|
92
102
|
"MAX_CONTENT_LENGTH",
|
|
93
103
|
"64M",
|
|
@@ -551,9 +561,34 @@ def _apply_restart_policy(svc_def: dict) -> None:
|
|
|
551
561
|
svc_def["restart"] = RESTART_POLICY
|
|
552
562
|
|
|
553
563
|
|
|
564
|
+
def resolve_product_placeholders(value: str, product: str, *, prod: bool) -> str:
|
|
565
|
+
"""Resolve ``__PRODUCT__`` in a feature's value for this product.
|
|
566
|
+
|
|
567
|
+
``__PRODUCT___web`` is the product's web container, which is
|
|
568
|
+
``<product>_web`` in development and ``<product>_web_deploy`` in
|
|
569
|
+
production (the prod compose template names it). Every other
|
|
570
|
+
``__PRODUCT__`` is the product name. product:build and
|
|
571
|
+
product:env --merge --prod both write .env.deploy, so both go through
|
|
572
|
+
this: the merge used to replace the plain name only and point a feature
|
|
573
|
+
at the development container's name in production.
|
|
574
|
+
"""
|
|
575
|
+
if "__PRODUCT__" not in value:
|
|
576
|
+
return value
|
|
577
|
+
if prod:
|
|
578
|
+
value = value.replace("__PRODUCT___web", f"{product}_web_deploy")
|
|
579
|
+
return value.replace("__PRODUCT__", product)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def local_or_registry_image(product: str) -> str:
|
|
583
|
+
"""The web image of a deploy: ``SPLENT_IMAGE`` when set, else the local build."""
|
|
584
|
+
return "${SPLENT_IMAGE:-" + product + ":latest}"
|
|
585
|
+
|
|
586
|
+
|
|
554
587
|
def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
555
588
|
"""Give every deployed service a restart policy, and the web and database
|
|
556
|
-
services a healthcheck, unless the compose file already set one.
|
|
589
|
+
services a healthcheck, unless the compose file already set one. The web
|
|
590
|
+
image becomes ``${SPLENT_IMAGE:-<product>:latest}``, so a host can run the
|
|
591
|
+
image a release published instead of building its own.
|
|
557
592
|
|
|
558
593
|
Applied by product:build to the merged deploy compose, so every product
|
|
559
594
|
gets it on its next build, whatever its docker-compose.prod.yml says and
|
|
@@ -570,6 +605,11 @@ def apply_production_policies(compose_data: dict, product: str) -> dict:
|
|
|
570
605
|
)
|
|
571
606
|
if web_name and isinstance(services[web_name], dict):
|
|
572
607
|
services[web_name].setdefault("healthcheck", dict(WEB_HEALTHCHECK))
|
|
608
|
+
# The local build, unless .env.deploy names a registry image. Only the
|
|
609
|
+
# plain local name is rewritten: an image the product chose on purpose
|
|
610
|
+
# is left alone.
|
|
611
|
+
if services[web_name].get("image") == f"{product}:latest":
|
|
612
|
+
services[web_name]["image"] = local_or_registry_image(product)
|
|
573
613
|
|
|
574
614
|
db_name = next(
|
|
575
615
|
(n for n in (f"{product}_db_deploy", f"{product}_db") if n in services),
|
|
@@ -937,6 +977,37 @@ def _collect_feature_assets(workspace, declared_features, docker_path, env="prod
|
|
|
937
977
|
return copied
|
|
938
978
|
|
|
939
979
|
|
|
980
|
+
def unresolved_pins(
|
|
981
|
+
workspace: str, product: str, features: Sequence[str]
|
|
982
|
+
) -> list[tuple[str, str]]:
|
|
983
|
+
"""Pinned features this workspace has not resolved, each with the reason.
|
|
984
|
+
|
|
985
|
+
product:build reads every feature's env file and [tool.splent.config]
|
|
986
|
+
from the release the product pins, and copies the product's features/
|
|
987
|
+
links into the image. Both are product:resolve's work. Without it the
|
|
988
|
+
build used to carry on with whatever copy of the feature sat at the
|
|
989
|
+
workspace root, usually an old one: the .env.deploy.example lost every
|
|
990
|
+
setting the old copy did not declare, product:deploy then dropped them
|
|
991
|
+
from .env.deploy, and the image shipped links naming the old versions.
|
|
992
|
+
Editable features (no version) are not pinned and are not checked.
|
|
993
|
+
"""
|
|
994
|
+
problems = []
|
|
995
|
+
for feat in features:
|
|
996
|
+
clean = compose.normalize_feature_ref(feat)
|
|
997
|
+
org_safe, _, name_ver = clean.partition("/")
|
|
998
|
+
if "@" not in name_ver:
|
|
999
|
+
continue
|
|
1000
|
+
cached = os.path.join(
|
|
1001
|
+
workspace, ".splent_cache", "features", org_safe, name_ver
|
|
1002
|
+
)
|
|
1003
|
+
linked = os.path.join(workspace, product, "features", org_safe, name_ver)
|
|
1004
|
+
if not os.path.isdir(cached):
|
|
1005
|
+
problems.append((name_ver, "not in .splent_cache"))
|
|
1006
|
+
elif not os.path.isdir(linked):
|
|
1007
|
+
problems.append((name_ver, f"not linked in {product}/features/"))
|
|
1008
|
+
return problems
|
|
1009
|
+
|
|
1010
|
+
|
|
940
1011
|
@click.command(
|
|
941
1012
|
"product:build",
|
|
942
1013
|
short_help="Build deployment artifacts: env, compose, and Docker image.",
|
|
@@ -961,6 +1032,27 @@ def product_build(no_image, skip_preflight):
|
|
|
961
1032
|
|
|
962
1033
|
click.echo(f"🏗️ Building deployment artifacts for product: {product}\n")
|
|
963
1034
|
|
|
1035
|
+
# Read declared features from pyproject.toml (not glob)
|
|
1036
|
+
pydata = load_toml(pyproject_path, what="pyproject.toml")
|
|
1037
|
+
declared_features = read_features_from_data(pydata, "prod")
|
|
1038
|
+
|
|
1039
|
+
# ---------------------------------------------------------
|
|
1040
|
+
# The releases the product pins, on disk
|
|
1041
|
+
# ---------------------------------------------------------
|
|
1042
|
+
unresolved = unresolved_pins(workspace, product, declared_features)
|
|
1043
|
+
if unresolved:
|
|
1044
|
+
click.secho(
|
|
1045
|
+
" The build reads the releases pyproject.toml pins, and these are "
|
|
1046
|
+
"not resolved in this workspace.",
|
|
1047
|
+
fg="red",
|
|
1048
|
+
)
|
|
1049
|
+
width = max(len(name) for name, _ in unresolved)
|
|
1050
|
+
for name, why in unresolved:
|
|
1051
|
+
click.echo(f" {name.ljust(width)} {why}")
|
|
1052
|
+
click.echo()
|
|
1053
|
+
click.echo(" Run splent product:resolve and build again.")
|
|
1054
|
+
raise SystemExit(1)
|
|
1055
|
+
|
|
964
1056
|
# ---------------------------------------------------------
|
|
965
1057
|
# Pre-flight checks (UVL + feature contracts)
|
|
966
1058
|
# ---------------------------------------------------------
|
|
@@ -987,10 +1079,6 @@ def product_build(no_image, skip_preflight):
|
|
|
987
1079
|
# unnamed courses with no categories in production.
|
|
988
1080
|
product_env, tunable_keys = load_env_file_with_markers(product_env_file)
|
|
989
1081
|
|
|
990
|
-
# Read declared features from pyproject.toml (not glob)
|
|
991
|
-
pydata = load_toml(pyproject_path, what="pyproject.toml")
|
|
992
|
-
declared_features = read_features_from_data(pydata, "prod")
|
|
993
|
-
|
|
994
1082
|
# Unversioned features, and what actually happens to them.
|
|
995
1083
|
#
|
|
996
1084
|
# This warning used to say the build would fail. It does not, and that is
|
|
@@ -1061,10 +1149,7 @@ def product_build(no_image, skip_preflight):
|
|
|
1061
1149
|
# share the product network, so the prod service cannot simply take the
|
|
1062
1150
|
# dev name as an alias. The CLI owns both names, so it owns the mapping.
|
|
1063
1151
|
for k, v in env_result.items():
|
|
1064
|
-
|
|
1065
|
-
env_result[k] = v.replace(
|
|
1066
|
-
"__PRODUCT___web", f"{product}_web_deploy"
|
|
1067
|
-
).replace("__PRODUCT__", product)
|
|
1152
|
+
env_result[k] = resolve_product_placeholders(v, product, prod=True)
|
|
1068
1153
|
|
|
1069
1154
|
# Remove __FEATURE_HOST_DIR__ variables — not needed in prod
|
|
1070
1155
|
# (templates are baked into Docker images, no bind mounts)
|
|
@@ -13,7 +13,12 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
|
13
13
|
|
|
14
14
|
from splent_cli.utils.path_utils import PathUtils
|
|
15
15
|
from splent_cli.services import context, spl_store
|
|
16
|
-
from splent_cli.utils.template_drift import
|
|
16
|
+
from splent_cli.utils.template_drift import (
|
|
17
|
+
DEFAULT_UPGRADE_SCHEDULE,
|
|
18
|
+
is_reserved_repo_name,
|
|
19
|
+
not_a_product_reason,
|
|
20
|
+
)
|
|
21
|
+
from splent_cli.utils.template_drift import _forge_host as _template_forge_host
|
|
17
22
|
|
|
18
23
|
try:
|
|
19
24
|
from importlib.metadata import version as _pkg_version
|
|
@@ -218,6 +223,10 @@ def make_product(name, spl_name, spl_doi, features_file, force):
|
|
|
218
223
|
# Dropping it here made every product created this way fall back to
|
|
219
224
|
# "<name>.uvl" and 404, with nothing local left that knew the real name.
|
|
220
225
|
"spl_file": pin.file or "",
|
|
226
|
+
# The forge workflows. The same values product:sync-template --ci
|
|
227
|
+
# renders, so a new product does not start out drifted.
|
|
228
|
+
"forge_host": _template_forge_host(),
|
|
229
|
+
"upgrade_schedule": DEFAULT_UPGRADE_SCHEDULE,
|
|
221
230
|
# Production persistence. These switch on the named-volume mounts in
|
|
222
231
|
# the prod compose template so uploads survive redeploys. Products
|
|
223
232
|
# scaffolded before these variables existed render without them and
|
|
@@ -304,6 +313,8 @@ def _write_product(env, name, base_path, template_ctx):
|
|
|
304
313
|
"scripts/05_0_start_app_dev.sh": "product/product_05_0_start_app_dev.sh.j2",
|
|
305
314
|
"scripts/05_1_start_app_prod.sh": "product/product_05_1_start_app_prod.sh.j2",
|
|
306
315
|
".gitignore": "product/product_.gitignore.j2",
|
|
316
|
+
".forgejo/workflows/splent-validate.yml": "product/product_ci_validate.yml.j2",
|
|
317
|
+
".forgejo/workflows/splent-upgrade.yml": "product/product_ci_upgrade.yml.j2",
|
|
307
318
|
"LICENSE": "product/product_LICENSE.j2",
|
|
308
319
|
"package.json": "product/product_package.json.j2",
|
|
309
320
|
"pyproject.toml": "product/product_pyproject.toml.j2",
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
3
|
import subprocess
|
|
4
|
+
from dataclasses import dataclass, field
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
|
-
from splent_cli.utils.io_utils import atomic_write
|
|
8
|
+
from splent_cli.utils.io_utils import atomic_write, env_line
|
|
8
9
|
import yaml
|
|
9
10
|
from splent_cli.services import compose, context
|
|
10
11
|
from splent_cli.commands.product.product_build import (
|
|
11
12
|
OPTIONAL_DEPLOY_KEYS,
|
|
12
13
|
host_docker_dir_env,
|
|
14
|
+
is_user_tunable_marker,
|
|
13
15
|
load_env_file_with_markers,
|
|
14
16
|
render_deploy_env,
|
|
15
17
|
)
|
|
@@ -45,60 +47,205 @@ def suggest_value(key: str, product: str) -> str | None:
|
|
|
45
47
|
ENV_DEPLOY_MODE = 0o600
|
|
46
48
|
|
|
47
49
|
|
|
50
|
+
#: The template product:deploy merged into .env.deploy last time, kept next to
|
|
51
|
+
#: it. Comparing against it is how a deploy tells a value the operator edited
|
|
52
|
+
#: from one the template changed: without it, both look like "the file differs
|
|
53
|
+
#: from the template". Ignored by git and by the image like every .env.*.
|
|
54
|
+
ENV_DEPLOY_BASE_SUFFIX = ".base"
|
|
55
|
+
|
|
56
|
+
#: Keys the CLI derives for the product and the operator has no reason to edit.
|
|
57
|
+
#: Without a recorded base they still follow the template, so an upgrade that
|
|
58
|
+
#: renames one (the database host, the network) reaches deployments made
|
|
59
|
+
#: before the base existed.
|
|
60
|
+
_DERIVED_KEYS = frozenset({"SPLENT_ENV", "SPLENT_NETWORK"})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class TemplateSync:
|
|
65
|
+
"""What merging the template into an operator's env file did."""
|
|
66
|
+
|
|
67
|
+
env: dict
|
|
68
|
+
#: Keys the template changed and the operator had left alone.
|
|
69
|
+
updated: list = field(default_factory=list)
|
|
70
|
+
#: Keys the operator set that the template now sets differently.
|
|
71
|
+
kept: list = field(default_factory=list)
|
|
72
|
+
#: Keys the operator file had and the template no longer names.
|
|
73
|
+
dropped: list = field(default_factory=list)
|
|
74
|
+
#: User-tunable keys the operator deleted, which stay deleted.
|
|
75
|
+
deleted_tunables: set = field(default_factory=set)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def derived_keys(product: str) -> frozenset:
|
|
79
|
+
"""The keys the CLI writes into every product's production template.
|
|
80
|
+
|
|
81
|
+
Read from the CLI's own template rather than listed here, so a key the
|
|
82
|
+
template gains is covered too. Secrets (<SET>) and user-tunable keys are
|
|
83
|
+
the operator's and are left out.
|
|
84
|
+
"""
|
|
85
|
+
from splent_cli.utils.template_drift import product_ctx, render_template
|
|
86
|
+
|
|
87
|
+
keys = set(_DERIVED_KEYS)
|
|
88
|
+
text = render_template("product/product_.env.prod.example.j2", product_ctx(product))
|
|
89
|
+
tunable_next = False
|
|
90
|
+
for line in text.splitlines():
|
|
91
|
+
stripped = line.strip()
|
|
92
|
+
if is_user_tunable_marker(stripped):
|
|
93
|
+
tunable_next = True
|
|
94
|
+
continue
|
|
95
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
96
|
+
continue
|
|
97
|
+
key, _, value = stripped.partition("=")
|
|
98
|
+
if not tunable_next and value.strip() != "<SET>":
|
|
99
|
+
keys.add(key.strip())
|
|
100
|
+
tunable_next = False
|
|
101
|
+
return frozenset(keys)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def base_path(env_path: str) -> str:
|
|
105
|
+
"""Where the template behind ``env_path`` is recorded."""
|
|
106
|
+
return env_path + ENV_DEPLOY_BASE_SUFFIX
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_base(env_path: str) -> dict | None:
|
|
110
|
+
"""The template last merged into ``env_path``, or None if never recorded."""
|
|
111
|
+
path = base_path(env_path)
|
|
112
|
+
if not os.path.isfile(path):
|
|
113
|
+
return None
|
|
114
|
+
values, _ = load_env_file_with_markers(path)
|
|
115
|
+
return values
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def write_base(env_path: str, template_vars: dict) -> None:
|
|
119
|
+
"""Record the template just merged into ``env_path`` for the next merge."""
|
|
120
|
+
atomic_write(
|
|
121
|
+
base_path(env_path),
|
|
122
|
+
"".join(env_line(k, v) for k, v in template_vars.items()),
|
|
123
|
+
dir_owner=True,
|
|
124
|
+
mode=ENV_DEPLOY_MODE,
|
|
125
|
+
force_mode=True,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
48
129
|
def sync_with_template(
|
|
49
|
-
example_vars: dict,
|
|
50
|
-
|
|
130
|
+
example_vars: dict,
|
|
131
|
+
tunable_keys: set,
|
|
132
|
+
existing_vars: dict,
|
|
133
|
+
base_vars: dict | None = None,
|
|
134
|
+
derived: frozenset = frozenset(),
|
|
135
|
+
) -> TemplateSync:
|
|
51
136
|
"""Bring an operator's env file up to date with its template.
|
|
52
137
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
138
|
+
A three-way merge. ``base_vars`` is the template as it was when the file
|
|
139
|
+
was last merged, so for every key that differs from the template now:
|
|
140
|
+
|
|
141
|
+
* the operator's value equals the old template: the operator never
|
|
142
|
+
touched it, the template moved, and the new template value is taken;
|
|
143
|
+
* otherwise the operator changed it and their value is kept.
|
|
144
|
+
|
|
145
|
+
Before this the template won every disagreement except secrets and
|
|
146
|
+
user-tunable keys, so a setting the operator turned on (publishing in the
|
|
147
|
+
marketplace, an API key the template ships empty) was silently undone on
|
|
148
|
+
every deploy.
|
|
149
|
+
|
|
150
|
+
With no recorded base (a file merged by an older CLI) nothing tells the
|
|
151
|
+
two apart, so the operator's value is kept and reported, except for the
|
|
152
|
+
``derived`` keys the CLI computes for the product, which follow the
|
|
153
|
+
template as they always did.
|
|
154
|
+
|
|
155
|
+
Secrets the template ships as <SET> and user-tunable keys are the
|
|
156
|
+
operator's in every case, and a tunable the operator deleted stays
|
|
157
|
+
deleted. Keys the template no longer names are dropped and reported.
|
|
158
|
+
|
|
159
|
+
product:deploy and product:env --merge --prod both write .env.deploy
|
|
160
|
+
through this, so one cannot undo what the other kept.
|
|
64
161
|
"""
|
|
162
|
+
result = TemplateSync(env={})
|
|
65
163
|
# Keys the operator removed on purpose. Only meaningful for tunables: a
|
|
66
|
-
# template-owned key that went missing is
|
|
67
|
-
deleted_tunables = {
|
|
164
|
+
# template-owned key that went missing is restored below.
|
|
165
|
+
result.deleted_tunables = {
|
|
68
166
|
k for k in tunable_keys if k in example_vars and k not in existing_vars
|
|
69
167
|
}
|
|
70
168
|
|
|
71
|
-
env_vars = {}
|
|
72
|
-
updated = []
|
|
73
169
|
for k, v in example_vars.items():
|
|
74
|
-
if k in deleted_tunables:
|
|
170
|
+
if k in result.deleted_tunables:
|
|
75
171
|
# The operator deleted this tunable. Leave it out and let the
|
|
76
172
|
# compose default apply.
|
|
77
173
|
continue
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
):
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
elif
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
174
|
+
if k not in existing_vars or existing_vars[k] == v:
|
|
175
|
+
result.env[k] = v
|
|
176
|
+
continue
|
|
177
|
+
mine = existing_vars[k]
|
|
178
|
+
if k in tunable_keys:
|
|
179
|
+
# Operator-owned value, kept exactly as they wrote it.
|
|
180
|
+
result.env[k] = mine
|
|
181
|
+
elif v.strip() == "<SET>":
|
|
182
|
+
# A secret, filled in unless the file still says <SET>.
|
|
183
|
+
result.env[k] = v if mine.strip() == "<SET>" else mine
|
|
184
|
+
elif base_vars is not None and k in base_vars:
|
|
185
|
+
if mine == base_vars[k]:
|
|
186
|
+
result.env[k] = v
|
|
187
|
+
result.updated.append(k)
|
|
188
|
+
else:
|
|
189
|
+
result.env[k] = mine
|
|
190
|
+
if base_vars[k] != v:
|
|
191
|
+
result.kept.append(k)
|
|
192
|
+
elif k in derived:
|
|
193
|
+
result.env[k] = v
|
|
194
|
+
result.updated.append(k)
|
|
92
195
|
else:
|
|
93
|
-
|
|
196
|
+
result.env[k] = mine
|
|
197
|
+
result.kept.append(k)
|
|
198
|
+
|
|
94
199
|
# Optional operator settings (GUNICORN_WORKERS and the like) are not
|
|
95
200
|
# live lines in any template, so the loop above never sees them. One the
|
|
96
201
|
# operator set is theirs and stays, or uncommenting it in .env.deploy
|
|
97
202
|
# would last until the next deploy.
|
|
98
203
|
for k in sorted(OPTIONAL_DEPLOY_KEYS):
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
204
|
+
if (
|
|
205
|
+
k in existing_vars
|
|
206
|
+
and k not in result.env
|
|
207
|
+
and k not in result.deleted_tunables
|
|
208
|
+
):
|
|
209
|
+
result.env[k] = existing_vars[k]
|
|
210
|
+
|
|
211
|
+
result.dropped = [
|
|
212
|
+
k
|
|
213
|
+
for k in existing_vars
|
|
214
|
+
if k not in result.env and k not in result.deleted_tunables
|
|
215
|
+
]
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def report_sync(sync: TemplateSync, env_file: str) -> None:
|
|
220
|
+
"""Say what merging the template changed, by key name only.
|
|
221
|
+
|
|
222
|
+
Values stay out of the output: half of them are passwords and keys.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def say(text: str) -> None:
|
|
226
|
+
click.echo(click.style(" env ", dim=True) + text)
|
|
227
|
+
|
|
228
|
+
if sync.updated:
|
|
229
|
+
say(
|
|
230
|
+
f"updated {len(sync.updated)} variable(s) from the template: "
|
|
231
|
+
+ ", ".join(sync.updated)
|
|
232
|
+
)
|
|
233
|
+
if sync.kept:
|
|
234
|
+
say(
|
|
235
|
+
f"kept your value for {len(sync.kept)} variable(s) the template sets "
|
|
236
|
+
f"differently: " + ", ".join(sync.kept)
|
|
237
|
+
)
|
|
238
|
+
say(f" delete a line from {env_file} to take the template's value")
|
|
239
|
+
if sync.dropped:
|
|
240
|
+
say(
|
|
241
|
+
f"dropped {len(sync.dropped)} variable(s) the template no longer "
|
|
242
|
+
f"has: " + ", ".join(sync.dropped)
|
|
243
|
+
)
|
|
244
|
+
if sync.deleted_tunables:
|
|
245
|
+
say(
|
|
246
|
+
f"keeping {len(sync.deleted_tunables)} removed variable(s) out: "
|
|
247
|
+
+ ", ".join(sorted(sync.deleted_tunables))
|
|
248
|
+
)
|
|
102
249
|
|
|
103
250
|
|
|
104
251
|
# Env vars whose value is a directory the running app writes to. Their contents
|
|
@@ -266,22 +413,15 @@ def product_deploy(down, ci):
|
|
|
266
413
|
# ---------------------------------------------------------
|
|
267
414
|
existing_vars, _ = load_env_file_with_markers(env_path)
|
|
268
415
|
example_vars, tunable_keys = load_env_file_with_markers(env_example_path)
|
|
269
|
-
|
|
270
|
-
example_vars,
|
|
416
|
+
sync = sync_with_template(
|
|
417
|
+
example_vars,
|
|
418
|
+
tunable_keys,
|
|
419
|
+
existing_vars,
|
|
420
|
+
base_vars=read_base(env_path),
|
|
421
|
+
derived=derived_keys(product),
|
|
271
422
|
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
click.echo(
|
|
275
|
-
click.style(" env ", dim=True)
|
|
276
|
-
+ f"updated {len(updated)} variable(s) from template"
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
if deleted_tunables:
|
|
280
|
-
click.echo(
|
|
281
|
-
click.style(" env ", dim=True)
|
|
282
|
-
+ f"keeping {len(deleted_tunables)} removed variable(s) out: "
|
|
283
|
-
+ ", ".join(sorted(deleted_tunables))
|
|
284
|
-
)
|
|
423
|
+
env_vars = sync.env
|
|
424
|
+
report_sync(sync, "docker/.env.deploy")
|
|
285
425
|
|
|
286
426
|
# ---------------------------------------------------------
|
|
287
427
|
# Ask interactively for missing (<SET>) values
|
|
@@ -301,9 +441,18 @@ def product_deploy(down, ci):
|
|
|
301
441
|
)
|
|
302
442
|
continue
|
|
303
443
|
if ci:
|
|
304
|
-
|
|
444
|
+
# The environment first. Without it, whatever the interactive
|
|
445
|
+
# deploy would offer as a default is taken (a random string
|
|
446
|
+
# for a secret, the conventional database names), so a feature
|
|
447
|
+
# that ships a <SET> secret does not stop an unattended deploy.
|
|
448
|
+
# What only the operator knows still has to come from them.
|
|
449
|
+
env_value = os.getenv(key) or suggest_value(key, product)
|
|
305
450
|
if env_value:
|
|
306
451
|
env_vars[key] = env_value
|
|
452
|
+
if not os.getenv(key):
|
|
453
|
+
click.echo(
|
|
454
|
+
click.style(" secret ", dim=True) + f"{key} generated"
|
|
455
|
+
)
|
|
307
456
|
else:
|
|
308
457
|
missing_vars.append(key)
|
|
309
458
|
else:
|
|
@@ -345,6 +494,7 @@ def product_deploy(down, ci):
|
|
|
345
494
|
mode=ENV_DEPLOY_MODE,
|
|
346
495
|
force_mode=True,
|
|
347
496
|
)
|
|
497
|
+
write_base(env_path, example_vars)
|
|
348
498
|
|
|
349
499
|
# ---------------------------------------------------------
|
|
350
500
|
# Say when the app will not believe its proxy
|
|
@@ -483,6 +633,41 @@ def product_deploy(down, ci):
|
|
|
483
633
|
# container starts.
|
|
484
634
|
compose.ensure_network(compose.network_name(product))
|
|
485
635
|
|
|
636
|
+
# A registry image (SPLENT_IMAGE) is pulled first: Compose only pulls an
|
|
637
|
+
# image it does not have, so a host that already ran an older build of
|
|
638
|
+
# the same tag would otherwise redeploy that one.
|
|
639
|
+
image = (env_vars.get("SPLENT_IMAGE") or "").strip()
|
|
640
|
+
if "/" in image:
|
|
641
|
+
click.echo(click.style(" image ", dim=True) + f"pulling {image}")
|
|
642
|
+
pulled = subprocess.run(
|
|
643
|
+
[
|
|
644
|
+
"docker",
|
|
645
|
+
"compose",
|
|
646
|
+
"-p",
|
|
647
|
+
compose.deploy_project_name(product),
|
|
648
|
+
"-f",
|
|
649
|
+
compose_path,
|
|
650
|
+
"--env-file",
|
|
651
|
+
env_path,
|
|
652
|
+
"pull",
|
|
653
|
+
f"{product}_web_deploy",
|
|
654
|
+
],
|
|
655
|
+
capture_output=True,
|
|
656
|
+
text=True,
|
|
657
|
+
cwd=docker_dir,
|
|
658
|
+
env={**os.environ, **host_docker_dir_env(workspace, product)},
|
|
659
|
+
)
|
|
660
|
+
if pulled.returncode != 0:
|
|
661
|
+
click.secho(f" Could not pull {image}.", fg="red")
|
|
662
|
+
detail = (pulled.stderr or pulled.stdout or "").strip().splitlines()
|
|
663
|
+
for line in detail[-5:]:
|
|
664
|
+
click.echo(f" {line}")
|
|
665
|
+
click.echo(
|
|
666
|
+
" Check SPLENT_IMAGE in docker/.env.deploy, and docker login to "
|
|
667
|
+
"that registry when the image is private."
|
|
668
|
+
)
|
|
669
|
+
raise SystemExit(1)
|
|
670
|
+
|
|
486
671
|
click.echo(click.style(" deploying ", dim=True) + f"{product} (prod)...")
|
|
487
672
|
|
|
488
673
|
try:
|
|
@@ -183,8 +183,9 @@ def product_derive(mode):
|
|
|
183
183
|
|
|
184
184
|
\b
|
|
185
185
|
--prod runs (after pre-flight):
|
|
186
|
-
1. product:
|
|
187
|
-
2. product:
|
|
186
|
+
1. product:resolve clone the pinned releases and link them
|
|
187
|
+
2. product:build merge env + compose + build Docker image
|
|
188
|
+
3. product:deploy start production containers
|
|
188
189
|
|
|
189
190
|
\b
|
|
190
191
|
The pipeline stops at the first step that fails.
|
|
@@ -354,6 +355,13 @@ def product_derive(mode):
|
|
|
354
355
|
step(product_port, env_flag="dev")
|
|
355
356
|
|
|
356
357
|
elif mode == "prod":
|
|
358
|
+
# The build reads the pinned releases from the cache and refuses
|
|
359
|
+
# without them, so resolve them first, as the dev path does.
|
|
360
|
+
click.echo(
|
|
361
|
+
click.style(" sync ", dim=True) + "resolving the pinned features..."
|
|
362
|
+
)
|
|
363
|
+
step(product_sync, force=False, mode="prod", strict=True)
|
|
364
|
+
|
|
357
365
|
click.echo(" build merging env + compose + Docker image")
|
|
358
366
|
step(product_build, no_image=False, skip_preflight=True)
|
|
359
367
|
|
|
@@ -368,26 +368,46 @@ def product_env(generate, merge, env_name, process_all):
|
|
|
368
368
|
# second product of the same line came up.
|
|
369
369
|
merged["SPLENT_NETWORK"] = compose.network_name(product)
|
|
370
370
|
|
|
371
|
-
# Replace __PRODUCT__ placeholder with the actual product name
|
|
371
|
+
# Replace __PRODUCT__ placeholder with the actual product name.
|
|
372
|
+
# product_build imports this module, so the helper is imported here.
|
|
373
|
+
from splent_cli.commands.product.product_build import (
|
|
374
|
+
resolve_product_placeholders,
|
|
375
|
+
)
|
|
376
|
+
|
|
372
377
|
product_replaced = []
|
|
373
378
|
for k, v in merged.items():
|
|
374
379
|
if "__PRODUCT__" in v:
|
|
375
|
-
merged[k] =
|
|
380
|
+
merged[k] = resolve_product_placeholders(
|
|
381
|
+
v, product, prod=env_name == "prod"
|
|
382
|
+
)
|
|
376
383
|
product_replaced.append(k)
|
|
377
384
|
|
|
378
385
|
if env_name == "prod":
|
|
379
386
|
# .env.deploy is the operator's file: it holds the passwords and
|
|
380
387
|
# the SECRET_KEY the running database was created with. The same
|
|
381
|
-
#
|
|
382
|
-
#
|
|
388
|
+
# three-way merge product:deploy uses keeps every value the
|
|
389
|
+
# operator set and takes every one the template changed.
|
|
383
390
|
from splent_cli.commands.product.product_deploy import (
|
|
384
391
|
ENV_DEPLOY_MODE,
|
|
392
|
+
derived_keys,
|
|
393
|
+
read_base,
|
|
394
|
+
report_sync,
|
|
385
395
|
sync_with_template,
|
|
396
|
+
write_base,
|
|
386
397
|
)
|
|
387
398
|
|
|
399
|
+
template = dict(merged)
|
|
388
400
|
if os.path.isfile(target_env):
|
|
389
401
|
existing, _ = load_env_file_with_markers(target_env)
|
|
390
|
-
|
|
402
|
+
sync = sync_with_template(
|
|
403
|
+
template,
|
|
404
|
+
tunable_keys,
|
|
405
|
+
existing,
|
|
406
|
+
base_vars=read_base(target_env),
|
|
407
|
+
derived=derived_keys(product),
|
|
408
|
+
)
|
|
409
|
+
merged = sync.env
|
|
410
|
+
report_sync(sync, os.path.relpath(target_env, product_path))
|
|
391
411
|
backup_file(target_env, mode=ENV_DEPLOY_MODE)
|
|
392
412
|
atomic_write(
|
|
393
413
|
target_env,
|
|
@@ -396,6 +416,7 @@ def product_env(generate, merge, env_name, process_all):
|
|
|
396
416
|
mode=ENV_DEPLOY_MODE,
|
|
397
417
|
force_mode=True,
|
|
398
418
|
)
|
|
419
|
+
write_base(target_env, template)
|
|
399
420
|
else:
|
|
400
421
|
atomic_write(
|
|
401
422
|
target_env,
|