fujin-cli 0.2.0__py3-none-any.whl → 0.4.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/__main__.py +22 -4
- fujin/commands/__init__.py +1 -2
- fujin/commands/_base.py +34 -41
- fujin/commands/app.py +78 -10
- fujin/commands/config.py +16 -125
- fujin/commands/deploy.py +93 -31
- fujin/commands/docs.py +16 -0
- fujin/commands/down.py +33 -15
- fujin/commands/init.py +82 -0
- fujin/commands/proxy.py +71 -0
- fujin/commands/prune.py +42 -0
- fujin/commands/redeploy.py +39 -10
- fujin/commands/rollback.py +49 -0
- fujin/commands/secrets.py +11 -0
- fujin/commands/server.py +65 -30
- fujin/commands/up.py +4 -5
- fujin/config.py +186 -27
- fujin/connection.py +75 -0
- fujin/hooks.py +48 -8
- fujin/process_managers/__init__.py +16 -5
- fujin/process_managers/systemd.py +98 -48
- fujin/proxies/__init__.py +23 -6
- fujin/proxies/caddy.py +195 -30
- fujin/proxies/dummy.py +16 -3
- fujin/proxies/nginx.py +109 -36
- fujin/templates/simple.service +14 -0
- fujin/templates/web.service +12 -11
- fujin/templates/web.socket +2 -2
- fujin_cli-0.4.0.dist-info/METADATA +66 -0
- fujin_cli-0.4.0.dist-info/RECORD +35 -0
- fujin/host.py +0 -85
- fujin/templates/other.service +0 -15
- fujin_cli-0.2.0.dist-info/METADATA +0 -52
- fujin_cli-0.2.0.dist-info/RECORD +0 -29
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.4.0.dist-info}/WHEEL +0 -0
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.4.0.dist-info}/entry_points.txt +0 -0
- {fujin_cli-0.2.0.dist-info → fujin_cli-0.4.0.dist-info}/licenses/LICENSE.txt +0 -0
fujin/proxies/caddy.py
CHANGED
|
@@ -1,52 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
4
|
+
import urllib.request
|
|
5
|
+
from pathlib import Path
|
|
2
6
|
|
|
3
7
|
import msgspec
|
|
4
8
|
|
|
5
9
|
from fujin.config import Config
|
|
6
|
-
from fujin.
|
|
10
|
+
from fujin.connection import Connection
|
|
11
|
+
|
|
12
|
+
DEFAULT_VERSION = "2.8.4"
|
|
13
|
+
GH_TAR_FILENAME = "caddy_{version}_linux_amd64.tar.gz"
|
|
14
|
+
GH_DOWNL0AD_URL = (
|
|
15
|
+
"https://github.com/caddyserver/caddy/releases/download/v{version}/"
|
|
16
|
+
+ GH_TAR_FILENAME
|
|
17
|
+
)
|
|
18
|
+
GH_RELEASE_LATEST_URL = "https://api.github.com/repos/caddyserver/caddy/releases/latest"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TODO: let the user write the configuration with a simple syntax and export use caddy adapter, same for exporting,
|
|
22
|
+
# don't export to json
|
|
7
23
|
|
|
8
24
|
|
|
9
25
|
class WebProxy(msgspec.Struct):
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
conn: Connection
|
|
27
|
+
domain_name: str
|
|
28
|
+
app_name: str
|
|
29
|
+
upstream: str
|
|
30
|
+
statics: dict[str, str]
|
|
31
|
+
local_config_dir: Path
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def config_file(self) -> Path:
|
|
35
|
+
return self.local_config_dir / "caddy.json"
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def create(cls, config: Config, conn: Connection) -> WebProxy:
|
|
39
|
+
return cls(
|
|
40
|
+
conn=conn,
|
|
41
|
+
domain_name=config.host.domain_name,
|
|
42
|
+
upstream=config.webserver.upstream,
|
|
43
|
+
app_name=config.app_name,
|
|
44
|
+
local_config_dir=config.local_config_dir,
|
|
45
|
+
statics=config.webserver.statics,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def run_pty(self, *args, **kwargs):
|
|
49
|
+
return self.conn.run(*args, **kwargs, pty=True)
|
|
12
50
|
|
|
13
51
|
def install(self):
|
|
14
|
-
|
|
15
|
-
|
|
52
|
+
version = get_latest_gh_tag()
|
|
53
|
+
download_url = GH_DOWNL0AD_URL.format(version=version)
|
|
54
|
+
filename = GH_TAR_FILENAME.format(version=version)
|
|
55
|
+
with self.conn.cd("/tmp"):
|
|
56
|
+
self.conn.run(f"curl -O -L {download_url}")
|
|
57
|
+
self.conn.run(f"tar -xzvf {filename}")
|
|
58
|
+
self.run_pty("sudo mv caddy /usr/bin/")
|
|
59
|
+
self.conn.run(f"rm {filename}")
|
|
60
|
+
self.conn.run("rm LICENSE && rm README.md")
|
|
61
|
+
self.run_pty("sudo groupadd --force --system caddy")
|
|
62
|
+
self.conn.run(
|
|
63
|
+
"sudo useradd --system --gid caddy --create-home --home-dir /var/lib/caddy --shell /usr/sbin/nologin --comment 'Caddy web server' caddy",
|
|
64
|
+
pty=True,
|
|
65
|
+
warn=True,
|
|
66
|
+
)
|
|
67
|
+
self.conn.run(
|
|
68
|
+
f"echo '{systemd_service}' | sudo tee /etc/systemd/system/caddy-api.service",
|
|
69
|
+
hide="out",
|
|
70
|
+
pty=True,
|
|
71
|
+
)
|
|
72
|
+
self.run_pty("sudo systemctl daemon-reload")
|
|
73
|
+
self.run_pty("sudo systemctl enable --now caddy-api")
|
|
74
|
+
# to initialize the caddy config, when running setup on a fresh caddy setup it fails because the key config/apps was not previously defined
|
|
75
|
+
# TODO this will reset the config of any existing server, should probably do I check before running this
|
|
76
|
+
self.conn.run("""curl --silent http://localhost:2019/config/ -d '{"apps":{"http": {"servers": {}}}}' -H 'Content-Type: application/json'""")
|
|
77
|
+
|
|
78
|
+
def uninstall(self):
|
|
79
|
+
self.stop()
|
|
80
|
+
self.run_pty("sudo systemctl disable caddy-api")
|
|
81
|
+
self.run_pty("sudo rm /usr/bin/caddy")
|
|
82
|
+
self.run_pty("sudo rm /etc/systemd/system/caddy-api.service")
|
|
83
|
+
self.run_pty("sudo userdel caddy")
|
|
16
84
|
|
|
17
85
|
def setup(self):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
self.
|
|
21
|
-
|
|
22
|
-
|
|
86
|
+
config = (
|
|
87
|
+
json.loads(self.config_file.read_text())
|
|
88
|
+
if self.config_file.exists()
|
|
89
|
+
else self._get_config()
|
|
90
|
+
)
|
|
91
|
+
self.conn.run(f"echo '{json.dumps(config)}' > caddy.json")
|
|
92
|
+
self.conn.run(
|
|
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
|
|
23
96
|
|
|
24
97
|
def teardown(self):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
98
|
+
self.conn.run(f"echo '{json.dumps({})}' > caddy.json")
|
|
99
|
+
self.conn.run(
|
|
100
|
+
f"curl localhost:2019/config/apps/http/servers/{self.app_name} -H 'Content-Type: application/json' -d @caddy.json"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def start(self) -> None:
|
|
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:
|
|
122
|
+
handle = []
|
|
123
|
+
config = {
|
|
124
|
+
"listen": [":443"],
|
|
125
|
+
"routes": [
|
|
126
|
+
{
|
|
127
|
+
"match": [{"host": [self.domain_name]}],
|
|
128
|
+
"handle": handle,
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
}
|
|
132
|
+
reverse_proxy = {
|
|
133
|
+
"handler": "reverse_proxy",
|
|
134
|
+
"upstreams": [{"dial": self.upstream}],
|
|
135
|
+
}
|
|
136
|
+
if not self.statics:
|
|
137
|
+
handle.append(reverse_proxy)
|
|
138
|
+
return config
|
|
139
|
+
routes = []
|
|
140
|
+
handle.append({"handler": "subroute", "routes": routes})
|
|
141
|
+
for path, directory in self.statics.items():
|
|
142
|
+
strip_path_prefix = path.replace("/*", "")
|
|
143
|
+
if strip_path_prefix.endswith("/"):
|
|
144
|
+
strip_path_prefix = strip_path_prefix[:-1]
|
|
145
|
+
routes.append(
|
|
146
|
+
{
|
|
147
|
+
"handle": [
|
|
148
|
+
{
|
|
149
|
+
"handler": "subroute",
|
|
35
150
|
"routes": [
|
|
36
151
|
{
|
|
37
|
-
"match": [{"host": [self.host.config.domain_name]}],
|
|
38
152
|
"handle": [
|
|
39
153
|
{
|
|
40
|
-
"handler": "
|
|
41
|
-
"
|
|
42
|
-
{"dial": self.config.webserver.upstream}
|
|
43
|
-
],
|
|
154
|
+
"handler": "rewrite",
|
|
155
|
+
"strip_path_prefix": strip_path_prefix,
|
|
44
156
|
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"handle": [
|
|
161
|
+
{"handler": "vars", "root": directory},
|
|
162
|
+
{
|
|
163
|
+
"handler": "file_server",
|
|
164
|
+
},
|
|
165
|
+
]
|
|
166
|
+
},
|
|
47
167
|
],
|
|
48
168
|
}
|
|
49
|
-
|
|
169
|
+
],
|
|
170
|
+
"match": [{"path": [path]}],
|
|
50
171
|
}
|
|
51
|
-
|
|
52
|
-
}
|
|
172
|
+
)
|
|
173
|
+
routes.append({"handle": [reverse_proxy]})
|
|
174
|
+
return config
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_latest_gh_tag() -> str:
|
|
178
|
+
with urllib.request.urlopen(GH_RELEASE_LATEST_URL) as response:
|
|
179
|
+
if response.status != 200:
|
|
180
|
+
return DEFAULT_VERSION
|
|
181
|
+
try:
|
|
182
|
+
data = json.loads(response.read().decode())
|
|
183
|
+
return data["tag_name"][1:]
|
|
184
|
+
except (KeyError, json.JSONDecodeError):
|
|
185
|
+
return DEFAULT_VERSION
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
systemd_service = """
|
|
189
|
+
# caddy-api.service
|
|
190
|
+
#
|
|
191
|
+
# For using Caddy with its API.
|
|
192
|
+
#
|
|
193
|
+
# This unit is "durable" in that it will automatically resume
|
|
194
|
+
# the last active configuration if the service is restarted.
|
|
195
|
+
#
|
|
196
|
+
# See https://caddyserver.com/docs/install for instructions.
|
|
197
|
+
|
|
198
|
+
[Unit]
|
|
199
|
+
Description=Caddy
|
|
200
|
+
Documentation=https://caddyserver.com/docs/
|
|
201
|
+
After=network.target network-online.target
|
|
202
|
+
Requires=network-online.target
|
|
203
|
+
|
|
204
|
+
[Service]
|
|
205
|
+
Type=notify
|
|
206
|
+
User=caddy
|
|
207
|
+
Group=www-data
|
|
208
|
+
ExecStart=/usr/bin/caddy run --environ --resume
|
|
209
|
+
TimeoutStopSec=5s
|
|
210
|
+
LimitNOFILE=1048576
|
|
211
|
+
PrivateTmp=true
|
|
212
|
+
ProtectSystem=full
|
|
213
|
+
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
|
214
|
+
|
|
215
|
+
[Install]
|
|
216
|
+
WantedBy=multi-user.target
|
|
217
|
+
"""
|
fujin/proxies/dummy.py
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
1
5
|
from fujin.config import Config
|
|
2
|
-
from fujin.
|
|
6
|
+
from fujin.connection import Connection
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
class WebProxy:
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
config_file: Path
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def create(cls, _: Config, __: Connection) -> WebProxy:
|
|
14
|
+
return cls()
|
|
8
15
|
|
|
9
16
|
def install(self):
|
|
10
17
|
pass
|
|
11
18
|
|
|
19
|
+
def uninstall(self):
|
|
20
|
+
pass
|
|
21
|
+
|
|
12
22
|
def setup(self):
|
|
13
23
|
pass
|
|
14
24
|
|
|
15
25
|
def teardown(self):
|
|
16
26
|
pass
|
|
27
|
+
|
|
28
|
+
def export_config(self):
|
|
29
|
+
pass
|
fujin/proxies/nginx.py
CHANGED
|
@@ -1,59 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
1
6
|
import msgspec
|
|
2
7
|
|
|
3
8
|
from fujin.config import Config
|
|
4
|
-
from fujin.
|
|
5
|
-
|
|
6
|
-
CERTBOT_EMAIL = ""
|
|
9
|
+
from fujin.connection import Connection
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
CERTBOT_EMAIL = os.getenv("CERTBOT_EMAIL")
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class WebProxy(msgspec.Struct):
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
conn: Connection
|
|
16
|
+
domain_name: str
|
|
17
|
+
app_name: str
|
|
18
|
+
upstream: str
|
|
19
|
+
statics: dict[str, str]
|
|
20
|
+
local_config_dir: Path
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def config_file(self) -> Path:
|
|
24
|
+
return self.local_config_dir / f"{self.app_name}.conf"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def create(cls, config: Config, conn: Connection) -> WebProxy:
|
|
28
|
+
return cls(
|
|
29
|
+
conn=conn,
|
|
30
|
+
domain_name=config.host.domain_name,
|
|
31
|
+
upstream=config.webserver.upstream,
|
|
32
|
+
app_name=config.app_name,
|
|
33
|
+
local_config_dir=config.local_config_dir,
|
|
34
|
+
statics=config.webserver.statics,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def run_pty(self, *args, **kwargs):
|
|
38
|
+
return self.conn.run(*args, **kwargs, pty=True)
|
|
14
39
|
|
|
15
40
|
def install(self):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"apt install -y nginx libpq-dev python3-dev python3-certbot-nginx sqlite3"
|
|
41
|
+
self.conn.run(
|
|
42
|
+
"sudo apt install -y nginx libpq-dev python3-dev python3-certbot-nginx"
|
|
19
43
|
)
|
|
20
44
|
|
|
45
|
+
def uninstall(self):
|
|
46
|
+
self.stop()
|
|
47
|
+
self.conn.run("sudo apt remove -y nginx")
|
|
48
|
+
self.conn.run(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
|
|
49
|
+
self.conn.run(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
|
|
50
|
+
self.conn.run("sudo systemctl disable certbot.timer")
|
|
51
|
+
self.conn.run("sudo apt remove -y python3-certbot-nginx")
|
|
52
|
+
|
|
21
53
|
def setup(self):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
conf = (
|
|
55
|
+
self.config_file.read_text()
|
|
56
|
+
if self.config_file.exists()
|
|
57
|
+
else self._get_config()
|
|
25
58
|
)
|
|
26
|
-
self.
|
|
27
|
-
f"
|
|
28
|
-
|
|
29
|
-
self.host.sudo(
|
|
30
|
-
f"certbot --nginx -d {self.host.config.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
|
|
59
|
+
self.run_pty(
|
|
60
|
+
f"sudo echo '{conf}' | sudo tee /etc/nginx/sites-available/{self.app_name}.conf",
|
|
61
|
+
hide="out",
|
|
31
62
|
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
f"/etc/nginx/sites-available/{self.config.app}",
|
|
35
|
-
f".fujin/{self.config.app}",
|
|
63
|
+
self.run_pty(
|
|
64
|
+
f"sudo ln -sf /etc/nginx/sites-available/{self.app_name}.conf /etc/nginx/sites-enabled/{self.app_name}.conf",
|
|
36
65
|
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
if CERTBOT_EMAIL:
|
|
67
|
+
cert_path = f"/etc/letsencrypt/live/{self.domain_name}/fullchain.pem"
|
|
68
|
+
cert_exists = self.conn.run(f"sudo test -f {cert_path}", warn=True).ok
|
|
69
|
+
|
|
70
|
+
if not cert_exists:
|
|
71
|
+
self.conn.run(
|
|
72
|
+
f"certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
|
|
73
|
+
)
|
|
74
|
+
self.config_file.parent.mkdir(exist_ok=True)
|
|
75
|
+
self.conn.get(
|
|
76
|
+
f"/etc/nginx/sites-available/{self.app_name}.conf",
|
|
77
|
+
str(self.config_file),
|
|
78
|
+
)
|
|
79
|
+
self.run_pty("sudo systemctl enable certbot.timer")
|
|
80
|
+
self.run_pty("sudo systemctl start certbot.timer")
|
|
81
|
+
self.restart()
|
|
40
82
|
|
|
41
83
|
def teardown(self):
|
|
42
|
-
|
|
84
|
+
self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
|
|
85
|
+
self.run_pty(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
|
|
86
|
+
self.run_pty(
|
|
87
|
+
"sudo systemctl restart nginx",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def start(self) -> None:
|
|
91
|
+
self.run_pty("sudo systemctl start nginx")
|
|
92
|
+
|
|
93
|
+
def stop(self) -> None:
|
|
94
|
+
self.run_pty("sudo systemctl stop nginx")
|
|
95
|
+
|
|
96
|
+
def status(self) -> None:
|
|
97
|
+
self.run_pty("sudo systemctl status nginx", warn=True)
|
|
98
|
+
|
|
99
|
+
def restart(self) -> None:
|
|
100
|
+
self.run_pty("sudo systemctl restart nginx")
|
|
101
|
+
|
|
102
|
+
def logs(self) -> None:
|
|
103
|
+
self.run_pty(f"sudo journalctl -u nginx -f", warn=True)
|
|
104
|
+
|
|
105
|
+
def export_config(self) -> None:
|
|
106
|
+
self.config_file.write_text(self._get_config())
|
|
43
107
|
|
|
44
108
|
def _get_config(self) -> str:
|
|
109
|
+
static_locations = ""
|
|
110
|
+
for path, directory in self.statics.items():
|
|
111
|
+
static_locations += f"""
|
|
112
|
+
location {path} {{
|
|
113
|
+
alias {directory};
|
|
114
|
+
}}
|
|
115
|
+
"""
|
|
116
|
+
|
|
45
117
|
return f"""
|
|
46
118
|
server {{
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
}}
|
|
119
|
+
listen 80;
|
|
120
|
+
server_name {self.domain_name};
|
|
58
121
|
|
|
122
|
+
{static_locations}
|
|
123
|
+
|
|
124
|
+
location / {{
|
|
125
|
+
proxy_pass {self.upstream};
|
|
126
|
+
proxy_set_header Host $host;
|
|
127
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
128
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
129
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
130
|
+
}}
|
|
131
|
+
}}
|
|
59
132
|
"""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# All options are documented here https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
|
|
2
|
+
[Unit]
|
|
3
|
+
Description={app_name} Worker
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
User={user}
|
|
7
|
+
Group={user}
|
|
8
|
+
WorkingDirectory={app_dir}
|
|
9
|
+
ExecStart={app_dir}/{command}
|
|
10
|
+
EnvironmentFile={app_dir}/.env
|
|
11
|
+
Restart=always
|
|
12
|
+
|
|
13
|
+
[Install]
|
|
14
|
+
WantedBy=multi-user.target
|
fujin/templates/web.service
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# All options are documented here https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
|
|
2
|
+
# Inspiration was taken from here https://docs.gunicorn.org/en/stable/deploy.html#systemd
|
|
2
3
|
[Unit]
|
|
3
|
-
Description={
|
|
4
|
-
Requires={
|
|
4
|
+
Description={app_name} daemon
|
|
5
|
+
Requires={app_name}.socket
|
|
5
6
|
After=network.target
|
|
6
7
|
|
|
7
8
|
[Service]
|
|
8
|
-
Type=notify
|
|
9
|
-
NotifyAccess=main
|
|
9
|
+
#Type=notify
|
|
10
|
+
#NotifyAccess=main
|
|
10
11
|
User={user}
|
|
11
|
-
Group=
|
|
12
|
-
RuntimeDirectory={
|
|
13
|
-
WorkingDirectory={
|
|
14
|
-
ExecStart={
|
|
15
|
-
EnvironmentFile={
|
|
12
|
+
Group={user}
|
|
13
|
+
RuntimeDirectory={app_name}
|
|
14
|
+
WorkingDirectory={app_dir}
|
|
15
|
+
ExecStart={app_dir}/{command}
|
|
16
|
+
EnvironmentFile={app_dir}/.env
|
|
16
17
|
ExecReload=/bin/kill -s HUP $MAINPID
|
|
17
18
|
KillMode=mixed
|
|
18
19
|
TimeoutStopSec=5
|
|
19
20
|
PrivateTmp=true
|
|
20
21
|
# if your app does not need administrative capabilities, let systemd know
|
|
21
|
-
|
|
22
|
+
ProtectSystem=strict
|
|
22
23
|
|
|
23
24
|
[Install]
|
|
24
25
|
WantedBy=multi-user.target
|
fujin/templates/web.socket
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: fujin-cli
|
|
3
|
+
Version: 0.4.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
|
+
Keywords: caddy,deployment,django,fastapi,litestar,python,systemd
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: cappa>=0.24
|
|
24
|
+
Requires-Dist: fabric>=3.2.2
|
|
25
|
+
Requires-Dist: msgspec[toml]>=0.18.6
|
|
26
|
+
Requires-Dist: rich>=13.9.2
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# fujin
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/fujin-cli)
|
|
32
|
+
[](https://pypi.org/project/fujin-cli)
|
|
33
|
+
[](https://github.com/falcopackages/fujin/blob/main/LICENSE.txt)
|
|
34
|
+
[](https://pypi.org/project/fujin-cli)
|
|
35
|
+
-----
|
|
36
|
+
|
|
37
|
+
> [!IMPORTANT]
|
|
38
|
+
> This package currently contains minimal features and is a work-in-progress
|
|
39
|
+
|
|
40
|
+
`fujin` is a simple deployment tool that helps you get your project up and running on a VPS in a few minutes. It manages your app processes using `systemd` and runs your apps behind [caddy](https://caddyserver.com/). For Python projects,
|
|
41
|
+
it expects your app to be a packaged Python application ideally with a CLI entry point defined. For other languages, you need to provide a self-contained single executable file with all necessary dependencies.
|
|
42
|
+
The main job of `fujin` is to bootstrap your server (installing caddy, etc.), copy the files onto the server with a structure that supports rollback, and automatically generate configs for systemd and caddy that you can manually edit if needed.
|
|
43
|
+
|
|
44
|
+
Check out the [documentation📚](https://fujin.readthedocs.io/en/latest/) for installation, features, and usage guides.
|
|
45
|
+
|
|
46
|
+
## Why?
|
|
47
|
+
|
|
48
|
+
I wanted [kamal](https://kamal-deploy.org/) but without Docker, and I thought the idea was fun. At its core, this project automates versions of this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu). If you've been a Django beginner
|
|
49
|
+
trying to get your app in production, you probably went through this. I'm using caddy instead of nginx because the configuration file simpler and it's is a no-brainer for SSL certificates. Systemd is the default on most Linux distributions and does a good enough job.
|
|
50
|
+
|
|
51
|
+
Fujin was initially planned to be a Python-only project, but the core concepts can be applied to any language that can produce a single distributable file (e.g., Go, Rust). I wanted to recreate kamal's nice local-to-remote app management API, but I'm skipping Docker to keep things simple.
|
|
52
|
+
I'm currently rocking SQLite in production for my side projects and ths setup is enough for my use case.
|
|
53
|
+
|
|
54
|
+
The goal is to automate deployment while leaving you in full control of your Linux box. It's not a CLI PaaS - it's simple and expects you to be able to SSH into your server and troubleshoot if necessary. For beginners, it makes the initial deployment easier while you get your hands dirty with Linux.
|
|
55
|
+
If you need a never-break, worry-free, set-it-and-forget-it setup that auto-scales and does all the magic, fujin probably isn't for you.
|
|
56
|
+
|
|
57
|
+
## Inspiration and alternatives
|
|
58
|
+
|
|
59
|
+
Fujin draws inspiration from the following tools for their developer experience. These are better alternatives if you need a more robust, set-and-forget solution
|
|
60
|
+
|
|
61
|
+
- [fly.io](https://fly.io/)
|
|
62
|
+
- [kamal](https://kamal-deploy.org/) (you probably can't just forget this one)
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
`fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
|
|
3
|
+
fujin/config.py,sha256=OYcQPhYYcLJ2ImVP-BNodWvaJ6Y5mRIeB9R3cohbV94,8678
|
|
4
|
+
fujin/connection.py,sha256=ZkYaNykRFj9Yr-K-vOrZtVVGUDurDm6W7OQrgct71CA,2428
|
|
5
|
+
fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
|
|
6
|
+
fujin/hooks.py,sha256=QHIqxLxujG2U70UkN1BpUplE6tTqn7pFJP5oHde1tUQ,1350
|
|
7
|
+
fujin/commands/__init__.py,sha256=uIGGXt8YofL5RZn8KIy153ioWGoCl32ffHtqOhB-6ZM,78
|
|
8
|
+
fujin/commands/_base.py,sha256=o3R4-c3XeFWTIW3stiUdrcCPwdjzfjUVIpZy2L1-gZ4,2525
|
|
9
|
+
fujin/commands/app.py,sha256=LUC4mmvkp1F4a96UP9hgd5y_R9HA8Xc_TtNAsYtXvvs,5153
|
|
10
|
+
fujin/commands/config.py,sha256=HAiea_ebhvBzUG7vddMH3ldjZKRHU1KFaZlDZI1XmMg,2322
|
|
11
|
+
fujin/commands/deploy.py,sha256=mUvlDF6yv05wnQqhsFC-LCPVvaJVfnQnK97CFB_ttVY,4427
|
|
12
|
+
fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
|
|
13
|
+
fujin/commands/down.py,sha256=JrwDZjcqyx5Mx7IzcAkzq1zAPBD5MQMIS4gzBEUz3JM,1721
|
|
14
|
+
fujin/commands/init.py,sha256=MqZeEH7OL4knOuR8D84MxmeurDMXSr8XEXqcGxf6Xoo,2895
|
|
15
|
+
fujin/commands/proxy.py,sha256=w8XOhirD5lLnmW3SC27bahoIt2XtgydQ-8LxOP-DpYM,2552
|
|
16
|
+
fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
|
|
17
|
+
fujin/commands/redeploy.py,sha256=suSr6S54oLsnKGjJgq3uHxCRFcrCX5C-5BkmH5MzQks,1864
|
|
18
|
+
fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
|
|
19
|
+
fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
|
|
20
|
+
fujin/commands/server.py,sha256=n0FZbNI4IO5AOMJTIVfJqgb32-QSrWEu0IVwfREsRbY,3637
|
|
21
|
+
fujin/commands/up.py,sha256=DgDN-1mc_mMHJRCIvcB947Cd5a7phunu9NpXloGK0UU,419
|
|
22
|
+
fujin/process_managers/__init__.py,sha256=MhhfTBhm64zWRAKgjvsZRIToOUJus60vGScbAjqpQ6Y,994
|
|
23
|
+
fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAPBObk,5603
|
|
24
|
+
fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
|
|
25
|
+
fujin/proxies/caddy.py,sha256=OkQkQXlVVw3KYkVNV8tAfpHnqZEImo5bStr8dbkM2F0,7540
|
|
26
|
+
fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
|
|
27
|
+
fujin/proxies/nginx.py,sha256=8AkbJAjj6B0fxgv671mGDbx3LY_dY5wxFov80XmSfUY,4139
|
|
28
|
+
fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
|
|
29
|
+
fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
|
|
30
|
+
fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
|
|
31
|
+
fujin_cli-0.4.0.dist-info/METADATA,sha256=Vf2x0v7d1QVV9xE9UQn2LNtDyVCFmASfDcwTcpgpAgU,4396
|
|
32
|
+
fujin_cli-0.4.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
33
|
+
fujin_cli-0.4.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
|
|
34
|
+
fujin_cli-0.4.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
|
|
35
|
+
fujin_cli-0.4.0.dist-info/RECORD,,
|