fujin-cli 0.5.0__py3-none-any.whl → 0.7.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 CHANGED
@@ -1,3 +1,7 @@
1
+ from gevent import monkey
2
+
3
+ monkey.patch_all()
4
+
1
5
  import shlex
2
6
  import sys
3
7
  from pathlib import Path
@@ -16,6 +20,7 @@ from fujin.commands.redeploy import Redeploy
16
20
  from fujin.commands.rollback import Rollback
17
21
  from fujin.commands.server import Server
18
22
  from fujin.commands.up import Up
23
+ from fujin.commands.printenv import Printenv
19
24
 
20
25
  if sys.version_info >= (3, 11):
21
26
  import tomllib
@@ -38,6 +43,7 @@ class Fujin:
38
43
  | Down
39
44
  | Rollback
40
45
  | Prune
46
+ | Printenv
41
47
  ]
42
48
 
43
49
 
fujin/commands/deploy.py CHANGED
@@ -7,6 +7,7 @@ import cappa
7
7
 
8
8
  from fujin.commands import BaseCommand
9
9
  from fujin.config import InstallationMode
10
+ from fujin.secrets import resolve_secrets
10
11
  from fujin.connection import Connection
11
12
 
12
13
 
@@ -15,14 +16,16 @@ from fujin.connection import Connection
15
16
  )
16
17
  class Deploy(BaseCommand):
17
18
  def __call__(self):
19
+ parsed_env = self.parse_envfile()
18
20
  self.build_app()
19
21
 
20
22
  with self.connection() as conn:
21
23
  process_manager = self.create_process_manager(conn)
22
24
  conn.run(f"mkdir -p {self.app_dir}")
25
+ conn.run(f"mkdir -p {self.versioned_assets_dir}")
23
26
  with conn.cd(self.app_dir):
24
27
  self.create_hook_manager(conn).pre_deploy()
25
- self.transfer_files(conn)
28
+ self.transfer_files(conn, env=parsed_env)
26
29
  self.install_project(conn)
27
30
  with self.app_environment() as app_conn:
28
31
  self.release(app_conn)
@@ -48,16 +51,31 @@ class Deploy(BaseCommand):
48
51
  def versioned_assets_dir(self) -> str:
49
52
  return f"{self.app_dir}/v{self.config.version}"
50
53
 
51
- def transfer_files(self, conn: Connection):
54
+ def parse_envfile(self) -> str:
52
55
  if not self.config.host.envfile.exists():
53
56
  raise cappa.Exit(f"{self.config.host.envfile} not found", code=1)
54
- conn.put(str(self.config.host.envfile), f"{self.app_dir}/.env")
55
- conn.run(f"mkdir -p {self.versioned_assets_dir}")
57
+ if self.config.secret_config:
58
+ self.stdout.output("[blue]Reading secrets....[/blue]")
59
+ return resolve_secrets(self.config.host.envfile, self.config.secret_config)
60
+ return self.config.host.envfile.read_text()
61
+
62
+ def transfer_files(
63
+ self, conn: Connection, env: str, skip_requirements: bool = False
64
+ ):
65
+ conn.run(f"echo '{env}' > {self.app_dir}/.env")
56
66
  distfile_path = self.config.get_distfile_path()
57
67
  conn.put(
58
68
  str(distfile_path),
59
69
  f"{self.versioned_assets_dir}/{distfile_path.name}",
60
70
  )
71
+ if not skip_requirements and self.config.requirements:
72
+ requirements = Path(self.config.requirements)
73
+ if not requirements.exists():
74
+ raise cappa.Exit(f"{self.config.requirements} not found", code=1)
75
+ conn.put(
76
+ Path(self.config.requirements).resolve(),
77
+ f"{self.versioned_assets_dir}/requirements.txt",
78
+ )
61
79
 
