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.
- kubechain-0.1.0/LICENSE +21 -0
- kubechain-0.1.0/PKG-INFO +135 -0
- kubechain-0.1.0/README.md +107 -0
- kubechain-0.1.0/pyproject.toml +58 -0
- kubechain-0.1.0/setup.cfg +4 -0
- kubechain-0.1.0/src/kubechain/__init__.py +3 -0
- kubechain-0.1.0/src/kubechain/__main__.py +5 -0
- kubechain-0.1.0/src/kubechain/catalog.py +152 -0
- kubechain-0.1.0/src/kubechain/check.py +933 -0
- kubechain-0.1.0/src/kubechain/cli.py +98 -0
- kubechain-0.1.0/src/kubechain/clients.json +210 -0
- kubechain-0.1.0/src/kubechain/live.py +154 -0
- kubechain-0.1.0/src/kubechain/load.py +50 -0
- kubechain-0.1.0/src/kubechain/model.py +71 -0
- kubechain-0.1.0/src/kubechain/render.py +64 -0
- kubechain-0.1.0/src/kubechain.egg-info/PKG-INFO +135 -0
- kubechain-0.1.0/src/kubechain.egg-info/SOURCES.txt +21 -0
- kubechain-0.1.0/src/kubechain.egg-info/dependency_links.txt +1 -0
- kubechain-0.1.0/src/kubechain.egg-info/entry_points.txt +2 -0
- kubechain-0.1.0/src/kubechain.egg-info/requires.txt +5 -0
- kubechain-0.1.0/src/kubechain.egg-info/top_level.txt +1 -0
- kubechain-0.1.0/tests/test_check.py +69 -0
- kubechain-0.1.0/tests/test_cli.py +126 -0
kubechain-0.1.0/LICENSE
ADDED
|
@@ -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.
|
kubechain-0.1.0/PKG-INFO
ADDED
|
@@ -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,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
|
+
)
|