nullforge 0.2.0__tar.gz → 0.3.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.
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/labeler.yaml +2 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/ci-code-quality.yaml +8 -2
- nullforge-0.3.0/.github/workflows/ci-docs.yaml +37 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/ci-tests.yaml +2 -2
- nullforge-0.3.0/.github/workflows/deploy-docs.yaml +62 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/release-please.yaml +3 -3
- nullforge-0.3.0/.markdownlint-cli2.jsonc +25 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.pre-commit-config.yaml +8 -3
- nullforge-0.3.0/.release-please-manifest.json +3 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/CHANGELOG.md +19 -0
- nullforge-0.3.0/PKG-INFO +46 -0
- nullforge-0.3.0/README.md +29 -0
- nullforge-0.3.0/docs/concepts/architecture.md +61 -0
- nullforge-0.3.0/docs/concepts/inventories.md +165 -0
- nullforge-0.3.0/docs/concepts/molds.md +69 -0
- nullforge-0.3.0/docs/concepts/runes.md +74 -0
- nullforge-0.3.0/docs/contributing/conventions.md +72 -0
- nullforge-0.3.0/docs/contributing/development.md +68 -0
- nullforge-0.3.0/docs/features/base.md +71 -0
- nullforge-0.3.0/docs/features/containers.md +46 -0
- nullforge-0.3.0/docs/features/dns.md +58 -0
- nullforge-0.3.0/docs/features/haproxy.md +32 -0
- nullforge-0.3.0/docs/features/index.md +29 -0
- nullforge-0.3.0/docs/features/monitoring.md +60 -0
- nullforge-0.3.0/docs/features/netsec.md +97 -0
- nullforge-0.3.0/docs/features/profiles.md +56 -0
- nullforge-0.3.0/docs/features/telemt.md +62 -0
- nullforge-0.3.0/docs/features/tor.md +29 -0
- nullforge-0.3.0/docs/features/users.md +54 -0
- nullforge-0.3.0/docs/features/warp.md +56 -0
- nullforge-0.3.0/docs/features/xray.md +30 -0
- nullforge-0.3.0/docs/features/zerotrust.md +41 -0
- nullforge-0.3.0/docs/getting-started/cli.md +85 -0
- nullforge-0.3.0/docs/getting-started/installation.md +40 -0
- nullforge-0.3.0/docs/getting-started/quickstart.md +75 -0
- nullforge-0.3.0/docs/guides/custom-runes.md +79 -0
- nullforge-0.3.0/docs/guides/version-pinning.md +60 -0
- nullforge-0.3.0/docs/index.md +72 -0
- nullforge-0.3.0/nullforge/__init__.py +1 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/containers.py +8 -6
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/profiles.py +11 -9
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/xray.py +12 -5
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/cloudflare.py +3 -2
- nullforge-0.3.0/nullforge/smithy/http.py +186 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/install.py +2 -3
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/deploy.py +2 -2
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/versions.py +3 -3
- nullforge-0.3.0/pyproject.toml +136 -0
- nullforge-0.3.0/tests/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/components/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/components/completion/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/components/completion/test_controller.py +77 -0
- nullforge-0.3.0/tests/cli/components/completion/test_powershell.py +97 -0
- nullforge-0.3.0/tests/cli/components/foundry/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/components/foundry/test_controller.py +141 -0
- nullforge-0.3.0/tests/cli/conftest.py +20 -0
- nullforge-0.3.0/tests/cli/core/__init__.py +0 -0
- nullforge-0.3.0/tests/cli/core/test_application.py +120 -0
- nullforge-0.3.0/tests/cli/test_types.py +127 -0
- nullforge-0.3.0/tests/conftest.py +50 -0
- nullforge-0.3.0/tests/test_foundry.py +56 -0
- nullforge-0.3.0/tests/test_foundry_cast.py +60 -0
- nullforge-0.3.0/tests/test_foundry_pyinfra.py +94 -0
- nullforge-0.3.0/tests/test_models_dns.py +42 -0
- nullforge-0.3.0/tests/test_molds_dns.py +50 -0
- nullforge-0.3.0/tests/test_molds_monitoring.py +78 -0
- nullforge-0.3.0/tests/test_molds_netsec.py +44 -0
- nullforge-0.3.0/tests/test_molds_telemt.py +65 -0
- nullforge-0.3.0/tests/test_molds_user.py +21 -0
- nullforge-0.3.0/tests/test_molds_utils.py +177 -0
- nullforge-0.3.0/tests/test_runes_dns.py +19 -0
- nullforge-0.3.0/tests/test_runes_meta.py +163 -0
- nullforge-0.3.0/tests/test_runes_misc.py +33 -0
- nullforge-0.3.0/tests/test_runes_netsec.py +204 -0
- nullforge-0.3.0/tests/test_runes_profiles.py +114 -0
- nullforge-0.3.0/tests/test_runes_telemt.py +105 -0
- nullforge-0.3.0/tests/test_runes_users.py +62 -0
- nullforge-0.3.0/tests/test_smithy_arch.py +53 -0
- nullforge-0.3.0/tests/test_smithy_github.py +153 -0
- nullforge-0.3.0/tests/test_smithy_http.py +312 -0
- nullforge-0.3.0/tests/test_smithy_install.py +119 -0
- nullforge-0.3.0/tests/test_smithy_network.py +128 -0
- nullforge-0.3.0/tests/test_smithy_nezha.py +44 -0
- nullforge-0.3.0/tests/test_smithy_nezha_dashboard.py +84 -0
- nullforge-0.3.0/tests/test_smithy_packages.py +44 -0
- nullforge-0.3.0/tests/test_smithy_service.py +61 -0
- nullforge-0.3.0/tests/test_smithy_sni.py +224 -0
- nullforge-0.3.0/tests/test_smithy_swap.py +54 -0
- nullforge-0.3.0/tests/test_smithy_system.py +101 -0
- nullforge-0.3.0/tests/test_smithy_versions.py +178 -0
- nullforge-0.3.0/tests/test_templates.py +19 -0
- nullforge-0.3.0/tests/test_templates_telemt.py +108 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/uv.lock +328 -63
- nullforge-0.3.0/zensical.toml +110 -0
- nullforge-0.2.0/.release-please-manifest.json +0 -3
- nullforge-0.2.0/PKG-INFO +0 -68
- nullforge-0.2.0/README.md +0 -52
- nullforge-0.2.0/nullforge/__init__.py +0 -1
- nullforge-0.2.0/nullforge/smithy/http.py +0 -85
- nullforge-0.2.0/pyproject.toml +0 -45
- {nullforge-0.2.0 → nullforge-0.3.0}/.gitattributes +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/CODEOWNERS +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/CONTRIBUTING.md +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/ISSUE_TEMPLATE/config.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/dependabot.yml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/labels.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/release.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/check-commit-message-pr.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/label-prs.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.github/workflows/labels.yaml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/.gitignore +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/LICENSE +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/__main__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/app.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/component.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/controller.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/errors.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/completion/powershell.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/component.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/controller.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/foundry/errors.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/component.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/components/runes/controller.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/application.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/component.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/controller.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/errors.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/core/types.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/display.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/cli/types.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/README.md +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/_pyinfra.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/cast.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/foundry/full_cast.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/inventories/README.md +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/inventories/example.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/containers.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/dns.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/base.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/monitoring/nezha.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/netsec.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/profiles.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/system.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/users.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/warp.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/models/zerotrust.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/base_mold.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/containers.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/defaults.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/dns.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/features.py +1 -1
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/haproxy.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/monitoring/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/monitoring/nezha.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/netsec.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/profiles.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/system.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/telemt.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/tor.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/user.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/utils.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/warp.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/xray.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/molds/zerotrust.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/base.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/dns.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/haproxy.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/monitoring.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/netsec.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/prepare.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/telemt.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/tor.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/users.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/warp.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/runes/zerotrust.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/admin.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/arch.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/blocky.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/github.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/agent.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/monitoring/nezha/dashboard.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/network.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/packages.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/service.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/sni.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/swap.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/smithy/system.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/__init__.py +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/cloudflared/tunnel.yml.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/blocky.yaml.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/dns.yaml.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/resolv.conf.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/dns/resolved.conf.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/etc/default/zramswap.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/nvim/nvim_patch.lua.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/direnv.toml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/starship.toml +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/tmux.conf +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/profiles/zshrc.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/telemt-synfix.sh +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/teleproxy-warp.sh +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/warp-v6-policy.sh +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/scripts/zt-tunnel-warp.sh +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/blocky.service.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/cloudflare-tunnel.service.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/cloudflare-warp.service.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/dns-internal.service.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/systemd/telemt.service.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/telemt/telemt.toml.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/nullforge/templates/tor/torrc.j2 +0 -0
- {nullforge-0.2.0 → nullforge-0.3.0}/release-please-config.json +0 -0
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
with:
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
|
|
28
|
-
- uses: astral-sh/setup-uv@
|
|
28
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
29
29
|
with:
|
|
30
30
|
enable-cache: true
|
|
31
31
|
cache-dependency-glob: uv.lock
|
|
@@ -41,6 +41,12 @@ jobs:
|
|
|
41
41
|
- name: Type check
|
|
42
42
|
run: uv run ty check
|
|
43
43
|
|
|
44
|
+
- name: Check import contracts
|
|
45
|
+
run: uv run lint-imports
|
|
46
|
+
|
|
47
|
+
- name: Check dependency hygiene
|
|
48
|
+
run: uv run deptry .
|
|
49
|
+
|
|
44
50
|
hooks:
|
|
45
51
|
name: 🪝 prek hooks
|
|
46
52
|
runs-on: ubuntu-latest
|
|
@@ -50,7 +56,7 @@ jobs:
|
|
|
50
56
|
with:
|
|
51
57
|
persist-credentials: false
|
|
52
58
|
|
|
53
|
-
- uses: astral-sh/setup-uv@
|
|
59
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
54
60
|
with:
|
|
55
61
|
enable-cache: true
|
|
56
62
|
cache-dependency-glob: uv.lock
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: 📚 Docs build check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "zensical.toml"
|
|
10
|
+
- "pyproject.toml"
|
|
11
|
+
- "uv.lock"
|
|
12
|
+
- ".github/workflows/ci-docs.yaml"
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
concurrency:
|
|
18
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
19
|
+
cancel-in-progress: true
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
build:
|
|
23
|
+
name: 📚 Build documentation
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 10
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
28
|
+
with:
|
|
29
|
+
persist-credentials: false
|
|
30
|
+
|
|
31
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
cache-dependency-glob: uv.lock
|
|
35
|
+
|
|
36
|
+
- name: Build documentation
|
|
37
|
+
run: uv run --group docs zensical build --clean --strict
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
with:
|
|
26
26
|
persist-credentials: false
|
|
27
27
|
|
|
28
|
-
- uses: astral-sh/setup-uv@
|
|
28
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
29
29
|
with:
|
|
30
30
|
enable-cache: true
|
|
31
31
|
cache-dependency-glob: uv.lock
|
|
@@ -43,7 +43,7 @@ jobs:
|
|
|
43
43
|
with:
|
|
44
44
|
persist-credentials: false
|
|
45
45
|
|
|
46
|
-
- uses: astral-sh/setup-uv@
|
|
46
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
47
47
|
with:
|
|
48
48
|
enable-cache: true
|
|
49
49
|
cache-dependency-glob: uv.lock
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: 📚 Deploy documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "zensical.toml"
|
|
10
|
+
- "pyproject.toml"
|
|
11
|
+
- "uv.lock"
|
|
12
|
+
- ".github/workflows/deploy-docs.yaml"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: false
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build:
|
|
24
|
+
name: 📚 Build documentation
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 10
|
|
27
|
+
permissions:
|
|
28
|
+
contents: read
|
|
29
|
+
pages: read
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
|
|
35
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
36
|
+
with:
|
|
37
|
+
enable-cache: true
|
|
38
|
+
cache-dependency-glob: uv.lock
|
|
39
|
+
|
|
40
|
+
- name: Build documentation
|
|
41
|
+
run: uv run --group docs zensical build --clean --strict
|
|
42
|
+
|
|
43
|
+
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
|
44
|
+
|
|
45
|
+
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
|
46
|
+
with:
|
|
47
|
+
path: site
|
|
48
|
+
|
|
49
|
+
deploy:
|
|
50
|
+
name: 🚀 Deploy to GitHub Pages
|
|
51
|
+
needs: build
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
timeout-minutes: 10
|
|
54
|
+
permissions:
|
|
55
|
+
pages: write
|
|
56
|
+
id-token: write
|
|
57
|
+
environment:
|
|
58
|
+
name: github-pages
|
|
59
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
60
|
+
steps:
|
|
61
|
+
- id: deployment
|
|
62
|
+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
|
@@ -52,7 +52,7 @@ jobs:
|
|
|
52
52
|
with:
|
|
53
53
|
persist-credentials: false
|
|
54
54
|
|
|
55
|
-
- uses: astral-sh/setup-uv@
|
|
55
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
56
56
|
with:
|
|
57
57
|
enable-cache: false
|
|
58
58
|
|
|
@@ -95,7 +95,7 @@ jobs:
|
|
|
95
95
|
path: dist/
|
|
96
96
|
|
|
97
97
|
- name: Generate artifact attestations
|
|
98
|
-
uses: actions/attest-build-provenance@
|
|
98
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
99
99
|
with:
|
|
100
100
|
subject-path: dist/*
|
|
101
101
|
|
|
@@ -111,7 +111,7 @@ jobs:
|
|
|
111
111
|
permissions:
|
|
112
112
|
id-token: write
|
|
113
113
|
steps:
|
|
114
|
-
- uses: astral-sh/setup-uv@
|
|
114
|
+
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
115
115
|
|
|
116
116
|
- name: Download distribution artifacts
|
|
117
117
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"gitignore": true,
|
|
3
|
+
|
|
4
|
+
// Generated, boilerplate and vendored - not authored to this style.
|
|
5
|
+
"ignores": [
|
|
6
|
+
"CHANGELOG.md",
|
|
7
|
+
".github/PULL_REQUEST_TEMPLATE.md",
|
|
8
|
+
".github/ISSUE_TEMPLATE/**",
|
|
9
|
+
],
|
|
10
|
+
|
|
11
|
+
"config": {
|
|
12
|
+
"default": true,
|
|
13
|
+
|
|
14
|
+
// Prose is one sentence per line; tables pad cells but not the delimiter row.
|
|
15
|
+
"MD013": false,
|
|
16
|
+
"MD060": false,
|
|
17
|
+
|
|
18
|
+
"MD046": false,
|
|
19
|
+
|
|
20
|
+
// mkdocs admonition blocks
|
|
21
|
+
"MD033": {
|
|
22
|
+
"allowed_elements": ["div"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
}
|
|
@@ -5,7 +5,7 @@ default_install_hook_types:
|
|
|
5
5
|
- post-rewrite
|
|
6
6
|
repos:
|
|
7
7
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
8
|
-
rev: v2.
|
|
8
|
+
rev: v2.28.0
|
|
9
9
|
hooks:
|
|
10
10
|
- id: pyproject-fmt
|
|
11
11
|
|
|
@@ -19,12 +19,12 @@ repos:
|
|
|
19
19
|
- id: end-of-file-fixer
|
|
20
20
|
|
|
21
21
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
22
|
-
rev: 0.
|
|
22
|
+
rev: 0.12.5
|
|
23
23
|
hooks:
|
|
24
24
|
- id: uv-lock
|
|
25
25
|
|
|
26
26
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
27
|
-
rev: v0.16.
|
|
27
|
+
rev: v0.16.3
|
|
28
28
|
hooks:
|
|
29
29
|
- id: ruff-check
|
|
30
30
|
args: [--fix]
|
|
@@ -40,3 +40,8 @@ repos:
|
|
|
40
40
|
- id: python-no-log-warn
|
|
41
41
|
- id: python-use-type-annotations
|
|
42
42
|
- id: text-unicode-replacement-char
|
|
43
|
+
|
|
44
|
+
- repo: https://github.com/DavidAnson/markdownlint-cli2
|
|
45
|
+
rev: v0.23.2
|
|
46
|
+
hooks:
|
|
47
|
+
- id: markdownlint-cli2
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/wlix13/NullForge/compare/v0.2.0...v0.3.0) (2026-08-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **molds:** deploy warp before other features
|
|
9
|
+
* **smithy:** fall back to WARP interface for filtered downloads
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **molds:** deploy warp before other features ([a97cbfe](https://github.com/wlix13/NullForge/commit/a97cbfe9020bf0708acec4adf370cfa7113ea964))
|
|
14
|
+
* **smithy:** fall back to WARP interface for filtered downloads ([25f0b28](https://github.com/wlix13/NullForge/commit/25f0b28cb7d0efb89cf7710aa922d52a9651c985))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* describe WARP download fallback ([8b57441](https://github.com/wlix13/NullForge/commit/8b57441c8ecc234db4f9131e5ab9a603e3384b4c))
|
|
20
|
+
* **site:** add documentation ([d6b8575](https://github.com/wlix13/NullForge/commit/d6b857541d8ed35a5fe8fa3a32cdef95d8d8bf8c))
|
|
21
|
+
|
|
3
22
|
## [0.2.0](https://github.com/wlix13/NullForge/compare/v0.1.0...v0.2.0) (2026-07-25)
|
|
4
23
|
|
|
5
24
|
|
nullforge-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nullforge
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Forge the server's baseline from null
|
|
5
|
+
Author-email: wlix13 <github@wlix13.dev>
|
|
6
|
+
Maintainer-email: wlix13 <github@wlix13.dev>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Python: <3.14,>=3.13
|
|
11
|
+
Requires-Dist: click
|
|
12
|
+
Requires-Dist: pydantic
|
|
13
|
+
Requires-Dist: pyinfra
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Requires-Dist: rich-click
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# NullForge
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/nullforge/)
|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
[](https://wlix13.github.io/NullForge/)
|
|
28
|
+
|
|
29
|
+
Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
|
|
30
|
+
|
|
31
|
+
> [!WARNING]
|
|
32
|
+
> **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv tool install nullforge
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
|
|
43
|
+
|
|
44
|
+
## Contributing
|
|
45
|
+
|
|
46
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# NullForge
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/nullforge/)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
[](https://wlix13.github.io/NullForge/)
|
|
11
|
+
|
|
12
|
+
Forge the server's baseline from null - an infrastructure-as-code framework built on [pyinfra](https://pyinfra.com), themed around a blacksmith's forge: **inventories** define hosts, **molds** shape the configuration, **runes** are idempotent operation sets, the **foundry** casts them onto targets, and the **smithy** holds cross-distro helpers.
|
|
13
|
+
|
|
14
|
+
> [!WARNING]
|
|
15
|
+
> **NullForge is in active development.** Until the `v1.0.0` release, the CLI, mold schemas and deploy behaviour may change at any time - breaking changes can land in **any** release, including patch versions. Pin an exact version (e.g. `nullforge==0.2.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv tool install nullforge
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Documentation
|
|
24
|
+
|
|
25
|
+
Everything else - getting started, concepts, the full feature reference, and guides - lives at **[wlix13.github.io/NullForge](https://wlix13.github.io/NullForge/)**.
|
|
26
|
+
|
|
27
|
+
## Contributing
|
|
28
|
+
|
|
29
|
+
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
NullForge is a thin, opinionated layer over pyinfra.
|
|
4
|
+
Every layer has one job, and imports only flow downward.
|
|
5
|
+
|
|
6
|
+
## Data flow
|
|
7
|
+
|
|
8
|
+
```mermaid
|
|
9
|
+
flowchart TB
|
|
10
|
+
subgraph plan [Control node]
|
|
11
|
+
CLI[cli - nullforge cast] --> FY["foundry - full_cast.py / cast.py"]
|
|
12
|
+
INV[inventories] --> FY
|
|
13
|
+
FY --> RN["runes/*.py"]
|
|
14
|
+
RN --> SM[smithy]
|
|
15
|
+
RN --> TP["templates (Jinja2)"]
|
|
16
|
+
MD["molds (Pydantic)"] --> INV
|
|
17
|
+
MD --> RN
|
|
18
|
+
ML["models (domain types)"] --> MD
|
|
19
|
+
end
|
|
20
|
+
RN -->|pyinfra operations over SSH| HOSTS[(Target hosts)]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
1. **Inventories** define hosts and attach `system` and `features` data, built by [merging layers](inventories.md) over the defaults.
|
|
24
|
+
2. The **foundry** is the deploy entry point.
|
|
25
|
+
`full_cast.py` coerces inventory data through the molds, always includes `prepare` then `base`, and then includes the rune of every active feature.
|
|
26
|
+
`cast.py` is the selective variant used by `nullforge cast -r ...`.
|
|
27
|
+
3. **Runes** are self-contained pyinfra operation sets - one file per concern.
|
|
28
|
+
They read validated configuration from `host.data` and emit idempotent operations.
|
|
29
|
+
4. **Molds** are Pydantic schemas for all configuration; [`FeaturesMold`](molds.md) composes the per-feature sub-molds.
|
|
30
|
+
5. **Models** hold pure domain types and constants consumed by molds.
|
|
31
|
+
6. The **smithy** provides cross-distro abstractions: package-name mapping (apt/dnf), release-binary installs with checksum verification, version pinning, networking facts, swap, service users.
|
|
32
|
+
7. **Templates** are Jinja2 files for systemd units, service configs, and shell profiles.
|
|
33
|
+
|
|
34
|
+
## Layer contracts
|
|
35
|
+
|
|
36
|
+
The layering is enforced by import contracts:
|
|
37
|
+
|
|
38
|
+
| Contract | Meaning |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Deploy spine | `cli` -> `foundry` -> `runes` -> `smithy` -> `molds`/`templates` -> `models`; a layer may import only layers below it |
|
|
41
|
+
| Models are pure | `models` imports no other NullForge package |
|
|
42
|
+
| Templates are a leaf | `templates` imports no other NullForge package, `models` included - its spine position only says which layers may import *it* |
|
|
43
|
+
| Molds describe, never provision | `molds` cannot import runes, smithy, or templates |
|
|
44
|
+
| Runes are independent | no rune imports another rune |
|
|
45
|
+
|
|
46
|
+
Why rune independence matters - and how runes coordinate without it - is covered in [Runes](runes.md#independence).
|
|
47
|
+
|
|
48
|
+
## Execution model
|
|
49
|
+
|
|
50
|
+
pyinfra runs in two phases:
|
|
51
|
+
|
|
52
|
+
1. **Plan** - fact gathering and operation collection on the control node.
|
|
53
|
+
Python-level branching (`if host.get_fact(...)`) happens here.
|
|
54
|
+
2. **Execute** - the collected operations run against each host, in a deterministic order shared by all hosts.
|
|
55
|
+
|
|
56
|
+
The [conventions](../contributing/conventions.md) around `host.loop` and change detection exist to keep that shared ordering stable.
|
|
57
|
+
|
|
58
|
+
## The CLI wrapper
|
|
59
|
+
|
|
60
|
+
`nullforge cast` is a thin planner around pyinfra: it resolves the [cast stages](../getting-started/cli.md#stages) and hands them to pyinfra.
|
|
61
|
+
Everything else - connections, facts, operations, parallelism - is stock pyinfra, which is why unknown CLI options are proxied through verbatim.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Inventories
|
|
2
|
+
|
|
3
|
+
An inventory is a standard [pyinfra inventory](https://docs.pyinfra.com/en/3.x/inventory-data.html): a Python file with a `hosts` list.
|
|
4
|
+
Each host is either a bare address or an `(address, data)` tuple.
|
|
5
|
+
NullForge reads two data keys - `system` and `features` - plus an optional `versions` map for [pin overrides](../guides/version-pinning.md).
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from nullforge.molds.defaults import BASE_FEATURES, BASE_SYSTEM
|
|
9
|
+
from nullforge.molds.utils import merge_features, merge_system
|
|
10
|
+
|
|
11
|
+
hosts = [
|
|
12
|
+
(
|
|
13
|
+
"203.0.113.10",
|
|
14
|
+
{
|
|
15
|
+
"system": merge_system(BASE_SYSTEM, {"hostname": "node1.example.com"}),
|
|
16
|
+
"features": merge_features(BASE_FEATURES, ...),
|
|
17
|
+
},
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Defaults and layering
|
|
23
|
+
|
|
24
|
+
`BASE_SYSTEM` and `BASE_FEATURES` (from `nullforge.molds.defaults`) are plain default-constructed molds.
|
|
25
|
+
`merge_system` / `merge_features` (from `nullforge.molds.utils`) start from a base and deep-merge each layer in order - later layers win.
|
|
26
|
+
|
|
27
|
+
A features layer can be any of:
|
|
28
|
+
|
|
29
|
+
| Layer type | Example |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Full `FeaturesMold` | another host's merged result |
|
|
32
|
+
| Any feature sub-mold | `WarpMold(install=True)` - merged under its own key |
|
|
33
|
+
| `dict` fragment | `{"warp": {"install": True}}` (sub-molds allowed as values) |
|
|
34
|
+
| `None` | skipped; useful for conditional layers |
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from nullforge.molds import DnsMold, UserMold, WarpMold
|
|
38
|
+
|
|
39
|
+
common = (
|
|
40
|
+
UserMold(manage=True, name="core"),
|
|
41
|
+
WarpMold(install=True),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
features = merge_features(BASE_FEATURES, *common, {"dns": {"ecs": True}})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The set of allowed sub-mold types is derived from `FeaturesMold.model_fields`, so a new feature is mergeable without touching the merge logic.
|
|
48
|
+
|
|
49
|
+
!!! warning "Deep-merge is per-key"
|
|
50
|
+
|
|
51
|
+
Dictionaries merge recursively, but any non-dict value - including lists - replaces the previous value outright.
|
|
52
|
+
A layer that sets `netsec.firewall_rules` replaces the whole rule list; extend `BASE_FEATURES.netsec.firewall_rules` in Python if you want "default rules plus mine".
|
|
53
|
+
|
|
54
|
+
Because inventories are Python, you can factor shared preset tuples, per-environment modules, or host loops however you like - the only contract is the final `hosts` list.
|
|
55
|
+
|
|
56
|
+
## Validation and coercion
|
|
57
|
+
|
|
58
|
+
The foundry coerces whatever the inventory provided - `None`, a `dict`, or a mold instance - into validated `SystemMold` / `FeaturesMold` objects before any rune runs (`ensure_system` / `ensure_features`).
|
|
59
|
+
Missing keys fall back to mold defaults; unknown keys are rejected because every mold forbids extra fields.
|
|
60
|
+
A typo like `{"warp": {"instal": True}}` fails the cast at validation time, before anything touches a host.
|
|
61
|
+
|
|
62
|
+
## Scaling to a fleet
|
|
63
|
+
|
|
64
|
+
Inventories are Python, so fleet structure is ordinary code.
|
|
65
|
+
A pattern that works well in practice: per-zone feature presets, a YAML host registry, and a small builder that turns them into pyinfra groups.
|
|
66
|
+
|
|
67
|
+
Shared mold instances compose into one `FeaturesMold` per zone:
|
|
68
|
+
|
|
69
|
+
```python title="inventory/zones.py"
|
|
70
|
+
from nullforge.models.netsec import SshHostKeyType
|
|
71
|
+
from nullforge.molds import ContainersMold, FeaturesMold, FirewallRule, HaproxyMold, NetSecMold, SshMold, UserMold
|
|
72
|
+
from nullforge.molds.defaults import BASE_FEATURES
|
|
73
|
+
from nullforge.molds.utils import merge_features
|
|
74
|
+
|
|
75
|
+
netsec = NetSecMold(
|
|
76
|
+
firewall_rules=[
|
|
77
|
+
FirewallRule(port=443, comment="HTTPS"),
|
|
78
|
+
FirewallRule(port=22, proto="tcp", from_ip="203.0.113.7", comment="Admin SSH"),
|
|
79
|
+
],
|
|
80
|
+
ssh=SshMold(host_keys=[SshHostKeyType.ED25519]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
users = UserMold(name="ops", fetch_key_from_github="your-github-username")
|
|
84
|
+
|
|
85
|
+
ZONE_FEATURES: dict[str, FeaturesMold] = {
|
|
86
|
+
"web": merge_features(BASE_FEATURES, netsec, users, HaproxyMold(install=True)),
|
|
87
|
+
"workers": merge_features(BASE_FEATURES, netsec, users, ContainersMold(install=True)),
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Host membership lives in data, not code - one YAML entry per host, with an optional `overrides` fragment deep-merged onto the zone preset:
|
|
92
|
+
|
|
93
|
+
```yaml title="inventory/hosts.yaml"
|
|
94
|
+
web:
|
|
95
|
+
- address: 203.0.113.10
|
|
96
|
+
hostname: web1.example.com
|
|
97
|
+
- address: 203.0.113.11
|
|
98
|
+
hostname: web2.example.com
|
|
99
|
+
overrides:
|
|
100
|
+
netsec:
|
|
101
|
+
firewall: false
|
|
102
|
+
workers:
|
|
103
|
+
- address: 203.0.113.20
|
|
104
|
+
hostname: worker1.example.com
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The builder exposes one module-level list per zone - pyinfra treats each as a named group, so `--limit web` targets a whole zone:
|
|
108
|
+
|
|
109
|
+
```python title="inventory/main.py"
|
|
110
|
+
from pathlib import Path
|
|
111
|
+
|
|
112
|
+
import yaml
|
|
113
|
+
|
|
114
|
+
from nullforge.molds.defaults import BASE_SYSTEM
|
|
115
|
+
from nullforge.molds.utils import merge_features, merge_system
|
|
116
|
+
|
|
117
|
+
from inventory.zones import ZONE_FEATURES
|
|
118
|
+
|
|
119
|
+
DATA: dict = yaml.safe_load((Path(__file__).parent / "hosts.yaml").read_text()) or {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build(zone: str) -> list:
|
|
123
|
+
rows = []
|
|
124
|
+
for entry in DATA.get(zone, []):
|
|
125
|
+
rows.append(
|
|
126
|
+
(
|
|
127
|
+
entry["address"],
|
|
128
|
+
{
|
|
129
|
+
"zone": zone,
|
|
130
|
+
"system": merge_system(BASE_SYSTEM, {"hostname": entry["hostname"]}),
|
|
131
|
+
"features": merge_features(ZONE_FEATURES[zone], entry.get("overrides")),
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return rows
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
for zone in ZONE_FEATURES:
|
|
139
|
+
globals()[zone] = _build(zone)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
nullforge cast -i inventory/main.py --limit web --dry
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Details worth stealing:
|
|
147
|
+
|
|
148
|
+
- `entry.get("overrides")` is either a dict fragment or `None` - both are valid `merge_features` layers, so per-host overrides cost one line and still go through mold validation.
|
|
149
|
+
- Extra data keys (like `zone` above) ride along on `host.data` untouched; [custom runes](../guides/custom-runes.md) can branch on them.
|
|
150
|
+
- Adding a host is a YAML edit, reviewable in a PR and scriptable from CI.
|
|
151
|
+
|
|
152
|
+
This is the pattern behind the deploy repo of the **Conglomerate** proxy fleet: zone presets over NullForge molds, a YAML host registry edited from CI workflows, and per-zone `--limit` casts.
|
|
153
|
+
|
|
154
|
+
## Secrets in inventories
|
|
155
|
+
|
|
156
|
+
Inventories are code; secrets in them (tunnel tokens, proxy user secrets) end up on disk.
|
|
157
|
+
Keep real inventories out of public repos, or load secrets from the environment.
|
|
158
|
+
|
|
159
|
+
Mold fields marked sensitive (user password, Zero Trust token, Nezha secrets, Telemt users) are redacted as `***` in pyinfra's `--debug-inventory` output, so inspecting a plan does not leak them.
|
|
160
|
+
|
|
161
|
+
## Debugging
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
nullforge cast -i inventory.py --debug-inventory # dump merged host data (pass-through to pyinfra)
|
|
165
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Molds
|
|
2
|
+
|
|
3
|
+
Molds are Pydantic models that shape and validate every piece of NullForge configuration.
|
|
4
|
+
If a value reaches a rune, it went through a mold first.
|
|
5
|
+
|
|
6
|
+
## BaseMold
|
|
7
|
+
|
|
8
|
+
Every mold extends `BaseMold` (`nullforge/molds/base_mold.py`), which sets the shared behaviour:
|
|
9
|
+
|
|
10
|
+
- **`extra="forbid"`** - unknown keys are validation errors, so typos fail the cast instead of silently deploying defaults.
|
|
11
|
+
- **`is_active`** - the activation protocol used for [rune dispatch](runes.md#dispatch); each feature sub-mold implements it.
|
|
12
|
+
- **`to_json()`** - JSON-mode serialization for pyinfra's `--debug-inventory`, with `_sensitive_fields` values redacted as `***` (recursively, through nested molds, lists and dicts).
|
|
13
|
+
|
|
14
|
+
## The top-level molds
|
|
15
|
+
|
|
16
|
+
Two molds cover a host:
|
|
17
|
+
|
|
18
|
+
- **`SystemMold`** - base system state: packages, locales, timezone, hostname, swap, IPv6.
|
|
19
|
+
Consumed by the always-on [base rune](../features/base.md).
|
|
20
|
+
- **`FeaturesMold`** - one field per feature, each a sub-mold with its own defaults:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
class FeaturesMold(BaseMold):
|
|
24
|
+
warp: WarpMold = Field(default_factory=WarpMold)
|
|
25
|
+
dns: DnsMold = Field(default_factory=DnsMold)
|
|
26
|
+
users: UserMold = Field(default_factory=UserMold)
|
|
27
|
+
netsec: NetSecMold = Field(default_factory=NetSecMold)
|
|
28
|
+
profiles: ProfilesMold = Field(default_factory=ProfilesMold)
|
|
29
|
+
zerotrust: ZeroTrustTunnelMold = Field(default_factory=ZeroTrustTunnelMold)
|
|
30
|
+
containers: ContainersMold = Field(default_factory=ContainersMold)
|
|
31
|
+
monitoring: MonitoringMold = Field(default_factory=MonitoringMold)
|
|
32
|
+
haproxy: HaproxyMold = Field(default_factory=HaproxyMold)
|
|
33
|
+
xray: XrayCoreMold = Field(default_factory=XrayCoreMold)
|
|
34
|
+
tor: TorMold = Field(default_factory=TorMold)
|
|
35
|
+
telemt: TelemtMold = Field(default_factory=TelemtMold)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Field order is deploy order.
|
|
39
|
+
Everything else is derived from these fields - the allowed [merge layers](inventories.md), the feature-to-mold mapping, and rune dispatch - so adding a feature means adding a field, not editing plumbing.
|
|
40
|
+
|
|
41
|
+
## Activation
|
|
42
|
+
|
|
43
|
+
Every feature sub-mold implements `is_active`.
|
|
44
|
+
Most simply return `self.install`; the exceptions encode their own semantics:
|
|
45
|
+
|
|
46
|
+
- `DnsMold` - active while `mode != "none"`.
|
|
47
|
+
- `UserMold` - active while `manage` is true.
|
|
48
|
+
- `ProfilesMold` - active while `for_root` or `for_user` is true.
|
|
49
|
+
|
|
50
|
+
A sub-mold may also rename its rune or opt out of automatic dispatch - see [dispatch](runes.md#dispatch).
|
|
51
|
+
|
|
52
|
+
## Validation beyond types
|
|
53
|
+
|
|
54
|
+
Molds validate coherence, not just shapes - a bad combination fails at plan time with a readable error, never mid-deploy.
|
|
55
|
+
Each [feature page](../features/index.md) documents its own validators.
|
|
56
|
+
|
|
57
|
+
## Models vs molds
|
|
58
|
+
|
|
59
|
+
Domain types and constants - `DnsMode`, `Shell`, `WarpEngineType`, `SwapType`, ... - live in `nullforge.models.<domain>` and are imported from there directly:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from nullforge.models.dns import DnsMode
|
|
63
|
+
from nullforge.molds import DnsMold
|
|
64
|
+
|
|
65
|
+
dns = DnsMold(mode=DnsMode.DOT_RESOLVED)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`nullforge.molds` exports only the molds.
|
|
69
|
+
The split is enforced by an import contract: models import nothing from the rest of the package.
|