62
80
  def install_project(
63
81
  self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
@@ -71,14 +89,6 @@ class Deploy(BaseCommand):
71
89
  def _install_python_package(
72
90
  self, conn: Connection, version: str, skip_setup: bool = False
73
91
  ):
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
- )
82
92
  appenv = f"""
83
93
  set -a # Automatically export all variables
84
94
  source .env
fujin/commands/init.py CHANGED
@@ -54,7 +54,6 @@ def simple_config(app_name) -> dict:
54
54
  },
55
55
  "aliases": {"shell": "server exec --appenv -i bash"},
56
56
  "host": {
57
- "ip": "127.0.0.1",
58
57
  "user": "root",
59
58
  "domain_name": f"{app_name}.com",
60
59
  "envfile": ".env.prod",
@@ -111,7 +110,6 @@ def binary_config(app_name: str) -> dict:
111
110
  "processes": {"web": f"{app_name} prodserver"},
112
111
  "aliases": {"shell": "server exec --appenv -i bash"},
113
112
  "host": {
114
- "ip": "127.0.0.1",
115
113
  "user": "root",
116
114
  "domain_name": f"{app_name}.com",
117
115
  "envfile": ".env.prod",
@@ -0,0 +1,16 @@
1
+ import cappa
2
+
3
+ from fujin.commands import BaseCommand
4
+ from fujin.secrets import resolve_secrets
5
+
6
+
7
+ @cappa.command(
8
+ help="Print the content of the envfile with extracted secrets (for debugging)"
9
+ )
10
+ class Printenv(BaseCommand):
11
+ def __call__(self):
12
+ if self.config.secret_config:
13
+ result = resolve_secrets(self.config.host.envfile, self.config.secret_config)
14
+ else:
15
+ result = self.config.host.envfile.read_text()
16
+ self.stdout.output(result)
@@ -15,13 +15,17 @@ from fujin.connection import Connection
15
15
  class Redeploy(BaseCommand):
16
16
  def __call__(self):
17
17
  deploy = Deploy()
18
+ parsed_env = deploy.parse_envfile()
18
19
  deploy.build_app()
19
20
 
20
21
  with self.app_environment() as conn:
21
22
  hook_manager = self.create_hook_manager(conn)
22
23
  hook_manager.pre_deploy()
23
- deploy.transfer_files(conn)
24
+ conn.run(f"mkdir -p {deploy.versioned_assets_dir}")
24
25
  requirements_copied = self._copy_requirements_if_needed(conn)
26
+ deploy.transfer_files(
27
+ conn, env=parsed_env, skip_requirements=requirements_copied
28
+ )
25
29
  deploy.install_project(conn, skip_setup=requirements_copied)
26
30
  deploy.release(conn)
27
31
  self.create_process_manager(conn).restart_services()
fujin/config.py CHANGED
@@ -14,6 +14,10 @@ python_version
14
14
  The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file. This is only
15
15
  required if the installation mode is set to ``python-package``
16
16
 
17
+ requirements
18
+ ------------
19
+ Optional path to your requirements file. This will only be used when the installation mode is set to ``python-package``
20
+
17
21
  versions_to_keep
18
22
  ----------------
19
23
  The number of versions to keep on the host. After each deploy, older versions are pruned based on this setting. By default, it keeps the latest 5 versions,
@@ -39,9 +43,22 @@ release_command
39
43
  ---------------
40
44
  Optional command to run at the end of deployment (e.g., database migrations).
41
45
 
42
- requirements
43
- ------------
44
- Optional path to your requirements file. This will only be used when the installation mode is set to ``python-package``
46
+ secrets
47
+ -------
48
+
49
+ Optional secrets configuration. If set, Fujin will load secrets from the specified secret management service.
50
+ Check out the `secrets </secrets.html>`_ page for more information.
51
+
52
+ adapter
53
+ ~~~~~~~
54
+ The secret management service to use. Available options:
55
+
56
+ - ``bitwarden``
57
+ - ``1password``
58
+
59
+ password_env
60
+ ~~~~~~~~~~~~
61
+ Environment variable containing the password for the service account. This is only required for certain adapters.
45
62
 
46
63
  Webserver
47
64
  ---------
@@ -61,6 +78,10 @@ The address where your web application listens for requests. Supports any value
61
78
  - HTTP address (e.g., ``localhost:8000``)
62
79
  - Unix socket (e.g., ``unix//run/project.sock``)
63
80
 
81
+ certbot_email
82
+ ~~~~~~~~~~~~~
83
+ Required when Nginx is used as a proxy, to obtain SSL certificates.
84
+
64
85
  statics
65
86
  ~~~~~~~
66
87
 
@@ -101,7 +122,7 @@ Host Configuration
101
122
 
102
123
  ip
103
124
  ~~
104
- The IP address or hostname of the remote host.
125
+ The IP address or anything that resolves to the remote host IP's. This is use to communicate via ssh with the server, if omitted it's value will default to the one of the ``domain_name``.
105
126
 
106
127
  domain_name
107
128
  ~~~~~~~~~~~
@@ -162,7 +183,6 @@ from __future__ import annotations
162
183
 
163
184
  import os
164
185
  import sys
165
- from functools import cached_property
166
186
  from pathlib import Path
167
187
 
168
188
  import msgspec
@@ -182,6 +202,16 @@ class InstallationMode(StrEnum):
182
202
  BINARY = "binary"
183
203
 
184
204
 
205
+ class SecretAdapter(StrEnum):
206
+ BITWARDEN = "bitwarden"
207
+ ONE_PASSWORD = "1password"
208
+
209
+
210
+ class SecretConfig(msgspec.Struct):
211
+ adapter: SecretAdapter
212
+ password_env: str | None = None
213
+
214
+
185
215
  class Config(msgspec.Struct, kw_only=True):
186
216
  app_name: str = msgspec.field(name="app")
187
217
  version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
@@ -199,6 +229,7 @@ class Config(msgspec.Struct, kw_only=True):
199
229
  requirements: str | None = None
200
230
  hooks: HooksDict = msgspec.field(default_factory=dict)
201
231
  local_config_dir: Path = Path(".fujin")
232
+ secret_config: SecretConfig | None = msgspec.field(name="secrets", default=None)
202
233
 
203
234
  def __post_init__(self):
204
235
  if self.installation_mode == InstallationMode.PY_PACKAGE:
@@ -234,7 +265,7 @@ class Config(msgspec.Struct, kw_only=True):
234
265
 
235
266
 
236
267
  class HostConfig(msgspec.Struct, kw_only=True):
237
- ip: str
268
+ ip: str | None = None
238
269
  domain_name: str
239
270
  user: str
240
271
  _envfile: str = msgspec.field(name="envfile")
@@ -245,6 +276,7 @@ class HostConfig(msgspec.Struct, kw_only=True):
245
276
 
246
277
  def __post_init__(self):
247
278
  self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
279
+ self.ip = self.ip or self.domain_name
248
280
 
249
281
  def to_dict(self):
250
282
  d = {f: getattr(self, f) for f in self.__struct_fields__}
@@ -280,6 +312,7 @@ class HostConfig(msgspec.Struct, kw_only=True):
280
312
  class Webserver(msgspec.Struct):
281
313
  upstream: str
282
314
  type: str = "fujin.proxies.caddy"
315
+ certbot_email: str | None = None
283
316
  statics: dict[str, str] = msgspec.field(default_factory=dict)
284
317
 
285
318
 
fujin/proxies/nginx.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
 
6
5
  import msgspec
@@ -8,8 +7,6 @@ import msgspec
8
7
  from fujin.config import Config
9
8
  from fujin.connection import Connection
10
9
 
11
- CERTBOT_EMAIL = os.getenv("CERTBOT_EMAIL")
12
-
13
10
 
14
11
  class WebProxy(msgspec.Struct):
15
12
  conn: Connection
@@ -18,6 +15,7 @@ class WebProxy(msgspec.Struct):
18
15
  upstream: str
19
16
  statics: dict[str, str]
20
17
  local_config_dir: Path
18
+ certbot_email: str | None = None
21
19
 
22
20
  @property
23
21
  def config_file(self) -> Path:
@@ -32,23 +30,24 @@ class WebProxy(msgspec.Struct):
32
30
  app_name=config.app_name,
33
31
  local_config_dir=config.local_config_dir,
34
32
  statics=config.webserver.statics,
33
+ certbot_email=config.webserver.certbot_email,
35
34
  )
36
35
 
37
36
  def run_pty(self, *args, **kwargs):
38
- return self.conn.run(*args, **kwargs, pty=True)
37
+ return self.run_pty(*args, **kwargs, pty=True)
39
38
 
40
39
  def install(self):
41
- self.conn.run(
40
+ self.run_pty(
42
41
  "sudo apt install -y nginx libpq-dev python3-dev python3-certbot-nginx"
43
42
  )
44
43
 
45
44
  def uninstall(self):
46
45
  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")
46
+ self.run_pty("sudo apt remove -y nginx")
47
+ self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
48
+ self.run_pty(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
49
+ self.run_pty("sudo systemctl disable certbot.timer")
50
+ self.run_pty("sudo apt remove -y python3-certbot-nginx")
52
51
 
53
52
  def setup(self):
54
53
  conf = (
@@ -63,13 +62,13 @@ class WebProxy(msgspec.Struct):
63
62
  self.run_pty(
64
63
  f"sudo ln -sf /etc/nginx/sites-available/{self.app_name}.conf /etc/nginx/sites-enabled/{self.app_name}.conf",
65
64
  )
66
- if CERTBOT_EMAIL:
65
+ if self.certbot_email:
67
66
  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
67
+ cert_exists = self.run_pty(f"sudo test -f {cert_path}", warn=True).ok
69
68
 
70
69
  if not cert_exists:
71
- self.conn.run(
72
- f"certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
70
+ self.run_pty(
71
+ f"sudo certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {self.certbot_email} --redirect"
73
72
  )
74
73
  self.config_file.parent.mkdir(exist_ok=True)
75
74
  self.conn.get(
@@ -83,9 +82,7 @@ class WebProxy(msgspec.Struct):
83
82
  def teardown(self):
84
83
  self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
85
84
  self.run_pty(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
86
- self.run_pty(
87
- "sudo systemctl restart nginx",
88
- )
85
+ self.run_pty("sudo systemctl restart nginx")
89
86
 
90
87
  def start(self) -> None:
91
88
  self.run_pty("sudo systemctl start nginx")
@@ -109,9 +106,9 @@ class WebProxy(msgspec.Struct):
109
106
  static_locations = ""
110
107
  for path, directory in self.statics.items():
111
108
  static_locations += f"""
