wasm-cli 0.14.4__tar.gz → 0.15.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-0.14.4/src/wasm_cli.egg-info → wasm_cli-0.15.0}/PKG-INFO +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/pyproject.toml +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/setup.py +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/__init__.py +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/backup.py +115 -2
- wasm_cli-0.15.0/src/wasm/cli/commands/env.py +220 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/webapp.py +120 -4
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/parser.py +158 -3
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/dependencies.py +36 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/exceptions.py +10 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/store.py +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/__init__.py +2 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/base.py +104 -4
- wasm_cli-0.15.0/src/wasm/deployers/docker_compose.py +818 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/__init__.py +4 -0
- wasm_cli-0.15.0/src/wasm/deployers/helpers/env_manager.py +525 -0
- wasm_cli-0.15.0/src/wasm/deployers/helpers/nginx_config.py +289 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/monorepo.py +20 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/registry.py +2 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/main.py +8 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/backup_manager.py +175 -8
- wasm_cli-0.15.0/src/wasm/managers/backup_scheduler.py +266 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/postgres.py +152 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/redis.py +88 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/nginx_manager.py +26 -1
- wasm_cli-0.15.0/src/wasm/templates/nginx/advanced.conf.j2 +117 -0
- wasm_cli-0.15.0/src/wasm/templates/systemd/backup-service.j2 +9 -0
- wasm_cli-0.15.0/src/wasm/templates/systemd/backup-timer.j2 +13 -0
- wasm_cli-0.15.0/src/wasm/templates/systemd/docker-compose.service.j2 +39 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/css/main.css +28 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/index.html +6 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/api.js +2 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/router.js +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/ui.js +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/main.js +2 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/apps.js +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/databases.js +1 -10
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/jobs.js +19 -4
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/logs.js +11 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/monitor.js +12 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.0/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm_cli.egg-info/SOURCES.txt +9 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/LICENSE +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/MANIFEST.in +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/README.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/docs/MONITOR.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/docs/assets/logo.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/man/wasm.1 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/setup.cfg +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/__main__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/cert.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/db.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/health.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/commands/web.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/cli/interactive.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/completions/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/logger.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/core/utils.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/turbo.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/helpers/workspace.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/python.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/base_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/cert_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/service_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/monitor/ai_analyzer.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/monitor/process_monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/monitor/threat_store.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/validators/port.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/validators/source.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/apps.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/auth.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/router.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/services.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/sites.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/auth.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/jobs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/server.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/components/cards.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/components/jobs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/components/metrics.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/components/skeleton.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/dialogs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/notifications.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/search.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/shortcuts.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/theme.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/core/websocket.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/backups.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/certs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/config.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/dashboard.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/services.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/js/pages/sites.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/login.html +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/static/logo.webp +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm/web/websockets/router.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.0}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.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 = "0.
|
|
7
|
+
version = "0.15.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="0.
|
|
12
|
+
version="0.15.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",
|
|
@@ -29,6 +29,8 @@ def handle_backup(args: Namespace) -> int:
|
|
|
29
29
|
return _backup_info(args, verbose)
|
|
30
30
|
elif action == "storage":
|
|
31
31
|
return _backup_storage(args, verbose)
|
|
32
|
+
elif action == "schedule":
|
|
33
|
+
return _backup_schedule(args, verbose)
|
|
32
34
|
else:
|
|
33
35
|
logger = Logger(verbose=verbose)
|
|
34
36
|
logger.error(f"Unknown backup action: {action}")
|
|
@@ -480,9 +482,120 @@ def _backup_storage(args: Namespace, verbose: bool) -> int:
|
|
|
480
482
|
app_str = f"{app_bytes:.1f} TB"
|
|
481
483
|
|
|
482
484
|
logger.info(f" {app_name}: {app_str} ({app_usage['count']} backups)")
|
|
483
|
-
|
|
485
|
+
|
|
484
486
|
return 0
|
|
485
|
-
|
|
487
|
+
|
|
486
488
|
except Exception as e:
|
|
487
489
|
logger.error(f"Error: {e}")
|
|
488
490
|
return 1
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _backup_schedule(args: Namespace, verbose: bool) -> int:
|
|
494
|
+
"""Handle backup schedule subcommands."""
|
|
495
|
+
logger = Logger(verbose=verbose)
|
|
496
|
+
schedule_action = getattr(args, "schedule_action", None)
|
|
497
|
+
|
|
498
|
+
if not schedule_action:
|
|
499
|
+
logger.error("Schedule requires an action: create, list, or delete")
|
|
500
|
+
return 1
|
|
501
|
+
|
|
502
|
+
if schedule_action == "create":
|
|
503
|
+
return _backup_schedule_create(args, verbose)
|
|
504
|
+
elif schedule_action in ("list", "ls"):
|
|
505
|
+
return _backup_schedule_list(args, verbose)
|
|
506
|
+
elif schedule_action in ("delete", "remove", "rm"):
|
|
507
|
+
return _backup_schedule_delete(args, verbose)
|
|
508
|
+
else:
|
|
509
|
+
logger.error(f"Unknown schedule action: {schedule_action}")
|
|
510
|
+
return 1
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _backup_schedule_create(args: Namespace, verbose: bool) -> int:
|
|
514
|
+
"""Create a backup schedule."""
|
|
515
|
+
logger = Logger(verbose=verbose)
|
|
516
|
+
|
|
517
|
+
domain = getattr(args, "domain", None)
|
|
518
|
+
schedule = getattr(args, "schedule", "daily")
|
|
519
|
+
retention_count = getattr(args, "retention_count", 7)
|
|
520
|
+
retention_days = getattr(args, "retention_days", 30)
|
|
521
|
+
|
|
522
|
+
if not domain:
|
|
523
|
+
logger.error("Domain is required")
|
|
524
|
+
return 1
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
from wasm.managers.backup_scheduler import BackupScheduler, BackupSchedule
|
|
528
|
+
from wasm.core.utils import domain_to_app_name
|
|
529
|
+
|
|
530
|
+
scheduler = BackupScheduler(verbose=verbose)
|
|
531
|
+
backup_schedule = BackupSchedule(
|
|
532
|
+
domain=domain,
|
|
533
|
+
app_name=domain_to_app_name(domain),
|
|
534
|
+
schedule=schedule,
|
|
535
|
+
retention_count=retention_count,
|
|
536
|
+
retention_days=retention_days,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
scheduler.create_schedule(backup_schedule)
|
|
540
|
+
logger.success(f"Backup schedule created for {domain}")
|
|
541
|
+
logger.info(f" Schedule: {backup_schedule.on_calendar}")
|
|
542
|
+
logger.info(f" Retention: {retention_count} backups / {retention_days} days")
|
|
543
|
+
return 0
|
|
544
|
+
|
|
545
|
+
except Exception as e:
|
|
546
|
+
logger.error(f"Failed to create schedule: {e}")
|
|
547
|
+
if verbose:
|
|
548
|
+
import traceback
|
|
549
|
+
traceback.print_exc()
|
|
550
|
+
return 1
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _backup_schedule_list(args: Namespace, verbose: bool) -> int:
|
|
554
|
+
"""List backup schedules."""
|
|
555
|
+
logger = Logger(verbose=verbose)
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
from wasm.managers.backup_scheduler import BackupScheduler
|
|
559
|
+
|
|
560
|
+
scheduler = BackupScheduler(verbose=verbose)
|
|
561
|
+
schedules = scheduler.list_schedules()
|
|
562
|
+
|
|
563
|
+
if not schedules:
|
|
564
|
+
logger.info("No backup schedules found")
|
|
565
|
+
return 0
|
|
566
|
+
|
|
567
|
+
logger.header("Backup Schedules")
|
|
568
|
+
for sched in schedules:
|
|
569
|
+
logger.info(
|
|
570
|
+
f" {sched['app_name']}: "
|
|
571
|
+
f"next={sched.get('next_run', '?')} "
|
|
572
|
+
f"last={sched.get('last_run', 'never')}"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return 0
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Failed to list schedules: {e}")
|
|
579
|
+
return 1
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _backup_schedule_delete(args: Namespace, verbose: bool) -> int:
|
|
583
|
+
"""Delete a backup schedule."""
|
|
584
|
+
logger = Logger(verbose=verbose)
|
|
585
|
+
|
|
586
|
+
domain = getattr(args, "domain", None)
|
|
587
|
+
if not domain:
|
|
588
|
+
logger.error("Domain is required")
|
|
589
|
+
return 1
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
from wasm.managers.backup_scheduler import BackupScheduler
|
|
593
|
+
|
|
594
|
+
scheduler = BackupScheduler(verbose=verbose)
|
|
595
|
+
scheduler.remove_schedule(domain)
|
|
596
|
+
logger.success(f"Backup schedule removed for {domain}")
|
|
597
|
+
return 0
|
|
598
|
+
|
|
599
|
+
except Exception as e:
|
|
600
|
+
logger.error(f"Failed to remove schedule: {e}")
|
|
601
|
+
return 1
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025 Yago López Prado
|
|
2
|
+
# Licensed under WASM-NCSAL 1.0 (Commercial use prohibited)
|
|
3
|
+
# https://github.com/Perkybeet/wasm/blob/main/LICENSE
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Environment variable CLI commands for WASM.
|
|
7
|
+
|
|
8
|
+
Provides CLI handlers for managing application environment variables:
|
|
9
|
+
- Interactive configuration from .env.example templates
|
|
10
|
+
- Viewing current values with secret masking
|
|
11
|
+
- Exporting variables to files
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from argparse import Namespace
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from wasm.core.config import Config
|
|
19
|
+
from wasm.core.logger import Logger
|
|
20
|
+
from wasm.core.exceptions import WASMError
|
|
21
|
+
from wasm.core.utils import domain_to_app_name
|
|
22
|
+
from wasm.deployers.helpers.env_manager import EnvManager, EnvConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def handle_env(args: Namespace) -> int:
|
|
26
|
+
"""
|
|
27
|
+
Handle wasm env <action> commands.
|
|
28
|
+
|
|
29
|
+
Routes to the appropriate sub-handler based on the action.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
args: Parsed command-line arguments.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Exit code (0 for success, non-zero for error).
|
|
36
|
+
"""
|
|
37
|
+
action = getattr(args, "action", None)
|
|
38
|
+
verbose = getattr(args, "verbose", False)
|
|
39
|
+
|
|
40
|
+
if not action:
|
|
41
|
+
print("Error: env requires an action", file=sys.stderr)
|
|
42
|
+
print("Use: wasm env --help", file=sys.stderr)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
handlers = {
|
|
46
|
+
"configure": _env_configure,
|
|
47
|
+
"config": _env_configure,
|
|
48
|
+
"setup": _env_configure,
|
|
49
|
+
"show": _env_show,
|
|
50
|
+
"list": _env_show,
|
|
51
|
+
"ls": _env_show,
|
|
52
|
+
"export": _env_export,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
handler = handlers.get(action)
|
|
56
|
+
if not handler:
|
|
57
|
+
print(f"Unknown env action: {action}", file=sys.stderr)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
return handler(args, verbose)
|
|
62
|
+
except WASMError as e:
|
|
63
|
+
logger = Logger(verbose=verbose)
|
|
64
|
+
logger.error(e.message)
|
|
65
|
+
if e.details:
|
|
66
|
+
print(f" {e.details}")
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _env_configure(args: Namespace, verbose: bool) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Run interactive environment configuration.
|
|
73
|
+
|
|
74
|
+
Discovers variables from .env.example files, prompts the user
|
|
75
|
+
for values, auto-generates secrets, and writes .env files.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
args: Parsed arguments (requires domain).
|
|
79
|
+
verbose: Whether to enable verbose output.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Exit code.
|
|
83
|
+
"""
|
|
84
|
+
logger = Logger(verbose=verbose)
|
|
85
|
+
config = Config()
|
|
86
|
+
|
|
87
|
+
domain = getattr(args, "domain", None)
|
|
88
|
+
if not domain:
|
|
89
|
+
logger.error("Domain is required")
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
app_name = domain_to_app_name(domain)
|
|
93
|
+
app_path = config.apps_directory / app_name
|
|
94
|
+
|
|
95
|
+
if not app_path.exists():
|
|
96
|
+
logger.error(f"Application not found: {domain}")
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
manager = EnvManager(verbose=verbose)
|
|
100
|
+
|
|
101
|
+
logger.header(f"Environment Configuration: {domain}")
|
|
102
|
+
|
|
103
|
+
# Discover variables
|
|
104
|
+
variables = manager.discover(app_path)
|
|
105
|
+
if not variables:
|
|
106
|
+
logger.info("No .env.example files found")
|
|
107
|
+
return 0
|
|
108
|
+
|
|
109
|
+
logger.info(f"Found {len(variables)} variables")
|
|
110
|
+
|
|
111
|
+
# Load existing values
|
|
112
|
+
existing = manager.get_current_values(app_path)
|
|
113
|
+
|
|
114
|
+
# Prompt for values
|
|
115
|
+
values = manager.prompt_variables(variables, existing)
|
|
116
|
+
|
|
117
|
+
# Write .env file
|
|
118
|
+
written = manager.write_env_files(app_path, values)
|
|
119
|
+
for path in written:
|
|
120
|
+
logger.success(f"Written: {path}")
|
|
121
|
+
|
|
122
|
+
# Save config
|
|
123
|
+
env_config = EnvConfig(variables=variables)
|
|
124
|
+
manager.save_config(app_path, env_config)
|
|
125
|
+
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _env_show(args: Namespace, verbose: bool) -> int:
|
|
130
|
+
"""
|
|
131
|
+
Display current environment variables.
|
|
132
|
+
|
|
133
|
+
Shows all variables from the application's .env file, with
|
|
134
|
+
secret values masked by default.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
args: Parsed arguments (requires domain, optional --unmask).
|
|
138
|
+
verbose: Whether to enable verbose output.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Exit code.
|
|
142
|
+
"""
|
|
143
|
+
logger = Logger(verbose=verbose)
|
|
144
|
+
config = Config()
|
|
145
|
+
|
|
146
|
+
domain = getattr(args, "domain", None)
|
|
147
|
+
if not domain:
|
|
148
|
+
logger.error("Domain is required")
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
unmask = getattr(args, "unmask", False)
|
|
152
|
+
|
|
153
|
+
app_name = domain_to_app_name(domain)
|
|
154
|
+
app_path = config.apps_directory / app_name
|
|
155
|
+
|
|
156
|
+
if not app_path.exists():
|
|
157
|
+
logger.error(f"Application not found: {domain}")
|
|
158
|
+
return 1
|
|
159
|
+
|
|
160
|
+
manager = EnvManager(verbose=verbose)
|
|
161
|
+
values = manager.get_current_values(app_path)
|
|
162
|
+
|
|
163
|
+
if not values:
|
|
164
|
+
logger.info(f"No environment variables found for {domain}")
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
logger.header(f"Environment: {domain}")
|
|
168
|
+
|
|
169
|
+
for key in sorted(values.keys()):
|
|
170
|
+
value = values[key]
|
|
171
|
+
if not unmask:
|
|
172
|
+
value = manager.mask_value(key, value)
|
|
173
|
+
logger.key_value(f" {key}", value)
|
|
174
|
+
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _env_export(args: Namespace, verbose: bool) -> int:
|
|
179
|
+
"""
|
|
180
|
+
Export environment variables to a file.
|
|
181
|
+
|
|
182
|
+
Reads the application's .env file and writes it to the
|
|
183
|
+
specified output path.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
args: Parsed arguments (requires domain, optional --output).
|
|
187
|
+
verbose: Whether to enable verbose output.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Exit code.
|
|
191
|
+
"""
|
|
192
|
+
logger = Logger(verbose=verbose)
|
|
193
|
+
config = Config()
|
|
194
|
+
|
|
195
|
+
domain = getattr(args, "domain", None)
|
|
196
|
+
output = getattr(args, "output", ".env")
|
|
197
|
+
|
|
198
|
+
if not domain:
|
|
199
|
+
logger.error("Domain is required")
|
|
200
|
+
return 1
|
|
201
|
+
|
|
202
|
+
app_name = domain_to_app_name(domain)
|
|
203
|
+
app_path = config.apps_directory / app_name
|
|
204
|
+
|
|
205
|
+
if not app_path.exists():
|
|
206
|
+
logger.error(f"Application not found: {domain}")
|
|
207
|
+
return 1
|
|
208
|
+
|
|
209
|
+
manager = EnvManager(verbose=verbose)
|
|
210
|
+
values = manager.get_current_values(app_path)
|
|
211
|
+
|
|
212
|
+
if not values:
|
|
213
|
+
logger.info(f"No environment variables found for {domain}")
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
output_path = Path(output)
|
|
217
|
+
manager._write_single_env_file(output_path, values)
|
|
218
|
+
logger.success(f"Exported {len(values)} variables to {output_path}")
|
|
219
|
+
|
|
220
|
+
return 0
|
|
@@ -207,6 +207,10 @@ def _handle_create(args: Namespace) -> int:
|
|
|
207
207
|
if app_type == "monorepo":
|
|
208
208
|
return _handle_monorepo_create(args, domain, env_vars, logger)
|
|
209
209
|
|
|
210
|
+
# Handle docker-compose deployments specially
|
|
211
|
+
if app_type == "docker-compose":
|
|
212
|
+
return _handle_docker_compose_create(args, domain, env_vars, logger)
|
|
213
|
+
|
|
210
214
|
# Get deployer
|
|
211
215
|
deployer = get_deployer(app_type, verbose=args.verbose)
|
|
212
216
|
|
|
@@ -269,6 +273,31 @@ def _handle_monorepo_create(args: Namespace, domain: str, env_vars: Dict, logger
|
|
|
269
273
|
return 0
|
|
270
274
|
|
|
271
275
|
|
|
276
|
+
def _handle_docker_compose_create(
|
|
277
|
+
args: Namespace, domain: str, env_vars: Dict, logger: Logger
|
|
278
|
+
) -> int:
|
|
279
|
+
"""Handle Docker Compose deployment."""
|
|
280
|
+
from wasm.deployers.docker_compose import DockerComposeDeployer
|
|
281
|
+
|
|
282
|
+
deployer = DockerComposeDeployer(verbose=args.verbose)
|
|
283
|
+
|
|
284
|
+
deployer.configure(
|
|
285
|
+
domain=domain,
|
|
286
|
+
source=args.source,
|
|
287
|
+
webserver=args.webserver,
|
|
288
|
+
ssl=not args.no_ssl,
|
|
289
|
+
branch=args.branch,
|
|
290
|
+
env_vars=env_vars,
|
|
291
|
+
compose_file=getattr(args, "compose_file", None),
|
|
292
|
+
compose_profiles=getattr(args, "compose_profiles", None),
|
|
293
|
+
port=args.port,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
deployer.deploy()
|
|
297
|
+
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
|
|
272
301
|
def _handle_list(args: Namespace) -> int:
|
|
273
302
|
"""Handle webapp list command."""
|
|
274
303
|
logger = Logger(verbose=args.verbose)
|
|
@@ -600,6 +629,12 @@ def _handle_update(args: Namespace) -> int:
|
|
|
600
629
|
args, app_path, app_name, domain, logger, total_steps
|
|
601
630
|
)
|
|
602
631
|
|
|
632
|
+
# Handle docker-compose updates
|
|
633
|
+
if app_type == "docker-compose":
|
|
634
|
+
return _handle_docker_compose_update(
|
|
635
|
+
args, app_path, app_name, domain, logger
|
|
636
|
+
)
|
|
637
|
+
|
|
603
638
|
deployer = get_deployer(app_type, verbose=args.verbose)
|
|
604
639
|
deployer.app_path = app_path
|
|
605
640
|
deployer.app_name = app_name
|
|
@@ -734,6 +769,38 @@ def _handle_monorepo_update(args, app_path, app_name, domain, logger, total_step
|
|
|
734
769
|
return 0
|
|
735
770
|
|
|
736
771
|
|
|
772
|
+
def _handle_docker_compose_update(args, app_path, app_name, domain, logger):
|
|
773
|
+
"""Handle update for Docker Compose applications."""
|
|
774
|
+
from wasm.deployers.docker_compose import DockerComposeDeployer
|
|
775
|
+
|
|
776
|
+
deployer = DockerComposeDeployer(verbose=args.verbose)
|
|
777
|
+
deployer.app_path = app_path
|
|
778
|
+
deployer.app_name = app_name
|
|
779
|
+
deployer.domain = domain
|
|
780
|
+
|
|
781
|
+
# Discover compose file
|
|
782
|
+
deployer._discover_compose_file()
|
|
783
|
+
|
|
784
|
+
logger.step(4, 5, "Rebuilding Docker images")
|
|
785
|
+
deployer._build_images()
|
|
786
|
+
|
|
787
|
+
logger.step(5, 5, "Restarting containers")
|
|
788
|
+
from wasm.core.utils import run_command
|
|
789
|
+
cmd = ["docker", "compose"]
|
|
790
|
+
if deployer.compose_path:
|
|
791
|
+
cmd.extend(["-f", str(deployer.compose_path)])
|
|
792
|
+
cmd.extend(["up", "-d", "--remove-orphans"])
|
|
793
|
+
result = run_command(cmd, cwd=app_path, timeout=300000)
|
|
794
|
+
|
|
795
|
+
if result.success:
|
|
796
|
+
logger.success(f"Docker Compose app updated: {domain}")
|
|
797
|
+
else:
|
|
798
|
+
logger.warning("Update may have issues. Check with: docker compose ps")
|
|
799
|
+
logger.warning(result.stderr)
|
|
800
|
+
|
|
801
|
+
return 0
|
|
802
|
+
|
|
803
|
+
|
|
737
804
|
def _handle_delete(args: Namespace) -> int:
|
|
738
805
|
"""Handle webapp delete command."""
|
|
739
806
|
logger = Logger(verbose=args.verbose)
|
|
@@ -807,8 +874,22 @@ def _handle_delete(args: Namespace) -> int:
|
|
|
807
874
|
|
|
808
875
|
total_steps = 5
|
|
809
876
|
|
|
810
|
-
# Stop
|
|
811
|
-
|
|
877
|
+
# Stop Docker Compose containers if applicable
|
|
878
|
+
if app and app.app_type == "docker-compose":
|
|
879
|
+
logger.step(1, total_steps, "Stopping Docker Compose containers")
|
|
880
|
+
from wasm.core.utils import run_command as _run_cmd
|
|
881
|
+
for compose_name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
|
|
882
|
+
compose_file = app_path / compose_name
|
|
883
|
+
if compose_file.exists():
|
|
884
|
+
_run_cmd(
|
|
885
|
+
["docker", "compose", "-f", str(compose_file), "down", "--remove-orphans"],
|
|
886
|
+
cwd=app_path,
|
|
887
|
+
)
|
|
888
|
+
break
|
|
889
|
+
else:
|
|
890
|
+
logger.step(1, total_steps, "Stopping service")
|
|
891
|
+
|
|
892
|
+
# Delete systemd service
|
|
812
893
|
service_manager = ServiceManager(verbose=args.verbose)
|
|
813
894
|
try:
|
|
814
895
|
service_manager.delete_service(app_name)
|
|
@@ -858,10 +939,45 @@ def _handle_logs(args: Namespace) -> int:
|
|
|
858
939
|
"""Handle webapp logs command."""
|
|
859
940
|
logger = Logger(verbose=args.verbose)
|
|
860
941
|
service_manager = ServiceManager(verbose=args.verbose)
|
|
861
|
-
|
|
942
|
+
|
|
862
943
|
domain = validate_domain(args.domain)
|
|
863
944
|
app_name = domain_to_app_name(domain)
|
|
864
|
-
|
|
945
|
+
|
|
946
|
+
# Check if this is a docker-compose app
|
|
947
|
+
from wasm.core.store import get_store
|
|
948
|
+
store = get_store()
|
|
949
|
+
app = store.get_app(domain)
|
|
950
|
+
|
|
951
|
+
if app and app.app_type == "docker-compose":
|
|
952
|
+
config = Config()
|
|
953
|
+
app_path = Path(app.app_path) if app.app_path else config.apps_directory / app_name
|
|
954
|
+
from wasm.core.utils import run_command as _run_cmd
|
|
955
|
+
import subprocess
|
|
956
|
+
|
|
957
|
+
# Find compose file
|
|
958
|
+
compose_file = None
|
|
959
|
+
for name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
|
|
960
|
+
cf = app_path / name
|
|
961
|
+
if cf.exists():
|
|
962
|
+
compose_file = str(cf)
|
|
963
|
+
break
|
|
964
|
+
|
|
965
|
+
cmd = ["docker", "compose"]
|
|
966
|
+
if compose_file:
|
|
967
|
+
cmd.extend(["-f", compose_file])
|
|
968
|
+
cmd.extend(["logs", "--tail", str(args.lines)])
|
|
969
|
+
|
|
970
|
+
if args.follow:
|
|
971
|
+
cmd.append("-f")
|
|
972
|
+
try:
|
|
973
|
+
subprocess.run(cmd, cwd=app_path)
|
|
974
|
+
except KeyboardInterrupt:
|
|
975
|
+
pass
|
|
976
|
+
else:
|
|
977
|
+
result = _run_cmd(cmd, cwd=app_path)
|
|
978
|
+
print(result.stdout if result.success else result.stderr)
|
|
979
|
+
return 0
|
|
980
|
+
|
|
865
981
|
# Get resolved service name (handles both legacy wasm-* and new format)
|
|
866
982
|
service_name = service_manager._resolve_service_name(app_name)
|
|
867
983
|
|