kubechain 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KubeChain contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: kubechain
3
+ Version: 0.1.0
4
+ Summary: Decide whether a blockchain workload on Kubernetes is production ready.
5
+ Author: ehsanhajian
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/ehsanhajian/kubechain
8
+ Project-URL: Issues, https://github.com/ehsanhajian/kubechain/issues
9
+ Project-URL: Changelog, https://github.com/ehsanhajian/kubechain/releases
10
+ Keywords: kubernetes,ethereum,geth,lighthouse,statefulset,node,cli
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Clustering
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: kubernetes>=31
24
+ Requires-Dist: pyyaml>=6
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # KubeChain
30
+
31
+ KubeChain answers one question: **is my Kubernetes blockchain deployment production ready?**
32
+
33
+ It reads workloads, Services, Secrets, storage, network policy, and disruption settings. A live check uses get, list, and watch. Secret bytes are discarded. Volume snapshots are observed and never created. The cluster is left as it was found.
34
+
35
+ ValidatorPulse raises an alert. ChainDiff decides whether a client upgrade is safe. ChainTrace is the view of the process. KubeChain is the view of that same node as a Kubernetes workload.
36
+
37
+ ## Install
38
+
39
+ Python 3.11 or newer.
40
+
41
+ ```bash
42
+ pip install kubechain
43
+ kubechain --version
44
+ ```
45
+
46
+ The package is published from a GitHub release to [PyPI](https://pypi.org/project/kubechain/). This README is the reference.
47
+
48
+ ## Commands
49
+
50
+ | Question | Command |
51
+ | --- | --- |
52
+ | Does this manifest set pass? | `check --manifests DIR` |
53
+ | Does this namespace pass? | `check --namespace NS` |
54
+ | One line for a journal? | `check --brief` |
55
+ | Can I keep the answer? | `report` |
56
+
57
+ `--manifests` needs no cluster. `--context NAME` selects a kubeconfig context for a live read. The two modes are separate. `--json` prints one document. `--brief` and `--json` together print the JSON document only.
58
+
59
+ ## Examples
60
+
61
+ `examples/ready` is a geth and lighthouse pair that passes. `examples/broken` is the cases that are not ready. `examples/oom` is a saved pod status.
62
+
63
+ ```bash
64
+ kubechain check --manifests examples/ready
65
+ kubechain check --manifests examples/broken
66
+ kubechain check --manifests examples/ready --brief
67
+ kubechain check --manifests examples/ready --profile examples/geth.json
68
+ kubechain report --manifests examples/ready
69
+ kubechain check --namespace nodes
70
+ ```
71
+
72
+ `report` writes `kubechain-report.md`. `--output PATH` chooses the file. A directory is refused. An existing file is overwritten.
73
+
74
+ ## Verdicts
75
+
76
+ | Verdict | Exit | When |
77
+ | --- | --- | --- |
78
+ | READY | 0 | No warning or critical finding. An info line still exits 0. |
79
+ | REVIEW | 1 | A warning. The workload runs, and a production gap is named. |
80
+ | NOT READY | 2 | A critical finding. Chain data, a secret, or an upgrade would be unsafe. |
81
+ | ERROR | 3 | The manifests, the profile, or the cluster could not be read. |
82
+
83
+ Every finding has an id, a severity, the object name, a reason, and an action. A check that cannot name an action does not emit a finding.
84
+
85
+ ## What a ready workload looks like
86
+
87
+ A chain node is a StatefulSet with a claim template, a headless Service, and a JWT mounted as a file. Probes survive a long sync. The disruption budget matches the replica count. A backup signal exists before the next rolling upgrade. A Deployment is for a stateless proxy, not for chain data.
88
+
89
+ A single replica is a warning. It runs, and it has no failover.
90
+
91
+ `--manifests` has no pod status and no kubelet stats. The report says restarts were not read, and storage performance was not read. That absence is not a finding. A live read reports OOMKilled, CrashLoopBackOff, and restart counts. Volume usage is reported when kubelet stats can be read. Above 80% is a warning. Above 95% is critical. When the stats endpoint is unavailable, the report says storage performance was not read.
92
+
93
+ ## Clients
94
+
95
+ The catalog recognizes geth, reth, nethermind, erigon, besu, op-geth, lighthouse, prysm, teku, nimbus, lodestar, op-node, and mev-boost. Each profile sets ports, the data volume, whether a JWT is required, and a floor for CPU, memory, storage, and shutdown grace. The floor is a KubeChain floor. The source line is in `clients.json`.
96
+
97
+ An unrecognized workload is still listed and is judged only by the generic Kubernetes checks. op-node is the consensus side for op-geth. mev-boost does not sync and does not hold chain data.
98
+
99
+ `--profile FILE` replaces the built-in profile when the container name matches `comm`. `examples/geth.json` is the sample.
100
+
101
+ ```json
102
+ {
103
+ "comm": "geth",
104
+ "p2p_ports": [30303],
105
+ "rpc_ports": [8545, 8546],
106
+ "engine_port": 8551,
107
+ "jwt": true,
108
+ "data_volume": "data",
109
+ "cpu": "2",
110
+ "memory": "8Gi",
111
+ "storage": "100Gi",
112
+ "grace_seconds": 300
113
+ }
114
+ ```
115
+
116
+ An unknown field is exit 3.
117
+
118
+ ## Live read
119
+
120
+ `deploy/kubechain-reader.yaml` is a Role and a binding for the object kinds a live check reads. The verbs are get, list, and watch. Apply it yourself. KubeChain does not apply it.
121
+
122
+ ```bash
123
+ kubectl apply -f deploy/kubechain-reader.yaml -n nodes
124
+ ```
125
+
126
+ Listing a Secret returns its bytes from the API. The CLI discards them. The report prints the Secret name and the mount path.
127
+
128
+ StorageClass and kubelet volume stats are cluster-scoped. The Role comment says which extra rules those reads need. If they are not granted, the report says so and continues.
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ pip install -e ".[dev]"
134
+ pytest
135
+ ```
@@ -0,0 +1,107 @@
1
+ # KubeChain
2
+
3
+ KubeChain answers one question: **is my Kubernetes blockchain deployment production ready?**
4
+
5
+ It reads workloads, Services, Secrets, storage, network policy, and disruption settings. A live check uses get, list, and watch. Secret bytes are discarded. Volume snapshots are observed and never created. The cluster is left as it was found.
6
+
7
+ ValidatorPulse raises an alert. ChainDiff decides whether a client upgrade is safe. ChainTrace is the view of the process. KubeChain is the view of that same node as a Kubernetes workload.
8
+
9
+ ## Install
10
+
11
+ Python 3.11 or newer.
12
+
13
+ ```bash
14
+ pip install kubechain
15
+ kubechain --version
16
+ ```
17
+
18
+ The package is published from a GitHub release to [PyPI](https://pypi.org/project/kubechain/). This README is the reference.
19
+
20
+ ## Commands
21
+
22
+ | Question | Command |
23
+ | --- | --- |
24
+ | Does this manifest set pass? | `check --manifests DIR` |
25
+ | Does this namespace pass? | `check --namespace NS` |
26
+ | One line for a journal? | `check --brief` |
27
+ | Can I keep the answer? | `report` |
28
+
29
+ `--manifests` needs no cluster. `--context NAME` selects a kubeconfig context for a live read. The two modes are separate. `--json` prints one document. `--brief` and `--json` together print the JSON document only.
30
+
31
+ ## Examples
32
+
33
+ `examples/ready` is a geth and lighthouse pair that passes. `examples/broken` is the cases that are not ready. `examples/oom` is a saved pod status.
34
+
35
+ ```bash
36
+ kubechain check --manifests examples/ready
37
+ kubechain check --manifests examples/broken
38
+ kubechain check --manifests examples/ready --brief
39
+ kubechain check --manifests examples/ready --profile examples/geth.json
40
+ kubechain report --manifests examples/ready
41
+ kubechain check --namespace nodes
42
+ ```
43
+
44
+ `report` writes `kubechain-report.md`. `--output PATH` chooses the file. A directory is refused. An existing file is overwritten.
45
+
46
+ ## Verdicts
47
+
48
+ | Verdict | Exit | When |
49
+ | --- | --- | --- |
50
+ | READY | 0 | No warning or critical finding. An info line still exits 0. |
51
+ | REVIEW | 1 | A warning. The workload runs, and a production gap is named. |
52
+ | NOT READY | 2 | A critical finding. Chain data, a secret, or an upgrade would be unsafe. |
53
+ | ERROR | 3 | The manifests, the profile, or the cluster could not be read. |
54
+
55
+ Every finding has an id, a severity, the object name, a reason, and an action. A check that cannot name an action does not emit a finding.
56
+
57
+ ## What a ready workload looks like
58
+
59
+ A chain node is a StatefulSet with a claim template, a headless Service, and a JWT mounted as a file. Probes survive a long sync. The disruption budget matches the replica count. A backup signal exists before the next rolling upgrade. A Deployment is for a stateless proxy, not for chain data.
60
+
61
+ A single replica is a warning. It runs, and it has no failover.
62
+
63
+ `--manifests` has no pod status and no kubelet stats. The report says restarts were not read, and storage performance was not read. That absence is not a finding. A live read reports OOMKilled, CrashLoopBackOff, and restart counts. Volume usage is reported when kubelet stats can be read. Above 80% is a warning. Above 95% is critical. When the stats endpoint is unavailable, the report says storage performance was not read.
64
+
65
+ ## Clients
66
+
67
+ The catalog recognizes geth, reth, nethermind, erigon, besu, op-geth, lighthouse, prysm, teku, nimbus, lodestar, op-node, and mev-boost. Each profile sets ports, the data volume, whether a JWT is required, and a floor for CPU, memory, storage, and shutdown grace. The floor is a KubeChain floor. The source line is in `clients.json`.
68
+
69
+ An unrecognized workload is still listed and is judged only by the generic Kubernetes checks. op-node is the consensus side for op-geth. mev-boost does not sync and does not hold chain data.
70
+
71
+ `--profile FILE` replaces the built-in profile when the container name matches `comm`. `examples/geth.json` is the sample.
72
+
73
+ ```json
74
+ {
75
+ "comm": "geth",
76
+ "p2p_ports": [30303],
77
+ "rpc_ports": [8545, 8546],
78
+ "engine_port": 8551,
79
+ "jwt": true,
80
+ "data_volume": "data",
81
+ "cpu": "2",
82
+ "memory": "8Gi",
83
+ "storage": "100Gi",
84
+ "grace_seconds": 300
85
+ }
86
+ ```
87
+
88
+ An unknown field is exit 3.
89
+
90
+ ## Live read
91
+
92
+ `deploy/kubechain-reader.yaml` is a Role and a binding for the object kinds a live check reads. The verbs are get, list, and watch. Apply it yourself. KubeChain does not apply it.
93
+
94
+ ```bash
95
+ kubectl apply -f deploy/kubechain-reader.yaml -n nodes
96
+ ```
97
+
98
+ Listing a Secret returns its bytes from the API. The CLI discards them. The report prints the Secret name and the mount path.
99
+
100
+ StorageClass and kubelet volume stats are cluster-scoped. The Role comment says which extra rules those reads need. If they are not granted, the report says so and continues.
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ pip install -e ".[dev]"
106
+ pytest
107
+ ```
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "kubechain"
3
+ version = "0.1.0"
4
+ description = "Decide whether a blockchain workload on Kubernetes is production ready."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.11"
9
+ keywords = [
10
+ "kubernetes",
11
+ "ethereum",
12
+ "geth",
13
+ "lighthouse",
14
+ "statefulset",
15
+ "node",
16
+ "cli",
17
+ ]
18
+ authors = [{ name = "ehsanhajian" }]
19
+ classifiers = [
20
+ "Development Status :: 3 - Alpha",
21
+ "Environment :: Console",
22
+ "Intended Audience :: System Administrators",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: System :: Clustering",
28
+ "Topic :: System :: Systems Administration",
29
+ ]
30
+ dependencies = [
31
+ "kubernetes>=31",
32
+ "pyyaml>=6",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest>=8"]
37
+
38
+ [project.urls]
39
+ Repository = "https://github.com/ehsanhajian/kubechain"
40
+ Issues = "https://github.com/ehsanhajian/kubechain/issues"
41
+ Changelog = "https://github.com/ehsanhajian/kubechain/releases"
42
+
43
+ [project.scripts]
44
+ kubechain = "kubechain.cli:main"
45
+
46
+ [build-system]
47
+ requires = ["setuptools>=84"]
48
+ build-backend = "setuptools.build_meta"
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.setuptools.package-data]
54
+ kubechain = ["clients.json"]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ pythonpath = [".", "src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Decide whether a blockchain workload on Kubernetes is production ready."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from kubechain.cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,152 @@
1
+ """Built-in client profiles and a local file that overrides one of them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, replace
7
+ from importlib.resources import files
8
+ from pathlib import Path
9
+
10
+
11
+ class ProfileError(Exception):
12
+ """The profile file cannot be used."""
13
+
14
+
15
+ ALLOWED = {
16
+ "comm",
17
+ "role",
18
+ "images",
19
+ "p2p_ports",
20
+ "rpc_ports",
21
+ "engine_port",
22
+ "jwt",
23
+ "data_volume",
24
+ "cpu",
25
+ "memory",
26
+ "storage",
27
+ "grace_seconds",
28
+ "exempt_probes",
29
+ "source",
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Profile:
35
+ comm: str
36
+ role: str
37
+ images: tuple[str, ...]
38
+ p2p_ports: tuple[int, ...]
39
+ rpc_ports: tuple[int, ...]
40
+ engine_port: int | None
41
+ jwt: bool
42
+ data_volume: str
43
+ cpu: str
44
+ memory: str
45
+ storage: str
46
+ grace_seconds: int
47
+ exempt_probes: bool
48
+ source: str
49
+
50
+
51
+ def catalog() -> dict[str, Profile]:
52
+ raw = json.loads(files("kubechain").joinpath("clients.json").read_text())
53
+ return {name: _profile(body) for name, body in raw.items()}
54
+
55
+
56
+ def load_profile(path: Path) -> dict:
57
+ try:
58
+ data = json.loads(path.read_text())
59
+ except json.JSONDecodeError as exc:
60
+ raise ProfileError(f"profile is not JSON: {path}") from exc
61
+ if not isinstance(data, dict):
62
+ raise ProfileError("profile must be a JSON object")
63
+ extra = sorted(set(data) - ALLOWED)
64
+ if extra:
65
+ raise ProfileError(f"unknown profile field: {extra[0]}")
66
+ if "comm" not in data:
67
+ raise ProfileError("profile needs comm")
68
+ return data
69
+
70
+
71
+ def resolve(container: dict, profiles: dict[str, Profile], overlay: dict | None) -> Profile | None:
72
+ name = str(container.get("name") or "")
73
+ image = str(container.get("image") or "")
74
+ chosen = profiles.get(name) or _by_image(image, profiles)
75
+ if overlay and overlay.get("comm") == name:
76
+ return _merge(chosen, overlay)
77
+ return chosen
78
+
79
+
80
+ def _by_image(image: str, profiles: dict[str, Profile]) -> Profile | None:
81
+ path = image.split("@", 1)[0]
82
+ slash = path.rfind("/")
83
+ colon = path.rfind(":")
84
+ if colon > slash:
85
+ path = path[:colon]
86
+ segments = path.split("/")
87
+ best: Profile | None = None
88
+ best_len = -1
89
+ for profile in profiles.values():
90
+ for hint in profile.images:
91
+ if hint in segments and len(hint) > best_len:
92
+ best = profile
93
+ best_len = len(hint)
94
+ return best
95
+
96
+
97
+ def _merge(base: Profile | None, overlay: dict) -> Profile:
98
+ if base is None:
99
+ base = Profile(
100
+ comm=str(overlay["comm"]),
101
+ role=str(overlay.get("role") or "unknown"),
102
+ images=(),
103
+ p2p_ports=(),
104
+ rpc_ports=(),
105
+ engine_port=None,
106
+ jwt=bool(overlay.get("jwt", False)),
107
+ data_volume=str(overlay.get("data_volume") or ""),
108
+ cpu=str(overlay.get("cpu") or ""),
109
+ memory=str(overlay.get("memory") or ""),
110
+ storage=str(overlay.get("storage") or ""),
111
+ grace_seconds=int(overlay.get("grace_seconds") or 30),
112
+ exempt_probes=bool(overlay.get("exempt_probes", False)),
113
+ source="Local profile.",
114
+ )
115
+ values = {}
116
+ for key in ("role", "data_volume", "cpu", "memory", "storage", "source"):
117
+ if key in overlay:
118
+ values[key] = overlay[key]
119
+ if "engine_port" in overlay:
120
+ values["engine_port"] = overlay["engine_port"]
121
+ if "jwt" in overlay:
122
+ values["jwt"] = bool(overlay["jwt"])
123
+ if "exempt_probes" in overlay:
124
+ values["exempt_probes"] = bool(overlay["exempt_probes"])
125
+ if "grace_seconds" in overlay:
126
+ values["grace_seconds"] = int(overlay["grace_seconds"])
127
+ if "p2p_ports" in overlay:
128
+ values["p2p_ports"] = tuple(int(port) for port in overlay["p2p_ports"])
129
+ if "rpc_ports" in overlay:
130
+ values["rpc_ports"] = tuple(int(port) for port in overlay["rpc_ports"])
131
+ if "images" in overlay:
132
+ values["images"] = tuple(overlay["images"])
133
+ return replace(base, **values)
134
+
135
+
136
+ def _profile(body: dict) -> Profile:
137
+ return Profile(
138
+ comm=body["comm"],
139
+ role=body["role"],
140
+ images=tuple(body.get("images") or ()),
141
+ p2p_ports=tuple(body.get("p2p_ports") or ()),
142
+ rpc_ports=tuple(body.get("rpc_ports") or ()),
143
+ engine_port=body.get("engine_port"),
144
+ jwt=bool(body.get("jwt")),
145
+ data_volume=body.get("data_volume") or "",
146
+ cpu=body.get("cpu") or "",
147
+ memory=body.get("memory") or "",
148
+ storage=body.get("storage") or "",
149
+ grace_seconds=int(body.get("grace_seconds") or 30),
150
+ exempt_probes=bool(body.get("exempt_probes")),
151
+ source=body.get("source") or "KubeChain floor.",
152
+ )