wasm-cli 0.14.4__tar.gz → 0.15.1__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.1}/PKG-INFO +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/pyproject.toml +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/setup.py +25 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/__init__.py +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/backup.py +115 -2
- wasm_cli-0.15.1/src/wasm/cli/commands/env.py +220 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/webapp.py +132 -9
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/parser.py +158 -3
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/dependencies.py +36 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/exceptions.py +10 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/logger.py +20 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/store.py +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/__init__.py +2 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/base.py +109 -7
- wasm_cli-0.15.1/src/wasm/deployers/docker_compose.py +848 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/__init__.py +4 -0
- wasm_cli-0.15.1/src/wasm/deployers/helpers/env_manager.py +525 -0
- wasm_cli-0.15.1/src/wasm/deployers/helpers/nginx_config.py +289 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/monorepo.py +20 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/python.py +3 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/registry.py +2 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/main.py +8 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/backup_manager.py +175 -8
- wasm_cli-0.15.1/src/wasm/managers/backup_scheduler.py +266 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/base_manager.py +4 -8
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/postgres.py +152 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/redis.py +88 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/nginx_manager.py +26 -1
- wasm_cli-0.15.1/src/wasm/templates/nginx/advanced.conf.j2 +117 -0
- wasm_cli-0.15.1/src/wasm/templates/systemd/backup-service.j2 +9 -0
- wasm_cli-0.15.1/src/wasm/templates/systemd/backup-timer.j2 +13 -0
- wasm_cli-0.15.1/src/wasm/templates/systemd/docker-compose.service.j2 +39 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/jobs.py +26 -19
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/server.py +12 -8
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/css/main.css +28 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/index.html +6 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/api.js +2 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/router.js +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/ui.js +1 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/main.js +2 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/apps.js +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/databases.js +1 -10
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/jobs.js +19 -4
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/logs.js +11 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/monitor.js +12 -2
- {wasm_cli-0.14.4 → wasm_cli-0.15.1/src/wasm_cli.egg-info}/PKG-INFO +1 -1
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/SOURCES.txt +9 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/LICENSE +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/MANIFEST.in +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/README.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/MONITOR.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/OBS_SETUP.md +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/assets/logo.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/docs/assets/logo_bg.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/man/wasm.1 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/setup.cfg +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/__main__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/cert.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/db.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/health.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/service.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/setup.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/site.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/store.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/version.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/commands/web.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/cli/interactive.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/_wasm +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/wasm.bash +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/completions/wasm.fish +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/update_checker.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/core/utils.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/package_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/path_resolver.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/prisma.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/turbo.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/helpers/workspace.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/nextjs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/nodejs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/static.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/deployers/vite.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/apache_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/cert_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/base.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/mongodb.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/mysql.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/database/registry.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/service_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/managers/source_manager.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/ai_analyzer.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/email_notifier.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/process_monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/monitor/threat_store.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/apache/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/apache/static.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/monorepo.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/proxy.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/nginx/static.conf.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/templates/systemd/app.service.j2 +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/domain.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/port.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/source.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/validators/ssh.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/apps.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/auth.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/backups.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/certs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/config.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/databases.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/jobs.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/monitor.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/router.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/services.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/sites.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/api/system.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/auth.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/cards.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/jobs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/metrics.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/components/skeleton.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/dialogs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/notifications.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/search.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/shortcuts.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/theme.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/core/websocket.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/backups.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/certs.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/config.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/dashboard.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/services.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/js/pages/sites.js +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/login.html +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/logo.png +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/static/logo.webp +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/websockets/__init__.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm/web/websockets/router.py +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/dependency_links.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/entry_points.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/src/wasm_cli.egg-info/requires.txt +0 -0
- {wasm_cli-0.14.4 → wasm_cli-0.15.1}/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.1"
|
|
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.1",
|
|
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",
|
|
@@ -24,10 +24,33 @@ setup(
|
|
|
24
24
|
],
|
|
25
25
|
extras_require={
|
|
26
26
|
"interactive": ["inquirer>=3.1.0"],
|
|
27
|
+
"web": [
|
|
28
|
+
"fastapi>=0.109.0",
|
|
29
|
+
"uvicorn[standard]>=0.27.0",
|
|
30
|
+
"python-jose[cryptography]>=3.3.0",
|
|
31
|
+
"passlib[bcrypt]>=1.7.4",
|
|
32
|
+
"psutil>=5.9.0",
|
|
33
|
+
"aiofiles>=23.0.0",
|
|
34
|
+
],
|
|
35
|
+
"monitor": [
|
|
36
|
+
"psutil>=5.9.0",
|
|
37
|
+
"httpx>=0.25.0",
|
|
38
|
+
],
|
|
39
|
+
"all": [
|
|
40
|
+
"inquirer>=3.1.0",
|
|
41
|
+
"rich>=13.0",
|
|
42
|
+
"psutil>=5.9.0",
|
|
43
|
+
"httpx>=0.25.0",
|
|
44
|
+
"fastapi>=0.109.0",
|
|
45
|
+
"uvicorn[standard]>=0.27.0",
|
|
46
|
+
"python-jose[cryptography]>=3.3.0",
|
|
47
|
+
"passlib[bcrypt]>=1.7.4",
|
|
48
|
+
"aiofiles>=23.0.0",
|
|
49
|
+
],
|
|
27
50
|
},
|
|
28
51
|
entry_points={
|
|
29
52
|
"console_scripts": [
|
|
30
|
-
"wasm=wasm.main:
|
|
53
|
+
"wasm=wasm.main:cli",
|
|
31
54
|
],
|
|
32
55
|
},
|
|
33
56
|
data_files=[
|
|
@@ -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)
|
|
@@ -587,12 +616,19 @@ def _handle_update(args: Namespace) -> int:
|
|
|
587
616
|
|
|
588
617
|
# Step 3: Detect app type and configure deployer
|
|
589
618
|
logger.step(3, total_steps, "Detecting application type")
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
619
|
+
|
|
620
|
+
# Prefer stored app type from database (initial deploy already determined it correctly)
|
|
621
|
+
stored_type = app.app_type if app else None
|
|
622
|
+
if stored_type and stored_type != "unknown":
|
|
623
|
+
app_type = stored_type
|
|
595
624
|
logger.substep(f"Detected: {app_type}")
|
|
625
|
+
else:
|
|
626
|
+
app_type = detect_app_type(app_path, verbose=args.verbose)
|
|
627
|
+
if not app_type:
|
|
628
|
+
app_type = "nodejs"
|
|
629
|
+
logger.substep(f"Using default: {app_type}")
|
|
630
|
+
else:
|
|
631
|
+
logger.substep(f"Detected: {app_type}")
|
|
596
632
|
|
|
597
633
|
# Handle monorepo updates with dedicated flow
|
|
598
634
|
if app_type == "monorepo":
|
|
@@ -600,6 +636,12 @@ def _handle_update(args: Namespace) -> int:
|
|
|
600
636
|
args, app_path, app_name, domain, logger, total_steps
|
|
601
637
|
)
|
|
602
638
|
|
|
639
|
+
# Handle docker-compose updates
|
|
640
|
+
if app_type == "docker-compose":
|
|
641
|
+
return _handle_docker_compose_update(
|
|
642
|
+
args, app_path, app_name, domain, logger
|
|
643
|
+
)
|
|
644
|
+
|
|
603
645
|
deployer = get_deployer(app_type, verbose=args.verbose)
|
|
604
646
|
deployer.app_path = app_path
|
|
605
647
|
deployer.app_name = app_name
|
|
@@ -734,6 +776,38 @@ def _handle_monorepo_update(args, app_path, app_name, domain, logger, total_step
|
|
|
734
776
|
return 0
|
|
735
777
|
|
|
736
778
|
|
|
779
|
+
def _handle_docker_compose_update(args, app_path, app_name, domain, logger):
|
|
780
|
+
"""Handle update for Docker Compose applications."""
|
|
781
|
+
from wasm.deployers.docker_compose import DockerComposeDeployer
|
|
782
|
+
|
|
783
|
+
deployer = DockerComposeDeployer(verbose=args.verbose)
|
|
784
|
+
deployer.app_path = app_path
|
|
785
|
+
deployer.app_name = app_name
|
|
786
|
+
deployer.domain = domain
|
|
787
|
+
|
|
788
|
+
# Discover compose file
|
|
789
|
+
deployer._discover_compose_file()
|
|
790
|
+
|
|
791
|
+
logger.step(4, 5, "Rebuilding Docker images")
|
|
792
|
+
deployer._build_images()
|
|
793
|
+
|
|
794
|
+
logger.step(5, 5, "Restarting containers")
|
|
795
|
+
from wasm.core.utils import run_command
|
|
796
|
+
cmd = ["docker", "compose"]
|
|
797
|
+
if deployer.compose_path:
|
|
798
|
+
cmd.extend(["-f", str(deployer.compose_path)])
|
|
799
|
+
cmd.extend(["up", "-d", "--remove-orphans"])
|
|
800
|
+
result = run_command(cmd, cwd=app_path, timeout=300000)
|
|
801
|
+
|
|
802
|
+
if result.success:
|
|
803
|
+
logger.success(f"Docker Compose app updated: {domain}")
|
|
804
|
+
else:
|
|
805
|
+
logger.warning("Update may have issues. Check with: docker compose ps")
|
|
806
|
+
logger.warning(result.stderr)
|
|
807
|
+
|
|
808
|
+
return 0
|
|
809
|
+
|
|
810
|
+
|
|
737
811
|
def _handle_delete(args: Namespace) -> int:
|
|
738
812
|
"""Handle webapp delete command."""
|
|
739
813
|
logger = Logger(verbose=args.verbose)
|
|
@@ -807,8 +881,22 @@ def _handle_delete(args: Namespace) -> int:
|
|
|
807
881
|
|
|
808
882
|
total_steps = 5
|
|
809
883
|
|
|
810
|
-
# Stop
|
|
811
|
-
|
|
884
|
+
# Stop Docker Compose containers if applicable
|
|
885
|
+
if app and app.app_type == "docker-compose":
|
|
886
|
+
logger.step(1, total_steps, "Stopping Docker Compose containers")
|
|
887
|
+
from wasm.core.utils import run_command as _run_cmd
|
|
888
|
+
for compose_name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
|
|
889
|
+
compose_file = app_path / compose_name
|
|
890
|
+
if compose_file.exists():
|
|
891
|
+
_run_cmd(
|
|
892
|
+
["docker", "compose", "-f", str(compose_file), "down", "--remove-orphans"],
|
|
893
|
+
cwd=app_path,
|
|
894
|
+
)
|
|
895
|
+
break
|
|
896
|
+
else:
|
|
897
|
+
logger.step(1, total_steps, "Stopping service")
|
|
898
|
+
|
|
899
|
+
# Delete systemd service
|
|
812
900
|
service_manager = ServiceManager(verbose=args.verbose)
|
|
813
901
|
try:
|
|
814
902
|
service_manager.delete_service(app_name)
|
|
@@ -858,10 +946,45 @@ def _handle_logs(args: Namespace) -> int:
|
|
|
858
946
|
"""Handle webapp logs command."""
|
|
859
947
|
logger = Logger(verbose=args.verbose)
|
|
860
948
|
service_manager = ServiceManager(verbose=args.verbose)
|
|
861
|
-
|
|
949
|
+
|
|
862
950
|
domain = validate_domain(args.domain)
|
|
863
951
|
app_name = domain_to_app_name(domain)
|
|
864
|
-
|
|
952
|
+
|
|
953
|
+
# Check if this is a docker-compose app
|
|
954
|
+
from wasm.core.store import get_store
|
|
955
|
+
store = get_store()
|
|
956
|
+
app = store.get_app(domain)
|
|
957
|
+
|
|
958
|
+
if app and app.app_type == "docker-compose":
|
|
959
|
+
config = Config()
|
|
960
|
+
app_path = Path(app.app_path) if app.app_path else config.apps_directory / app_name
|
|
961
|
+
from wasm.core.utils import run_command as _run_cmd
|
|
962
|
+
import subprocess
|
|
963
|
+
|
|
964
|
+
# Find compose file
|
|
965
|
+
compose_file = None
|
|
966
|
+
for name in ["docker-compose.prod.yml", "docker-compose.yml", "compose.yml"]:
|
|
967
|
+
cf = app_path / name
|
|
968
|
+
if cf.exists():
|
|
969
|
+
compose_file = str(cf)
|
|
970
|
+
break
|
|
971
|
+
|
|
972
|
+
cmd = ["docker", "compose"]
|
|
973
|
+
if compose_file:
|
|
974
|
+
cmd.extend(["-f", compose_file])
|
|
975
|
+
cmd.extend(["logs", "--tail", str(args.lines)])
|
|
976
|
+
|
|
977
|
+
if args.follow:
|
|
978
|
+
cmd.append("-f")
|
|
979
|
+
try:
|
|
980
|
+
subprocess.run(cmd, cwd=app_path)
|
|
981
|
+
except KeyboardInterrupt:
|
|
982
|
+
pass
|
|
983
|
+
else:
|
|
984
|
+
result = _run_cmd(cmd, cwd=app_path)
|
|
985
|
+
print(result.stdout if result.success else result.stderr)
|
|
986
|
+
return 0
|
|
987
|
+
|
|
865
988
|
# Get resolved service name (handles both legacy wasm-* and new format)
|
|
866
989
|
service_name = service_manager._resolve_service_name(app_name)
|
|
867
990
|
|