fujin-cli 0.6.0__py3-none-any.whl → 0.7.1__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
 
@@ -1,2 +1 @@
1
1
  from ._base import BaseCommand # noqa
2
- from ._base import BaseCommand # noqa
fujin/commands/_base.py CHANGED
@@ -6,7 +6,8 @@ from functools import cached_property
6
6
  import cappa
7
7
 
8
8
  from fujin.config import Config
9
- from fujin.connection import host_connection, Connection
9
+ from fujin.connection import Connection
10
+ from fujin.connection import host_connection
10
11
  from fujin.errors import ImproperlyConfiguredError
11
12
  from fujin.hooks import HookManager
12
13
  from fujin.process_managers import ProcessManager
fujin/commands/deploy.py CHANGED
@@ -8,6 +8,7 @@ import cappa
8
8
  from fujin.commands import BaseCommand
9
9
  from fujin.config import InstallationMode
10
10
  from fujin.connection import Connection
11
+ from fujin.secrets import resolve_secrets
11
12
 
12
13
 
13
14
  @cappa.command(
@@ -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
@@ -90,6 +100,7 @@ export PATH=".venv/bin:$PATH"
90
100
  conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
91
101
  versioned_assets_dir = f"{self.app_dir}/v{version}"
92
102
  if not skip_setup:
103
+ conn.run("rm -rf .venv")
93
104
  conn.run("uv venv")
94
105
  if self.config.requirements:
95
106
  conn.run(f"uv pip install -r {versioned_assets_dir}/requirements.txt")
fujin/commands/init.py CHANGED
@@ -8,7 +8,8 @@ import cappa
8
8
  import tomli_w
9
9
 
10
10
  from fujin.commands import BaseCommand
11
- from fujin.config import tomllib, InstallationMode
11
+ from fujin.config import InstallationMode
12
+ from fujin.config import tomllib
12
13
 
13
14
 
14
15
  @cappa.command(help="Generate a sample configuration file")
@@ -0,0 +1,18 @@
1
+ import cappa
2
+
3
+ from fujin.commands import BaseCommand
4
+ from fujin.secrets import resolve_secrets
5
+
6
+
7
+ @cappa.command(
8
+ help="Display the contents of the envfile with resolved secrets (for debugging purposes)"
9
+ )
10
+ class Printenv(BaseCommand):
11
+ def __call__(self):
12
+ if self.config.secret_config:
13
+ result = resolve_secrets(
14
+ self.config.host.envfile, self.config.secret_config
15
+ )
16
+ else:
17
+ result = self.config.host.envfile.read_text()
18
+ self.stdout.output(result)
@@ -5,8 +5,8 @@ from pathlib import Path
5
5
 
6
6
  import cappa
7
7
 
8
- from fujin.commands import BaseCommand
9
8
  from .deploy import Deploy
9
+ from fujin.commands import BaseCommand
10
10
  from fujin.config import InstallationMode
11
11
  from fujin.connection import Connection
12
12
 
@@ -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()
@@ -1,7 +1,8 @@
1
1
  from dataclasses import dataclass
2
2
 
3
3
  import cappa
4
- from rich.prompt import Prompt, Confirm
4
+ from rich.prompt import Confirm
5
+ from rich.prompt import Prompt
5
6
 
6
7
  from fujin.commands import BaseCommand
7
8
  from fujin.commands.deploy import Deploy
fujin/commands/up.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import cappa
2
- from fujin.commands import BaseCommand
3
2
 
4
3
  from .deploy import Deploy
5
4
  from .server import Server
5
+ from fujin.commands import BaseCommand
6
6
 
7
7
 
8
8
  @cappa.command(help="Run everything required to deploy an application to a fresh host.")
fujin/config.py CHANGED
@@ -43,6 +43,23 @@ release_command
43
43
  ---------------
44
44
  Optional command to run at the end of deployment (e.g., database migrations).
45
45
 
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.
62
+
46
63
  Webserver
47
64
  ---------
48
65
 
@@ -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
 
@@ -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:
@@ -281,6 +312,7 @@ class HostConfig(msgspec.Struct, kw_only=True):
281
312
  class Webserver(msgspec.Struct):
282
313
  upstream: str
283
314
  type: str = "fujin.proxies.caddy"
315
+ certbot_email: str | None = None
284
316
  statics: dict[str, str] = msgspec.field(default_factory=dict)
285
317
 
286
318
 
fujin/connection.py CHANGED
@@ -8,11 +8,9 @@ import cappa
8
8
  from fabric import Connection
9
9
  from invoke import Responder
10
10
  from invoke.exceptions import UnexpectedExit
11
- from paramiko.ssh_exception import (
12
- AuthenticationException,
13
- NoValidConnectionsError,
14
- SSHException,
15
- )
11
+ from paramiko.ssh_exception import AuthenticationException
12
+ from paramiko.ssh_exception import NoValidConnectionsError
13
+ from paramiko.ssh_exception import SSHException
16
14
 
17
15
  if TYPE_CHECKING:
18
16
  from fujin.config import HostConfig
fujin/hooks.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from fujin.connection import Connection
4
3
  from rich import print as rich_print
5
4
 
5
+ from fujin.connection import Connection
6
+
6
7
  try:
7
8
  from enum import StrEnum
8
9
  except ImportError:
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,6 +30,7 @@ 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):
@@ -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
67
  cert_exists = self.run_pty(f"sudo test -f {cert_path}", warn=True).ok
