Manifest Builder
Generate materialized Kubernetes manifests from various types of configuration
Installation
To install or upgrade to the latest version:
uv pip install --upgrade manifest-builder
Development
This project is using uv for development. To set up your dev environment,
run uv sync. Tests and checks can be run with the following commands:
uv run ruff checkuv run ruff format --checkuv run ty checkuv run pytest
Requirements
- Python 3.14+
- Helm 3.x (must be installed and available in PATH)
- Git (required for
--create-commitfeature)
Python API
Use manifest_builder.generate to generate manifests from Python:
from pathlib import Path
from manifest_builder import generate
written_paths = generate(Path("conf"), Path("output"))
Extra template variables can be supplied directly from Python. They are merged
with the [variables] table in config.toml just like values loaded with
--vars-from:
generate(Path("conf"), Path("output"), vars={"domain": "example.com"})
Targets and sections
A configuration directory comes in one of two layouts, told apart by the
version field of its top-level config.toml.
By default, or with version = 1, config.toml declares the config blocks
directly.
With version = 2, config.toml declares targets instead. A target names
the sections it is built from, and carries the variables those sections are
rendered with. This lets one configuration directory describe several
deployments of the same sections:
version = 2
[[target]]
name = "platform-dev"
sections = ["base", "platform"]
[target.vars]
cluster_name = "platform-dev"
vanity_domain = "portswigger.com"
[[target]]
name = "platform-prod"
sections = ["base", "platform"]
[target.vars]
cluster_name = "platform-prod"
vanity_domain = "portswigger.net"
A section is a subdirectory of the configuration directory holding a
section.toml of blocks — the same content a version = 1 config.toml would
hold:
conf/
├── config.toml # targets only
├── base/
│ ├── section.toml # [[helm]], [[simple]], … blocks
│ └── argocd/
│ └── values.yaml
├── platform/
│ ├── section.toml
│ └── idcat/
│ └── idcat.toml
├── images.toml
├── releases.yaml
├── owners/
└── plugins/
Which target to generate is selected with --target, and is required for a
version = 2 directory:
manifest-builder --config-dir conf --output-dir output --target platform-dev
From Python it is the target argument of generate:
generate(Path("conf"), Path("output"), target="platform-dev")
Notes:
- A section's blocks are read from that section's own
section.toml, so the paths they reference resolve inside the section directory. In the layout above,base/section.tomlrefers to its values file asargocd/values.yaml. Two sections can therefore use the same relative path without colliding. - A section file may also be named
config.tomlormanifest-builder.toml, so moving an existing top-level config file into a section directory works unchanged.section.tomlwins if more than one is present. - A section may add a
[variables]table of its own. It is merged with the target'svars, as are variables from--vars-fromandgenerate(vars=...). A variable defined by more than one of these is an error rather than one silently winning. images.toml,releases.yaml,owners/, andplugins/stay at the top of the configuration directory and are shared by every target.- Only the sections the selected target names are loaded.
Image template variables
Shared container image definitions can be placed in images.toml in the
configuration directory:
[git]
repo = "alpine/git"
version = "2.47.2"
[static-web-server]
repo = "ghcr.io/static-web-server/static-web-server"
version = "2.36.1"
Each entry is made available to Mustache templates as both the full image reference and the version. Dashes in image names are converted to underscores:
{{git_image}}renders asalpine/git:2.47.2{{git_version}}renders as2.47.2{{static_web_server_image}}renders asghcr.io/static-web-server/static-web-server:2.36.1{{static_web_server_version}}renders as2.36.1
Config block plugins
[[copy]] is the one config block shipped here, in a module under
manifest_builder/blocks/, discovered at startup. Every other block belongs to
the configuration directory that uses it, dropped into a plugins/
subdirectory:
conf/
├── config.toml
└── plugins/
├── helm.py # defines HelmBlock
├── simple.py # defines SimpleBlock
├── public_repo.py # defines PublicRepoBlock
├── templates/
│ ├── simple/ # namespaced per block
│ └── public_repo/
│ └── repository.yaml
└── tests/
└── test_public_repo.py
Blocks live with their configuration because that is where they change: a
deployment's idea of what a [[simple]] app needs is the deployment's business,
and two configuration directories are free to disagree. This package keeps the
toolkit they are built from — chart pulling, releases.yaml parsing,
Kubernetes naming, ConfigMap construction, YAML output routing — so a block
stays small.
Every module in plugins/ is imported, and any concrete ConfigBlock
subclass it defines is registered under the top-level TOML key its
top_level_config_name() returns. A plugin block is used exactly like the
built-in one:
[[public-repo]]
name = "idcat"
A plugin module implements the same interface as a built-in block:
from manifest_builder.blocks import ConfigBlock, GenerationContext
from manifest_builder.output import write_documents
class PublicRepoBlock(ConfigBlock[PublicRepoConfig]):
def top_level_config_name(self) -> str:
return "public-repo"
...
Notes:
- Modules whose names start with
_or.are skipped, as are test modules (test_*.py,*_test.py,conftest.py) andtests/directories, so a plugin directory can keep its own tests beside the code. - Plugins are imported under the
manifest_builder_pluginspackage rather than ontosys.path, so a plugin namedjson.pycannot shadow an installed module. Sibling modules are reachable with a relative import (from .helpers import ...). - Blocks are registered in a stable order, sorted by their top-level key, so a run does not depend on filesystem order. Two blocks claiming the same key is an error.
- Bundled templates should be resolved relative to the plugin module, for
example
Path(__file__).parent / "templates" / "public_repo".
Plugin modules are imported from the configuration directory, so that directory is trusted to the same degree as the manifest-builder installation itself.
Plugins in a long-running process
A process that calls generate() repeatedly may be pointed at a configuration
directory that has been checked out again between calls, possibly at the same
path. Every call re-reads the plugins rather than reusing what it imported
before, so no cache invalidation is needed from the caller:
- Plugin modules are dropped from
sys.modulesand imported afresh on each call, so replaced module source takes effect. - Import-system directory caches are invalidated, so plugins added or removed since the last call are seen.
- No
__pycache__is written for plugin modules. This keeps the configuration checkout clean, and avoids the one case where a re-import could still pick up stale code, since bytecode validity is judged on source mtime truncated to the second plus file size. - Templates should be read at generation time rather than cached at import, as in the example above; combined with the module reload, they then always come from the current checkout.
Plugin loading is serialized with a lock, so concurrent generation from two
different configuration directories cannot interleave one load with another.
manifest_builder.discovery.forget_plugin_modules() is available for a caller
that wants to release imported plugin modules itself, but calling it is not
required.
Externally-owned output roots
When the output repository is shared with other services or pipelines that make their own commits, manifest-builder can be told which top-level output directories it does not own. Files in those directories are left alone during cleanup, and generation fails fast if any output would land in one of them.
To declare ownership, add an owners/ directory to your config directory and
drop one or more TOML files into it. Each file may set:
# A single output root owned by another pipeline:
owned = "team-a"
# Or a list of output roots:
owned = ["cluster", "monitoring", "logging"]
Entries from all owners/*.toml files are merged into a single set of
externally-owned output roots.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file manifest_builder-0.7.2-py3-none-any.whl.
File metadata
- Download URL: manifest_builder-0.7.2-py3-none-any.whl
- Upload date:
- Size: 43.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f85de3cc97dd1b77761d103422e4257219f8485fb20eb631eb8753288eb3fd61
|
|
| MD5 |
6ad5e180edfc7453873a4be5e0467f54
|
|
| BLAKE2b-256 |
13b2264640bafb1c61db3dbf8af8d6347c7b59a9ae429e67b126acde8319b5e8
|