Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,11 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

# macOS
.DS_Store

# Repository specific (inventories)
nullforge/inventories/*
!nullforge/inventories/example.py
!nullforge/inventories/README.md
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0"
".": "0.0.0"
}
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# NullForge

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.

> [!WARNING]
> **NullForge is in active development.** Until the `v1.0.0` release, the mold schemas, runes 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.1.0`) and check the [release notes](https://github.com/wlix13/NullForge/releases) before upgrading.

## Install

```bash
uv sync
```

## Deploy

```bash
# Cast the full baseline (all enabled features from the inventory)
uv run pyinfra nullforge/inventories/example.py nullforge/foundry/full_cast.py
```

## Contributing

See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for the full setup, commit conventions and pull request flow.
2 changes: 1 addition & 1 deletion nullforge/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.0" # x-release-please-version
__version__ = "0.0.0" # x-release-please-version
Empty file added nullforge/foundry/README.md
Empty file.
7 changes: 7 additions & 0 deletions nullforge/foundry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Foundry deploy entry points for Pyinfra."""

from pathlib import Path
from typing import Final


FOUNDRY_DIR: Final[Path] = Path(__file__).parent
22 changes: 22 additions & 0 deletions nullforge/foundry/full_cast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pyinfra import local
from pyinfra.context import host

from nullforge.molds.features import iter_runes
from nullforge.molds.utils import ensure_features, ensure_system
from nullforge.runes import rune_path


def cast_full() -> None:
host.data.features = ensure_features(getattr(host.data, "features", None))
host.data.system = ensure_system(getattr(host.data, "system", None))

local.include(str(rune_path("prepare")))

local.include(str(rune_path("base")))

for name, active in host.loop(iter_runes(host.data.features)):
if active:
local.include(str(rune_path(name)))


cast_full()
Empty file.
87 changes: 87 additions & 0 deletions nullforge/inventories/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from nullforge.models.dns import DnsMode
from nullforge.models.users import Shell
from nullforge.molds import DnsMold, MonitoringMold, TelemtMold, UserMold, WarpMold
from nullforge.molds.defaults import BASE_FEATURES, BASE_SYSTEM
from nullforge.molds.monitoring import NezhaBackend
from nullforge.molds.utils import merge_features, merge_system


users = UserMold(
manage=True,
name="example",
shell=Shell.ZSH,
)
"""User configuration preset
with user management enabled and the user "example".
with shell set to ZSH (default behavior).
"""

warp = WarpMold(
install=True,
iface="warp-example",
)
"""WARP configuration preset
setup Cloudflare WARP
with default MASQUE engine and interface "warp-example".
"""

dns = DnsMold(
mode=DnsMode.BLOCKY,
)
"""DNS configuration preset
with DNS over HTTPS Blocky mode.
"""

monitoring = MonitoringMold(
install=True,
backend=NezhaBackend(
server="agent.status.example.com:443",
client_secret="REPLACE_WITH_NZ_CLIENT_SECRET", # noqa: S106 - example placeholder
dashboard_url="https://dash.example.com",
api_token="REPLACE_WITH_DASHBOARD_PAT", # noqa: S106 - example placeholder
),
)
"""Monitoring configuration preset
installing Nezha agent and renaming dashboard entry to host hostname.
The dashboard_url/api_token are used on control node for rename API call.
"""

telemt = TelemtMold(
install=True,
tls_domain="example.com",
users={
"example": "bf777cca8384a074a671460d51e4e31f",
},
route_via_warp=True,
synfix=True,
)
"""Telemt MTProto proxy preset with Fake-TLS masking.
Telegram-bound egress is routed through WARP (enabled above) via per-uid policy routing,
and the MEKO SYN rate-limiting fix is applied.
"""

overrides = (
users,
warp,
dns,
monitoring,
telemt,
)
"""Wrappers for the features to be merged with the base features."""

hosts = [
(
"203.0.113.10",
{
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node1.local"}),
"features": merge_features(BASE_FEATURES, *overrides),
},
),
(
"203.0.113.20",
{
"system": merge_system(BASE_SYSTEM, {"hostname": "example-node2.local"}),
"features": merge_features(BASE_FEATURES, *overrides),
},
),
]
1 change: 1 addition & 0 deletions nullforge/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal domain types for NullForge - pure value types."""
60 changes: 60 additions & 0 deletions nullforge/models/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Containers configuration models."""

from enum import StrEnum
from typing import Annotated, Literal

from pydantic import BaseModel, Field


class ContainersBackendType(StrEnum):
DOCKER = "docker"
PODMAN = "podman"
CRIO = "crio"


class ContainersRuntimeType(StrEnum):
DEFAULT = "default"
CRUN = "crun"
GVISOR = "gvisor"


class _ContainersBackendBase(BaseModel):
"""Base for a containers backend."""

type: ContainersBackendType = Field(description="The type of containers backend")
runtime: ContainersRuntimeType = Field(description="The type of containers runtime")


class DockerContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.DOCKER] = ContainersBackendType.DOCKER
runtime: Literal[ContainersRuntimeType.GVISOR] = ContainersRuntimeType.GVISOR


class PodmanContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.PODMAN] = ContainersBackendType.PODMAN
runtime: Literal[ContainersRuntimeType.CRUN] = ContainersRuntimeType.CRUN


class CrioContainersBackend(_ContainersBackendBase):
type: Literal[ContainersBackendType.CRIO] = ContainersBackendType.CRIO
runtime: Literal[ContainersRuntimeType.DEFAULT] = ContainersRuntimeType.DEFAULT


ContainersBackend = Annotated[
DockerContainersBackend | PodmanContainersBackend | CrioContainersBackend,
Field(discriminator="type"),
]


def containers_backend_factory(type: ContainersBackendType) -> ContainersBackend:
"""Factory function for containers backends."""

match type:
case ContainersBackendType.DOCKER:
return DockerContainersBackend()
case ContainersBackendType.PODMAN:
return PodmanContainersBackend()
case ContainersBackendType.CRIO:
return CrioContainersBackend()
case _:
raise ValueError(f"Unknown ContainersBackendType: {type}")
Loading
Loading