fujin-cli 0.1.0__py3-none-any.whl → 0.2.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/__init__.py +0 -2
- fujin/__main__.py +53 -0
- fujin/commands/__init__.py +3 -0
- fujin/commands/_base.py +83 -0
- fujin/commands/app.py +70 -0
- fujin/commands/config.py +174 -0
- fujin/commands/deploy.py +54 -0
- fujin/commands/down.py +30 -0
- fujin/commands/redeploy.py +19 -0
- fujin/commands/server.py +57 -0
- fujin/commands/up.py +16 -0
- fujin/config.py +131 -0
- fujin/errors.py +5 -0
- fujin/hooks.py +15 -0
- fujin/host.py +85 -0
- fujin/process_managers/__init__.py +29 -0
- fujin/process_managers/systemd.py +105 -0
- fujin/proxies/__init__.py +18 -0
- fujin/proxies/caddy.py +52 -0
- fujin/proxies/dummy.py +16 -0
- fujin/proxies/nginx.py +59 -0
- fujin/templates/other.service +15 -0
- fujin/templates/web.service +24 -0
- fujin/templates/web.socket +11 -0
- fujin_cli-0.2.0.dist-info/METADATA +52 -0
- fujin_cli-0.2.0.dist-info/RECORD +29 -0
- fujin_cli-0.2.0.dist-info/entry_points.txt +2 -0
- fujin/fabfile.py +0 -202
- fujin/utils.py +0 -60
- fujin_cli-0.1.0.dist-info/METADATA +0 -41
- fujin_cli-0.1.0.dist-info/RECORD +0 -8
- fujin_cli-0.1.0.dist-info/entry_points.txt +0 -2
- {fujin_cli-0.1.0.dist-info → fujin_cli-0.2.0.dist-info}/WHEEL +0 -0
- {fujin_cli-0.1.0.dist-info → fujin_cli-0.2.0.dist-info}/licenses/LICENSE.txt +0 -0
fujin/config.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import msgspec
|
|
8
|
+
|
|
9
|
+
from .errors import ImproperlyConfiguredError
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
import tomllib
|
|
13
|
+
else:
|
|
14
|
+
import tomli as tomllib
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from enum import StrEnum
|
|
18
|
+
except ImportError:
|
|
19
|
+
from enum import Enum
|
|
20
|
+
|
|
21
|
+
class StrEnum(str, Enum):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Hook(StrEnum):
|
|
26
|
+
PRE_DEPLOY = "pre_deploy"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Config(msgspec.Struct, kw_only=True):
|
|
30
|
+
app: str
|
|
31
|
+
app_bin: str = ".venv/bin/{app}"
|
|
32
|
+
version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
|
|
33
|
+
python_version: str = msgspec.field(default_factory=lambda: find_python_version())
|
|
34
|
+
build_command: str
|
|
35
|
+
_distfile: str = msgspec.field(name="distfile")
|
|
36
|
+
aliases: dict[str, str]
|
|
37
|
+
hosts: dict[str, HostConfig]
|
|
38
|
+
processes: dict[str, str]
|
|
39
|
+
process_manager: str = "fujin.process_managers.systemd"
|
|
40
|
+
webserver: Webserver
|
|
41
|
+
_requirements: str = msgspec.field(name="requirements", default="requirements.txt")
|
|
42
|
+
hooks: dict[Hook, str] = msgspec.field(default=dict)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
self.app_bin = self.app_bin.format(app=self.app)
|
|
46
|
+
self._distfile = self._distfile.format(version=self.version)
|
|
47
|
+
|
|
48
|
+
if "web" not in self.processes and self.webserver.type != "fujin.proxies.dummy":
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"Missing web process or set the proxy to 'fujin.proxies.dummy' to disable the use of a proxy"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def distfile(self) -> Path:
|
|
55
|
+
return Path(self._distfile)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def requirements(self) -> Path:
|
|
59
|
+
return Path(self._requirements)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def read(cls) -> Config:
|
|
63
|
+
fujin_toml = Path("fujin.toml")
|
|
64
|
+
if not fujin_toml.exists():
|
|
65
|
+
raise ImproperlyConfiguredError(
|
|
66
|
+
"No fujin.toml file found in the current directory"
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
return msgspec.toml.decode(fujin_toml.read_text(), type=cls)
|
|
70
|
+
except msgspec.ValidationError as e:
|
|
71
|
+
raise ImproperlyConfiguredError(f"Improperly configured, {e}") from e
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HostConfig(msgspec.Struct, kw_only=True):
|
|
75
|
+
ip: str
|
|
76
|
+
domain_name: str
|
|
77
|
+
user: str
|
|
78
|
+
_envfile: str = msgspec.field(name="envfile")
|
|
79
|
+
projects_dir: str = "/home/{user}/.local/share/fujin"
|
|
80
|
+
password_env: str | None = None
|
|
81
|
+
ssh_port: int = 22
|
|
82
|
+
_key_filename: str | None = msgspec.field(name="key_filename", default=None)
|
|
83
|
+
default: bool = False
|
|
84
|
+
|
|
85
|
+
def __post_init__(self):
|
|
86
|
+
self.projects_dir = self.projects_dir.format(user=self.user)
|
|
87
|
+
|
|
88
|
+
def to_dict(self):
|
|
89
|
+
return {f: getattr(self, f) for f in self.__struct_fields__}
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def envfile(self) -> Path:
|
|
93
|
+
return Path(self._envfile)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def key_filename(self) -> Path | None:
|
|
97
|
+
if self._key_filename:
|
|
98
|
+
return Path(self._key_filename)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def password(self) -> str | None:
|
|
102
|
+
if not self.password_env:
|
|
103
|
+
return
|
|
104
|
+
password = os.getenv(self.password_env)
|
|
105
|
+
if not password:
|
|
106
|
+
msg = f"Env {self.password_env} can not be found"
|
|
107
|
+
raise ImproperlyConfiguredError(msg)
|
|
108
|
+
return password
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Webserver(msgspec.Struct):
|
|
112
|
+
upstream: str
|
|
113
|
+
type: str = "fujin.proxies.caddy"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def read_version_from_pyproject():
|
|
117
|
+
try:
|
|
118
|
+
return tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
|
|
119
|
+
except (FileNotFoundError, KeyError) as e:
|
|
120
|
+
raise msgspec.ValidationError(
|
|
121
|
+
"Project version was not found in the pyproject.toml file, define it manually"
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def find_python_version():
|
|
126
|
+
py_version_file = Path(".python-version")
|
|
127
|
+
if not py_version_file.exists():
|
|
128
|
+
raise msgspec.ValidationError(
|
|
129
|
+
f"Add a python_version key or a .python-version file"
|
|
130
|
+
)
|
|
131
|
+
return py_version_file.read_text().strip()
|
fujin/errors.py
ADDED
fujin/hooks.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from fujin.config import Config
|
|
4
|
+
from fujin.host import Host
|
|
5
|
+
from fujin.config import Hook
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class HookManager:
|
|
10
|
+
config: Config
|
|
11
|
+
host: Host
|
|
12
|
+
|
|
13
|
+
def pre_deploy(self):
|
|
14
|
+
if pre_deploy := self.config.hooks.get(Hook.PRE_DEPLOY):
|
|
15
|
+
self.host.run(pre_deploy)
|
fujin/host.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
import cappa
|
|
8
|
+
from fabric import Connection
|
|
9
|
+
from invoke import Responder
|
|
10
|
+
from paramiko.ssh_exception import AuthenticationException
|
|
11
|
+
|
|
12
|
+
from .config import HostConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Host:
|
|
17
|
+
config: HostConfig
|
|
18
|
+
|
|
19
|
+
def __str__(self):
|
|
20
|
+
return self.config.ip
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def watchers(self) -> list[Responder]:
|
|
24
|
+
if not self.config.password:
|
|
25
|
+
return []
|
|
26
|
+
return [
|
|
27
|
+
Responder(
|
|
28
|
+
pattern=r"\[sudo\] password:",
|
|
29
|
+
response=f"{self.config.password}\n",
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
@cached_property
|
|
34
|
+
def connection(self) -> Connection:
|
|
35
|
+
connect_kwargs = None
|
|
36
|
+
if self.config.key_filename:
|
|
37
|
+
connect_kwargs = {"key_filename": str(self.config.key_filename)}
|
|
38
|
+
elif self.config.password:
|
|
39
|
+
connect_kwargs = {"password": self.config.password}
|
|
40
|
+
|
|
41
|
+
return Connection(
|
|
42
|
+
self.config.ip,
|
|
43
|
+
user=self.config.user,
|
|
44
|
+
port=self.config.ssh_port,
|
|
45
|
+
connect_kwargs=connect_kwargs,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def run(self, args: str, **kwargs):
|
|
49
|
+
try:
|
|
50
|
+
return self.connection.run(args, **kwargs, watchers=self.watchers)
|
|
51
|
+
except AuthenticationException as e:
|
|
52
|
+
msg = f"Authentication failed for {self.config.user}@{self.config.ip} -p {self.config.ssh_port}.\n"
|
|
53
|
+
if self.config.key_filename:
|
|
54
|
+
msg += f"An SSH key was provided at {self.config.key_filename.resolve()}. Please verify its validity and correctness."
|
|
55
|
+
elif self.config.password:
|
|
56
|
+
msg += f"A password was provided through the environment variable {self.config.password_env}. Please ensure it is correct for the user {self.config.user}."
|
|
57
|
+
else:
|
|
58
|
+
msg += "No password or SSH key was provided. Ensure your current host has SSH access to the target host."
|
|
59
|
+
raise cappa.Exit(msg, code=1) from e
|
|
60
|
+
|
|
61
|
+
def put(self, *args, **kwargs):
|
|
62
|
+
return self.connection.put(args, **kwargs, watchers=self.watchers)
|
|
63
|
+
|
|
64
|
+
def get(self, *args, **kwargs):
|
|
65
|
+
return self.connection.get(args, **kwargs, watchers=self.watchers)
|
|
66
|
+
|
|
67
|
+
def sudo(self, args: str, **kwargs):
|
|
68
|
+
return self.connection.sudo(args, **kwargs)
|
|
69
|
+
|
|
70
|
+
def run_uv(self, args: str, **kwargs):
|
|
71
|
+
return self.run(f"/home/{self.config.user}/.cargo/bin/uv {args}", **kwargs)
|
|
72
|
+
|
|
73
|
+
def run_caddy(self, args: str, **kwargs):
|
|
74
|
+
return self.run(f"/home/{self.config.user}/.local/bin/caddy {args}", **kwargs)
|
|
75
|
+
|
|
76
|
+
def make_project_dir(self, project_name: str):
|
|
77
|
+
self.run(f"mkdir -p {self.project_dir(project_name)}")
|
|
78
|
+
|
|
79
|
+
def project_dir(self, project_name: str) -> str:
|
|
80
|
+
return f"{self.config.projects_dir}/{project_name}"
|
|
81
|
+
|
|
82
|
+
@contextmanager
|
|
83
|
+
def cd_project_dir(self, project_name: str):
|
|
84
|
+
with self.connection.cd(self.project_dir(project_name)):
|
|
85
|
+
yield
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from fujin.config import Config
|
|
7
|
+
from fujin.host import Host
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProcessManager(Protocol):
|
|
11
|
+
host: Host
|
|
12
|
+
config: Config
|
|
13
|
+
service_names: list[str]
|
|
14
|
+
|
|
15
|
+
def get_service_name(self, name: str): ...
|
|
16
|
+
|
|
17
|
+
def install_services(self) -> None: ...
|
|
18
|
+
|
|
19
|
+
def uninstall_services(self) -> None: ...
|
|
20
|
+
|
|
21
|
+
def start_services(self, *names) -> None: ...
|
|
22
|
+
|
|
23
|
+
def restart_services(self, *names) -> None: ...
|
|
24
|
+
|
|
25
|
+
def stop_services(self, *names) -> None: ...
|
|
26
|
+
|
|
27
|
+
def service_logs(self, name: str, follow: bool = False): ...
|
|
28
|
+
|
|
29
|
+
def reload_configuration(self) -> None: ...
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fujin.config import Config
|
|
8
|
+
from fujin.host import Host
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class SystemdFile:
|
|
13
|
+
name: str
|
|
14
|
+
body: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class ProcessManager:
|
|
19
|
+
config: Config
|
|
20
|
+
host: Host
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def service_names(self) -> list[str]:
|
|
24
|
+
return [self.get_service_name(name) for name in self.config.processes]
|
|
25
|
+
|
|
26
|
+
def get_service_name(self, name: str):
|
|
27
|
+
if name == "web":
|
|
28
|
+
return self.config.app
|
|
29
|
+
return f"{self.config.app}-{name}.service"
|
|
30
|
+
|
|
31
|
+
def install_services(self) -> None:
|
|
32
|
+
conf_files = self.get_configuration_files()
|
|
33
|
+
for conf_file in conf_files:
|
|
34
|
+
self.host.sudo(
|
|
35
|
+
f"echo '{conf_file.body}' | sudo tee /etc/systemd/system/{conf_file.name}",
|
|
36
|
+
hide="out",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.host.sudo(f"systemctl enable --now {self.config.app}.socket")
|
|
40
|
+
for name in self.service_names:
|
|
41
|
+
# the main web service is launched by the socket service
|
|
42
|
+
if name != f"{self.config.app}.service":
|
|
43
|
+
self.host.sudo(f"systemctl enable {name}")
|
|
44
|
+
|
|
45
|
+
def get_configuration_files(self) -> list[SystemdFile]:
|
|
46
|
+
templates_folder = (
|
|
47
|
+
Path(importlib.util.find_spec("fujin").origin).parent / "templates"
|
|
48
|
+
)
|
|
49
|
+
web_service_content = (templates_folder / "web.service").read_text()
|
|
50
|
+
web_socket_content = (templates_folder / "web.socket").read_text()
|
|
51
|
+
other_service_content = (templates_folder / "other.service").read_text()
|
|
52
|
+
context = {
|
|
53
|
+
"app": self.config.app,
|
|
54
|
+
"user": self.host.config.user,
|
|
55
|
+
"project_dir": self.host.project_dir(self.config.app),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
files = []
|
|
59
|
+
for name, command in self.config.processes.items():
|
|
60
|
+
name = self.get_service_name(name)
|
|
61
|
+
if name == "web":
|
|
62
|
+
body = web_service_content.format(**context, command=command)
|
|
63
|
+
files.append(
|
|
64
|
+
SystemdFile(
|
|
65
|
+
name=f"{self.config.app}.socket",
|
|
66
|
+
body=web_socket_content.format(**context),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
body = other_service_content.format(**context, command=command)
|
|
71
|
+
files.append(SystemdFile(name=name, body=body))
|
|
72
|
+
return files
|
|
73
|
+
|
|
74
|
+
def uninstall_services(self) -> None:
|
|
75
|
+
self.stop_services()
|
|
76
|
+
self.host.sudo(f"systemctl disable {self.config.app}.socket")
|
|
77
|
+
for name in self.service_names:
|
|
78
|
+
# was never enabled in the first place, look at the code above
|
|
79
|
+
if name != f"{self.config.app}.service":
|
|
80
|
+
self.host.sudo(f"systemctl disable {name}")
|
|
81
|
+
|
|
82
|
+
def start_services(self, *names) -> None:
|
|
83
|
+
names = names or self.service_names
|
|
84
|
+
for name in names:
|
|
85
|
+
if name in self.service_names:
|
|
86
|
+
self.host.sudo(f"systemctl start {name}")
|
|
87
|
+
|
|
88
|
+
def restart_services(self, *names) -> None:
|
|
89
|
+
names = names or self.service_names
|
|
90
|
+
for name in names:
|
|
91
|
+
if name in self.service_names:
|
|
92
|
+
self.host.sudo(f"systemctl restart {name}")
|
|
93
|
+
|
|
94
|
+
def stop_services(self, *names) -> None:
|
|
95
|
+
names = names or self.service_names
|
|
96
|
+
for name in names:
|
|
97
|
+
if name in self.service_names:
|
|
98
|
+
self.host.sudo(f"systemctl stop {name}")
|
|
99
|
+
|
|
100
|
+
def service_logs(self, name: str, follow: bool = False):
|
|
101
|
+
# TODO: add more options here
|
|
102
|
+
self.host.sudo(f"journalctl -u {name} -r {'-f' if follow else ''}")
|
|
103
|
+
|
|
104
|
+
def reload_configuration(self) -> None:
|
|
105
|
+
self.host.sudo(f"systemctl daemon-reload")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Protocol
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from fujin.config import Config
|
|
7
|
+
from fujin.host import Host
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WebProxy(Protocol):
|
|
11
|
+
host: Host
|
|
12
|
+
config: Config
|
|
13
|
+
|
|
14
|
+
def install(self) -> None: ...
|
|
15
|
+
|
|
16
|
+
def setup(self) -> None: ...
|
|
17
|
+
|
|
18
|
+
def teardown(self) -> None: ...
|
fujin/proxies/caddy.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from fujin.config import Config
|
|
6
|
+
from fujin.host import Host
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WebProxy(msgspec.Struct):
|
|
10
|
+
host: Host
|
|
11
|
+
config: Config
|
|
12
|
+
|
|
13
|
+
def install(self):
|
|
14
|
+
self.host.run_uv("tool install caddy-bin")
|
|
15
|
+
self.host.run_caddy("start", pty=True)
|
|
16
|
+
|
|
17
|
+
def setup(self):
|
|
18
|
+
with self.host.cd_project_dir(self.config.app):
|
|
19
|
+
self.host.run(f"echo '{json.dumps(self._generate_config())}' > caddy.json")
|
|
20
|
+
self.host.run(
|
|
21
|
+
f"curl localhost:2019/load -H 'Content-Type: application/json' -d @caddy.json"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def teardown(self):
|
|
25
|
+
# TODO
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def _generate_config(self) -> dict:
|
|
29
|
+
return {
|
|
30
|
+
"apps": {
|
|
31
|
+
"http": {
|
|
32
|
+
"servers": {
|
|
33
|
+
self.config.app: {
|
|
34
|
+
"listen": [":443"],
|
|
35
|
+
"routes": [
|
|
36
|
+
{
|
|
37
|
+
"match": [{"host": [self.host.config.domain_name]}],
|
|
38
|
+
"handle": [
|
|
39
|
+
{
|
|
40
|
+
"handler": "reverse_proxy",
|
|
41
|
+
"upstreams": [
|
|
42
|
+
{"dial": self.config.webserver.upstream}
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
fujin/proxies/dummy.py
ADDED
fujin/proxies/nginx.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
|
|
3
|
+
from fujin.config import Config
|
|
4
|
+
from fujin.host import Host
|
|
5
|
+
|
|
6
|
+
CERTBOT_EMAIL = ""
|
|
7
|
+
|
|
8
|
+
# TODO: this is a wip
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WebProxy(msgspec.Struct):
|
|
12
|
+
host: Host
|
|
13
|
+
config: Config
|
|
14
|
+
|
|
15
|
+
def install(self):
|
|
16
|
+
# TODO: won"t always install the latest version, install certbot with uv ?
|
|
17
|
+
self.host.sudo(
|
|
18
|
+
"apt install -y nginx libpq-dev python3-dev python3-certbot-nginx sqlite3"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def setup(self):
|
|
22
|
+
self.host.sudo(
|
|
23
|
+
f"echo '{self._get_config()}' | sudo tee /etc/nginx/sites-available/{self.config.app}",
|
|
24
|
+
hide="out",
|
|
25
|
+
)
|
|
26
|
+
self.host.sudo(
|
|
27
|
+
f"ln -sf /etc/nginx/sites-available/{self.config.app} /etc/nginx/sites-enabled/{self.config.app}"
|
|
28
|
+
)
|
|
29
|
+
self.host.sudo(
|
|
30
|
+
f"certbot --nginx -d {self.host.config.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
|
|
31
|
+
)
|
|
32
|
+
# Updating local Nginx configuration
|
|
33
|
+
self.host.get(
|
|
34
|
+
f"/etc/nginx/sites-available/{self.config.app}",
|
|
35
|
+
f".fujin/{self.config.app}",
|
|
36
|
+
)
|
|
37
|
+
# Enabling certificate auto-renewal
|
|
38
|
+
self.host.sudo("systemctl enable certbot.timer")
|
|
39
|
+
self.host.sudo("systemctl start certbot.timer")
|
|
40
|
+
|
|
41
|
+
def teardown(self):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def _get_config(self) -> str:
|
|
45
|
+
return f"""
|
|
46
|
+
server {{
|
|
47
|
+
listen 80;
|
|
48
|
+
server_name {self.host.config.domain_name};
|
|
49
|
+
|
|
50
|
+
location / {{
|
|
51
|
+
proxy_pass {self.config.webserver.upstream};
|
|
52
|
+
proxy_set_header Host $host;
|
|
53
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
54
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
55
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
56
|
+
}}
|
|
57
|
+
}}
|
|
58
|
+
|
|
59
|
+
"""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description={app} Worker
|
|
3
|
+
|
|
4
|
+
[Service]
|
|
5
|
+
User={user}
|
|
6
|
+
Group=www-data
|
|
7
|
+
WorkingDirectory={project_dir}
|
|
8
|
+
ExecStart={command}
|
|
9
|
+
EnvironmentFile={project_dir}/.env
|
|
10
|
+
Restart=always
|
|
11
|
+
#StandardOutput=append:/var/log/your_project/qcluster.out.log
|
|
12
|
+
#StandardError=append:/var/log/your_project/qcluster.err.log
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TODO: add tons of comments in this file
|
|
2
|
+
[Unit]
|
|
3
|
+
Description={app} daemon
|
|
4
|
+
Requires={app}.socket
|
|
5
|
+
After=network.target
|
|
6
|
+
|
|
7
|
+
[Service]
|
|
8
|
+
Type=notify
|
|
9
|
+
NotifyAccess=main
|
|
10
|
+
User={user}
|
|
11
|
+
Group=www-data
|
|
12
|
+
RuntimeDirectory={app}
|
|
13
|
+
WorkingDirectory={project_dir}
|
|
14
|
+
ExecStart={project_dir}/{command}
|
|
15
|
+
EnvironmentFile={project_dir}/.env
|
|
16
|
+
ExecReload=/bin/kill -s HUP $MAINPID
|
|
17
|
+
KillMode=mixed
|
|
18
|
+
TimeoutStopSec=5
|
|
19
|
+
PrivateTmp=true
|
|
20
|
+
# if your app does not need administrative capabilities, let systemd know
|
|
21
|
+
# ProtectSystem=strict
|
|
22
|
+
|
|
23
|
+
[Install]
|
|
24
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fujin-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Project-URL: Documentation, https://github.com/falcopackages/fujin#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/falcopackages/fujin/issues
|
|
7
|
+
Project-URL: Source, https://github.com/falcopackages/fujin
|
|
8
|
+
Author-email: Tobi DEGNON <tobidegnon@proton.me>
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: cappa>=0.24.0
|
|
22
|
+
Requires-Dist: fabric>=3.2.2
|
|
23
|
+
Requires-Dist: msgspec[toml]>=0.18.6
|
|
24
|
+
Requires-Dist: rich>=13.9.2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# fujin
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/fujin-cli)
|
|
30
|
+
[](https://pypi.org/project/fujin-cli)
|
|
31
|
+
|
|
32
|
+
-----
|
|
33
|
+
|
|
34
|
+
> [!IMPORTANT]
|
|
35
|
+
> This package currently contains no features and is a work-in-progress
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
**Table of Contents**
|
|
39
|
+
|
|
40
|
+
- [fujin](#fujin)
|
|
41
|
+
- [Installation](#installation)
|
|
42
|
+
- [License](#license)
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
pip install fujin-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
`fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
fujin/__main__.py,sha256=cmbkKqDbGyc_himfDstDjC5SRi07-rU3oxWgoL_v_6c,1278
|
|
3
|
+
fujin/config.py,sha256=5g_KYGc3IDdHLZRLYiw55hGob85Yxovm-47JHiEXcYE,3896
|
|
4
|
+
fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
|
|
5
|
+
fujin/hooks.py,sha256=Nmi0psj0cwomSaJM6l0r2V5iVn1x0xdrazW1CRQ2wyw,346
|
|
6
|
+
fujin/host.py,sha256=uNYeYO2J5PQ7U3W8Olng7MuRK6uzIoom2yDNX914F14,3023
|
|
7
|
+
fujin/commands/__init__.py,sha256=-9mrBJHtwQELh58HlMEjCE60nowtGzySy4-inph3YY0,78
|
|
8
|
+
fujin/commands/_base.py,sha256=IYK7rXB5QW2Ac4pcgafQTjB1SPPht1ERWE_5WFzdSfo,2765
|
|
9
|
+
fujin/commands/app.py,sha256=Cu3EW1FbOqR40ArsbBpwA3nLCX41leys398obc5lRbM,2347
|
|
10
|
+
fujin/commands/config.py,sha256=bfWCyGYz-n-VNtcs-HiKXB7eP-K_YQkBGTPAqetVYYU,6136
|
|
11
|
+
fujin/commands/deploy.py,sha256=gHrqPdZ8btB9CcwjVv1BDwbeD0RIoMALIeh7cNzUsm8,2032
|
|
12
|
+
fujin/commands/down.py,sha256=iUneVfELnWmh-9NqVGCAwhF1nGSp4mZQAmW6_ER_DTg,1109
|
|
13
|
+
fujin/commands/redeploy.py,sha256=3xtExNvNHDoXgWsY3-evZnBHAEaGKRVyAB4h-9YiVpM,533
|
|
14
|
+
fujin/commands/server.py,sha256=UOl4pQWfj_sCc5l5EBx2PQ8UqIrMuEBJyuT-DvXFK1E,2186
|
|
15
|
+
fujin/commands/up.py,sha256=kkHbNSzTogA_vGqPwzK8a85Hey3rPHSmz2YRqWeQ100,450
|
|
16
|
+
fujin/process_managers/__init__.py,sha256=izJCtaGO-Fwptx4NHHjp8eOFuWfgjRMjt6YiQ0ZkXbA,672
|
|
17
|
+
fujin/process_managers/systemd.py,sha256=Ltt9i1EaY2Ip4WdbcSmuRVvv-Z04lm5izuOT3hxJB1M,3770
|
|
18
|
+
fujin/proxies/__init__.py,sha256=j3YdWdDBAfJ0-EL4APR0gkwTFkZ0BcUJQ_wg8bWVsGE,335
|
|
19
|
+
fujin/proxies/caddy.py,sha256=ehngeBOpUWqR7bJLjdVtoMhEqwStS3FZrZBzzhdA0-g,1620
|
|
20
|
+
fujin/proxies/dummy.py,sha256=Ur1mNxQGBmVLNJ0PV2ODR0fDw8VDItJ6-P1GOUc6o1o,222
|
|
21
|
+
fujin/proxies/nginx.py,sha256=1vNzxtulx6tWe-lW5JV9fK7XrZWdf9_v-VjyHdgHEXQ,1686
|
|
22
|
+
fujin/templates/other.service,sha256=XJUaQ0MxRuqT6KNkcn5ZPrjVtWh1zY9qbOzIhxGYOms,331
|
|
23
|
+
fujin/templates/web.service,sha256=TVMELmlOVo_SZxUqkpyizvZeX0T4b1sL1WVpbJ0D-DE,528
|
|
24
|
+
fujin/templates/web.socket,sha256=W5EtR-J1GNMaMqZDQduSjG2JY7gFFPKEDpPolFus9c0,163
|
|
25
|
+
fujin_cli-0.2.0.dist-info/METADATA,sha256=ahTCcNWwl7V9Zs0RWVH1UJ_L0EURdJliSr7d-wuK1Js,1660
|
|
26
|
+
fujin_cli-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
27
|
+
fujin_cli-0.2.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
|
|
28
|
+
fujin_cli-0.2.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
|
|
29
|
+
fujin_cli-0.2.0.dist-info/RECORD,,
|