112
- location {path} {{
113
- alias {directory};
114
- }}
109
+ location {path} {{
110
+ alias {directory};
111
+ }}
115
112
  """
116
113
 
117
114
  return f"""
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import gevent
5
+ from dotenv import dotenv_values
6
+ from typing import Callable
7
+
8
+ from fujin.config import SecretConfig, SecretAdapter
9
+ from .bitwarden import bitwarden
10
+ from .onepassword import one_password
11
+
12
+
13
+ secret_reader = Callable[[str], str]
14
+ secret_adapter_context = Callable[[SecretConfig], secret_reader]
15
+
16
+ adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
17
+ SecretAdapter.BITWARDEN: bitwarden,
18
+ SecretAdapter.ONE_PASSWORD: one_password,
19
+ }
20
+
21
+
22
+ def resolve_secrets(envfile: Path, secret_config: SecretConfig) -> str:
23
+ env_dict = dotenv_values(envfile)
24
+ secrets = {key: value for key, value in env_dict.items() if value.startswith("$")}
25
+ adapter_context = adapter_to_context[secret_config.adapter]
26
+ parsed_secrets = {}
27
+ with adapter_context(secret_config) as secret_reader:
28
+ for key, secret in secrets.items():
29
+ parsed_secrets[key] = gevent.spawn(secret_reader, secret[1:])
30
+ gevent.joinall(parsed_secrets.values())
31
+ env_dict.update({key: thread.value for key, thread in parsed_secrets.items()})
32
+ return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+ import cappa
3
+ import os
4
+
5
+
6
+ from fujin.config import SecretConfig
7
+ import subprocess
8
+ from contextlib import contextmanager
9
+ from typing import Generator, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from . import secret_reader
13
+
14
+
15
+ @contextmanager
16
+ def bitwarden(secret_config: SecretConfig) -> Generator[secret_reader, None, None]:
17
+ session = os.getenv("BW_SESSION")
18
+ if not session:
19
+ if not secret_config.password_env:
20
+ raise cappa.Exit(
21
+ "You need to set the password_env to use the bitwarden adapter or set the BW_SESSION environment variable",
22
+ code=1,
23
+ )
24
+ session = _signin(secret_config.password_env)
25
+
26
+ def read_secret(name: str) -> str:
27
+ result = subprocess.run(
28
+ ["bw", "get", "password", name, "--raw", "--session", session, "--nointeraction"],
29
+ capture_output=True,
30
+ text=True,
31
+ )
32
+ if result.returncode != 0:
33
+ raise cappa.Exit(f"Password not found for {name}")
34
+ return result.stdout.strip()
35
+
36
+ try:
37
+ yield read_secret
38
+ finally:
39
+ pass
40
+ # subprocess.run(["bw", "lock"], capture_output=True)
41
+
42
+
43
+ def _signin(password_env) -> str:
44
+ sync_result = subprocess.run(["bw", "sync"], capture_output=True, text=True)
45
+ if sync_result.returncode != 0:
46
+ raise cappa.Exit(f"Bitwarden sync failed: {sync_result.stdout}", code=1)
47
+ unlock_result = subprocess.run(
48
+ [
49
+ "bw",
50
+ "unlock",
51
+ "--nointeraction",
52
+ "--passwordenv",
53
+ password_env,
54
+ "--raw",
55
+ ],
56
+ capture_output=True,
57
+ text=True,
58
+ )
59
+ if unlock_result.returncode != 0:
60
+ raise cappa.Exit(f"Bitwarden unlock failed {unlock_result.stderr}", code=1)
61
+
62
+ return unlock_result.stdout.strip()
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+ import cappa
3
+
4
+
5
+ from fujin.config import SecretConfig
6
+ import subprocess
7
+
8
+ from contextlib import contextmanager
9
+ from typing import Generator, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from . import secret_reader
13
+
14
+
15
+ @contextmanager
16
+ def one_password(_: SecretConfig) -> Generator[secret_reader, None, None]:
17
+ def read_secret(name: str) -> str:
18
+ result = subprocess.run(["op", "read", name], capture_output=True, text=True)
19
+ if result.returncode != 0:
20
+ raise cappa.Exit(result.stderr)
21
+ return result.stdout.strip()
22
+
23
+ try:
24
+ yield read_secret
25
+ finally:
26
+ pass
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fujin-cli
3
- Version: 0.5.0
4
- Summary: Add your description here
3
+ Version: 0.7.0
4
+ Summary: Get your project up and running in a few minutes on your own vps.
5
5
  Project-URL: Documentation, https://github.com/falcopackages/fujin#readme