69
68
 
70
69
  if not cert_exists:
71
70
  self.run_pty(
72
- f"sudo certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
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(
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Callable
5
+
6
+ import gevent
7
+ from dotenv import dotenv_values
8
+
9
+ from .bitwarden import bitwarden
10
+ from .onepassword import one_password
11
+ from fujin.config import SecretAdapter
12
+ from fujin.config import SecretConfig
13
+
14
+
15
+ secret_reader = Callable[[str], str]
16
+ secret_adapter_context = Callable[[SecretConfig], secret_reader]
17
+
18
+ adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
19
+ SecretAdapter.BITWARDEN: bitwarden,
20
+ SecretAdapter.ONE_PASSWORD: one_password,
21
+ }
22
+
23
+
24
+ def resolve_secrets(envfile: Path, secret_config: SecretConfig) -> str:
25
+ env_dict = dotenv_values(envfile)
26
+ secrets = {key: value for key, value in env_dict.items() if value.startswith("$")}
27
+ adapter_context = adapter_to_context[secret_config.adapter]
28
+ parsed_secrets = {}
29
+ with adapter_context(secret_config) as secret_reader:
30
+ for key, secret in secrets.items():
31
+ parsed_secrets[key] = gevent.spawn(secret_reader, secret[1:])
32
+ gevent.joinall(parsed_secrets.values())
33
+ env_dict.update({key: thread.value for key, thread in parsed_secrets.items()})
34
+ return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from contextlib import contextmanager
6
+ from typing import Generator
7
+ from typing import TYPE_CHECKING
8
+
9
+ import cappa
10
+
11
+ from fujin.config import SecretConfig
12
+
13
+ if TYPE_CHECKING:
14
+ from . import secret_reader
15
+
16
+
17
+ @contextmanager
18
+ def bitwarden(secret_config: SecretConfig) -> Generator[secret_reader, None, None]:
19
+ session = os.getenv("BW_SESSION")
20
+ if not session:
21
+ if not secret_config.password_env:
22
+ raise cappa.Exit(
23
+ "You need to set the password_env to use the bitwarden adapter or set the BW_SESSION environment variable",
24
+ code=1,
25
+ )
26
+ session = _signin(secret_config.password_env)
27
+
28
+ def read_secret(name: str) -> str:
29
+ result = subprocess.run(
30
+ [
31
+ "bw",
32
+ "get",
33
+ "password",
34
+ name,
35
+ "--raw",
36
+ "--session",
37
+ session,
38
+ "--nointeraction",
39
+ ],
40
+ capture_output=True,
41
+ text=True,
42
+ )
43
+ if result.returncode != 0:
44
+ raise cappa.Exit(f"Password not found for {name}")
45
+ return result.stdout.strip()
46
+
47
+ try:
48
+ yield read_secret
49
+ finally:
50
+ pass
51
+ # subprocess.run(["bw", "lock"], capture_output=True)
52
+
53
+
54
+ def _signin(password_env) -> str:
55
+ sync_result = subprocess.run(["bw", "sync"], capture_output=True, text=True)
56
+ if sync_result.returncode != 0:
57
+ raise cappa.Exit(f"Bitwarden sync failed: {sync_result.stdout}", code=1)
58
+ unlock_result = subprocess.run(
59
+ [
60
+ "bw",
61
+ "unlock",
62
+ "--nointeraction",
63
+ "--passwordenv",
64
+ password_env,
65
+ "--raw",
66
+ ],
67
+ capture_output=True,
68
+ text=True,
69
+ )
70
+ if unlock_result.returncode != 0:
71
+ raise cappa.Exit(f"Bitwarden unlock failed {unlock_result.stderr}", code=1)
72
+
73
+ return unlock_result.stdout.strip()
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from contextlib import contextmanager
5
+ from typing import Generator
6
+ from typing import TYPE_CHECKING
7
+
8
+ import cappa
9
+
10
+ from fujin.config import SecretConfig
11
+
12
+ if TYPE_CHECKING:
13
+ from . import secret_reader
14
+
15
+
16
+ @contextmanager
17
+ def one_password(_: SecretConfig) -> Generator[secret_reader, None, None]:
18
+ def read_secret(name: str) -> str:
19
+ result = subprocess.run(["op", "read", name], capture_output=True, text=True)
20
+ if result.returncode != 0:
21
+ raise cappa.Exit(result.stderr)
22
+ return result.stdout.strip()
23
+
24
+ try:
25
+ yield read_secret
26
+ finally:
27
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fujin-cli
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
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
@@ -21,36 +21,48 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: cappa>=0.24
23
23
  Requires-Dist: fabric>=3.2.2
24
+ Requires-Dist: gevent[recommended]>=24.11.1
24
25
  Requires-Dist: msgspec[toml]>=0.18.6
26
+ Requires-Dist: python-dotenv>=1.0.1
25
27
  Requires-Dist: rich>=13.9.2
26
28
  Description-Content-Type: text/markdown
27
29
 
28
30
  # fujin
29
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)
30
40
  [![PyPI - Version](https://img.shields.io/pypi/v/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
31
41
  [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
32
42
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/falcopackages/fujin/blob/main/LICENSE.txt)
33
43
  [![Status](https://img.shields.io/pypi/status/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
34
- -----
35
44
 
36
- > [!IMPORTANT]
37
- > This package currently contains minimal features and is a work-in-progress
38
-
39
- <!-- content:start -->
45
+ ## Features
40
46
 
41
- `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,
42
- 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.
43
- 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
44
55
 
45
- Check out the [documentation📚](https://fujin.oluwatobi.dev/en/latest/) for installation, features, and usage guides.
56
+ For more details, check out the [documentation📚](https://fujin.oluwatobi.dev/en/latest/).
46
57
 
47
58
  ## Why?
48
59
 
49
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
50
- trying to get your app in production, you probably went through this. I'm using caddy 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.
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.
51
64
 
52
- 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.
53
- 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).
54
66
 
55
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.
56
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.
@@ -66,4 +78,4 @@ Fujin draws inspiration from the following tools for their developer experience.
66
78
 
67
79
  `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
68
80
 
69
- <!-- content:end -->
81
+ <!-- content:end -->
@@ -0,0 +1,38 @@
1
+ fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ fujin/__main__.py,sha256=VJMBzuQuxkQaAKNEySktGnms944bkRsrIAjj-XeaWR8,1813
3
+ fujin/config.py,sha256=pdWK4fM3BITpz0MAv_ydCXRcQbgC3eLz--APdOpehiQ,10412
4
+ fujin/connection.py,sha256=LL7LhX9p0X9FmiGdlSroD3Ht216QY0Kd51xkSrXmM3s,2479
5
+ fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
6
+ fujin/hooks.py,sha256=nTn2PHpFuKbl_iAXUxagkrGcuwT2Exx8zIq4YfleX8A,1351
7
+ fujin/commands/__init__.py,sha256=g0b13vzidPUbxne_Zo_Wv5jpEmwCiSK4AokCinH9uo4,39
8
+ fujin/commands/_base.py,sha256=NfBdRBx_l7tYnVg0L8A3tPFC_XmtuHq-toU66KT5CiE,2553
9
+ fujin/commands/app.py,sha256=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
10
+ fujin/commands/config.py,sha256=xdfd1OZLxw2YZldiAbW5rq5EBXEaXbUC-I7FKLRfzIQ,2387
11
+ fujin/commands/deploy.py,sha256=PfEdLS-rkdF12BfjtLEyqbnn1K4dEYRh7h6CFtWb2zs,5934
12
+ fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
13
+ fujin/commands/down.py,sha256=v1lAq70ApktjeHRB_1sCzjmKH8t6EXqyL4RTt7OE-f0,1716
14
+ fujin/commands/init.py,sha256=zREfkIQyC6etqqQ6hgvDqpNNWQT4bk_8IOPBBT5-YUE,3983
15
+ fujin/commands/printenv.py,sha256=bpGmOfc1t_dKWb8gy7EILYtwEyI9pIwhKg2XPKyJ9cQ,527
16
+ fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
17
+ fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
18
+ fujin/commands/redeploy.py,sha256=z1giY9SpINdJt8UagPlvUkJu30c8fgqapNqOxG4Jfuo,2378
19
+ fujin/commands/rollback.py,sha256=JsocJzQcdQelSnYD94klhjBh8UKkkdiRD9shfUfo4FI,2032
20
+ fujin/commands/server.py,sha256=0N_P_Luj31t56riZ8GfgRqW3vRHiw0cDrlp3PFoyWn8,3453
21
+ fujin/commands/up.py,sha256=OEK_n-6-mnnIUffFpR7QtVunr1V1F04pxlAAS1U62BY,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=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
26
+ fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
27
+ fujin/proxies/nginx.py,sha256=BNJNLxLLRVAmBIGVCk8pb16iiSJsOI9jXOZhdSQGtX8,4151
28
+ fujin/secrets/__init__.py,sha256=p4rY4J7yRoEEz6OXkomJ_Ov2AaaQ37-Zd_TJGpUDPgQ,1217
29
+ fujin/secrets/bitwarden.py,sha256=01GZL5hYwZzL6yXy5ab3L3kgBFBeOT8i3Yg9GC8YwFU,2008
30
+ fujin/secrets/onepassword.py,sha256=6Xj3XWttKfcjMbcoMZvXVpJW1KHxlD785DysmX_mqvk,654
31
+ fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
32
+ fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
33
+ fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
34
+ fujin_cli-0.7.1.dist-info/METADATA,sha256=kqTIyfQ_EtbpdoaSkBmrF5Wy6y838HLaqLJHbLko3L4,4576
35
+ fujin_cli-0.7.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
+ fujin_cli-0.7.1.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
37
+ fujin_cli-0.7.1.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
38
+ fujin_cli-0.7.1.dist-info/RECORD,,
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
@@ -1,35 +0,0 @@
1
- fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
3
- fujin/config.py,sha256=GecU6XklYNXjjR7bkRORBcX1-AYlIaTMw1mmYKSCSnA,9595
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=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
10
- fujin/commands/config.py,sha256=xdfd1OZLxw2YZldiAbW5rq5EBXEaXbUC-I7FKLRfzIQ,2387
11
- fujin/commands/deploy.py,sha256=oAOLUrtHjHkmIA3D0AiaJRWEsfVk51_I29kRC_FhjYo,5463
12
- fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
13
- fujin/commands/down.py,sha256=v1lAq70ApktjeHRB_1sCzjmKH8t6EXqyL4RTt7OE-f0,1716
14
- fujin/commands/init.py,sha256=t8uwwOi4SBqHjV8px_SkTHAeZIiIUJnFN-lf7DK6HhE,3959
15
- fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
16
- fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
17
- fujin/commands/redeploy.py,sha256=JvCJBZBcCKkUw1efZwRPJMLUAV8oqBAZeSbUBLHyn3k,2185
18
- fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
19
- fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
20
- fujin/commands/server.py,sha256=0N_P_Luj31t56riZ8GfgRqW3vRHiw0cDrlp3PFoyWn8,3453
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=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
26
- fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
27
- fujin/proxies/nginx.py,sha256=S2-tBaytGtehqMyeZZMPSPoXjV1GVv7S63eMtfhkGNM,4100
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.6.0.dist-info/METADATA,sha256=_rYue3Q7z_StHy_Z9DVdaU0RWijde8eWFlm_gwxN4GY,4452
32
- fujin_cli-0.6.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
33
- fujin_cli-0.6.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
34
- fujin_cli-0.6.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
35
- fujin_cli-0.6.0.dist-info/RECORD,,