nyl 0.10.4__tar.gz → 0.26.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.
- nyl-0.26.0/.gitignore +30 -0
- nyl-0.26.0/PKG-INFO +152 -0
- nyl-0.26.0/README.md +140 -0
- nyl-0.26.0/pyproject.toml +50 -0
- nyl-0.26.0/src/nyl/__init__.py +34 -0
- nyl-0.26.0/src/nyl/_version.py +1 -0
- nyl-0.26.0/src/nyl/client.py +113 -0
- nyl-0.26.0/src/nyl/errors.py +54 -0
- nyl-0.26.0/src/nyl/manifests.py +267 -0
- nyl-0.10.4/.changelog/0.10.0.toml +0 -31
- nyl-0.10.4/.changelog/0.10.1.toml +0 -13
- nyl-0.10.4/.changelog/0.10.2.toml +0 -31
- nyl-0.10.4/.changelog/0.10.3.toml +0 -13
- nyl-0.10.4/.changelog/0.7.0.toml +0 -70
- nyl-0.10.4/.changelog/0.7.1.toml +0 -7
- nyl-0.10.4/.changelog/0.7.2.toml +0 -31
- nyl-0.10.4/.changelog/0.8.0.toml +0 -7
- nyl-0.10.4/.changelog/0.8.1.toml +0 -7
- nyl-0.10.4/.changelog/0.9.0.toml +0 -31
- nyl-0.10.4/.changelog/0.9.1.toml +0 -25
- nyl-0.10.4/.changelog/0.9.2.toml +0 -13
- nyl-0.10.4/.changelog/0.9.3.toml +0 -7
- nyl-0.10.4/.changelog/0.9.4.toml +0 -7
- nyl-0.10.4/.envrc +0 -5
- nyl-0.10.4/.github/workflows/docker.yaml +0 -55
- nyl-0.10.4/.github/workflows/docs.yaml +0 -40
- nyl-0.10.4/.github/workflows/python.yaml +0 -87
- nyl-0.10.4/.github/workflows/update-flake-lock.yaml +0 -23
- nyl-0.10.4/.gitignore +0 -14
- nyl-0.10.4/PKG-INFO +0 -67
- nyl-0.10.4/README.md +0 -44
- nyl-0.10.4/argocd-cmp/Dockerfile +0 -111
- nyl-0.10.4/argocd-cmp/README.md +0 -11
- nyl-0.10.4/argocd-cmp/plugin.yaml +0 -47
- nyl-0.10.4/argocd-with-nyl/.envrc +0 -7
- nyl-0.10.4/argocd-with-nyl/.secrets.yaml +0 -29
- nyl-0.10.4/argocd-with-nyl/.sops.yaml +0 -5
- nyl-0.10.4/argocd-with-nyl/README.md +0 -51
- nyl-0.10.4/argocd-with-nyl/argocd.yaml +0 -84
- nyl-0.10.4/argocd-with-nyl/nyl-project.yaml +0 -1
- nyl-0.10.4/argocd-with-nyl/nyl-secrets.yaml +0 -2
- nyl-0.10.4/docs/content/changelog.md +0 -208
- nyl-0.10.4/docs/content/index.md +0 -22
- nyl-0.10.4/docs/content/reference/applysets.md +0 -10
- nyl-0.10.4/docs/content/reference/argocd-plugin.md +0 -254
- nyl-0.10.4/docs/content/reference/cluster-connectivity.md +0 -27
- nyl-0.10.4/docs/content/reference/configuration/environment.md +0 -71
- nyl-0.10.4/docs/content/reference/configuration/profiles.md +0 -241
- nyl-0.10.4/docs/content/reference/configuration/projects.md +0 -89
- nyl-0.10.4/docs/content/reference/configuration/secrets.md +0 -152
- nyl-0.10.4/docs/content/reference/configuration-api/profiles.md +0 -6
- nyl-0.10.4/docs/content/reference/configuration-api/secrets.md +0 -11
- nyl-0.10.4/docs/content/reference/glossary.md +0 -28
- nyl-0.10.4/docs/content/reference/templating/basics.md +0 -21
- nyl-0.10.4/docs/content/reference/templating/components.md +0 -83
- nyl-0.10.4/docs/content/reference/templating/inlining/helmcharts.md +0 -158
- nyl-0.10.4/docs/content/reference/templating/inlining/overview.md +0 -10
- nyl-0.10.4/docs/content/reference/templating/inlining/postprocessor.md +0 -156
- nyl-0.10.4/docs/content/reference/templating/locals.md +0 -26
- nyl-0.10.4/docs/content/reference/templating/secrets.md +0 -37
- nyl-0.10.4/docs/mkdocs.yml +0 -41
- nyl-0.10.4/docs/pyproject.toml +0 -11
- nyl-0.10.4/docs/uv.lock +0 -593
- nyl-0.10.4/example/manifests/postgres.yaml +0 -20
- nyl-0.10.4/example/nyl-profiles.toml +0 -10
- nyl-0.10.4/example/nyl-profiles.yaml +0 -10
- nyl-0.10.4/example/nyl-secrets.yaml +0 -3
- nyl-0.10.4/example/secrets.yaml +0 -26
- nyl-0.10.4/flake.lock +0 -348
- nyl-0.10.4/flake.nix +0 -113
- nyl-0.10.4/nix/uv.nix +0 -41
- nyl-0.10.4/pyproject.toml +0 -66
- nyl-0.10.4/renovate.json +0 -34
- nyl-0.10.4/src/kubernetes/README.md +0 -2
- nyl-0.10.4/src/kubernetes/dynamic/__init__.pyi +0 -3
- nyl-0.10.4/src/kubernetes/dynamic/client.pyi +0 -71
- nyl-0.10.4/src/kubernetes/dynamic/discovery.pyi +0 -30
- nyl-0.10.4/src/kubernetes/dynamic/exceptions.pyi +0 -47
- nyl-0.10.4/src/kubernetes/dynamic/resource.pyi +0 -43
- nyl-0.10.4/src/kubernetes/watch/__init__.pyi +0 -3
- nyl-0.10.4/src/kubernetes/watch/watch.pyi +0 -10
- nyl-0.10.4/src/nyl/__init__.py +0 -3
- nyl-0.10.4/src/nyl/commands/__init__.py +0 -159
- nyl-0.10.4/src/nyl/commands/add.py +0 -118
- nyl-0.10.4/src/nyl/commands/crds.py +0 -18
- nyl-0.10.4/src/nyl/commands/new.py +0 -134
- nyl-0.10.4/src/nyl/commands/profile.py +0 -40
- nyl-0.10.4/src/nyl/commands/run.py +0 -98
- nyl-0.10.4/src/nyl/commands/secrets.py +0 -93
- nyl-0.10.4/src/nyl/commands/template.py +0 -575
- nyl-0.10.4/src/nyl/commands/tools/__init__.py +0 -14
- nyl-0.10.4/src/nyl/commands/tools/bcrypt.py +0 -18
- nyl-0.10.4/src/nyl/commands/tools/sops.py +0 -99
- nyl-0.10.4/src/nyl/commands/tun.py +0 -126
- nyl-0.10.4/src/nyl/daemon.py +0 -344
- nyl-0.10.4/src/nyl/generator/__init__.py +0 -90
- nyl-0.10.4/src/nyl/generator/components.py +0 -92
- nyl-0.10.4/src/nyl/generator/dispatch.py +0 -109
- nyl-0.10.4/src/nyl/generator/dispatch_test.py +0 -56
- nyl-0.10.4/src/nyl/generator/helmchart.py +0 -221
- nyl-0.10.4/src/nyl/generator/helmchart_test.py +0 -44
- nyl-0.10.4/src/nyl/generator/statefulsecret.py +0 -27
- nyl-0.10.4/src/nyl/profiles/__init__.py +0 -175
- nyl-0.10.4/src/nyl/profiles/config.py +0 -182
- nyl-0.10.4/src/nyl/profiles/config_test.py +0 -60
- nyl-0.10.4/src/nyl/profiles/kubeconfig.py +0 -163
- nyl-0.10.4/src/nyl/profiles/tunnel.py +0 -248
- nyl-0.10.4/src/nyl/project/config.py +0 -217
- nyl-0.10.4/src/nyl/resources/__init__.py +0 -126
- nyl-0.10.4/src/nyl/resources/applyset.py +0 -239
- nyl-0.10.4/src/nyl/resources/applyset_test.py +0 -39
- nyl-0.10.4/src/nyl/resources/helmchart.py +0 -91
- nyl-0.10.4/src/nyl/resources/helmchart_test.py +0 -81
- nyl-0.10.4/src/nyl/resources/placeholder.py +0 -80
- nyl-0.10.4/src/nyl/resources/postprocessor.py +0 -206
- nyl-0.10.4/src/nyl/resources/postprocessor_test.py +0 -150
- nyl-0.10.4/src/nyl/resources/statefulsecret.py +0 -14
- nyl-0.10.4/src/nyl/secrets/__init__.py +0 -77
- nyl-0.10.4/src/nyl/secrets/config.py +0 -72
- nyl-0.10.4/src/nyl/secrets/config_test.py +0 -60
- nyl-0.10.4/src/nyl/secrets/kubernetes.py +0 -84
- nyl-0.10.4/src/nyl/secrets/null.py +0 -22
- nyl-0.10.4/src/nyl/secrets/sops.py +0 -160
- nyl-0.10.4/src/nyl/secrets/sops_test.py +0 -72
- nyl-0.10.4/src/nyl/templating.py +0 -244
- nyl-0.10.4/src/nyl/templating_test.py +0 -9
- nyl-0.10.4/src/nyl/tools/di.py +0 -84
- nyl-0.10.4/src/nyl/tools/fs.py +0 -67
- nyl-0.10.4/src/nyl/tools/fs_test.py +0 -17
- nyl-0.10.4/src/nyl/tools/kubectl.py +0 -179
- nyl-0.10.4/src/nyl/tools/kubernetes.py +0 -83
- nyl-0.10.4/src/nyl/tools/kvstore.py +0 -132
- nyl-0.10.4/src/nyl/tools/loads.py +0 -39
- nyl-0.10.4/src/nyl/tools/logging.py +0 -17
- nyl-0.10.4/src/nyl/tools/pyroscope.py +0 -58
- nyl-0.10.4/src/nyl/tools/shell.py +0 -6
- nyl-0.10.4/src/nyl/tools/testing.py +0 -12
- nyl-0.10.4/src/nyl/tools/typer.py +0 -9
- nyl-0.10.4/src/nyl/tools/types.py +0 -12
- nyl-0.10.4/src/nyl/tools/url.py +0 -33
- nyl-0.10.4/src/nyl/tools/yaml.py +0 -28
- nyl-0.10.4/src/pyroscope/__init__.pyi +0 -11
- nyl-0.10.4/testdata/helm-test-chart/Chart.yaml +0 -3
- nyl-0.10.4/testdata/helm-test-chart/templates/sa.yaml +0 -7
- nyl-0.10.4/testdata/helm-test-chart/values.yaml +0 -1
- nyl-0.10.4/tests/__init__.py +0 -0
- nyl-0.10.4/tests/conftest.py +0 -6
- nyl-0.10.4/uv.lock +0 -857
- /nyl-0.10.4/src/kubernetes/__init__.pyi → /nyl-0.26.0/src/nyl/py.typed +0 -0
nyl-0.26.0/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Nyl runtime cache
|
|
2
|
+
.nyl/
|
|
3
|
+
|
|
4
|
+
# Build artifacts
|
|
5
|
+
/result
|
|
6
|
+
|
|
7
|
+
# Rust
|
|
8
|
+
nyl/target/
|
|
9
|
+
nyl/Cargo.lock
|
|
10
|
+
nyl/book/book/
|
|
11
|
+
**/*.rs.bk
|
|
12
|
+
*.pdb
|
|
13
|
+
|
|
14
|
+
# Development
|
|
15
|
+
todo.txt
|
|
16
|
+
.claude/
|
|
17
|
+
.worktrees/
|
|
18
|
+
|
|
19
|
+
# Added by cargo
|
|
20
|
+
|
|
21
|
+
/target
|
|
22
|
+
.codex
|
|
23
|
+
|
|
24
|
+
# Python client
|
|
25
|
+
python/.venv/
|
|
26
|
+
python/dist/
|
|
27
|
+
__pycache__/
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.ruff_cache/
|
nyl-0.26.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nyl
|
|
3
|
+
Version: 0.26.0
|
|
4
|
+
Summary: Render and inspect Kubernetes manifests with the Nyl CLI
|
|
5
|
+
Project-URL: Homepage, https://niklasrosenstein.github.io/nyl/
|
|
6
|
+
Project-URL: Repository, https://github.com/NiklasRosenstein/nyl
|
|
7
|
+
Author: Niklas Rosenstein
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: ruamel-yaml<0.19,>=0.18.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Nyl Python client
|
|
14
|
+
|
|
15
|
+
`nyl` is a small Python 3.11+ library for rendering and inspecting Kubernetes
|
|
16
|
+
manifests. Install it with `uv add nyl` or `pip install nyl`.
|
|
17
|
+
|
|
18
|
+
Install the [Nyl CLI](https://niklasrosenstein.github.io/nyl/start/getting-started/)
|
|
19
|
+
separately and put it on `PATH`, or pass its executable path.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from nyl import NylClient
|
|
23
|
+
|
|
24
|
+
client = NylClient(project="/path/to/infrastructure", executable="nyl", timeout=120)
|
|
25
|
+
manifests = client.render("applications/workloads/web.yaml", target="development")
|
|
26
|
+
deployment = manifests.one("Deployment", "web")
|
|
27
|
+
|
|
28
|
+
assert deployment.namespace == "web"
|
|
29
|
+
for container in deployment.containers(include_init=True):
|
|
30
|
+
assert container.security_context["allowPrivilegeEscalation"] is False
|
|
31
|
+
assert container.env.value("PGHOST") == "database-rw"
|
|
32
|
+
assert container.env.secret_ref("PGPASSWORD") == ("database-app", "password")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`namespace` is the authored namespace. Resources without one return `None`,
|
|
36
|
+
even when the Release or Argo CD Application has a destination namespace.
|
|
37
|
+
|
|
38
|
+
## Rendering
|
|
39
|
+
|
|
40
|
+
`NylClient(project, executable="nyl", timeout=120)` fixes the invocation directory
|
|
41
|
+
and requires a positive, finite timeout in seconds. `render(source, *, target=None,
|
|
42
|
+
offline=False, allow_secret_inputs=False)` invokes only `nyl render`. Relative
|
|
43
|
+
source paths are relative to `project`; absolute paths are passed through.
|
|
44
|
+
Calls leave Python's working directory and environment unchanged and use no shell.
|
|
45
|
+
Timeouts terminate the invocation's process group on POSIX and its process tree
|
|
46
|
+
with Windows' built-in `taskkill`; diagnostics are collected after termination.
|
|
47
|
+
|
|
48
|
+
Rust owns configuration resolution, validation, rendering, and caching. Target-aware
|
|
49
|
+
Releases use the compiled target's rendering rules. Each call runs the CLI again;
|
|
50
|
+
Nyl's cache may reuse valid artifacts. Project-configured validation remains enabled
|
|
51
|
+
according to the CLI's defaults. A validation failure yields an exception even if
|
|
52
|
+
the CLI emits manifests. `offline=True` forwards `--offline` (cached Git references
|
|
53
|
+
and publication inputs); it is not a universal network prohibition for validators
|
|
54
|
+
or other external tools. `allow_secret_inputs=True` explicitly forwards the CLI's
|
|
55
|
+
opt-in for project secrets and admitted environment inputs.
|
|
56
|
+
|
|
57
|
+
Only single-file render entry points are supported. Full-tree loading needs a
|
|
58
|
+
documented manifest inventory: a published tree can also contain ownership indexes
|
|
59
|
+
and non-manifest state. This package neither downloads the executable nor applies,
|
|
60
|
+
publishes, or reconciles resources, and has no persistent Python cache.
|
|
61
|
+
|
|
62
|
+
## Inspecting declarations
|
|
63
|
+
|
|
64
|
+
`ManifestSet` is iterable, has a length, and preserves document order.
|
|
65
|
+
`select(kind=None, name=None, *, namespace=..., api_version=None)` returns another
|
|
66
|
+
`ManifestSet`. `one(kind, name=None, *, namespace=..., api_version=None)` requires
|
|
67
|
+
exactly one match. Omit `namespace` to search all namespaces; explicitly pass
|
|
68
|
+
`namespace=None` to select resources with no declared namespace. Selection errors
|
|
69
|
+
show resource identities without manifest bodies. `ManifestSet.from_yaml(text)`
|
|
70
|
+
safely reads multi-document YAML 1.2, ignores empty documents, and rejects invalid
|
|
71
|
+
resource envelopes. Nyl's emitted quoting preserves strings such as `"on"`, `"yes"`,
|
|
72
|
+
and `"001"`; no YAML 1.1 boolean coercion is applied by Python.
|
|
73
|
+
|
|
74
|
+
Resources expose `raw`, `api_version`, `kind`, `name`, and `namespace`.
|
|
75
|
+
`containers(include_init=False)` handles Pods, Deployments, StatefulSets, DaemonSets,
|
|
76
|
+
ReplicaSets, ReplicationControllers, Jobs, and CronJobs. It yields regular containers
|
|
77
|
+
in order, followed by init containers when requested. Unsupported kinds raise
|
|
78
|
+
`DeclarationError`.
|
|
79
|
+
|
|
80
|
+
Containers expose `raw`, `name`, `security_context` (an empty dictionary when
|
|
81
|
+
omitted), and `env`. `env.value(name)` requires an explicit literal string;
|
|
82
|
+
`env.secret_ref(name)` requires one Secret key reference and returns `(name, key)`.
|
|
83
|
+
Missing, duplicate, malformed, and incompatible declarations raise `DeclarationError`.
|
|
84
|
+
These helpers do not resolve Secrets, ConfigMaps, `envFrom`, Pod-level security
|
|
85
|
+
context inheritance, or live cluster data. Raw dictionaries remain mutable.
|
|
86
|
+
|
|
87
|
+
## Errors and compatibility
|
|
88
|
+
|
|
89
|
+
All library errors derive from `NylError` and are exported from `nyl`:
|
|
90
|
+
|
|
91
|
+
| Error | Meaning |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `ExecutableNotFoundError` | Install the CLI or correct its executable path |
|
|
94
|
+
| `RenderTimeoutError` | The subprocess exceeded its deadline |
|
|
95
|
+
| `RenderError` | A nonzero exit status or an OS execution error |
|
|
96
|
+
| `InvalidOutputError` | stdout is not valid rendered resource YAML |
|
|
97
|
+
| `ResourceNotFoundError` | `one()` found zero matches |
|
|
98
|
+
| `AmbiguousResourceError` | `one()` found several matches |
|
|
99
|
+
| `DeclarationError` | A helper cannot interpret the declaration |
|
|
100
|
+
|
|
101
|
+
Execution errors derive from `ExecutionError` and retain `command` (an argument
|
|
102
|
+
tuple), `exit_code` (`None` when unavailable), and `diagnostics` (stderr).
|
|
103
|
+
They do not retain or automatically print stdout. YAML errors omit input excerpts;
|
|
104
|
+
resource and container representations include only identities. CLI diagnostics
|
|
105
|
+
can contain sensitive information and should be handled accordingly.
|
|
106
|
+
|
|
107
|
+
Tested with Nyl CLI **0.5.9**. The package's version is available through
|
|
108
|
+
`importlib.metadata.version("nyl")`.
|
|
109
|
+
|
|
110
|
+
## Development and releases
|
|
111
|
+
|
|
112
|
+
From the repository root:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
mise run python-check # Ruff, type checking, parallel tests, checkout-built CLI
|
|
116
|
+
mise run python-build # Stamp Cargo-derived version, then build wheel and sdist
|
|
117
|
+
mise run pre-commit # Required Rust and Python checks
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or inside `python/`:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv sync --locked
|
|
124
|
+
uv run python scripts/version.py
|
|
125
|
+
uv run ruff check .
|
|
126
|
+
uv run ruff format --check .
|
|
127
|
+
uv run mypy
|
|
128
|
+
uv run pytest
|
|
129
|
+
uv build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The test fixture builds the CLI and reads Cargo's executable artifact, honoring
|
|
133
|
+
Cargo's configured target and output directory. Set `NYL_TEST_EXECUTABLE` to use
|
|
134
|
+
a binary you built separately.
|
|
135
|
+
|
|
136
|
+
`scripts/version.py` derives the release version from root `Cargo.toml` and stamps
|
|
137
|
+
`src/nyl/_version.py` before building. For CLI `0.MINOR.PATCH`, Python uses
|
|
138
|
+
`0.2MINOR.PATCH` (concatenation): `0.5.9` → `0.25.9`, `0.10.3` → `0.210.3`.
|
|
139
|
+
From CLI 1.x, versions match. Strict SemVer prereleases `alpha.N`, `beta.N`, and
|
|
140
|
+
`rc.N` map to PEP 440 `aN`, `bN`, and `rcN`. Other prereleases and build metadata
|
|
141
|
+
are rejected. The stamped file is build metadata, not a separately maintained
|
|
142
|
+
release counter. Stamp after synchronizing the development environment;
|
|
143
|
+
`uv run --no-sync` can use that environment when preparing a newer native release.
|
|
144
|
+
The source distribution includes its stamped version and builds without the Rust
|
|
145
|
+
repository, Cargo, or Nyl.
|
|
146
|
+
|
|
147
|
+
The handwritten `.github/workflows/python.yaml` tests and builds artifacts on
|
|
148
|
+
relevant pull requests, `develop` changes, and native `v*` tags. The tag must match
|
|
149
|
+
the Cargo version. Its publish job consumes the tested artifacts and uses PyPI
|
|
150
|
+
Trusted Publishing from the `pypi` environment for `NiklasRosenstein/nyl`.
|
|
151
|
+
Release through the repository's native release command (`scripts/release.sh X.Y.Z`);
|
|
152
|
+
no separate Python counter or PyPI API token is needed.
|
nyl-0.26.0/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Nyl Python client
|
|
2
|
+
|
|
3
|
+
`nyl` is a small Python 3.11+ library for rendering and inspecting Kubernetes
|
|
4
|
+
manifests. Install it with `uv add nyl` or `pip install nyl`.
|
|
5
|
+
|
|
6
|
+
Install the [Nyl CLI](https://niklasrosenstein.github.io/nyl/start/getting-started/)
|
|
7
|
+
separately and put it on `PATH`, or pass its executable path.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from nyl import NylClient
|
|
11
|
+
|
|
12
|
+
client = NylClient(project="/path/to/infrastructure", executable="nyl", timeout=120)
|
|
13
|
+
manifests = client.render("applications/workloads/web.yaml", target="development")
|
|
14
|
+
deployment = manifests.one("Deployment", "web")
|
|
15
|
+
|
|
16
|
+
assert deployment.namespace == "web"
|
|
17
|
+
for container in deployment.containers(include_init=True):
|
|
18
|
+
assert container.security_context["allowPrivilegeEscalation"] is False
|
|
19
|
+
assert container.env.value("PGHOST") == "database-rw"
|
|
20
|
+
assert container.env.secret_ref("PGPASSWORD") == ("database-app", "password")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`namespace` is the authored namespace. Resources without one return `None`,
|
|
24
|
+
even when the Release or Argo CD Application has a destination namespace.
|
|
25
|
+
|
|
26
|
+
## Rendering
|
|
27
|
+
|
|
28
|
+
`NylClient(project, executable="nyl", timeout=120)` fixes the invocation directory
|
|
29
|
+
and requires a positive, finite timeout in seconds. `render(source, *, target=None,
|
|
30
|
+
offline=False, allow_secret_inputs=False)` invokes only `nyl render`. Relative
|
|
31
|
+
source paths are relative to `project`; absolute paths are passed through.
|
|
32
|
+
Calls leave Python's working directory and environment unchanged and use no shell.
|
|
33
|
+
Timeouts terminate the invocation's process group on POSIX and its process tree
|
|
34
|
+
with Windows' built-in `taskkill`; diagnostics are collected after termination.
|
|
35
|
+
|
|
36
|
+
Rust owns configuration resolution, validation, rendering, and caching. Target-aware
|
|
37
|
+
Releases use the compiled target's rendering rules. Each call runs the CLI again;
|
|
38
|
+
Nyl's cache may reuse valid artifacts. Project-configured validation remains enabled
|
|
39
|
+
according to the CLI's defaults. A validation failure yields an exception even if
|
|
40
|
+
the CLI emits manifests. `offline=True` forwards `--offline` (cached Git references
|
|
41
|
+
and publication inputs); it is not a universal network prohibition for validators
|
|
42
|
+
or other external tools. `allow_secret_inputs=True` explicitly forwards the CLI's
|
|
43
|
+
opt-in for project secrets and admitted environment inputs.
|
|
44
|
+
|
|
45
|
+
Only single-file render entry points are supported. Full-tree loading needs a
|
|
46
|
+
documented manifest inventory: a published tree can also contain ownership indexes
|
|
47
|
+
and non-manifest state. This package neither downloads the executable nor applies,
|
|
48
|
+
publishes, or reconciles resources, and has no persistent Python cache.
|
|
49
|
+
|
|
50
|
+
## Inspecting declarations
|
|
51
|
+
|
|
52
|
+
`ManifestSet` is iterable, has a length, and preserves document order.
|
|
53
|
+
`select(kind=None, name=None, *, namespace=..., api_version=None)` returns another
|
|
54
|
+
`ManifestSet`. `one(kind, name=None, *, namespace=..., api_version=None)` requires
|
|
55
|
+
exactly one match. Omit `namespace` to search all namespaces; explicitly pass
|
|
56
|
+
`namespace=None` to select resources with no declared namespace. Selection errors
|
|
57
|
+
show resource identities without manifest bodies. `ManifestSet.from_yaml(text)`
|
|
58
|
+
safely reads multi-document YAML 1.2, ignores empty documents, and rejects invalid
|
|
59
|
+
resource envelopes. Nyl's emitted quoting preserves strings such as `"on"`, `"yes"`,
|
|
60
|
+
and `"001"`; no YAML 1.1 boolean coercion is applied by Python.
|
|
61
|
+
|
|
62
|
+
Resources expose `raw`, `api_version`, `kind`, `name`, and `namespace`.
|
|
63
|
+
`containers(include_init=False)` handles Pods, Deployments, StatefulSets, DaemonSets,
|
|
64
|
+
ReplicaSets, ReplicationControllers, Jobs, and CronJobs. It yields regular containers
|
|
65
|
+
in order, followed by init containers when requested. Unsupported kinds raise
|
|
66
|
+
`DeclarationError`.
|
|
67
|
+
|
|
68
|
+
Containers expose `raw`, `name`, `security_context` (an empty dictionary when
|
|
69
|
+
omitted), and `env`. `env.value(name)` requires an explicit literal string;
|
|
70
|
+
`env.secret_ref(name)` requires one Secret key reference and returns `(name, key)`.
|
|
71
|
+
Missing, duplicate, malformed, and incompatible declarations raise `DeclarationError`.
|
|
72
|
+
These helpers do not resolve Secrets, ConfigMaps, `envFrom`, Pod-level security
|
|
73
|
+
context inheritance, or live cluster data. Raw dictionaries remain mutable.
|
|
74
|
+
|
|
75
|
+
## Errors and compatibility
|
|
76
|
+
|
|
77
|
+
All library errors derive from `NylError` and are exported from `nyl`:
|
|
78
|
+
|
|
79
|
+
| Error | Meaning |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `ExecutableNotFoundError` | Install the CLI or correct its executable path |
|
|
82
|
+
| `RenderTimeoutError` | The subprocess exceeded its deadline |
|
|
83
|
+
| `RenderError` | A nonzero exit status or an OS execution error |
|
|
84
|
+
| `InvalidOutputError` | stdout is not valid rendered resource YAML |
|
|
85
|
+
| `ResourceNotFoundError` | `one()` found zero matches |
|
|
86
|
+
| `AmbiguousResourceError` | `one()` found several matches |
|
|
87
|
+
| `DeclarationError` | A helper cannot interpret the declaration |
|
|
88
|
+
|
|
89
|
+
Execution errors derive from `ExecutionError` and retain `command` (an argument
|
|
90
|
+
tuple), `exit_code` (`None` when unavailable), and `diagnostics` (stderr).
|
|
91
|
+
They do not retain or automatically print stdout. YAML errors omit input excerpts;
|
|
92
|
+
resource and container representations include only identities. CLI diagnostics
|
|
93
|
+
can contain sensitive information and should be handled accordingly.
|
|
94
|
+
|
|
95
|
+
Tested with Nyl CLI **0.5.9**. The package's version is available through
|
|
96
|
+
`importlib.metadata.version("nyl")`.
|
|
97
|
+
|
|
98
|
+
## Development and releases
|
|
99
|
+
|
|
100
|
+
From the repository root:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
mise run python-check # Ruff, type checking, parallel tests, checkout-built CLI
|
|
104
|
+
mise run python-build # Stamp Cargo-derived version, then build wheel and sdist
|
|
105
|
+
mise run pre-commit # Required Rust and Python checks
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or inside `python/`:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
uv sync --locked
|
|
112
|
+
uv run python scripts/version.py
|
|
113
|
+
uv run ruff check .
|
|
114
|
+
uv run ruff format --check .
|
|
115
|
+
uv run mypy
|
|
116
|
+
uv run pytest
|
|
117
|
+
uv build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The test fixture builds the CLI and reads Cargo's executable artifact, honoring
|
|
121
|
+
Cargo's configured target and output directory. Set `NYL_TEST_EXECUTABLE` to use
|
|
122
|
+
a binary you built separately.
|
|
123
|
+
|
|
124
|
+
`scripts/version.py` derives the release version from root `Cargo.toml` and stamps
|
|
125
|
+
`src/nyl/_version.py` before building. For CLI `0.MINOR.PATCH`, Python uses
|
|
126
|
+
`0.2MINOR.PATCH` (concatenation): `0.5.9` → `0.25.9`, `0.10.3` → `0.210.3`.
|
|
127
|
+
From CLI 1.x, versions match. Strict SemVer prereleases `alpha.N`, `beta.N`, and
|
|
128
|
+
`rc.N` map to PEP 440 `aN`, `bN`, and `rcN`. Other prereleases and build metadata
|
|
129
|
+
are rejected. The stamped file is build metadata, not a separately maintained
|
|
130
|
+
release counter. Stamp after synchronizing the development environment;
|
|
131
|
+
`uv run --no-sync` can use that environment when preparing a newer native release.
|
|
132
|
+
The source distribution includes its stamped version and builds without the Rust
|
|
133
|
+
repository, Cargo, or Nyl.
|
|
134
|
+
|
|
135
|
+
The handwritten `.github/workflows/python.yaml` tests and builds artifacts on
|
|
136
|
+
relevant pull requests, `develop` changes, and native `v*` tags. The tag must match
|
|
137
|
+
the Cargo version. Its publish job consumes the tested artifacts and uses PyPI
|
|
138
|
+
Trusted Publishing from the `pypi` environment for `NiklasRosenstein/nyl`.
|
|
139
|
+
Release through the repository's native release command (`scripts/release.sh X.Y.Z`);
|
|
140
|
+
no separate Python counter or PyPI API token is needed.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nyl"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Render and inspect Kubernetes manifests with the Nyl CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{name = "Niklas Rosenstein"}]
|
|
13
|
+
dependencies = ["ruamel.yaml>=0.18.6,<0.19"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://niklasrosenstein.github.io/nyl/"
|
|
17
|
+
Repository = "https://github.com/NiklasRosenstein/nyl"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"build>=1.2,<2",
|
|
22
|
+
"hatchling>=1.27,<2",
|
|
23
|
+
"mypy>=1.15,<2",
|
|
24
|
+
"pip>=25",
|
|
25
|
+
"pytest>=8,<10",
|
|
26
|
+
"pytest-xdist>=3.6,<4",
|
|
27
|
+
"ruff>=0.11,<1",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.version]
|
|
31
|
+
path = "src/nyl/_version.py"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.sdist]
|
|
34
|
+
include = ["src/nyl", "README.md", "pyproject.toml"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
target-version = "py311"
|
|
38
|
+
line-length = 100
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
python_version = "3.11"
|
|
45
|
+
strict = true
|
|
46
|
+
files = ["src/nyl", "scripts/version.py"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
addopts = "-n auto --maxprocesses=4"
|
|
50
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Render Kubernetes manifests through an independently installed Nyl CLI."""
|
|
2
|
+
|
|
3
|
+
from .client import NylClient
|
|
4
|
+
from .errors import (
|
|
5
|
+
AmbiguousResourceError,
|
|
6
|
+
DeclarationError,
|
|
7
|
+
ExecutableNotFoundError,
|
|
8
|
+
ExecutionError,
|
|
9
|
+
InvalidOutputError,
|
|
10
|
+
NylError,
|
|
11
|
+
RenderError,
|
|
12
|
+
RenderTimeoutError,
|
|
13
|
+
ResourceNotFoundError,
|
|
14
|
+
SelectionError,
|
|
15
|
+
)
|
|
16
|
+
from .manifests import Container, Environment, ManifestSet, Resource
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AmbiguousResourceError",
|
|
20
|
+
"Container",
|
|
21
|
+
"DeclarationError",
|
|
22
|
+
"Environment",
|
|
23
|
+
"ExecutableNotFoundError",
|
|
24
|
+
"ExecutionError",
|
|
25
|
+
"InvalidOutputError",
|
|
26
|
+
"ManifestSet",
|
|
27
|
+
"NylClient",
|
|
28
|
+
"NylError",
|
|
29
|
+
"RenderError",
|
|
30
|
+
"RenderTimeoutError",
|
|
31
|
+
"Resource",
|
|
32
|
+
"ResourceNotFoundError",
|
|
33
|
+
"SelectionError",
|
|
34
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.26.0"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""The subprocess boundary delegates rendering policy and caching to Rust."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .errors import ExecutableNotFoundError, RenderError, RenderTimeoutError
|
|
11
|
+
from .manifests import ManifestSet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NylClient:
|
|
15
|
+
"""Invoke an installed Nyl executable from a fixed project directory."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
project: str | os.PathLike[str],
|
|
20
|
+
executable: str | os.PathLike[str] = "nyl",
|
|
21
|
+
timeout: float = 120,
|
|
22
|
+
) -> None:
|
|
23
|
+
if not math.isfinite(timeout) or timeout <= 0:
|
|
24
|
+
raise ValueError("timeout must be positive and finite")
|
|
25
|
+
self.project = Path(project).resolve()
|
|
26
|
+
self.executable = os.fspath(executable)
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
|
|
29
|
+
def render(
|
|
30
|
+
self,
|
|
31
|
+
source: str | os.PathLike[str],
|
|
32
|
+
*,
|
|
33
|
+
target: str | None = None,
|
|
34
|
+
offline: bool = False,
|
|
35
|
+
allow_secret_inputs: bool = False,
|
|
36
|
+
) -> ManifestSet:
|
|
37
|
+
"""Render one source file, preserving the project's validation defaults.
|
|
38
|
+
|
|
39
|
+
Relative source paths are resolved by the CLI from ``project``. Each call
|
|
40
|
+
executes the CLI afresh; its own cache decides which artifacts to reuse.
|
|
41
|
+
"""
|
|
42
|
+
command = [self.executable, "render"]
|
|
43
|
+
if target is not None:
|
|
44
|
+
command.extend(["--target", target])
|
|
45
|
+
if offline:
|
|
46
|
+
command.append("--offline")
|
|
47
|
+
if allow_secret_inputs:
|
|
48
|
+
command.append("--allow-secret-inputs")
|
|
49
|
+
command.extend(["--", os.fspath(source)])
|
|
50
|
+
try:
|
|
51
|
+
with subprocess.Popen(
|
|
52
|
+
command,
|
|
53
|
+
shell=False,
|
|
54
|
+
cwd=self.project,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.PIPE,
|
|
57
|
+
start_new_session=sys.platform != "win32",
|
|
58
|
+
) as process:
|
|
59
|
+
try:
|
|
60
|
+
stdout, stderr = process.communicate(timeout=self.timeout)
|
|
61
|
+
except BaseException as error:
|
|
62
|
+
_terminate_tree(process)
|
|
63
|
+
_, stderr = process.communicate(timeout=5)
|
|
64
|
+
if isinstance(error, subprocess.TimeoutExpired):
|
|
65
|
+
error.stderr = stderr
|
|
66
|
+
raise
|
|
67
|
+
exit_code = process.returncode
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
if not self.project.is_dir():
|
|
70
|
+
raise RenderError(
|
|
71
|
+
f"Project directory does not exist: {self.project}", command=command
|
|
72
|
+
) from None
|
|
73
|
+
raise ExecutableNotFoundError(
|
|
74
|
+
f"Nyl executable not found: {self.executable!r}; install the Nyl CLI separately",
|
|
75
|
+
command=command,
|
|
76
|
+
) from None
|
|
77
|
+
except subprocess.TimeoutExpired as error:
|
|
78
|
+
raise RenderTimeoutError(
|
|
79
|
+
f"Nyl rendering exceeded {self.timeout} seconds",
|
|
80
|
+
command=command,
|
|
81
|
+
diagnostics=(error.stderr or b"").decode("utf-8", errors="replace"),
|
|
82
|
+
) from None
|
|
83
|
+
except OSError as error:
|
|
84
|
+
raise RenderError(f"Could not execute Nyl: {error.strerror}", command=command) from None
|
|
85
|
+
diagnostics = stderr.decode("utf-8", errors="replace")
|
|
86
|
+
if exit_code:
|
|
87
|
+
raise RenderError(
|
|
88
|
+
f"Nyl rendering failed with exit code {exit_code}",
|
|
89
|
+
command=command,
|
|
90
|
+
diagnostics=diagnostics,
|
|
91
|
+
exit_code=exit_code,
|
|
92
|
+
)
|
|
93
|
+
return ManifestSet.from_yaml(stdout)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _terminate_tree(process: subprocess.Popen[bytes]) -> None:
|
|
97
|
+
if sys.platform == "win32":
|
|
98
|
+
try:
|
|
99
|
+
subprocess.run(
|
|
100
|
+
["taskkill", "/F", "/T", "/PID", str(process.pid)],
|
|
101
|
+
stdout=subprocess.DEVNULL,
|
|
102
|
+
stderr=subprocess.DEVNULL,
|
|
103
|
+
timeout=5,
|
|
104
|
+
check=False,
|
|
105
|
+
)
|
|
106
|
+
finally:
|
|
107
|
+
if process.poll() is None:
|
|
108
|
+
process.kill()
|
|
109
|
+
else:
|
|
110
|
+
try:
|
|
111
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
112
|
+
except ProcessLookupError:
|
|
113
|
+
pass
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Errors contain diagnostics and identities, never rendered manifest bodies."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NylError(Exception):
|
|
5
|
+
"""Base class for the client's errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecutionError(NylError):
|
|
9
|
+
"""A CLI invocation failed; diagnostics may contain sensitive CLI messages."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
command: list[str],
|
|
16
|
+
diagnostics: str = "",
|
|
17
|
+
exit_code: int | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.command = tuple(command)
|
|
20
|
+
self.diagnostics = diagnostics
|
|
21
|
+
self.exit_code = exit_code
|
|
22
|
+
super().__init__(f"{message}\n{diagnostics}".rstrip())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExecutableNotFoundError(ExecutionError):
|
|
26
|
+
"""The configured Nyl executable is unavailable."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RenderTimeoutError(ExecutionError):
|
|
30
|
+
"""Rendering exceeded the client's timeout."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RenderError(ExecutionError):
|
|
34
|
+
"""The CLI returned a nonzero exit code or could not start."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidOutputError(NylError):
|
|
38
|
+
"""Rendered stdout is not a stream of Kubernetes resource dictionaries."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SelectionError(NylError):
|
|
42
|
+
"""A resource selector does not identify exactly one resource."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ResourceNotFoundError(SelectionError):
|
|
46
|
+
"""No resource matches a selector."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AmbiguousResourceError(SelectionError):
|
|
50
|
+
"""Several resources match a selector."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DeclarationError(NylError):
|
|
54
|
+
"""A container helper cannot interpret the authored declaration."""
|