fujin-cli 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
Potentially problematic release.
This version of fujin-cli might be problematic. Click here for more details.
- fujin/commands/app.py +3 -1
- fujin/commands/config.py +3 -2
- fujin/commands/deploy.py +51 -29
- fujin/commands/down.py +1 -1
- fujin/commands/init.py +53 -16
- fujin/commands/proxy.py +11 -0
- fujin/commands/redeploy.py +38 -25
- fujin/commands/server.py +0 -5
- fujin/config.py +30 -19
- fujin/proxies/caddy.py +61 -47
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.5.0.dist-info}/METADATA +1 -1
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.5.0.dist-info}/RECORD +15 -15
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.5.0.dist-info}/WHEEL +0 -0
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.5.0.dist-info}/entry_points.txt +0 -0
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
fujin/commands/app.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
import cappa
|
|
6
6
|
|
|
7
7
|
from fujin.commands import BaseCommand
|
|
8
|
+
from fujin.config import InstallationMode
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@cappa.command(help="Run application-related tasks")
|
|
@@ -25,11 +26,12 @@ class App(BaseCommand):
|
|
|
25
26
|
"app_bin": self.config.app_bin,
|
|
26
27
|
"local_version": self.config.version,
|
|
27
28
|
"remote_version": remote_version,
|
|
28
|
-
"python_version": self.config.python_version,
|
|
29
29
|
"rollback_targets": ", ".join(rollback_targets.split("\n"))
|
|
30
30
|
if rollback_targets
|
|
31
31
|
else "N/A",
|
|
32
32
|
}
|
|
33
|
+
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
34
|
+
infos["python_version"] = self.config.python_version
|
|
33
35
|
pm = self.create_process_manager(conn)
|
|
34
36
|
services: dict[str, bool] = pm.is_active()
|
|
35
37
|
|
fujin/commands/config.py
CHANGED
|
@@ -16,13 +16,14 @@ class ConfigCMD(BaseCommand):
|
|
|
16
16
|
"app": self.config.app_name,
|
|
17
17
|
"app_bin": self.config.app_bin,
|
|
18
18
|
"version": self.config.version,
|
|
19
|
-
"python_version": self.config.python_version,
|
|
20
19
|
"build_command": self.config.build_command,
|
|
21
20
|
"release_command": self.config.release_command,
|
|
21
|
+
"installation_mode": self.config.installation_mode,
|
|
22
22
|
"distfile": self.config.distfile,
|
|
23
|
-
"requirements": self.config.requirements,
|
|
24
23
|
"webserver": f"{{ upstream = '{self.config.webserver.upstream}', type = '{self.config.webserver.type}' }}",
|
|
25
24
|
}
|
|
25
|
+
if self.config.python_version:
|
|
26
|
+
general_config["python_version"] = self.config.python_version
|
|
26
27
|
general_config_text = "\n".join(
|
|
27
28
|
f"[bold green]{key}:[/bold green] {value}"
|
|
28
29
|
for key, value in general_config.items()
|
fujin/commands/deploy.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import cappa
|
|
6
7
|
|
|
7
8
|
from fujin.commands import BaseCommand
|
|
9
|
+
from fujin.config import InstallationMode
|
|
8
10
|
from fujin.connection import Connection
|
|
9
11
|
|
|
10
12
|
|
|
@@ -16,21 +18,20 @@ class Deploy(BaseCommand):
|
|
|
16
18
|
self.build_app()
|
|
17
19
|
|
|
18
20
|
with self.connection() as conn:
|
|
21
|
+
process_manager = self.create_process_manager(conn)
|
|
19
22
|
conn.run(f"mkdir -p {self.app_dir}")
|
|
20
23
|
with conn.cd(self.app_dir):
|
|
21
24
|
self.create_hook_manager(conn).pre_deploy()
|
|
22
25
|
self.transfer_files(conn)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
self.update_version_history(conn)
|
|
33
|
-
self.prune_assets(conn)
|
|
26
|
+
self.install_project(conn)
|
|
27
|
+
with self.app_environment() as app_conn:
|
|
28
|
+
self.release(app_conn)
|
|
29
|
+
process_manager.install_services()
|
|
30
|
+
process_manager.reload_configuration()
|
|
31
|
+
process_manager.restart_services()
|
|
32
|
+
self.create_web_proxy(app_conn).setup()
|
|
33
|
+
self.update_version_history(app_conn)
|
|
34
|
+
self.prune_assets(app_conn)
|
|
34
35
|
self.create_hook_manager(conn).post_deploy()
|
|
35
36
|
self.stdout.output("[green]Project deployment completed successfully![/green]")
|
|
36
37
|
self.stdout.output(
|
|
@@ -47,24 +48,37 @@ class Deploy(BaseCommand):
|
|
|
47
48
|
def versioned_assets_dir(self) -> str:
|
|
48
49
|
return f"{self.app_dir}/v{self.config.version}"
|
|
49
50
|
|
|
50
|
-
def transfer_files(self, conn: Connection
|
|
51
|
+
def transfer_files(self, conn: Connection):
|
|
51
52
|
if not self.config.host.envfile.exists():
|
|
52
53
|
raise cappa.Exit(f"{self.config.host.envfile} not found", code=1)
|
|
53
|
-
|
|
54
|
-
if not self.config.requirements.exists():
|
|
55
|
-
raise cappa.Exit(f"{self.config.requirements} not found", code=1)
|
|
56
54
|
conn.put(str(self.config.host.envfile), f"{self.app_dir}/.env")
|
|
57
55
|
conn.run(f"mkdir -p {self.versioned_assets_dir}")
|
|
58
|
-
if not skip_requirements:
|
|
59
|
-
conn.put(
|
|
60
|
-
str(self.config.requirements),
|
|
61
|
-
f"{self.versioned_assets_dir}/requirements.txt",
|
|
62
|
-
)
|
|
63
56
|
distfile_path = self.config.get_distfile_path()
|
|
64
57
|
conn.put(
|
|
65
58
|
str(distfile_path),
|
|
66
59
|
f"{self.versioned_assets_dir}/{distfile_path.name}",
|
|
67
60
|
)
|
|
61
|
+
|
|
62
|
+
def install_project(
|
|
63
|
+
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
64
|
+
):
|
|
65
|
+
version = version or self.config.version
|
|
66
|
+
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
67
|
+
self._install_python_package(conn, version, skip_setup)
|
|
68
|
+
else:
|
|
69
|
+
self._install_binary(conn, version)
|
|
70
|
+
|
|
71
|
+
def _install_python_package(
|
|
72
|
+
self, conn: Connection, version: str, skip_setup: bool = False
|
|
73
|
+
):
|
|
74
|
+
if not skip_setup and self.config.requirements:
|
|
75
|
+
requirements = Path(self.config.requirements)
|
|
76
|
+
if not requirements.exists():
|
|
77
|
+
raise cappa.Exit(f"{self.config.requirements} not found", code=1)
|
|
78
|
+
conn.put(
|
|
79
|
+
Path(self.config.requirements).resolve(),
|
|
80
|
+
f"{self.versioned_assets_dir}/requirements.txt",
|
|
81
|
+
)
|
|
68
82
|
appenv = f"""
|
|
69
83
|
set -a # Automatically export all variables
|
|
70
84
|
source .env
|
|
@@ -73,22 +87,30 @@ export UV_COMPILE_BYTECODE=1
|
|
|
73
87
|
export UV_PYTHON=python{self.config.python_version}
|
|
74
88
|
export PATH=".venv/bin:$PATH"
|
|
75
89
|
"""
|
|
76
|
-
conn.run(f"echo '{appenv.strip()}' > .appenv")
|
|
77
|
-
|
|
78
|
-
def install_project(
|
|
79
|
-
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
80
|
-
):
|
|
81
|
-
if self.config.skip_project_install:
|
|
82
|
-
return
|
|
83
|
-
version = version or self.config.version
|
|
90
|
+
conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
|
|
84
91
|
versioned_assets_dir = f"{self.app_dir}/v{version}"
|
|
85
92
|
if not skip_setup:
|
|
86
93
|
conn.run("uv venv")
|
|
87
|
-
|
|
94
|
+
if self.config.requirements:
|
|
95
|
+
conn.run(f"uv pip install -r {versioned_assets_dir}/requirements.txt")
|
|
88
96
|
conn.run(
|
|
89
97
|
f"uv pip install {versioned_assets_dir}/{self.config.get_distfile_path(version).name}"
|
|
90
98
|
)
|
|
91
99
|
|
|
100
|
+
def _install_binary(self, conn: Connection, version: str):
|
|
101
|
+
appenv = f"""
|
|
102
|
+
set -a # Automatically export all variables
|
|
103
|
+
source .env
|
|
104
|
+
set +a # Stop automatic export
|
|
105
|
+
export PATH="{self.app_dir}:$PATH"
|
|
106
|
+
"""
|
|
107
|
+
conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
|
|
108
|
+
full_path_app_bin = f"{self.app_dir}/{self.config.app_bin}"
|
|
109
|
+
conn.run(f"rm {full_path_app_bin}", warn=True)
|
|
110
|
+
conn.run(
|
|
111
|
+
f"ln -s {self.versioned_assets_dir}/{self.config.get_distfile_path(version).name} {full_path_app_bin}"
|
|
112
|
+
)
|
|
113
|
+
|
|
92
114
|
def release(self, conn: Connection):
|
|
93
115
|
if self.config.release_command:
|
|
94
116
|
conn.run(f"source .env && {self.config.release_command}")
|
fujin/commands/down.py
CHANGED
|
@@ -28,7 +28,7 @@ class Down(BaseCommand):
|
|
|
28
28
|
confirm = Confirm.ask(
|
|
29
29
|
f"""[red]You are about to delete all project files, stop all services, and remove all configurations on the host {self.config.host.ip} for the project {self.config.app_name}. Any assets in your project folder will be lost (sqlite not in there ?). Are you sure you want to proceed? This action is irreversible.[/red]""",
|
|
30
30
|
)
|
|
31
|
-
except KeyboardInterrupt
|
|
31
|
+
except KeyboardInterrupt:
|
|
32
32
|
raise cappa.Exit("Teardown aborted", code=0)
|
|
33
33
|
if not confirm:
|
|
34
34
|
return
|
fujin/commands/init.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Annotated
|
|
5
6
|
|
|
@@ -7,31 +8,28 @@ import cappa
|
|
|
7
8
|
import tomli_w
|
|
8
9
|
|
|
9
10
|
from fujin.commands import BaseCommand
|
|
10
|
-
from fujin.config import tomllib
|
|
11
|
+
from fujin.config import tomllib, InstallationMode
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@cappa.command(help="Generate a sample configuration file")
|
|
15
|
+
@dataclass
|
|
14
16
|
class Init(BaseCommand):
|
|
15
17
|
profile: Annotated[
|
|
16
|
-
str,
|
|
18
|
+
str,
|
|
19
|
+
cappa.Arg(choices=["simple", "falco", "binary"], short="-p", long="--profile"),
|
|
17
20
|
] = "simple"
|
|
18
21
|
|
|
19
22
|
def __call__(self):
|
|
20
23
|
fujin_toml = Path("fujin.toml")
|
|
21
24
|
if fujin_toml.exists():
|
|
22
25
|
raise cappa.Exit("fujin.toml file already exists", code=1)
|
|
23
|
-
profile_to_func = {
|
|
26
|
+
profile_to_func = {
|
|
27
|
+
"simple": simple_config,
|
|
28
|
+
"falco": falco_config,
|
|
29
|
+
"binary": binary_config,
|
|
30
|
+
}
|
|
24
31
|
app_name = Path().resolve().stem.replace("-", "_").replace(" ", "_").lower()
|
|
25
32
|
config = profile_to_func[self.profile](app_name)
|
|
26
|
-
if not Path(".python-version").exists():
|
|
27
|
-
config["python_version"] = "3.12"
|
|
28
|
-
pyproject_toml = Path("pyproject.toml")
|
|
29
|
-
if pyproject_toml.exists():
|
|
30
|
-
pyproject = tomllib.loads(pyproject_toml.read_text())
|
|
31
|
-
config["app"] = pyproject.get("project", {}).get("name", app_name)
|
|
32
|
-
if pyproject.get("project", {}).get("version"):
|
|
33
|
-
# fujin will read the version itself from the pyproject
|
|
34
|
-
config.pop("version")
|
|
35
33
|
fujin_toml.write_text(tomli_w.dumps(config))
|
|
36
34
|
self.stdout.output(
|
|
37
35
|
"[green]Sample configuration file generated successfully![/green]"
|
|
@@ -39,18 +37,20 @@ class Init(BaseCommand):
|
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
def simple_config(app_name) -> dict:
|
|
42
|
-
|
|
40
|
+
config = {
|
|
43
41
|
"app": app_name,
|
|
44
|
-
"version": "0.1
|
|
42
|
+
"version": "0.0.1",
|
|
45
43
|
"build_command": "uv build && uv pip compile pyproject.toml -o requirements.txt",
|
|
46
44
|
"distfile": f"dist/{app_name}-{{version}}-py3-none-any.whl",
|
|
45
|
+
"requirements": "requirements.txt",
|
|
47
46
|
"webserver": {
|
|
48
|
-
"upstream": "
|
|
47
|
+
"upstream": f"unix//run/{app_name}.sock",
|
|
49
48
|
"type": "fujin.proxies.caddy",
|
|
50
49
|
},
|
|
51
50
|
"release_command": f"{app_name} migrate",
|
|
51
|
+
"installation_mode": InstallationMode.PY_PACKAGE,
|
|
52
52
|
"processes": {
|
|
53
|
-
"web": f".venv/bin/gunicorn {app_name}.wsgi:application --bind
|
|
53
|
+
"web": f".venv/bin/gunicorn {app_name}.wsgi:application --bind unix//run/{app_name}.sock"
|
|
54
54
|
},
|
|
55
55
|
"aliases": {"shell": "server exec --appenv -i bash"},
|
|
56
56
|
"host": {
|
|
@@ -60,6 +60,16 @@ def simple_config(app_name) -> dict:
|
|
|
60
60
|
"envfile": ".env.prod",
|
|
61
61
|
},
|
|
62
62
|
}
|
|
63
|
+
if not Path(".python-version").exists():
|
|
64
|
+
config["python_version"] = "3.12"
|
|
65
|
+
pyproject_toml = Path("pyproject.toml")
|
|
66
|
+
if pyproject_toml.exists():
|
|
67
|
+
pyproject = tomllib.loads(pyproject_toml.read_text())
|
|
68
|
+
config["app"] = pyproject.get("project", {}).get("name", app_name)
|
|
69
|
+
if pyproject.get("project", {}).get("version"):
|
|
70
|
+
# fujin will read the version itself from the pyproject
|
|
71
|
+
config.pop("version")
|
|
72
|
+
return config
|
|
63
73
|
|
|
64
74
|
|
|
65
75
|
def falco_config(app_name: str) -> dict:
|
|
@@ -71,6 +81,10 @@ def falco_config(app_name: str) -> dict:
|
|
|
71
81
|
"web": f".venv/bin/{config['app']} prodserver",
|
|
72
82
|
"worker": f".venv/bin/{config['app']} qcluster",
|
|
73
83
|
},
|
|
84
|
+
"webserver": {
|
|
85
|
+
"upstream": "localhost:8000",
|
|
86
|
+
"type": "fujin.proxies.caddy",
|
|
87
|
+
},
|
|
74
88
|
"aliases": {
|
|
75
89
|
"console": "app exec -i shell_plus",
|
|
76
90
|
"dbconsole": "app exec -i dbshell",
|
|
@@ -80,3 +94,26 @@ def falco_config(app_name: str) -> dict:
|
|
|
80
94
|
}
|
|
81
95
|
)
|
|
82
96
|
return config
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def binary_config(app_name: str) -> dict:
|
|
100
|
+
return {
|
|
101
|
+
"app": app_name,
|
|
102
|
+
"version": "0.0.1",
|
|
103
|
+
"build_command": "just build-bin",
|
|
104
|
+
"distfile": f"dist/bin/{app_name}-{{version}}",
|
|
105
|
+
"webserver": {
|
|
106
|
+
"upstream": "localhost:8000",
|
|
107
|
+
"type": "fujin.proxies.caddy",
|
|
108
|
+
},
|
|
109
|
+
"release_command": f"{app_name} migrate",
|
|
110
|
+
"installation_mode": InstallationMode.BINARY,
|
|
111
|
+
"processes": {"web": f"{app_name} prodserver"},
|
|
112
|
+
"aliases": {"shell": "server exec --appenv -i bash"},
|
|
113
|
+
"host": {
|
|
114
|
+
"ip": "127.0.0.1",
|
|
115
|
+
"user": "root",
|
|
116
|
+
"domain_name": f"{app_name}.com",
|
|
117
|
+
"envfile": ".env.prod",
|
|
118
|
+
},
|
|
119
|
+
}
|
fujin/commands/proxy.py
CHANGED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import Annotated
|
|
3
3
|
|
|
4
4
|
import cappa
|
|
5
|
+
from rich.prompt import Confirm
|
|
5
6
|
|
|
6
7
|
from fujin.commands import BaseCommand
|
|
7
8
|
|
|
@@ -13,11 +14,21 @@ class Proxy(BaseCommand):
|
|
|
13
14
|
def install(self):
|
|
14
15
|
with self.connection() as conn:
|
|
15
16
|
self.create_web_proxy(conn).install()
|
|
17
|
+
self.stdout.output("[green]Proxy installed successfully![/green]")
|
|
16
18
|
|
|
17
19
|
@cappa.command(help="Uninstall the proxy from the remote host")
|
|
18
20
|
def uninstall(self):
|
|
21
|
+
try:
|
|
22
|
+
confirm = Confirm.ask(
|
|
23
|
+
f"[red]Uninstalling the proxy will remove all current configurations. Are you sure you want to proceed?"
|
|
24
|
+
)
|
|
25
|
+
except KeyboardInterrupt:
|
|
26
|
+
raise cappa.Exit("Teardown aborted", code=0)
|
|
27
|
+
if not confirm:
|
|
28
|
+
return
|
|
19
29
|
with self.connection() as conn:
|
|
20
30
|
self.create_web_proxy(conn).uninstall()
|
|
31
|
+
self.stdout.output("[green]Proxy uninstalled successfully![/green]")
|
|
21
32
|
|
|
22
33
|
@cappa.command(help="Start the proxy on the remote host")
|
|
23
34
|
def start(self):
|
fujin/commands/redeploy.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import cappa
|
|
6
7
|
|
|
7
8
|
from fujin.commands import BaseCommand
|
|
8
9
|
from .deploy import Deploy
|
|
10
|
+
from fujin.config import InstallationMode
|
|
11
|
+
from fujin.connection import Connection
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@cappa.command(help="Redeploy the application to apply code and environment changes")
|
|
@@ -13,36 +16,46 @@ class Redeploy(BaseCommand):
|
|
|
13
16
|
def __call__(self):
|
|
14
17
|
deploy = Deploy()
|
|
15
18
|
deploy.build_app()
|
|
16
|
-
|
|
17
|
-
self.config.requirements.read_bytes()
|
|
18
|
-
).hexdigest()
|
|
19
|
+
|
|
19
20
|
with self.app_environment() as conn:
|
|
20
21
|
hook_manager = self.create_hook_manager(conn)
|
|
21
22
|
hook_manager.pre_deploy()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
host_requirements = (
|
|
27
|
-
conn.run(
|
|
28
|
-
f"md5sum v{current_host_version}/requirements.txt",
|
|
29
|
-
warn=True,
|
|
30
|
-
hide=True,
|
|
31
|
-
)
|
|
32
|
-
.stdout.strip()
|
|
33
|
-
.split()[0]
|
|
34
|
-
)
|
|
35
|
-
skip_requirements = host_requirements == local_requirements
|
|
36
|
-
except IndexError:
|
|
37
|
-
skip_requirements = False
|
|
38
|
-
deploy.transfer_files(conn, skip_requirements=skip_requirements)
|
|
39
|
-
if skip_requirements and current_host_version != self.config.version:
|
|
40
|
-
conn.run(
|
|
41
|
-
f"cp v{current_host_version}/requirements.txt {deploy.versioned_assets_dir}/requirements.txt "
|
|
42
|
-
)
|
|
43
|
-
deploy.install_project(conn, skip_setup=skip_requirements)
|
|
23
|
+
deploy.transfer_files(conn)
|
|
24
|
+
requirements_copied = self._copy_requirements_if_needed(conn)
|
|
25
|
+
deploy.install_project(conn, skip_setup=requirements_copied)
|
|
44
26
|
deploy.release(conn)
|
|
45
27
|
self.create_process_manager(conn).restart_services()
|
|
46
28
|
deploy.update_version_history(conn)
|
|
47
29
|
hook_manager.post_deploy()
|
|
48
30
|
self.stdout.output("[green]Redeployment completed successfully![/green]")
|
|
31
|
+
|
|
32
|
+
def _copy_requirements_if_needed(self, conn: Connection) -> bool:
|
|
33
|
+
if (
|
|
34
|
+
not self.config.requirements
|
|
35
|
+
or self.config.installation_mode == InstallationMode.BINARY
|
|
36
|
+
):
|
|
37
|
+
return False
|
|
38
|
+
local_requirements = hashlib.md5(
|
|
39
|
+
Path(self.config.requirements).read_bytes()
|
|
40
|
+
).hexdigest()
|
|
41
|
+
current_host_version = conn.run(
|
|
42
|
+
"head -n 1 .versions", warn=True, hide=True
|
|
43
|
+
).stdout.strip()
|
|
44
|
+
try:
|
|
45
|
+
host_requirements = (
|
|
46
|
+
conn.run(
|
|
47
|
+
f"md5sum v{current_host_version}/requirements.txt",
|
|
48
|
+
warn=True,
|
|
49
|
+
hide=True,
|
|
50
|
+
)
|
|
51
|
+
.stdout.strip()
|
|
52
|
+
.split()[0]
|
|
53
|
+
)
|
|
54
|
+
skip_requirements = host_requirements == local_requirements
|
|
55
|
+
except IndexError:
|
|
56
|
+
return False
|
|
57
|
+
if skip_requirements and current_host_version != self.config.version:
|
|
58
|
+
conn.run(
|
|
59
|
+
f"cp v{current_host_version}/requirements.txt {self.app_dir}/v{self.config.version}/requirements.txt "
|
|
60
|
+
)
|
|
61
|
+
return True
|
fujin/commands/server.py
CHANGED
|
@@ -38,11 +38,6 @@ class Server(BaseCommand):
|
|
|
38
38
|
"[green]Server bootstrap completed successfully![/green]"
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
-
@cappa.command(help="Stop and uninstall the web proxy")
|
|
42
|
-
def uninstall_proxy(self):
|
|
43
|
-
with self.connection() as conn:
|
|
44
|
-
self.create_web_proxy(conn).uninstall()
|
|
45
|
-
|
|
46
41
|
@cappa.command(
|
|
47
42
|
help="Execute an arbitrary command on the server, optionally in interactive mode"
|
|
48
43
|
)
|
fujin/config.py
CHANGED
|
@@ -5,18 +5,14 @@ app
|
|
|
5
5
|
---
|
|
6
6
|
The name of your project or application. Must be a valid Python package name.
|
|
7
7
|
|
|
8
|
-
app_bin
|
|
9
|
-
-------
|
|
10
|
-
Path to your application's executable. Used by the **app** subcommand for remote execution.
|
|
11
|
-
Default: ``.venv/bin/{app}``
|
|
12
|
-
|
|
13
8
|
version
|
|
14
9
|
--------
|
|
15
10
|
The version of your project to build and deploy. If not specified, automatically parsed from ``pyproject.toml`` under ``project.version``.
|
|
16
11
|
|
|
17
12
|
python_version
|
|
18
13
|
--------------
|
|
19
|
-
The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file.
|
|
14
|
+
The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file. This is only
|
|
15
|
+
required if the installation mode is set to ``python-package``
|
|
20
16
|
|
|
21
17
|
versions_to_keep
|
|
22
18
|
----------------
|
|
@@ -32,14 +28,20 @@ distfile
|
|
|
32
28
|
Path to your project's distribution file. This should be the main artifact containing everything needed to run your project on the server.
|
|
33
29
|
Supports version placeholder, e.g., ``dist/app_name-{version}-py3-none-any.whl``
|
|
34
30
|
|
|
31
|
+
installation_mode
|
|
32
|
+
-----------------
|
|
33
|
+
|
|
34
|
+
Indicates whether the ``distfile`` is a Python package or a self-contained executable. The possible values are ``python-package`` and ``binary``.
|
|
35
|
+
The ``binary`` option disables specific Python-related features, such as virtual environment creation and requirements installation. ``fujin`` will assume the provided
|
|
36
|
+
``distfile`` already contains all the necessary dependencies to run your program.
|
|
37
|
+
|
|
35
38
|
release_command
|
|
36
39
|
---------------
|
|
37
40
|
Optional command to run at the end of deployment (e.g., database migrations).
|
|
38
41
|
|
|
39
42
|
requirements
|
|
40
43
|
------------
|
|
41
|
-
|
|
42
|
-
Default: ``requirements.txt``
|
|
44
|
+
Optional path to your requirements file. This will only be used when the installation mode is set to ``python-package``
|
|
43
45
|
|
|
44
46
|
Webserver
|
|
45
47
|
---------
|
|
@@ -63,7 +65,8 @@ statics
|
|
|
63
65
|
~~~~~~~
|
|
64
66
|
|
|
65
67
|
Defines the mapping of URL paths to local directories for serving static files. The syntax and support for static
|
|
66
|
-
file serving depend on the selected reverse proxy.
|
|
68
|
+
file serving depend on the selected reverse proxy. The directories you map should be accessible by the web server, meaning
|
|
69
|
+
with read permissions for the ``www-data`` group; a reliable choice is ``/var/www``.
|
|
67
70
|
|
|
68
71
|
Example:
|
|
69
72
|
|
|
@@ -72,7 +75,7 @@ Example:
|
|
|
72
75
|
[webserver]
|
|
73
76
|
upstream = "unix//run/project.sock"
|
|
74
77
|
type = "fujin.proxies.caddy"
|
|
75
|
-
statics = { "/static/*" = "/var/www/
|
|
78
|
+
statics = { "/static/*" = "/var/www/myproject/static/" }
|
|
76
79
|
|
|
77
80
|
processes
|
|
78
81
|
---------
|
|
@@ -159,6 +162,7 @@ from __future__ import annotations
|
|
|
159
162
|
|
|
160
163
|
import os
|
|
161
164
|
import sys
|
|
165
|
+
from functools import cached_property
|
|
162
166
|
from pathlib import Path
|
|
163
167
|
|
|
164
168
|
import msgspec
|
|
@@ -170,31 +174,36 @@ if sys.version_info >= (3, 11):
|
|
|
170
174
|
else:
|
|
171
175
|
import tomli as tomllib
|
|
172
176
|
|
|
173
|
-
from .hooks import HooksDict
|
|
177
|
+
from .hooks import HooksDict, StrEnum
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class InstallationMode(StrEnum):
|
|
181
|
+
PY_PACKAGE = "python-package"
|
|
182
|
+
BINARY = "binary"
|
|
174
183
|
|
|
175
184
|
|
|
176
185
|
class Config(msgspec.Struct, kw_only=True):
|
|
177
186
|
app_name: str = msgspec.field(name="app")
|
|
178
|
-
app_bin: str = ".venv/bin/{app}"
|
|
179
187
|
version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
|
|
180
188
|
versions_to_keep: int | None = 5
|
|
181
|
-
python_version: str
|
|
189
|
+
python_version: str | None = None
|
|
182
190
|
build_command: str
|
|
183
191
|
release_command: str | None = None
|
|
184
|
-
|
|
192
|
+
installation_mode: InstallationMode
|
|
185
193
|
distfile: str
|
|
186
194
|
aliases: dict[str, str] = msgspec.field(default_factory=dict)
|
|
187
195
|
host: HostConfig
|
|
188
196
|
processes: dict[str, str] = msgspec.field(default_factory=dict)
|
|
189
197
|
process_manager: str = "fujin.process_managers.systemd"
|
|
190
198
|
webserver: Webserver
|
|
191
|
-
|
|
199
|
+
requirements: str | None = None
|
|
192
200
|
hooks: HooksDict = msgspec.field(default_factory=dict)
|
|
193
201
|
local_config_dir: Path = Path(".fujin")
|
|
194
202
|
|
|
195
203
|
def __post_init__(self):
|
|
196
|
-
self.
|
|
197
|
-
|
|
204
|
+
if self.installation_mode == InstallationMode.PY_PACKAGE:
|
|
205
|
+
if not self.python_version:
|
|
206
|
+
self.python_version = find_python_version()
|
|
198
207
|
|
|
199
208
|
if "web" not in self.processes and self.webserver.type != "fujin.proxies.dummy":
|
|
200
209
|
raise ValueError(
|
|
@@ -202,8 +211,10 @@ class Config(msgspec.Struct, kw_only=True):
|
|
|
202
211
|
)
|
|
203
212
|
|
|
204
213
|
@property
|
|
205
|
-
def
|
|
206
|
-
|
|
214
|
+
def app_bin(self) -> str:
|
|
215
|
+
if self.installation_mode == InstallationMode.PY_PACKAGE:
|
|
216
|
+
return f".venv/bin/{self.app_name}"
|
|
217
|
+
return self.app_name
|
|
207
218
|
|
|
208
219
|
def get_distfile_path(self, version: str | None = None) -> Path:
|
|
209
220
|
version = version or self.version
|
fujin/proxies/caddy.py
CHANGED
|
@@ -49,6 +49,9 @@ class WebProxy(msgspec.Struct):
|
|
|
49
49
|
return self.conn.run(*args, **kwargs, pty=True)
|
|
50
50
|
|
|
51
51
|
def install(self):
|
|
52
|
+
result = self.conn.run(f"command -v caddy", warn=True, hide=True)
|
|
53
|
+
if result.ok:
|
|
54
|
+
return
|
|
52
55
|
version = get_latest_gh_tag()
|
|
53
56
|
download_url = GH_DOWNL0AD_URL.format(version=version)
|
|
54
57
|
filename = GH_TAR_FILENAME.format(version=version)
|
|
@@ -71,9 +74,9 @@ class WebProxy(msgspec.Struct):
|
|
|
71
74
|
)
|
|
72
75
|
self.run_pty("sudo systemctl daemon-reload")
|
|
73
76
|
self.run_pty("sudo systemctl enable --now caddy-api")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
self.conn.run(
|
|
78
|
+
"""curl --silent http://localhost:2019/config/ -d '{"apps":{"http": {"servers": {"srv0":{"listen":[":443"]}}}}}' -H 'Content-Type: application/json'"""
|
|
79
|
+
)
|
|
77
80
|
|
|
78
81
|
def uninstall(self):
|
|
79
82
|
self.stop()
|
|
@@ -83,51 +86,31 @@ class WebProxy(msgspec.Struct):
|
|
|
83
86
|
self.run_pty("sudo userdel caddy")
|
|
84
87
|
|
|
85
88
|
def setup(self):
|
|
86
|
-
|
|
89
|
+
current_config = json.loads(
|
|
90
|
+
self.conn.run(
|
|
91
|
+
"curl http://localhost:2019/config/apps/http/servers/srv0", hide=True
|
|
92
|
+
).stdout.strip()
|
|
93
|
+
)
|
|
94
|
+
existing_routes: list[dict] = current_config.get("routes", [])
|
|
95
|
+
new_routes = [r for r in existing_routes if r.get("group") != self.app_name]
|
|
96
|
+
routes = (
|
|
87
97
|
json.loads(self.config_file.read_text())
|
|
88
98
|
if self.config_file.exists()
|
|
89
|
-
else self.
|
|
99
|
+
else self._get_routes()
|
|
90
100
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
f"curl localhost:2019/config/apps/http/servers/{self.app_name} -H 'Content-Type: application/json' -d @caddy.json"
|
|
94
|
-
)
|
|
95
|
-
# TODO: stop when received an {"error":"loading config: loading new config: http app module: start: listening on :443: listen tcp :443: bind: permission denied"}, not a 200 ok
|
|
96
|
-
|
|
97
|
-
def teardown(self):
|
|
98
|
-
self.conn.run(f"echo '{json.dumps({})}' > caddy.json")
|
|
101
|
+
new_routes.append(routes)
|
|
102
|
+
current_config["routes"] = new_routes
|
|
99
103
|
self.conn.run(
|
|
100
|
-
f"curl localhost:2019/config/apps/http/servers/
|
|
104
|
+
f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'"
|
|
101
105
|
)
|
|
102
106
|
|
|
103
|
-
def
|
|
104
|
-
self.run_pty("sudo systemctl start caddy-api")
|
|
105
|
-
|
|
106
|
-
def stop(self) -> None:
|
|
107
|
-
self.run_pty("sudo systemctl stop caddy-api")
|
|
108
|
-
|
|
109
|
-
def status(self) -> None:
|
|
110
|
-
self.run_pty("sudo systemctl status caddy-api", warn=True)
|
|
111
|
-
|
|
112
|
-
def restart(self) -> None:
|
|
113
|
-
self.run_pty("sudo systemctl restart caddy-api")
|
|
114
|
-
|
|
115
|
-
def logs(self) -> None:
|
|
116
|
-
self.run_pty(f"sudo journalctl -u caddy-api -f", warn=True)
|
|
117
|
-
|
|
118
|
-
def export_config(self) -> None:
|
|
119
|
-
self.config_file.write_text(json.dumps(self._get_config()))
|
|
120
|
-
|
|
121
|
-
def _get_config(self) -> dict:
|
|
107
|
+
def _get_routes(self) -> dict:
|
|
122
108
|
handle = []
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"handle": handle,
|
|
129
|
-
}
|
|
130
|
-
],
|
|
109
|
+
routes = {
|
|
110
|
+
"group": self.app_name,
|
|
111
|
+
"match": [{"host": [self.domain_name]}],
|
|
112
|
+
"terminal": True,
|
|
113
|
+
"handle": handle,
|
|
131
114
|
}
|
|
132
115
|
reverse_proxy = {
|
|
133
116
|
"handler": "reverse_proxy",
|
|
@@ -135,14 +118,14 @@ class WebProxy(msgspec.Struct):
|
|
|
135
118
|
}
|
|
136
119
|
if not self.statics:
|
|
137
120
|
handle.append(reverse_proxy)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
handle.append({"handler": "subroute", "routes":
|
|
121
|
+
return routes
|
|
122
|
+
sub_routes = []
|
|
123
|
+
handle.append({"handler": "subroute", "routes": sub_routes})
|
|
141
124
|
for path, directory in self.statics.items():
|
|
142
125
|
strip_path_prefix = path.replace("/*", "")
|
|
143
126
|
if strip_path_prefix.endswith("/"):
|
|
144
127
|
strip_path_prefix = strip_path_prefix[:-1]
|
|
145
|
-
|
|
128
|
+
sub_routes.append(
|
|
146
129
|
{
|
|
147
130
|
"handle": [
|
|
148
131
|
{
|
|
@@ -170,8 +153,39 @@ class WebProxy(msgspec.Struct):
|
|
|
170
153
|
"match": [{"path": [path]}],
|
|
171
154
|
}
|
|
172
155
|
)
|
|
173
|
-
|
|
174
|
-
return
|
|
156
|
+
sub_routes.append({"handle": [reverse_proxy]})
|
|
157
|
+
return routes
|
|
158
|
+
|
|
159
|
+
def teardown(self):
|
|
160
|
+
current_config = json.loads(
|
|
161
|
+
self.conn.run(
|
|
162
|
+
"curl http://localhost:2019/config/apps/http/servers/srv0"
|
|
163
|
+
).stdout.strip()
|
|
164
|
+
)
|
|
165
|
+
existing_routes: list[dict] = current_config.get("routes", [])
|
|
166
|
+
new_routes = [r for r in existing_routes if r.get("group") != self.app_name]
|
|
167
|
+
current_config["routes"] = new_routes
|
|
168
|
+
self.conn.run(
|
|
169
|
+
f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def start(self) -> None:
|
|
173
|
+
self.run_pty("sudo systemctl start caddy-api")
|
|
174
|
+
|
|
175
|
+
def stop(self) -> None:
|
|
176
|
+
self.run_pty("sudo systemctl stop caddy-api")
|
|
177
|
+
|
|
178
|
+
def status(self) -> None:
|
|
179
|
+
self.run_pty("sudo systemctl status caddy-api", warn=True)
|
|
180
|
+
|
|
181
|
+
def restart(self) -> None:
|
|
182
|
+
self.run_pty("sudo systemctl restart caddy-api")
|
|
183
|
+
|
|
184
|
+
def logs(self) -> None:
|
|
185
|
+
self.run_pty(f"sudo journalctl -u caddy-api -f", warn=True)
|
|
186
|
+
|
|
187
|
+
def export_config(self) -> None:
|
|
188
|
+
self.config_file.write_text(json.dumps(self._get_routes()))
|
|
175
189
|
|
|
176
190
|
|
|
177
191
|
def get_latest_gh_tag() -> str:
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
|
|
3
|
-
fujin/config.py,sha256=
|
|
3
|
+
fujin/config.py,sha256=r2Rdd7ErGGTQPLMJlqZbPEQXd6fSSQB7PNbyaurEQsQ,9394
|
|
4
4
|
fujin/connection.py,sha256=ZkYaNykRFj9Yr-K-vOrZtVVGUDurDm6W7OQrgct71CA,2428
|
|
5
5
|
fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
|
|
6
6
|
fujin/hooks.py,sha256=QHIqxLxujG2U70UkN1BpUplE6tTqn7pFJP5oHde1tUQ,1350
|
|
7
7
|
fujin/commands/__init__.py,sha256=uIGGXt8YofL5RZn8KIy153ioWGoCl32ffHtqOhB-6ZM,78
|
|
8
8
|
fujin/commands/_base.py,sha256=o3R4-c3XeFWTIW3stiUdrcCPwdjzfjUVIpZy2L1-gZ4,2525
|
|
9
|
-
fujin/commands/app.py,sha256=
|
|
10
|
-
fujin/commands/config.py,sha256=
|
|
11
|
-
fujin/commands/deploy.py,sha256=
|
|
9
|
+
fujin/commands/app.py,sha256=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
|
|
10
|
+
fujin/commands/config.py,sha256=xdfd1OZLxw2YZldiAbW5rq5EBXEaXbUC-I7FKLRfzIQ,2387
|
|
11
|
+
fujin/commands/deploy.py,sha256=oAOLUrtHjHkmIA3D0AiaJRWEsfVk51_I29kRC_FhjYo,5463
|
|
12
12
|
fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
|
|
13
|
-
fujin/commands/down.py,sha256=
|
|
14
|
-
fujin/commands/init.py,sha256=
|
|
15
|
-
fujin/commands/proxy.py,sha256=
|
|
13
|
+
fujin/commands/down.py,sha256=v1lAq70ApktjeHRB_1sCzjmKH8t6EXqyL4RTt7OE-f0,1716
|
|
14
|
+
fujin/commands/init.py,sha256=IguExzQEPJ5CbWzG-mI6hIXUw2fd2rAEA04FK6-5l18,4021
|
|
15
|
+
fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
|
|
16
16
|
fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
|
|
17
|
-
fujin/commands/redeploy.py,sha256=
|
|
17
|
+
fujin/commands/redeploy.py,sha256=JvCJBZBcCKkUw1efZwRPJMLUAV8oqBAZeSbUBLHyn3k,2185
|
|
18
18
|
fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
|
|
19
19
|
fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
|
|
20
|
-
fujin/commands/server.py,sha256=
|
|
20
|
+
fujin/commands/server.py,sha256=0N_P_Luj31t56riZ8GfgRqW3vRHiw0cDrlp3PFoyWn8,3453
|
|
21
21
|
fujin/commands/up.py,sha256=DgDN-1mc_mMHJRCIvcB947Cd5a7phunu9NpXloGK0UU,419
|
|
22
22
|
fujin/process_managers/__init__.py,sha256=MhhfTBhm64zWRAKgjvsZRIToOUJus60vGScbAjqpQ6Y,994
|
|
23
23
|
fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAPBObk,5603
|
|
24
24
|
fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
|
|
25
|
-
fujin/proxies/caddy.py,sha256=
|
|
25
|
+
fujin/proxies/caddy.py,sha256=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
|
|
26
26
|
fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
|
|
27
27
|
fujin/proxies/nginx.py,sha256=8AkbJAjj6B0fxgv671mGDbx3LY_dY5wxFov80XmSfUY,4139
|
|
28
28
|
fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
|
|
29
29
|
fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
|
|
30
30
|
fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
|
|
31
|
-
fujin_cli-0.
|
|
32
|
-
fujin_cli-0.
|
|
33
|
-
fujin_cli-0.
|
|
34
|
-
fujin_cli-0.
|
|
35
|
-
fujin_cli-0.
|
|
31
|
+
fujin_cli-0.5.0.dist-info/METADATA,sha256=_-oV3GG6q1UZHhoXF5oOVTL8t1qOk_mRhmrUNz9CB-E,4396
|
|
32
|
+
fujin_cli-0.5.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
33
|
+
fujin_cli-0.5.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
|
|
34
|
+
fujin_cli-0.5.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
|
|
35
|
+
fujin_cli-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|