6
6
  Project-URL: Issues, https://github.com/falcopackages/fujin/issues
7
7
  Project-URL: Source, https://github.com/falcopackages/fujin
8
8
  Author-email: Tobi DEGNON <tobidegnon@proton.me>
9
- License-File: LICENSE.txt
10
9
  Keywords: caddy,deployment,django,fastapi,litestar,python,systemd
11
10
  Classifier: Development Status :: 3 - Alpha
12
11
  Classifier: Intended Audience :: Developers
@@ -22,34 +21,48 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
21
  Requires-Python: >=3.10
23
22
  Requires-Dist: cappa>=0.24
24
23
  Requires-Dist: fabric>=3.2.2
24
+ Requires-Dist: gevent[recommended]>=24.11.1
25
25
  Requires-Dist: msgspec[toml]>=0.18.6
26
+ Requires-Dist: python-dotenv>=1.0.1
26
27
  Requires-Dist: rich>=13.9.2
27
28
  Description-Content-Type: text/markdown
28
29
 
29
30
  # fujin
30
31
 
32
+ > [!IMPORTANT]
33
+ > This tool is currently contains minimal features and is a work-in-progress
34
+
35
+ <!-- content:start -->
36
+
37
+ `fujin` is a simple deployment tool that helps you get your project up and running on a VPS in minutes. It manages your app processes using [systemd](https://systemd.io) and runs your apps behind [caddy](https://caddyserver.com).
38
+
39
+ [![Publish Package](https://github.com/falcopackages/fujin/actions/workflows/publish.yml/badge.svg)](https://github.com/falcopackages/fujin/actions/workflows/publish.yml)
31
40
  [![PyPI - Version](https://img.shields.io/pypi/v/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
32
41
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
33
42
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/falcopackages/fujin/blob/main/LICENSE.txt)
34
43
  [![Status](https://img.shields.io/pypi/status/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
35
- -----
36
44
 
37
- > [!IMPORTANT]
38
- > This package currently contains minimal features and is a work-in-progress
45
+ ## Features
39
46
 
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.
47
+ - 🚀 One-command server bootstrap
48
+ - 🔄 Rollback broken deployments
49
+ - 🔐 Zero configuration SSL certificates
50
+ - 🔁 Swappable proxy ([caddy](https://caddyserver.com), [nginx](https://nginx.org/en/) and `dummy` to disable proxy)
51
+ - 🛠️ Secrets injection from password managers ([Bitwarden](https://bitwarden.com/), [1Password](https://1password.com), etc.)
52
+ - 📝 Easily customizable `systemd` and `proxy` configurations
53
+ - 👨‍💻 Remote application management and log streaming
54
+ - 🐍 Supports packaged python apps and self-contained binaries
43
55
 
44
- Check out the [documentation📚](https://fujin.readthedocs.io/en/latest/) for installation, features, and usage guides.
56
+ For more details, check out the [documentation📚](https://fujin.oluwatobi.dev/en/latest/).
45
57
 
46
58
  ## Why?
47
59
 
48
60
  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.
61
+ trying to get your app in production, you probably went through this.
62
+
63
+ I'm using `caddy` here by default instead of `nginx` because it's configurable via an API 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
64
 
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.
65
+ 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).
53
66
 
54
67
  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
68
  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.
@@ -64,3 +77,5 @@ Fujin draws inspiration from the following tools for their developer experience.
64
77
  ## License
65
78
 
66
79
  `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
80
+
81
+ <!-- content:end -->
@@ -1,6 +1,6 @@
1
1
  fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
3
- fujin/config.py,sha256=r2Rdd7ErGGTQPLMJlqZbPEQXd6fSSQB7PNbyaurEQsQ,9394
2
+ fujin/__main__.py,sha256=VJMBzuQuxkQaAKNEySktGnms944bkRsrIAjj-XeaWR8,1813
3
+ fujin/config.py,sha256=pdWK4fM3BITpz0MAv_ydCXRcQbgC3eLz--APdOpehiQ,10412
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
@@ -8,15 +8,15 @@ fujin/commands/__init__.py,sha256=uIGGXt8YofL5RZn8KIy153ioWGoCl32ffHtqOhB-6ZM,78
8
8
  fujin/commands/_base.py,sha256=o3R4-c3XeFWTIW3stiUdrcCPwdjzfjUVIpZy2L1-gZ4,2525
9
9
  fujin/commands/app.py,sha256=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
10
10
  fujin/commands/config.py,sha256=xdfd1OZLxw2YZldiAbW5rq5EBXEaXbUC-I7FKLRfzIQ,2387
11
- fujin/commands/deploy.py,sha256=oAOLUrtHjHkmIA3D0AiaJRWEsfVk51_I29kRC_FhjYo,5463
11
+ fujin/commands/deploy.py,sha256=SUI_aMqTmaWHVtyX03rjrbWMYESoysAykcvILsEtsWk,5897
12
12
  fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
13
13
  fujin/commands/down.py,sha256=v1lAq70ApktjeHRB_1sCzjmKH8t6EXqyL4RTt7OE-f0,1716
14
- fujin/commands/init.py,sha256=IguExzQEPJ5CbWzG-mI6hIXUw2fd2rAEA04FK6-5l18,4021
14
+ fujin/commands/init.py,sha256=t8uwwOi4SBqHjV8px_SkTHAeZIiIUJnFN-lf7DK6HhE,3959
15
+ fujin/commands/printenv.py,sha256=1xz80UCKpz64wsR9rYStro0K6pkKndNoNQvusEMYRuQ,486
15
16
  fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
16
17
  fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
17
- fujin/commands/redeploy.py,sha256=JvCJBZBcCKkUw1efZwRPJMLUAV8oqBAZeSbUBLHyn3k,2185
18
+ fujin/commands/redeploy.py,sha256=qYPM_Klj_2G1Up4ICu-8rklcqIbU-uohddmAzAA5sy0,2378
18
19
  fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
19
- fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
20
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
@@ -24,12 +24,15 @@ fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAP
24
24
  fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
25
25
  fujin/proxies/caddy.py,sha256=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
26
26
  fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
27
- fujin/proxies/nginx.py,sha256=8AkbJAjj6B0fxgv671mGDbx3LY_dY5wxFov80XmSfUY,4139
27
+ fujin/proxies/nginx.py,sha256=BNJNLxLLRVAmBIGVCk8pb16iiSJsOI9jXOZhdSQGtX8,4151
28
+ fujin/secrets/__init__.py,sha256=WFFe81fB6R3dzdvYsdPT9x-fE2LwpyrfF0nCuWrQNRw,1192
29
+ fujin/secrets/bitwarden.py,sha256=rJ_n7CQT02TwwqN9lHQkh56g5BETKhZBtjWi3bOA8rQ,1846
30
+ fujin/secrets/onepassword.py,sha256=2ySDY7fNW_sOqH-I89rfz3uMiBu0mJRHot4yKlTRbrE,636
28
31
  fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
29
32
  fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
30
33
  fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
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,,
34
+ fujin_cli-0.7.0.dist-info/METADATA,sha256=0H7E8Re_jONc6BsoAdATLKM3r8QIrA_lT1oRgG3gWIw,4576
35
+ fujin_cli-0.7.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
+ fujin_cli-0.7.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
37
+ fujin_cli-0.7.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
38
+ fujin_cli-0.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
fujin/commands/secrets.py DELETED
@@ -1,11 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
- import cappa
4
-
5
- from fujin.commands import BaseCommand
6
-
7
-
8
- @cappa.command(help="")
9
- @dataclass
10
- class Secrets(BaseCommand):
11
- pass