fujin-cli 0.7.0__tar.gz → 0.8.0__tar.gz
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_cli-0.7.0 → fujin_cli-0.8.0}/.pre-commit-config.yaml +7 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/CHANGELOG.md +12 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/PKG-INFO +1 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/printenv.rst +1 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/conf.py +1 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/configuration.rst +8 -4
- fujin_cli-0.8.0/docs/hooks.rst +41 -0
- fujin_cli-0.8.0/docs/requirements.txt +100 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/pyproject.toml +4 -4
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/__init__.py +0 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/_base.py +10 -6
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/deploy.py +8 -7
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/down.py +0 -3
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/init.py +2 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/printenv.py +4 -2
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/redeploy.py +7 -8
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/rollback.py +2 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/server.py +0 -3
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/config.py +3 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/connection.py +3 -5
- fujin_cli-0.8.0/src/fujin/hooks.py +59 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/secrets/__init__.py +4 -2
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/secrets/bitwarden.py +16 -5
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/secrets/onepassword.py +6 -5
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/uv.lock +328 -970
- fujin_cli-0.7.0/docs/hooks.rst +0 -2
- fujin_cli-0.7.0/docs/requirements.txt +0 -138
- fujin_cli-0.7.0/src/fujin/hooks.py +0 -55
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/.github/workflows/publish.yml +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/.gitignore +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/.readthedocs.yaml +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/LICENSE.txt +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/README.md +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/Vagrantfile +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/changelog.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/app.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/config.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/deploy.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/docs.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/down.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/index.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/init.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/proxy.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/prune.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/redeploy.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/rollback.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/server.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/commands/up.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/index.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/installation.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/secrets.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/docs/tutorial.rst +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/README.md +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/settings.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/urls.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/fujin.toml +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/manage.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/pyproject.toml +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/django/bookstore/requirements.txt +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/golang/pocketbase/.env.prod +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/examples/golang/pocketbase/fujin.toml +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/justfile +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/__init__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/__main__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/app.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/config.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/docs.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/proxy.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/prune.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/commands/up.py +1 -1
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/errors.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/process_managers/__init__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/process_managers/systemd.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/proxies/__init__.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/proxies/caddy.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/proxies/dummy.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/proxies/nginx.py +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/templates/simple.service +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/templates/web.service +0 -0
- {fujin_cli-0.7.0 → fujin_cli-0.8.0}/src/fujin/templates/web.socket +0 -0
|
@@ -34,3 +34,10 @@ repos:
|
|
|
34
34
|
- id: pyproject-fmt
|
|
35
35
|
args: [ "pyproject.toml" ]
|
|
36
36
|
exclude: ^(examples/)
|
|
37
|
+
|
|
38
|
+
# - repo: https://github.com/asottile/reorder_python_imports
|
|
39
|
+
# rev: v3.13.0
|
|
40
|
+
# hooks:
|
|
41
|
+
# - id: reorder-python-imports
|
|
42
|
+
# args:
|
|
43
|
+
# - "--application-directories=src"
|
|
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.8.0] - 2024-11-23
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- Rewrite hooks (#30)
|
|
12
|
+
|
|
13
|
+
## [0.7.1] - 2024-11-23
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes
|
|
16
|
+
|
|
17
|
+
- Broken .venv folder can fail deploy
|
|
18
|
+
|
|
7
19
|
## [0.7.0] - 2024-11-22
|
|
8
20
|
|
|
9
21
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fujin-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -13,20 +13,24 @@ This is a minimal working example.
|
|
|
13
13
|
|
|
14
14
|
.. tab-item:: python package
|
|
15
15
|
|
|
16
|
-
..
|
|
17
|
-
:
|
|
16
|
+
.. exec_code::
|
|
17
|
+
:language_output: toml
|
|
18
18
|
|
|
19
|
+
# --- hide: start ---
|
|
19
20
|
from fujin.commands.init import simple_config
|
|
20
21
|
from tomli_w import dumps
|
|
21
22
|
|
|
22
23
|
print(dumps(simple_config("bookstore")))
|
|
24
|
+
#hide:toggle
|
|
23
25
|
|
|
24
26
|
.. tab-item:: binary mode
|
|
25
27
|
|
|
26
|
-
..
|
|
27
|
-
:
|
|
28
|
+
.. exec_code::
|
|
29
|
+
:language_output: toml
|
|
28
30
|
|
|
31
|
+
# --- hide: start ---
|
|
29
32
|
from fujin.commands.init import binary_config
|
|
30
33
|
from tomli_w import dumps
|
|
31
34
|
|
|
32
35
|
print(dumps(binary_config("bookstore")))
|
|
36
|
+
#hide:toggle
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Hooks
|
|
2
|
+
=====
|
|
3
|
+
|
|
4
|
+
Hooks allow you to run scripts at specific points in the deployment process. These scripts can perform checks before deployment, run tests, or send a notification after a successful deployment.
|
|
5
|
+
|
|
6
|
+
The currently available ``hooks`` are:
|
|
7
|
+
|
|
8
|
+
- ``pre_build``: The first action executed when the ``deploy`` command is run, before your app is built.
|
|
9
|
+
- ``pre_deploy``: Executed after app is built before the code is pushed to the remote server.
|
|
10
|
+
- ``post_deploy``: Executed after a successful deployment.
|
|
11
|
+
|
|
12
|
+
.. note::
|
|
13
|
+
|
|
14
|
+
Hooks are run on your local machine, not on the remote server. If they fail, the deployment process is stopped.
|
|
15
|
+
|
|
16
|
+
You can specify hooks in the ``fujin.toml`` file like this:
|
|
17
|
+
|
|
18
|
+
.. code-block::
|
|
19
|
+
|
|
20
|
+
[hooks]
|
|
21
|
+
pre_build = "echo 'hello'"
|
|
22
|
+
|
|
23
|
+
.. tip::
|
|
24
|
+
|
|
25
|
+
Using a `justfile <https://just.systems/>`_ can be an easy way to write more complex scripts without adding new files.
|
|
26
|
+
|
|
27
|
+
.. code-block::
|
|
28
|
+
|
|
29
|
+
[hooks]
|
|
30
|
+
pre_build = "just run-some-checks"
|
|
31
|
+
|
|
32
|
+
Alternatively, you can create a file with the hook's name in the ``.fujin/hooks`` folder and ensure the file is executable.
|
|
33
|
+
|
|
34
|
+
.. code-block::
|
|
35
|
+
:caption: .fujin/hooks/pre_build
|
|
36
|
+
|
|
37
|
+
#!/usr/bin/env bash
|
|
38
|
+
|
|
39
|
+
echo 'hello'
|
|
40
|
+
|
|
41
|
+
The ``hooks`` configuration takes precedence over files in the ``.fujin/hooks`` folder; you cannot mix and match both.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# This file was autogenerated by uv via the following command:
|
|
2
|
+
# uv export --no-hashes --group docs --format requirements-txt
|
|
3
|
+
-e .
|
|
4
|
+
alabaster==1.0.0
|
|
5
|
+
annotated-types==0.7.0
|
|
6
|
+
anyio==4.6.2.post1
|
|
7
|
+
asgiref==3.8.1
|
|
8
|
+
astroid==3.3.5
|
|
9
|
+
babel==2.16.0
|
|
10
|
+
bcrypt==4.2.1
|
|
11
|
+
bracex==2.5.post1
|
|
12
|
+
bump-my-version==0.28.1
|
|
13
|
+
cappa==0.25.1
|
|
14
|
+
certifi==2024.8.30
|
|
15
|
+
cffi==1.17.1
|
|
16
|
+
charset-normalizer==3.4.0
|
|
17
|
+
click==8.1.7
|
|
18
|
+
colorama==0.4.6
|
|
19
|
+
cryptography==43.0.3
|
|
20
|
+
decorator==5.1.1
|
|
21
|
+
deprecated==1.2.15
|
|
22
|
+
django==5.1.3
|
|
23
|
+
docutils==0.21.2
|
|
24
|
+
exceptiongroup==1.2.2 ; python_full_version < '3.11'
|
|
25
|
+
fabric==3.2.2
|
|
26
|
+
faker==33.0.0
|
|
27
|
+
fastapi==0.115.5
|
|
28
|
+
gevent==24.11.1
|
|
29
|
+
git-cliff==2.7.0
|
|
30
|
+
greenlet==3.1.1 ; platform_python_implementation == 'CPython'
|
|
31
|
+
h11==0.14.0
|
|
32
|
+
httpcore==1.0.7
|
|
33
|
+
httpx==0.27.2
|
|
34
|
+
idna==3.10
|
|
35
|
+
imagesize==1.4.1
|
|
36
|
+
invoke==2.2.0
|
|
37
|
+
jinja2==3.1.4
|
|
38
|
+
litestar==2.13.0
|
|
39
|
+
litestar-htmx==0.3.0
|
|
40
|
+
markdown-it-py==3.0.0
|
|
41
|
+
markupsafe==3.0.2
|
|
42
|
+
mdit-py-plugins==0.4.2
|
|
43
|
+
mdurl==0.1.2
|
|
44
|
+
msgspec==0.18.6
|
|
45
|
+
multidict==6.1.0
|
|
46
|
+
myst-parser==4.0.0
|
|
47
|
+
packaging==24.2
|
|
48
|
+
paramiko==3.5.0
|
|
49
|
+
polyfactory==2.18.0
|
|
50
|
+
prompt-toolkit==3.0.48
|
|
51
|
+
psutil==6.1.0 ; sys_platform != 'win32' or platform_python_implementation == 'CPython'
|
|
52
|
+
pycparser==2.22
|
|
53
|
+
pydantic==2.10.1
|
|
54
|
+
pydantic-core==2.27.1
|
|
55
|
+
pydantic-settings==2.6.1
|
|
56
|
+
pygments==2.18.0
|
|
57
|
+
pynacl==1.5.0
|
|
58
|
+
python-dateutil==2.9.0.post0
|
|
59
|
+
python-dotenv==1.0.1
|
|
60
|
+
pyyaml==6.0.2
|
|
61
|
+
questionary==1.10.0
|
|
62
|
+
requests==2.32.3
|
|
63
|
+
rich==13.9.4
|
|
64
|
+
rich-click==1.8.4
|
|
65
|
+
setuptools==75.6.0
|
|
66
|
+
shibuya==2024.10.15
|
|
67
|
+
six==1.16.0
|
|
68
|
+
sniffio==1.3.1
|
|
69
|
+
snowballstemmer==2.2.0
|
|
70
|
+
sphinx==8.1.3
|
|
71
|
+
sphinx-autobuild==2024.10.3
|
|
72
|
+
sphinx-autodoc2==0.5.0
|
|
73
|
+
sphinx-copybutton==0.5.2
|
|
74
|
+
sphinx-design==0.6.1
|
|
75
|
+
sphinx-exec-code==0.14
|
|
76
|
+
sphinx-togglebutton==0.3.2
|
|
77
|
+
sphinxcontrib-applehelp==2.0.0
|
|
78
|
+
sphinxcontrib-devhelp==2.0.0
|
|
79
|
+
sphinxcontrib-htmlhelp==2.1.0
|
|
80
|
+
sphinxcontrib-jsmath==1.0.1
|
|
81
|
+
sphinxcontrib-qthelp==2.0.0
|
|
82
|
+
sphinxcontrib-serializinghtml==2.0.0
|
|
83
|
+
sqlparse==0.5.2
|
|
84
|
+
starlette==0.41.3
|
|
85
|
+
tomli==2.1.0 ; python_full_version < '3.11'
|
|
86
|
+
tomli-w==1.1.0
|
|
87
|
+
tomlkit==0.13.2
|
|
88
|
+
type-lens==0.2.3
|
|
89
|
+
typing-extensions==4.12.2
|
|
90
|
+
tzdata==2024.2 ; sys_platform == 'win32'
|
|
91
|
+
urllib3==2.2.3
|
|
92
|
+
uvicorn==0.32.1
|
|
93
|
+
watchfiles==0.24.0
|
|
94
|
+
wcmatch==10.0
|
|
95
|
+
wcwidth==0.2.13
|
|
96
|
+
websockets==14.1
|
|
97
|
+
wheel==0.45.1
|
|
98
|
+
wrapt==1.17.0
|
|
99
|
+
zope-event==5.0
|
|
100
|
+
zope-interface==7.1.1
|
|
@@ -7,7 +7,7 @@ requires = [
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "fujin-cli"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.8.0"
|
|
11
11
|
description = "Get your project up and running in a few minutes on your own vps."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
keywords = [
|
|
@@ -61,13 +61,13 @@ dev = [
|
|
|
61
61
|
"litestar>=2.12.1",
|
|
62
62
|
]
|
|
63
63
|
docs = [
|
|
64
|
-
"jupyter-sphinx>=0.5.3",
|
|
65
64
|
"myst-parser",
|
|
66
65
|
"shibuya",
|
|
67
66
|
"sphinx-autobuild",
|
|
68
67
|
"sphinx-autodoc2>=0.5",
|
|
69
68
|
"sphinx-copybutton",
|
|
70
69
|
"sphinx-design",
|
|
70
|
+
"sphinx-exec-code>=0.14",
|
|
71
71
|
"sphinx-togglebutton>=0.3.2",
|
|
72
72
|
]
|
|
73
73
|
|
|
@@ -116,12 +116,12 @@ lint.select = [
|
|
|
116
116
|
"B", # flake8-bugbear
|
|
117
117
|
"E", # Pycodestyle
|
|
118
118
|
"F", # Pyflakes
|
|
119
|
-
"I", # isort
|
|
120
119
|
"UP", # pyupgrade
|
|
121
120
|
]
|
|
122
121
|
lint.ignore = [
|
|
123
122
|
"E501",
|
|
124
123
|
"E741",
|
|
124
|
+
"I", # isort
|
|
125
125
|
] # temporary
|
|
126
126
|
# Tests can use magic values, assertions, and relative imports
|
|
127
127
|
lint.per-file-ignores."tests/**/*" = [
|
|
@@ -154,7 +154,7 @@ lint.isort.required-imports = [
|
|
|
154
154
|
lint.pyupgrade.keep-runtime-typing = true
|
|
155
155
|
|
|
156
156
|
[tool.bumpversion]
|
|
157
|
-
current_version = "0.
|
|
157
|
+
current_version = "0.8.0"
|
|
158
158
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
159
159
|
serialize = [
|
|
160
160
|
"{major}.{minor}.{patch}",
|
|
@@ -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
|
|
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
|
|
@@ -32,6 +33,14 @@ class BaseCommand:
|
|
|
32
33
|
def app_dir(self) -> str:
|
|
33
34
|
return self.config.host.get_app_dir(app_name=self.config.app_name)
|
|
34
35
|
|
|
36
|
+
@cached_property
|
|
37
|
+
def hook_manager(self) -> HookManager:
|
|
38
|
+
return HookManager(
|
|
39
|
+
hooks=self.config.hooks,
|
|
40
|
+
app_name=self.config.app_name,
|
|
41
|
+
local_config_dir=self.config.local_config_dir,
|
|
42
|
+
)
|
|
43
|
+
|
|
35
44
|
@contextmanager
|
|
36
45
|
def connection(self):
|
|
37
46
|
with host_connection(host=self.config.host) as conn:
|
|
@@ -69,8 +78,3 @@ class BaseCommand:
|
|
|
69
78
|
|
|
70
79
|
def create_process_manager(self, conn: Connection) -> ProcessManager:
|
|
71
80
|
return self.process_manager_class.create(conn=conn, config=self.config)
|
|
72
|
-
|
|
73
|
-
def create_hook_manager(self, conn: Connection) -> HookManager:
|
|
74
|
-
return HookManager(
|
|
75
|
-
conn=conn, hooks=self.config.hooks, app_name=self.config.app_name
|
|
76
|
-
)
|
|
@@ -7,8 +7,8 @@ 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
|
|
11
10
|
from fujin.connection import Connection
|
|
11
|
+
from fujin.secrets import resolve_secrets
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@cappa.command(
|
|
@@ -16,15 +16,15 @@ from fujin.connection import Connection
|
|
|
16
16
|
)
|
|
17
17
|
class Deploy(BaseCommand):
|
|
18
18
|
def __call__(self):
|
|
19
|
+
self.hook_manager.pre_build()
|
|
19
20
|
parsed_env = self.parse_envfile()
|
|
20
21
|
self.build_app()
|
|
21
|
-
|
|
22
|
+
self.hook_manager.pre_deploy()
|
|
22
23
|
with self.connection() as conn:
|
|
23
24
|
process_manager = self.create_process_manager(conn)
|
|
24
25
|
conn.run(f"mkdir -p {self.app_dir}")
|
|
25
26
|
conn.run(f"mkdir -p {self.versioned_assets_dir}")
|
|
26
27
|
with conn.cd(self.app_dir):
|
|
27
|
-
self.create_hook_manager(conn).pre_deploy()
|
|
28
28
|
self.transfer_files(conn, env=parsed_env)
|
|
29
29
|
self.install_project(conn)
|
|
30
30
|
with self.app_environment() as app_conn:
|
|
@@ -35,7 +35,7 @@ class Deploy(BaseCommand):
|
|
|
35
35
|
self.create_web_proxy(app_conn).setup()
|
|
36
36
|
self.update_version_history(app_conn)
|
|
37
37
|
self.prune_assets(app_conn)
|
|
38
|
-
|
|
38
|
+
self.hook_manager.post_deploy()
|
|
39
39
|
self.stdout.output("[green]Project deployment completed successfully![/green]")
|
|
40
40
|
self.stdout.output(
|
|
41
41
|
f"[blue]Access the deployed project at: https://{self.config.host.domain_name}[/blue]"
|
|
@@ -60,7 +60,7 @@ class Deploy(BaseCommand):
|
|
|
60
60
|
return self.config.host.envfile.read_text()
|
|
61
61
|
|
|
62
62
|
def transfer_files(
|
|
63
|
-
|
|
63
|
+
self, conn: Connection, env: str, skip_requirements: bool = False
|
|
64
64
|
):
|
|
65
65
|
conn.run(f"echo '{env}' > {self.app_dir}/.env")
|
|
66
66
|
distfile_path = self.config.get_distfile_path()
|
|
@@ -78,7 +78,7 @@ class Deploy(BaseCommand):
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
def install_project(
|
|
81
|
-
|
|
81
|
+
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
82
82
|
):
|
|
83
83
|
version = version or self.config.version
|
|
84
84
|
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
@@ -87,7 +87,7 @@ class Deploy(BaseCommand):
|
|
|
87
87
|
self._install_binary(conn, version)
|
|
88
88
|
|
|
89
89
|
def _install_python_package(
|
|
90
|
-
|
|
90
|
+
self, conn: Connection, version: str, skip_setup: bool = False
|
|
91
91
|
):
|
|
92
92
|
appenv = f"""
|
|
93
93
|
set -a # Automatically export all variables
|
|
@@ -100,6 +100,7 @@ export PATH=".venv/bin:$PATH"
|
|
|
100
100
|
conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
|
|
101
101
|
versioned_assets_dir = f"{self.app_dir}/v{version}"
|
|
102
102
|
if not skip_setup:
|
|
103
|
+
conn.run("rm -rf .venv")
|
|
103
104
|
conn.run("uv venv")
|
|
104
105
|
if self.config.requirements:
|
|
105
106
|
conn.run(f"uv pip install -r {versioned_assets_dir}/requirements.txt")
|
|
@@ -33,8 +33,6 @@ class Down(BaseCommand):
|
|
|
33
33
|
if not confirm:
|
|
34
34
|
return
|
|
35
35
|
with self.connection() as conn:
|
|
36
|
-
hook_manager = self.create_hook_manager(conn)
|
|
37
|
-
hook_manager.pre_teardown()
|
|
38
36
|
process_manager = self.create_process_manager(conn)
|
|
39
37
|
conn.run(f"rm -rf {self.app_dir}")
|
|
40
38
|
self.create_web_proxy(conn).teardown()
|
|
@@ -42,7 +40,6 @@ class Down(BaseCommand):
|
|
|
42
40
|
process_manager.reload_configuration()
|
|
43
41
|
if self.full:
|
|
44
42
|
self.create_web_proxy(conn).uninstall()
|
|
45
|
-
hook_manager.post_teardown()
|
|
46
43
|
self.stdout.output(
|
|
47
44
|
"[green]Project teardown completed successfully![/green]"
|
|
48
45
|
)
|
|
@@ -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
|
|
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")
|
|
@@ -5,12 +5,14 @@ from fujin.secrets import resolve_secrets
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@cappa.command(
|
|
8
|
-
help="
|
|
8
|
+
help="Display the contents of the envfile with resolved secrets (for debugging purposes)"
|
|
9
9
|
)
|
|
10
10
|
class Printenv(BaseCommand):
|
|
11
11
|
def __call__(self):
|
|
12
12
|
if self.config.secret_config:
|
|
13
|
-
result = resolve_secrets(
|
|
13
|
+
result = resolve_secrets(
|
|
14
|
+
self.config.host.envfile, self.config.secret_config
|
|
15
|
+
)
|
|
14
16
|
else:
|
|
15
17
|
result = self.config.host.envfile.read_text()
|
|
16
18
|
self.stdout.output(result)
|
|
@@ -6,21 +6,20 @@ from pathlib import Path
|
|
|
6
6
|
import cappa
|
|
7
7
|
|
|
8
8
|
from fujin.commands import BaseCommand
|
|
9
|
-
from .deploy import Deploy
|
|
10
9
|
from fujin.config import InstallationMode
|
|
11
10
|
from fujin.connection import Connection
|
|
11
|
+
from .deploy import Deploy
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@cappa.command(help="Redeploy the application to apply code and environment changes")
|
|
15
15
|
class Redeploy(BaseCommand):
|
|
16
16
|
def __call__(self):
|
|
17
17
|
deploy = Deploy()
|
|
18
|
+
self.hook_manager.pre_build()
|
|
18
19
|
parsed_env = deploy.parse_envfile()
|
|
19
20
|
deploy.build_app()
|
|
20
|
-
|
|
21
|
+
self.hook_manager.pre_deploy()
|
|
21
22
|
with self.app_environment() as conn:
|
|
22
|
-
hook_manager = self.create_hook_manager(conn)
|
|
23
|
-
hook_manager.pre_deploy()
|
|
24
23
|
conn.run(f"mkdir -p {deploy.versioned_assets_dir}")
|
|
25
24
|
requirements_copied = self._copy_requirements_if_needed(conn)
|
|
26
25
|
deploy.transfer_files(
|
|
@@ -30,13 +29,13 @@ class Redeploy(BaseCommand):
|
|
|
30
29
|
deploy.release(conn)
|
|
31
30
|
self.create_process_manager(conn).restart_services()
|
|
32
31
|
deploy.update_version_history(conn)
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
self.hook_manager.post_deploy()
|
|
33
|
+
self.stdout.output("[green]Redeployment completed successfully![/green]")
|
|
35
34
|
|
|
36
35
|
def _copy_requirements_if_needed(self, conn: Connection) -> bool:
|
|
37
36
|
if (
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
not self.config.requirements
|
|
38
|
+
or self.config.installation_mode == InstallationMode.BINARY
|
|
40
39
|
):
|
|
41
40
|
return False
|
|
42
41
|
local_requirements = hashlib.md5(
|
|
@@ -23,8 +23,6 @@ class Server(BaseCommand):
|
|
|
23
23
|
@cappa.command(help="Setup uv, web proxy, and install necessary dependencies")
|
|
24
24
|
def bootstrap(self):
|
|
25
25
|
with self.connection() as conn:
|
|
26
|
-
hook_manager = self.create_hook_manager(conn)
|
|
27
|
-
hook_manager.pre_bootstrap()
|
|
28
26
|
conn.run("sudo apt update && sudo apt upgrade -y", pty=True)
|
|
29
27
|
conn.run("sudo apt install -y sqlite3 curl rsync", pty=True)
|
|
30
28
|
result = conn.run("command -v uv", warn=True)
|
|
@@ -33,7 +31,6 @@ class Server(BaseCommand):
|
|
|
33
31
|
conn.run("uv tool update-shell")
|
|
34
32
|
conn.run("uv tool install fastfetch-bin-edge")
|
|
35
33
|
self.create_web_proxy(conn).install()
|
|
36
|
-
hook_manager.post_bootstrap()
|
|
37
34
|
self.stdout.output(
|
|
38
35
|
"[green]Server bootstrap completed successfully![/green]"
|
|
39
36
|
)
|
|
@@ -176,6 +176,9 @@ Example:
|
|
|
176
176
|
dbconsole = "app exec -i dbshell" # open an interactive django database shell
|
|
177
177
|
shell = "server exec --appenv -i bash" # SSH into the project directory with environment variables loaded
|
|
178
178
|
|
|
179
|
+
hooks
|
|
180
|
+
-----
|
|
181
|
+
Run custom scripts at specific points with hooks. Check out the `secrets </hooks.html>`_ page for more information.
|
|
179
182
|
|
|
180
183
|
"""
|
|
181
184
|
|
|
@@ -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
|
-
|
|
13
|
-
|
|
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
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import cappa
|
|
6
|
+
from rich import print as rich_print
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
except ImportError:
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
class StrEnum(str, Enum):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Hook(StrEnum):
|
|
18
|
+
PRE_BUILD = "pre_build"
|
|
19
|
+
PRE_DEPLOY = "pre_deploy"
|
|
20
|
+
POST_DEPLOY = "post_deploy"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
HooksDict = dict[Hook, str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class HookManager:
|
|
28
|
+
app_name: str
|
|
29
|
+
hooks: HooksDict
|
|
30
|
+
local_config_dir: Path
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
if self.hooks:
|
|
34
|
+
return
|
|
35
|
+
hooks_folder = self.local_config_dir / "hooks"
|
|
36
|
+
if not hooks_folder.exists():
|
|
37
|
+
return
|
|
38
|
+
self.hooks = {
|
|
39
|
+
h.value: f"./{hooks_folder / h.value}" # noqa
|
|
40
|
+
for h in Hook
|
|
41
|
+
if (hooks_folder / h.value).exists() # noqa
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def _run_hook(self, type_: Hook) -> None:
|
|
45
|
+
if cmd := self.hooks.get(type_):
|
|
46
|
+
rich_print(f"[blue]Running {type_} hook[/blue]")
|
|
47
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
48
|
+
if result.returncode != 0:
|
|
49
|
+
raise cappa.Exit(result.stderr)
|
|
50
|
+
rich_print(result.stdout)
|
|
51
|
+
|
|
52
|
+
def pre_build(self) -> None:
|
|
53
|
+
self._run_hook(Hook.PRE_BUILD)
|
|
54
|
+
|
|
55
|
+
def pre_deploy(self) -> None:
|
|
56
|
+
self._run_hook(Hook.PRE_DEPLOY)
|
|
57
|
+
|
|
58
|
+
def post_deploy(self) -> None:
|
|
59
|
+
self._run_hook(Hook.POST_DEPLOY)
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
4
6
|
import gevent
|
|
5
7
|
from dotenv import dotenv_values
|
|
6
|
-
from typing import Callable
|
|
7
8
|
|
|
8
|
-
from fujin.config import SecretConfig, SecretAdapter
|
|
9
9
|
from .bitwarden import bitwarden
|
|
10
10
|
from .onepassword import one_password
|
|
11
|
+
from fujin.config import SecretAdapter
|
|
12
|
+
from fujin.config import SecretConfig
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
secret_reader = Callable[[str], str]
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Generator
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
4
8
|
|
|
9
|
+
import cappa
|
|
5
10
|
|
|
6
11
|
from fujin.config import SecretConfig
|
|
7
|
-
import subprocess
|
|
8
|
-
from contextlib import contextmanager
|
|
9
|
-
from typing import Generator, TYPE_CHECKING
|
|
10
12
|
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from . import secret_reader
|
|
@@ -25,7 +27,16 @@ def bitwarden(secret_config: SecretConfig) -> Generator[secret_reader, None, Non
|
|
|
25
27
|
|
|
26
28
|
def read_secret(name: str) -> str:
|
|
27
29
|
result = subprocess.run(
|
|
28
|
-
[
|
|
30
|
+
[
|
|
31
|
+
"bw",
|
|
32
|
+
"get",
|
|
33
|
+
"password",
|
|
34
|
+
name,
|
|
35
|
+
"--raw",
|
|
36
|
+
"--session",
|
|
37
|
+
session,
|
|
38
|
+
"--nointeraction",
|
|
39
|
+
],
|
|
29
40
|
capture_output=True,
|
|
30
41
|
text=True,
|
|
31
42
|
)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
import cappa
|
|
3
|
-
|
|
4
2
|
|
|
5
|
-
from fujin.config import SecretConfig
|
|
6
3
|
import subprocess
|
|
7
|
-
|
|
8
4
|
from contextlib import contextmanager
|
|
9
|
-
from typing import Generator
|
|
5
|
+
from typing import Generator
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import cappa
|
|
9
|
+
|
|
10
|
+
from fujin.config import SecretConfig
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from . import secret_reader
|