wasm-cli 1.0.3__tar.gz → 1.2.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.
- {wasm_cli-1.0.3/src/wasm_cli.egg-info → wasm_cli-1.2.0}/PKG-INFO +1 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/pyproject.toml +1 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/setup.py +1 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/__init__.py +1 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/health.py +26 -35
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/web.py +137 -125
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/webapp.py +36 -27
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/interactive.py +5 -3
- wasm_cli-1.2.0/src/wasm/core/app_state.py +205 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/logger.py +206 -33
- wasm_cli-1.2.0/src/wasm/core/net.py +252 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/registry.py +24 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/service_manager.py +8 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/process_monitor.py +10 -1
- wasm_cli-1.2.0/src/wasm/web/events.py +214 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/jobs.py +20 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/server.py +125 -39
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/app.css +357 -10
- wasm_cli-1.2.0/src/wasm/web/static/panel.js +548 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/_macros.html +34 -6
- wasm_cli-1.2.0/src/wasm/web/templates/base.html +169 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/login.html +26 -11
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/activity.html +7 -6
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/app.html +21 -17
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/backups.html +4 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/certificates.html +12 -5
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/dashboard.html +8 -8
- wasm_cli-1.2.0/src/wasm/web/templates/pages/deploy.html +123 -0
- wasm_cli-1.2.0/src/wasm/web/templates/pages/failure.html +31 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/resources.html +13 -3
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/settings.html +2 -2
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/context.py +8 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/resources.py +146 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/router.py +301 -2
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/websockets/router.py +43 -12
- {wasm_cli-1.0.3 → wasm_cli-1.2.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm_cli.egg-info/SOURCES.txt +5 -1
- wasm_cli-1.0.3/src/wasm/web/static/panel.js +0 -233
- wasm_cli-1.0.3/src/wasm/web/static/vendor/alpine.min.js +0 -5
- wasm_cli-1.0.3/src/wasm/web/templates/base.html +0 -124
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/LICENSE +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/MANIFEST.in +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/README.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/MONITOR.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/assets/logo.png +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/superpowers/specs/2026-08-12-wasm-panel-design-direction.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/docs/superpowers/specs/2026-08-12-wasm-v1-refactor-design.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/man/wasm.1 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/setup.cfg +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/__main__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/app.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/backup.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/cert.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/db.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/env.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/monitor.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/cli/prompts.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/completions/README.md +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/config.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/dependencies.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/exceptions.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/fs.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/runner.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/store.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/core/utils.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/auto.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/base.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/docker_compose.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/env_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/health.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/nginx_config.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/preflight.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/registration.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/summary.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/turbo.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/helpers/workspace.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/interface.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/monorepo.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/pipeline.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/python.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/main.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/backup_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/backup_scheduler.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/base_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/cert_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/postgres.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/redis.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/nginx_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/managers/webserver.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/metrics.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/models.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/observation_store.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/monitor/signals.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/nginx/advanced.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/systemd/_escape.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/systemd/backup-service.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/systemd/backup-timer.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/templates/systemd/docker-compose.service.j2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/environment.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/names.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/port.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/source.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/apps.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/auth.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/deps.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/router.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/services.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/sites.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/auth.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/logo.webp +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-condensed-latin-600-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/htmx.min.js +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/uplot.min.css +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/uplot.min.js +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/xterm.css +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/static/vendor/xterm.js +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/fragments/machine.html +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/templates/pages/missing.html +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/machine.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/views/rendering.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-1.0.3 → wasm_cli-1.2.0}/src/wasm_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wasm-cli"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Web App System Management - Deploy and manage web applications on Linux servers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "WASM-NCSAL" }
|
|
@@ -9,7 +9,7 @@ from setuptools import setup, find_packages
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="wasm-cli",
|
|
12
|
-
version="1.0
|
|
12
|
+
version="1.2.0",
|
|
13
13
|
description="Web App System Management - Deploy and manage web applications on Linux servers",
|
|
14
14
|
author="Yago López Prado",
|
|
15
15
|
author_email="yago.lopez.adeje@gmail.com",
|
|
@@ -15,7 +15,7 @@ from importlib.metadata import version as _installed_version
|
|
|
15
15
|
#: as an OBS build directory. The single source of truth is the ``version``
|
|
16
16
|
#: field of pyproject.toml; ``scripts/release.py`` keeps this literal and the
|
|
17
17
|
#: distribution packaging files in step with it.
|
|
18
|
-
_FALLBACK_VERSION = "1.0
|
|
18
|
+
_FALLBACK_VERSION = "1.2.0"
|
|
19
19
|
|
|
20
20
|
try:
|
|
21
21
|
__version__ = _installed_version("wasm-cli")
|
|
@@ -28,11 +28,11 @@ from datetime import datetime
|
|
|
28
28
|
import click
|
|
29
29
|
|
|
30
30
|
from wasm.cli.app import Context, pass_context
|
|
31
|
+
from wasm.core.app_state import RUNNING, STATIC, resolve_states
|
|
31
32
|
from wasm.core.config import Config
|
|
32
|
-
from wasm.core.exceptions import
|
|
33
|
+
from wasm.core.exceptions import WASMError
|
|
33
34
|
from wasm.core.logger import Logger
|
|
34
35
|
from wasm.core.store import get_store
|
|
35
|
-
from wasm.core.utils import domain_to_app_name
|
|
36
36
|
from wasm.managers.apache_manager import ApacheManager
|
|
37
37
|
from wasm.managers.cert_manager import CertificateInfo, CertManager
|
|
38
38
|
from wasm.managers.nginx_manager import NginxManager
|
|
@@ -50,21 +50,14 @@ def _print_status(logger: Logger, key: str, value: str, status: str) -> None:
|
|
|
50
50
|
Print a key-value pair with status indicator.
|
|
51
51
|
|
|
52
52
|
Args:
|
|
53
|
-
logger: Logger of the current command
|
|
53
|
+
logger: Logger of the current command. Writing through it is what makes
|
|
54
|
+
``wasm --no-color health`` colourless; the escape codes used to be
|
|
55
|
+
written to stdout directly, so the flag did nothing here.
|
|
54
56
|
key: Name of the checked item.
|
|
55
57
|
value: Human readable result.
|
|
56
58
|
status: One of "ok", "warning", "error" or "info".
|
|
57
59
|
"""
|
|
58
|
-
|
|
59
|
-
indicator = "\033[32m[OK]\033[0m"
|
|
60
|
-
elif status == "warning":
|
|
61
|
-
indicator = "\033[33m[!]\033[0m"
|
|
62
|
-
elif status == "error":
|
|
63
|
-
indicator = "\033[31m[X]\033[0m"
|
|
64
|
-
else: # info
|
|
65
|
-
indicator = "\033[34m[i]\033[0m"
|
|
66
|
-
|
|
67
|
-
print(f" {indicator} {key}: {value}")
|
|
60
|
+
logger.check(key, value, status)
|
|
68
61
|
|
|
69
62
|
|
|
70
63
|
def _days_until(expiry: str) -> int | None:
|
|
@@ -222,37 +215,35 @@ def run_health_check(verbose: bool = False) -> int:
|
|
|
222
215
|
service_manager = ServiceManager(verbose=verbose)
|
|
223
216
|
|
|
224
217
|
apps = store.list_apps()
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if status.get("active"):
|
|
240
|
-
apps_running += 1
|
|
241
|
-
else:
|
|
242
|
-
apps_stopped += 1
|
|
243
|
-
warnings.append(f"App '{app.domain}' is not running")
|
|
218
|
+
|
|
219
|
+
# The same resolver `wasm list` uses. When these two commands each decided
|
|
220
|
+
# for themselves what "running" meant, list reported fifteen applications
|
|
221
|
+
# running while this reported seven stopped, and five of the seven were
|
|
222
|
+
# static sites that have no service to run in the first place.
|
|
223
|
+
states = resolve_states(apps, service_manager)
|
|
224
|
+
|
|
225
|
+
apps_running = sum(1 for s in states.values() if s.label == RUNNING)
|
|
226
|
+
apps_static = sum(1 for s in states.values() if s.label == STATIC)
|
|
227
|
+
unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
|
|
228
|
+
|
|
229
|
+
for domain, current in unhealthy:
|
|
230
|
+
warnings.append(f"App '{domain}' - {current.detail or current.label.lower()}")
|
|
244
231
|
|
|
245
232
|
total_apps = len(apps)
|
|
233
|
+
served = apps_running + apps_static
|
|
246
234
|
if total_apps > 0:
|
|
247
|
-
|
|
235
|
+
summary = f"{served}/{total_apps} serving"
|
|
236
|
+
if apps_static:
|
|
237
|
+
summary += f" ({apps_static} static)"
|
|
238
|
+
if unhealthy:
|
|
248
239
|
_print_status(
|
|
249
240
|
logger,
|
|
250
241
|
"Applications",
|
|
251
|
-
f"{
|
|
242
|
+
f"{summary}, {len(unhealthy)} need attention",
|
|
252
243
|
"warning",
|
|
253
244
|
)
|
|
254
245
|
else:
|
|
255
|
-
_print_status(logger, "Applications",
|
|
246
|
+
_print_status(logger, "Applications", summary, "ok")
|
|
256
247
|
else:
|
|
257
248
|
_print_status(logger, "Applications", "No applications deployed", "info")
|
|
258
249
|
|
|
@@ -40,7 +40,6 @@ Two structural notes about the Click migration:
|
|
|
40
40
|
from __future__ import annotations
|
|
41
41
|
|
|
42
42
|
import importlib.util
|
|
43
|
-
import ipaddress
|
|
44
43
|
import os
|
|
45
44
|
import signal
|
|
46
45
|
import socket
|
|
@@ -59,6 +58,13 @@ from wasm.cli.app import Context, enable_dry_run, pass_context
|
|
|
59
58
|
from wasm.core.exceptions import SecurityError, WASMError
|
|
60
59
|
from wasm.core.fs import get_fs
|
|
61
60
|
from wasm.core.logger import Logger, set_colors_disabled
|
|
61
|
+
from wasm.core.net import (
|
|
62
|
+
host_addresses,
|
|
63
|
+
is_loopback_host,
|
|
64
|
+
loopback_access_lines,
|
|
65
|
+
normalize_host,
|
|
66
|
+
strip_brackets,
|
|
67
|
+
)
|
|
62
68
|
from wasm.core.runner import get_runner
|
|
63
69
|
|
|
64
70
|
if TYPE_CHECKING:
|
|
@@ -68,23 +74,18 @@ if TYPE_CHECKING:
|
|
|
68
74
|
PID_FILE = Path("/var/run/wasm-web.pid")
|
|
69
75
|
PID_FILE_USER = Path.home() / ".wasm" / "web.pid"
|
|
70
76
|
|
|
71
|
-
#: The address a socket is given when it should answer on every interface. It
|
|
72
|
-
#: is named here to be recognised and refused, never bound by default.
|
|
73
|
-
ALL_INTERFACES = "0.0.0.0" # noqa: S104
|
|
74
|
-
|
|
75
|
-
#: Spellings of "every interface" that no resolver accepts, so they cannot be
|
|
76
|
-
#: classified by looking them up. The empty string is the one that mattered: a
|
|
77
|
-
#: version of this module kept a set of loopback *strings* with "" in it, and
|
|
78
|
-
#: ``wasm web start --host ""`` walked past the refusal below and bound the
|
|
79
|
-
#: root panel to every interface in cleartext.
|
|
80
|
-
WILDCARD_SPELLINGS = frozenset({"", "*"})
|
|
81
|
-
|
|
82
77
|
#: Package installation is slow on a cold cache but must not hang a session.
|
|
83
78
|
INSTALL_TIMEOUT = 900
|
|
84
79
|
|
|
85
80
|
#: Seconds between stopping and starting again, so the port is released.
|
|
86
81
|
RESTART_PAUSE = 1
|
|
87
82
|
|
|
83
|
+
#: How far past a taken port to look for a free one to name. The panel is not
|
|
84
|
+
#: moved automatically - see :func:`_start` - but the port offered instead has
|
|
85
|
+
#: to be one that will actually work, and the search has to finish while the
|
|
86
|
+
#: operator is still reading the error.
|
|
87
|
+
PORT_SUGGESTION_SPAN = 20
|
|
88
|
+
|
|
88
89
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
89
90
|
|
|
90
91
|
|
|
@@ -208,110 +209,6 @@ def _exit(code: int) -> NoReturn:
|
|
|
208
209
|
# How the panel is exposed
|
|
209
210
|
# ---------------------------------------------------------------------------
|
|
210
211
|
|
|
211
|
-
#: Address family alias, for the helpers below.
|
|
212
|
-
IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def _strip_brackets(host: str) -> str:
|
|
216
|
-
"""
|
|
217
|
-
Remove the brackets an IPv6 literal is often written with.
|
|
218
|
-
|
|
219
|
-
Args:
|
|
220
|
-
host: The spelling the operator typed.
|
|
221
|
-
|
|
222
|
-
Returns:
|
|
223
|
-
The spelling without a surrounding ``[]`` pair.
|
|
224
|
-
"""
|
|
225
|
-
candidate = host.strip()
|
|
226
|
-
if len(candidate) > 1 and candidate.startswith("[") and candidate.endswith("]"):
|
|
227
|
-
return candidate[1:-1]
|
|
228
|
-
return candidate
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _host_addresses(host: str) -> tuple[IPAddress, ...]:
|
|
232
|
-
"""
|
|
233
|
-
Return every address a host spelling would end up bound to.
|
|
234
|
-
|
|
235
|
-
Comparing host strings is what made this decision wrong: "" , "*", "0",
|
|
236
|
-
"0.0.0.0", "::" and a name in ``/etc/hosts`` pointing at 0.0.0.0 are six
|
|
237
|
-
spellings of the same socket, and a set of known-good strings misses five
|
|
238
|
-
of them. The resolver is asked instead, and its answer is classified with
|
|
239
|
-
:mod:`ipaddress`.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
host: The spelling the operator typed.
|
|
243
|
-
|
|
244
|
-
Returns:
|
|
245
|
-
The addresses, empty when the spelling cannot be resolved. Empty means
|
|
246
|
-
"unknown", and every caller treats unknown as exposed.
|
|
247
|
-
"""
|
|
248
|
-
candidate = _strip_brackets(host)
|
|
249
|
-
if candidate in WILDCARD_SPELLINGS:
|
|
250
|
-
return (ipaddress.ip_address(ALL_INTERFACES),)
|
|
251
|
-
|
|
252
|
-
try:
|
|
253
|
-
return (ipaddress.ip_address(candidate),)
|
|
254
|
-
except ValueError:
|
|
255
|
-
pass
|
|
256
|
-
|
|
257
|
-
try:
|
|
258
|
-
# A resolution covers names and also the inet_aton spellings ipaddress
|
|
259
|
-
# refuses but a socket accepts, such as "0" (INADDR_ANY) and "127.1".
|
|
260
|
-
infos = socket.getaddrinfo(candidate, None, type=socket.SOCK_STREAM)
|
|
261
|
-
except (OSError, UnicodeError, ValueError):
|
|
262
|
-
return ()
|
|
263
|
-
|
|
264
|
-
addresses = []
|
|
265
|
-
for info in infos:
|
|
266
|
-
# A link-local sockaddr carries a %scope suffix that ipaddress refuses.
|
|
267
|
-
text = str(info[4][0]).split("%", 1)[0]
|
|
268
|
-
try:
|
|
269
|
-
addresses.append(ipaddress.ip_address(text))
|
|
270
|
-
except ValueError:
|
|
271
|
-
return ()
|
|
272
|
-
return tuple(addresses)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
def _is_loopback_host(host: str) -> bool:
|
|
276
|
-
"""
|
|
277
|
-
Report whether only this machine could reach a panel bound to a host.
|
|
278
|
-
|
|
279
|
-
Args:
|
|
280
|
-
host: The spelling the operator typed.
|
|
281
|
-
|
|
282
|
-
Returns:
|
|
283
|
-
True only when the spelling is known to resolve to loopback addresses
|
|
284
|
-
and nothing else. A spelling that cannot be resolved is not loopback,
|
|
285
|
-
because guessing in the other direction publishes a root shell.
|
|
286
|
-
"""
|
|
287
|
-
addresses = _host_addresses(host)
|
|
288
|
-
return bool(addresses) and all(address.is_loopback for address in addresses)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def _normalize_host(host: str) -> str:
|
|
292
|
-
"""
|
|
293
|
-
Canonicalise a host spelling so what is checked is what is reported.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
host: The spelling the operator typed.
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
The canonical spelling. Every way of writing "every interface" becomes
|
|
300
|
-
the address it binds, so the refusal below names a real address instead
|
|
301
|
-
of quoting an empty string back at the operator.
|
|
302
|
-
"""
|
|
303
|
-
candidate = _strip_brackets(host)
|
|
304
|
-
addresses = _host_addresses(candidate)
|
|
305
|
-
if not addresses:
|
|
306
|
-
return candidate
|
|
307
|
-
if candidate in WILDCARD_SPELLINGS or all(address.is_unspecified for address in addresses):
|
|
308
|
-
return str(addresses[0])
|
|
309
|
-
try:
|
|
310
|
-
return str(ipaddress.ip_address(candidate))
|
|
311
|
-
except ValueError:
|
|
312
|
-
# A name: binding it is the resolver's business, not this module's.
|
|
313
|
-
return candidate
|
|
314
|
-
|
|
315
212
|
|
|
316
213
|
@dataclass(frozen=True)
|
|
317
214
|
class StartOptions:
|
|
@@ -422,11 +319,11 @@ def _build_security_config(options: StartOptions) -> SecurityConfig:
|
|
|
422
319
|
|
|
423
320
|
# Normalised first, so the exposure decision, the message and the address
|
|
424
321
|
# that is finally bound all talk about the same thing.
|
|
425
|
-
host =
|
|
322
|
+
host = normalize_host(options.host)
|
|
426
323
|
port = options.port
|
|
427
324
|
whitelist = list(options.allow_ip)
|
|
428
325
|
proxies = list(options.trusted_proxy)
|
|
429
|
-
local_only =
|
|
326
|
+
local_only = is_loopback_host(host)
|
|
430
327
|
|
|
431
328
|
if options.require_https and not (options.tls_cert and options.tls_key):
|
|
432
329
|
raise SecurityError(
|
|
@@ -443,7 +340,7 @@ def _build_security_config(options: StartOptions) -> SecurityConfig:
|
|
|
443
340
|
unresolved = (
|
|
444
341
|
f"WASM could not resolve {host!r}, so it cannot show that only this machine "
|
|
445
342
|
"would reach the panel, and treats it as exposed.\n"
|
|
446
|
-
if not
|
|
343
|
+
if not host_addresses(host)
|
|
447
344
|
else ""
|
|
448
345
|
)
|
|
449
346
|
raise SecurityError(
|
|
@@ -708,6 +605,116 @@ def _prompt_install(missing_apt: list[str], missing_pip: list[str], verbose: boo
|
|
|
708
605
|
# ---------------------------------------------------------------------------
|
|
709
606
|
|
|
710
607
|
|
|
608
|
+
def _port_in_use(host: str, port: int) -> bool:
|
|
609
|
+
"""
|
|
610
|
+
Ask whether the panel's address is already taken.
|
|
611
|
+
|
|
612
|
+
Binding is the only reliable answer: a connection attempt says nothing
|
|
613
|
+
about a socket bound with no backlog, and reading ``ss`` output means
|
|
614
|
+
parsing a different format on every distribution.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
host: Address the panel would bind.
|
|
618
|
+
port: Port the panel would bind.
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
True when binding would fail because something holds the address.
|
|
622
|
+
"""
|
|
623
|
+
family = socket.AF_INET6 if ":" in strip_brackets(host) else socket.AF_INET
|
|
624
|
+
probe = socket.socket(family, socket.SOCK_STREAM)
|
|
625
|
+
try:
|
|
626
|
+
# Deliberately not SO_REUSEADDR: with it the bind succeeds against a
|
|
627
|
+
# socket in TIME_WAIT, which is the case this is meant to catch.
|
|
628
|
+
probe.bind((strip_brackets(host), port))
|
|
629
|
+
except OSError:
|
|
630
|
+
return True
|
|
631
|
+
except (ValueError, socket.gaierror):
|
|
632
|
+
# An address this process cannot even parse is not this check's problem;
|
|
633
|
+
# the bind that follows will report it properly.
|
|
634
|
+
return False
|
|
635
|
+
finally:
|
|
636
|
+
probe.close()
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _first_free_port(host: str, port: int) -> int | None:
|
|
641
|
+
"""
|
|
642
|
+
Find a port to offer in place of one that is taken.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
host: Address the panel would bind.
|
|
646
|
+
port: The port that turned out to be taken.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
The first free port above it, or None when the whole search span is
|
|
650
|
+
held. Naming a port without checking it is what made the old suggestion
|
|
651
|
+
useless: it was the requested port plus one, and on a machine busy
|
|
652
|
+
enough to hit this at all that is frequently taken too.
|
|
653
|
+
"""
|
|
654
|
+
for candidate in range(port + 1, min(port + 1 + PORT_SUGGESTION_SPAN, 65536)):
|
|
655
|
+
if not _port_in_use(host, candidate):
|
|
656
|
+
return candidate
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _report_taken_port(host: str, port: int, logger: Logger) -> None:
|
|
661
|
+
"""
|
|
662
|
+
Explain a taken port, including why the panel does not move itself.
|
|
663
|
+
|
|
664
|
+
Moving to the next free port would be the obliging thing to do and it is
|
|
665
|
+
the wrong thing to do. The port is an address other things point at - an
|
|
666
|
+
SSH tunnel, a reverse proxy, a bookmark, a script - so a panel that lands
|
|
667
|
+
somewhere new whenever the old place is busy cannot be automated. It would
|
|
668
|
+
also paper over the case this check exists to catch: a second panel, still
|
|
669
|
+
running, still holding a root session.
|
|
670
|
+
|
|
671
|
+
None of that is obvious from the outside, so it is said rather than
|
|
672
|
+
implied.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
host: Address the panel would have bound.
|
|
676
|
+
port: The port something else holds.
|
|
677
|
+
logger: Logger for the report.
|
|
678
|
+
"""
|
|
679
|
+
logger.error(f"Something is already listening on {host}:{port}")
|
|
680
|
+
logger.info("WASM does not move the panel to another port on its own:")
|
|
681
|
+
logger.info(" the port is what your SSH tunnel, proxy and bookmarks point at,")
|
|
682
|
+
logger.info(" and moving it would hide a panel that is still running here.")
|
|
683
|
+
logger.info("If it is a panel this machine has forgotten about:")
|
|
684
|
+
logger.info(" wasm web stop")
|
|
685
|
+
logger.info("To see what holds the port:")
|
|
686
|
+
logger.info(f" ss -ltnp 'sport = :{port}'")
|
|
687
|
+
|
|
688
|
+
free = _first_free_port(host, port)
|
|
689
|
+
if free is None:
|
|
690
|
+
logger.info(
|
|
691
|
+
f"Nothing up to {port + PORT_SUGGESTION_SPAN} is free either. "
|
|
692
|
+
"Choose a port with --port."
|
|
693
|
+
)
|
|
694
|
+
return
|
|
695
|
+
logger.info(f"Or serve on {free}, which is free right now:")
|
|
696
|
+
logger.info(f" wasm web start --port {free}")
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _report_daemon_started(config: SecurityConfig, pid: int, logger: Logger) -> None:
|
|
700
|
+
"""
|
|
701
|
+
Report a backgrounded panel, and how to reach it.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
config: The configuration it was started with.
|
|
705
|
+
pid: Process id of the panel.
|
|
706
|
+
logger: Logger for the report.
|
|
707
|
+
"""
|
|
708
|
+
scheme = "https" if config.require_https else "http"
|
|
709
|
+
|
|
710
|
+
logger.success(f"Web server started in background (PID: {pid})")
|
|
711
|
+
logger.info(f"Server running at {scheme}://{config.host}:{config.port}")
|
|
712
|
+
for line in loopback_access_lines(config.host, config.port, scheme=scheme):
|
|
713
|
+
logger.info(line)
|
|
714
|
+
logger.info("Use 'wasm web status' to check status")
|
|
715
|
+
logger.info("Use 'wasm web stop' to stop the server")
|
|
716
|
+
|
|
717
|
+
|
|
711
718
|
def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> int:
|
|
712
719
|
"""
|
|
713
720
|
Start the panel, refusing an unsafe exposure first.
|
|
@@ -773,11 +780,20 @@ def _start(options: StartOptions, verbose: bool, *, dry_run: bool = False) -> in
|
|
|
773
780
|
# Process not running, remove stale PID file
|
|
774
781
|
get_fs().remove(pid_file, missing_ok=True)
|
|
775
782
|
|
|
783
|
+
# The PID file only catches a panel this machine still has a record of. A
|
|
784
|
+
# panel whose PID file was removed, or anything else on the port, walked
|
|
785
|
+
# past that check: the token was printed and then uvicorn failed with a
|
|
786
|
+
# bare OSError, leaving an operator holding a credential for a server that
|
|
787
|
+
# never started.
|
|
788
|
+
if not dry_run and _port_in_use(host, config.port):
|
|
789
|
+
_report_taken_port(host, config.port, logger)
|
|
790
|
+
return 1
|
|
791
|
+
|
|
776
792
|
if config.ip_whitelist:
|
|
777
793
|
logger.info(f"Only these clients may connect: {', '.join(config.ip_whitelist)}")
|
|
778
794
|
if config.trusted_proxies:
|
|
779
795
|
logger.info(f"Forwarding headers believed from: {', '.join(config.trusted_proxies)}")
|
|
780
|
-
elif
|
|
796
|
+
elif is_loopback_host(host) and not config.require_https:
|
|
781
797
|
logger.warning(
|
|
782
798
|
"Serving plain HTTP on loopback. Behind a TLS proxy, pass --trusted-proxy "
|
|
783
799
|
"so the session cookie is issued with the Secure flag."
|
|
@@ -835,17 +851,13 @@ def _start_daemon(config: SecurityConfig, verbose: bool) -> int:
|
|
|
835
851
|
Exit code.
|
|
836
852
|
"""
|
|
837
853
|
logger = Logger(verbose=verbose)
|
|
838
|
-
scheme = "https" if config.require_https else "http"
|
|
839
854
|
|
|
840
855
|
# Fork process
|
|
841
856
|
pid = os.fork()
|
|
842
857
|
|
|
843
858
|
if pid > 0:
|
|
844
859
|
# Parent process
|
|
845
|
-
|
|
846
|
-
logger.info(f"Server running at {scheme}://{config.host}:{config.port}")
|
|
847
|
-
logger.info("Use 'wasm web status' to check status")
|
|
848
|
-
logger.info("Use 'wasm web stop' to stop the server")
|
|
860
|
+
_report_daemon_started(config, pid, logger)
|
|
849
861
|
return 0
|
|
850
862
|
|
|
851
863
|
# Child process
|
|
@@ -33,19 +33,21 @@ from typing import Any, TypeVar
|
|
|
33
33
|
import click
|
|
34
34
|
|
|
35
35
|
from wasm.cli.app import Context, enable_dry_run, pass_context
|
|
36
|
+
from wasm.core.app_state import RUNNING, STATIC, resolve_states
|
|
36
37
|
from wasm.core.config import Config
|
|
37
38
|
from wasm.core.dependencies import check_deployment_ready
|
|
38
39
|
from wasm.core.exceptions import DeploymentError, ServiceError, WASMError
|
|
39
|
-
from wasm.core.logger import Logger, set_colors_disabled
|
|
40
|
+
from wasm.core.logger import Logger, set_colors_disabled, state, styled
|
|
40
41
|
from wasm.core.runner import (
|
|
41
42
|
CommandResult,
|
|
42
43
|
get_runner,
|
|
43
44
|
)
|
|
44
|
-
from wasm.core.store import
|
|
45
|
+
from wasm.core.store import get_store
|
|
45
46
|
from wasm.core.utils import domain_to_app_name, remove_directory
|
|
46
47
|
from wasm.deployers import detect_app_type, get_deployer
|
|
47
48
|
from wasm.deployers.docker_compose import DockerComposeDeployer
|
|
48
49
|
from wasm.deployers.monorepo import MonorepoDeployer
|
|
50
|
+
from wasm.deployers.registry import available_types
|
|
49
51
|
from wasm.managers.apache_manager import ApacheManager
|
|
50
52
|
from wasm.managers.backup_manager import RollbackManager
|
|
51
53
|
from wasm.managers.cert_manager import CertManager
|
|
@@ -67,17 +69,10 @@ _FOLLOW_TIMEOUT = 86400
|
|
|
67
69
|
#: Docker Compose pulls images and rebuilds; it needs room.
|
|
68
70
|
_COMPOSE_TIMEOUT = 1800
|
|
69
71
|
|
|
70
|
-
#: Application types ``create`` accepts, in the order they are offered.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"vite",
|
|
75
|
-
"python",
|
|
76
|
-
"static",
|
|
77
|
-
"monorepo",
|
|
78
|
-
"docker-compose",
|
|
79
|
-
"auto",
|
|
80
|
-
]
|
|
72
|
+
#: Application types ``create`` accepts, in the order they are offered. Read
|
|
73
|
+
#: from the registry rather than typed out here: a hand-written copy is a copy
|
|
74
|
+
#: that goes stale the first time a deployer is added, and it did.
|
|
75
|
+
APP_TYPES = [entry["type"] for entry in available_types()]
|
|
81
76
|
|
|
82
77
|
#: Node package managers ``create`` and ``update`` accept.
|
|
83
78
|
PACKAGE_MANAGERS = ["npm", "pnpm", "bun", "auto"]
|
|
@@ -464,30 +459,44 @@ def _list_apps(logger: Logger) -> int:
|
|
|
464
459
|
logger.info(" wasm deploy -d example.com -s https://github.com/user/repo")
|
|
465
460
|
return 0
|
|
466
461
|
|
|
462
|
+
# Asked of systemd and of the port, not read from the status column. That
|
|
463
|
+
# column is written at deploy time and never again, so it called everything
|
|
464
|
+
# Running while health, which did ask, reported half of them stopped.
|
|
465
|
+
states = resolve_states(apps, ServiceManager(verbose=logger.verbose))
|
|
466
|
+
|
|
467
467
|
headers = ["Domain", "Type", "Status", "Port", "SSL"]
|
|
468
468
|
rows = []
|
|
469
469
|
|
|
470
|
-
labels = {
|
|
471
|
-
AppStatus.RUNNING.value: "Running",
|
|
472
|
-
AppStatus.STOPPED.value: "Stopped",
|
|
473
|
-
AppStatus.DEPLOYING.value: "Deploying",
|
|
474
|
-
AppStatus.FAILED.value: "Failed",
|
|
475
|
-
}
|
|
476
|
-
|
|
477
470
|
for app in apps:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
471
|
+
current = states[app.domain]
|
|
472
|
+
rows.append(
|
|
473
|
+
[
|
|
474
|
+
styled(app.domain, "bold"),
|
|
475
|
+
app.app_type,
|
|
476
|
+
state(current.label),
|
|
477
|
+
styled(app.port, "") if app.port else state("static"),
|
|
478
|
+
state("yes") if app.ssl_enabled else state("no"),
|
|
479
|
+
]
|
|
480
|
+
)
|
|
481
481
|
|
|
482
|
-
|
|
482
|
+
logger.table(headers, rows, justify=["left", "left", "left", "right", "left"])
|
|
483
483
|
|
|
484
|
-
|
|
484
|
+
running = sum(1 for s in states.values() if s.label == RUNNING)
|
|
485
|
+
static = sum(1 for s in states.values() if s.label == STATIC)
|
|
486
|
+
unhealthy = [(domain, s) for domain, s in states.items() if not s.healthy]
|
|
485
487
|
|
|
486
488
|
logger.blank()
|
|
487
|
-
running = sum(1 for a in apps if a.status == AppStatus.RUNNING.value)
|
|
488
|
-
static = sum(1 for a in apps if a.is_static)
|
|
489
489
|
logger.info(f"Total: {len(apps)} apps ({running} running, {static} static)")
|
|
490
490
|
|
|
491
|
+
# The reason belongs next to the list. Making the operator run a second
|
|
492
|
+
# command to find out why something says Stopped is how the contradiction
|
|
493
|
+
# between these two commands went unnoticed for as long as it did.
|
|
494
|
+
if unhealthy:
|
|
495
|
+
logger.blank()
|
|
496
|
+
logger.warning(f"{len(unhealthy)} need attention:")
|
|
497
|
+
for domain, current in unhealthy:
|
|
498
|
+
logger.list_item(f"{domain} - {current.detail or current.label.lower()}")
|
|
499
|
+
|
|
491
500
|
return 0
|
|
492
501
|
|
|
493
502
|
|
|
@@ -34,9 +34,11 @@ class InteractiveMode:
|
|
|
34
34
|
raise WASMError(
|
|
35
35
|
"Interactive mode needs questionary, which is missing",
|
|
36
36
|
details=(
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
37
|
+
"Every other command works without it. To get it:\n"
|
|
38
|
+
" apt install python3-questionary Debian 13, Ubuntu 24.04+\n"
|
|
39
|
+
" dnf install python3-questionary Fedora 42+\n"
|
|
40
|
+
" pip install questionary anywhere else\n"
|
|
41
|
+
"It is not packaged for Debian 12, Ubuntu 22.04 or Leap 15."
|
|
40
42
|
),
|
|
41
43
|
)
|
|
42
44
|
|