mampok 3.0.4__py3-none-any.whl

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.
mampok/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Mampok — Kubernetes deployment manager for bioinformatics pipelines."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("mampok")
7
+ except PackageNotFoundError:
8
+ __version__ = "unknown"
@@ -0,0 +1,5 @@
1
+ """Mampok configuration module."""
2
+
3
+ from mampok.config.config import AuthProxyConfig, ClusterConfig, MampokConfig, S3Config
4
+
5
+ __all__ = ["MampokConfig", "ClusterConfig", "S3Config", "AuthProxyConfig"]
@@ -0,0 +1,272 @@
1
+ """MampokConfig — typed configuration for Mampok v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ import json
7
+ import logging
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, ClassVar
11
+
12
+ import jsonschema
13
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
14
+
15
+ from mampok import __version__
16
+
17
+ if TYPE_CHECKING:
18
+ from mampok.kubernetes.manager import DeploymentManager
19
+ from mampok.s3.s3 import S3
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _load_config_defaults() -> tuple[dict, dict, dict]:
25
+ schema_ref = (
26
+ importlib.resources.files("mampok.config")
27
+ .joinpath("schemas")
28
+ .joinpath("config_schema.json")
29
+ )
30
+ with schema_ref.open("r", encoding="utf-8") as f:
31
+ schema = json.load(f)
32
+ cluster_props = schema["definitions"]["ClusterConfig"]["properties"]
33
+ ap_props = schema["properties"]["auth_proxy"]["properties"]
34
+ s3_props = schema["properties"]["s3"]["properties"]
35
+ return (
36
+ {k: v["default"] for k, v in cluster_props.items() if "default" in v},
37
+ {k: v["default"] for k, v in ap_props.items() if "default" in v},
38
+ {k: v["default"] for k, v in s3_props.items() if "default" in v},
39
+ )
40
+
41
+
42
+ _CLUSTER_DEFAULTS, _AUTH_PROXY_DEFAULTS, _S3_DEFAULTS = _load_config_defaults()
43
+
44
+
45
+ @dataclass
46
+ class AuthProxyConfig:
47
+ """Auth proxy configuration for the Gatekeeper sidecar.
48
+
49
+ Args:
50
+ auth_proxy_image: Docker image of the Gatekeeper container.
51
+ proxy_port: Port on which the Gatekeeper listens.
52
+ auth_annotations: Extra Ingress annotations when auth=True.
53
+ image_pull_secrets: Pull secrets for the proxy image.
54
+ """
55
+
56
+ auth_proxy_image: str
57
+ proxy_port: int = 8080
58
+ auth_annotations: dict = field(default_factory=dict)
59
+ image_pull_secrets: list[str] = field(default_factory=list)
60
+ project_auth_path: str = ""
61
+
62
+
63
+ @dataclass
64
+ class ClusterConfig:
65
+ """Configuration for a single Kubernetes cluster profile.
66
+
67
+ Args:
68
+ host: Ingress host for this cluster.
69
+ namespace: Kubernetes namespace for deployments.
70
+ kubeconfig_path: Path to the kubeconfig file for this cluster.
71
+ annotations: Kubernetes Ingress annotations.
72
+ ingress_class: Ingress class name.
73
+ dnsissuer: Cert-manager issuer name for TLS.
74
+ dnssecret: DNS secret name for ACME challenge.
75
+ """
76
+
77
+ host: str
78
+ namespace: str
79
+ kubeconfig_path: str
80
+ annotations: dict = field(default_factory=dict)
81
+ ingress_class: str = ""
82
+ dnsissuer: str = ""
83
+ dnssecret: str = ""
84
+
85
+
86
+ @dataclass
87
+ class S3Config:
88
+ """S3-compatible storage credentials.
89
+
90
+ Args:
91
+ endpoint: S3 endpoint URL.
92
+ access_key: S3 access key ID.
93
+ secret_key: S3 secret access key.
94
+ secretname: Name of the pre-existing K8s Secret holding S3 credentials.
95
+ prefix: Optional prefix for S3 bucket names.
96
+ """
97
+
98
+ endpoint: str
99
+ access_key: str
100
+ secret_key: str
101
+ secretname: str
102
+ prefix: str = ""
103
+
104
+
105
+ @dataclass
106
+ class MampokConfig:
107
+ """Typed configuration for Mampok v2.
108
+
109
+ Loaded from a JSON file and validated against the config schema.
110
+ Provides factory methods to create Kubernetes and S3 clients.
111
+
112
+ Args:
113
+ clusters: Named cluster profiles (e.g. {'BN': ClusterConfig(...)}).
114
+ s3: S3 storage credentials.
115
+ mamplan_repo: Path to the Mamplan repository directory.
116
+ mamplates_path: Path to the Mamplates directory.
117
+ lifetime_days: Default deployment lifetime in days.
118
+ """
119
+
120
+ clusters: dict[str, ClusterConfig]
121
+ s3: S3Config
122
+ mamplan_repo: Path
123
+ mamplates_path: Path
124
+ lifetime_days: int
125
+ mampok_version: str
126
+ default_cluster: str | None = None
127
+ auth_proxy: AuthProxyConfig | None = None
128
+ """Gatekeeper proxy configuration. Required when deployments use auth=True."""
129
+
130
+ _schema_cache: ClassVar[dict | None] = None
131
+
132
+ def get_cluster(self, name: str) -> ClusterConfig:
133
+ """Return the named cluster profile.
134
+
135
+ Args:
136
+ name: Cluster name as used in the config and Mamplan.
137
+
138
+ Returns:
139
+ The ClusterConfig for the given name.
140
+
141
+ Raises:
142
+ KeyError: If the cluster name is not found in the config.
143
+ """
144
+ if name not in self.clusters:
145
+ raise KeyError(
146
+ f"Unknown cluster: {name!r}. Available: {list(self.clusters)}"
147
+ )
148
+ return self.clusters[name]
149
+
150
+ def build_deployment_manager(self, cluster_name: str) -> "DeploymentManager":
151
+ """Create a DeploymentManager for the named cluster profile.
152
+
153
+ Loads the kubeconfig, creates a Kubernetes ApiClient, wraps it in a
154
+ KubeClient, and returns a DeploymentManager. Used by interface layers.
155
+
156
+ Args:
157
+ cluster_name: Name of the cluster profile to connect to.
158
+
159
+ Returns:
160
+ Configured DeploymentManager for the cluster.
161
+ """
162
+ import kubernetes
163
+ from mampok.kubernetes.client import KubeClient
164
+ from mampok.kubernetes.manager import DeploymentManager
165
+
166
+ cluster = self.get_cluster(cluster_name)
167
+ kubernetes.config.load_kube_config(config_file=cluster.kubeconfig_path)
168
+ api_client = kubernetes.client.ApiClient()
169
+ kube_client = KubeClient(namespace=cluster.namespace, api_client=api_client)
170
+ return DeploymentManager(kube_client)
171
+
172
+ def build_s3_client(self, bucket: str) -> "S3":
173
+ """Create an S3 client with the configured credentials.
174
+
175
+ Used by interface layers to create the S3 dependency for Mampok.
176
+
177
+ Args:
178
+ bucket: S3 bucket name for this deployment.
179
+
180
+ Returns:
181
+ Configured S3 client.
182
+ """
183
+ from mampok.s3.s3 import S3
184
+
185
+ return S3(
186
+ bucket=bucket,
187
+ endpoint_url=self.s3.endpoint,
188
+ access_key=self.s3.access_key,
189
+ secret_key=self.s3.secret_key,
190
+ )
191
+
192
+ @classmethod
193
+ def from_dict(cls, data: dict) -> "MampokConfig":
194
+ """Create a MampokConfig from a dict, validated against the JSON schema.
195
+
196
+ Args:
197
+ data: Configuration dict (as loaded from JSON).
198
+
199
+ Returns:
200
+ Validated MampokConfig instance.
201
+
202
+ Raises:
203
+ jsonschema.ValidationError: If the data violates the schema.
204
+ """
205
+ if cls._schema_cache is None:
206
+ schema_ref = (
207
+ importlib.resources.files("mampok.config")
208
+ .joinpath("schemas")
209
+ .joinpath("config_schema.json")
210
+ )
211
+ with schema_ref.open("r", encoding="utf-8") as f:
212
+ cls._schema_cache = json.load(f)
213
+
214
+ jsonschema.validate(data, cls._schema_cache)
215
+
216
+ raw_version_spec = data["mampok_version"]
217
+ try:
218
+ spec = SpecifierSet(raw_version_spec)
219
+ except InvalidSpecifier as exc:
220
+ raise ValueError(
221
+ f"Invalid mampok_version specifier in config: {raw_version_spec!r}"
222
+ ) from exc
223
+ if __version__ not in spec:
224
+ raise ValueError(
225
+ f"Mampok version mismatch: config requires {raw_version_spec!r}, "
226
+ f"but installed version is {__version__!r}."
227
+ )
228
+
229
+ clusters = {
230
+ name: ClusterConfig(**{**_CLUSTER_DEFAULTS, **cluster_data})
231
+ for name, cluster_data in data["cluster"].items()
232
+ }
233
+
234
+ s3 = S3Config(**{**_S3_DEFAULTS, **data["s3"]})
235
+
236
+ auth_proxy = None
237
+ if _ap := data.get("auth_proxy"):
238
+ auth_proxy = AuthProxyConfig(**{**_AUTH_PROXY_DEFAULTS, **_ap})
239
+
240
+ return cls(
241
+ clusters=clusters,
242
+ s3=s3,
243
+ mamplan_repo=Path(data["mamplan_repo"]),
244
+ mamplates_path=Path(data["mamplates_path"]),
245
+ lifetime_days=data["lifetime_days"],
246
+ mampok_version=data["mampok_version"],
247
+ default_cluster=data.get("default_cluster"),
248
+ auth_proxy=auth_proxy,
249
+ )
250
+
251
+ @classmethod
252
+ def from_file(cls, path: Path) -> "MampokConfig":
253
+ """Load a MampokConfig from a JSON file.
254
+
255
+ Args:
256
+ path: Path to the JSON config file.
257
+
258
+ Returns:
259
+ Validated MampokConfig instance.
260
+
261
+ Raises:
262
+ FileNotFoundError: If the file does not exist.
263
+ json.JSONDecodeError: If the file is not valid JSON.
264
+ jsonschema.ValidationError: If the content violates the schema.
265
+ """
266
+ path = Path(path)
267
+ if not path.exists():
268
+ raise FileNotFoundError(f"Config file not found: {path}")
269
+ logger.info("loading config: %s", path)
270
+ with path.open("r", encoding="utf-8") as f:
271
+ data = json.load(f)
272
+ return cls.from_dict(data)
@@ -0,0 +1,149 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "MampokConfig",
4
+ "description": "Schema for Mampok v2 configuration files.",
5
+ "type": "object",
6
+ "required": ["cluster", "s3", "mamplan_repo", "mamplates_path", "lifetime_days", "mampok_version"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "cluster": {
10
+ "type": "object",
11
+ "description": "Named cluster profiles. Each key is a cluster name (e.g. 'BN', 'BN_public').",
12
+ "minProperties": 1,
13
+ "additionalProperties": {
14
+ "$ref": "#/definitions/ClusterConfig"
15
+ }
16
+ },
17
+ "s3": {
18
+ "type": "object",
19
+ "description": "S3-compatible storage credentials (shared across all clusters).",
20
+ "required": ["endpoint", "access_key", "secret_key", "secretname"],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "endpoint": {
24
+ "type": "string",
25
+ "description": "S3 endpoint URL (e.g. 'https://s3.example.com')."
26
+ },
27
+ "access_key": {
28
+ "type": "string",
29
+ "description": "S3 access key ID."
30
+ },
31
+ "secret_key": {
32
+ "type": "string",
33
+ "description": "S3 secret access key."
34
+ },
35
+ "secretname": {
36
+ "type": "string",
37
+ "description": "Name of the pre-existing Kubernetes Secret holding S3 credentials (e.g. 'mpis')."
38
+ },
39
+ "prefix": {
40
+ "type": "string",
41
+ "description": "Optional prefix for S3 bucket names.",
42
+ "default": ""
43
+ }
44
+ }
45
+ },
46
+ "mamplan_repo": {
47
+ "type": "string",
48
+ "description": "Path to the Mamplan repository directory."
49
+ },
50
+ "mamplates_path": {
51
+ "type": "string",
52
+ "description": "Path to the Mamplates directory."
53
+ },
54
+ "lifetime_days": {
55
+ "type": "integer",
56
+ "description": "Default deployment lifetime in days.",
57
+ "minimum": 1
58
+ },
59
+ "default_cluster": {
60
+ "type": "string",
61
+ "description": "Default cluster name used when no cluster is specified (e.g. for Software Hub deployments). Must match a key in the 'cluster' object."
62
+ },
63
+ "auth_proxy": {
64
+ "type": "object",
65
+ "description": "Gatekeeper proxy configuration. Only needed when deployments use auth=True.",
66
+ "required": ["auth_proxy_image"],
67
+ "additionalProperties": false,
68
+ "properties": {
69
+ "auth_proxy_image": {
70
+ "type": "string",
71
+ "description": "Docker image of the Gatekeeper container."
72
+ },
73
+ "proxy_port": {
74
+ "type": "integer",
75
+ "description": "Port on which the Gatekeeper listens.",
76
+ "default": 8080,
77
+ "minimum": 1,
78
+ "maximum": 65535
79
+ },
80
+ "auth_annotations": {
81
+ "type": "object",
82
+ "description": "Extra Ingress annotations when auth=True.",
83
+ "additionalProperties": {"type": "string"},
84
+ "default": {}
85
+ },
86
+ "image_pull_secrets": {
87
+ "type": "array",
88
+ "description": "Pull secret names for the proxy image.",
89
+ "items": {"type": "string"},
90
+ "default": []
91
+ },
92
+ "project_auth_path": {
93
+ "type": "string",
94
+ "description": "Absolute path to project_auth.json (for the Flask API /openProject endpoint).",
95
+ "default": ""
96
+ }
97
+ }
98
+ },
99
+ "mampok_version": {
100
+ "type": "string",
101
+ "description": "PEP 440 version specifier restricting which Mampok versions may run on this repository (e.g. '>=2.0.0,<3.0.0'). Required."
102
+ }
103
+ },
104
+ "definitions": {
105
+ "ClusterConfig": {
106
+ "type": "object",
107
+ "description": "Configuration for a single Kubernetes cluster.",
108
+ "required": ["host", "namespace", "kubeconfig_path"],
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "host": {
112
+ "type": "string",
113
+ "description": "Ingress host for this cluster (e.g. 'bioinformatics-cluster.example.com')."
114
+ },
115
+ "namespace": {
116
+ "type": "string",
117
+ "description": "Kubernetes namespace for deployments on this cluster."
118
+ },
119
+ "kubeconfig_path": {
120
+ "type": "string",
121
+ "description": "Absolute path to the kubeconfig file for this cluster."
122
+ },
123
+ "annotations": {
124
+ "type": "object",
125
+ "description": "Kubernetes Ingress annotations for this cluster.",
126
+ "additionalProperties": {
127
+ "type": "string"
128
+ },
129
+ "default": {}
130
+ },
131
+ "ingress_class": {
132
+ "type": "string",
133
+ "description": "Kubernetes Ingress class name (e.g. 'nginx').",
134
+ "default": ""
135
+ },
136
+ "dnsissuer": {
137
+ "type": "string",
138
+ "description": "Cert-manager issuer name for TLS certificates (optional).",
139
+ "default": ""
140
+ },
141
+ "dnssecret": {
142
+ "type": "string",
143
+ "description": "DNS secret name for ACME challenge (optional).",
144
+ "default": ""
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
@@ -0,0 +1 @@
1
+ """Interfaces module — CLI (Typer) and Python API."""