configmap_reader 0.1.0__tar.gz → 0.2.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.
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/PKG-INFO +2 -2
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/pyproject.toml +4 -3
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/src/configmap_reader/cli.py +0 -6
- configmap_reader-0.2.0/src/configmap_reader/config_api.py +57 -0
- configmap_reader-0.2.0/src/configmap_reader/config_dir.py +32 -0
- configmap_reader-0.2.0/src/configmap_reader/main.py +61 -0
- configmap_reader-0.1.0/src/configmap_reader/main.py +0 -123
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/LICENSE +0 -0
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/README.md +0 -0
- {configmap_reader-0.1.0 → configmap_reader-0.2.0}/src/configmap_reader/__init__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "configmap_reader"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "microservice to read and return content of a configmap"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Siak Hooi",email = "siakhooi@gmail.com"}
|
|
7
7
|
]
|
|
@@ -37,10 +37,11 @@ packages = [{include = "configmap_reader", from = "src"}]
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
[tool.poetry.group.dev.dependencies]
|
|
40
|
-
pytest = "^9.0.
|
|
40
|
+
pytest = "^9.0.2"
|
|
41
41
|
pytest-cov = "^7.0.0"
|
|
42
42
|
flake8 = "^7.2.0"
|
|
43
43
|
pytest-mock = "^3.15.1"
|
|
44
|
+
httpx = "^0.28.1"
|
|
44
45
|
|
|
45
46
|
[build-system]
|
|
46
47
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import sys
|
|
3
2
|
from importlib.metadata import version
|
|
4
3
|
from . import main
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
def print_to_stderr_and_exit(e: Exception, exit_code: int) -> None:
|
|
8
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
9
|
-
exit(exit_code)
|
|
10
|
-
|
|
11
|
-
|
|
12
6
|
def run() -> None:
|
|
13
7
|
__version__: str = version("configmap-reader")
|
|
14
8
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
|
|
3
|
+
_k8s_client = None
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _get_k8s_client():
|
|
7
|
+
global _k8s_client
|
|
8
|
+
if _k8s_client is not None:
|
|
9
|
+
return _k8s_client
|
|
10
|
+
try:
|
|
11
|
+
from kubernetes import client, config
|
|
12
|
+
|
|
13
|
+
# In-cluster config first; fallback to local kubeconfig for dev
|
|
14
|
+
try:
|
|
15
|
+
config.load_incluster_config()
|
|
16
|
+
except Exception:
|
|
17
|
+
config.load_kube_config()
|
|
18
|
+
_k8s_client = client.CoreV1Api()
|
|
19
|
+
return _k8s_client
|
|
20
|
+
except Exception as e:
|
|
21
|
+
raise HTTPException(
|
|
22
|
+
status_code=500, detail=f"Failed to init Kubernetes client: {e}"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read(configmap_name: str, namespace: str) -> dict:
|
|
27
|
+
"""
|
|
28
|
+
Read ConfigMap via Kubernetes API.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
configmap_name: Name of the ConfigMap to read
|
|
32
|
+
namespace: Kubernetes namespace
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
dict: ConfigMap data as filename -> string content
|
|
36
|
+
"""
|
|
37
|
+
if not configmap_name:
|
|
38
|
+
raise HTTPException(
|
|
39
|
+
status_code=500,
|
|
40
|
+
detail="CONFIGMAP_NAME is not set for API read mode", # noqa: E501
|
|
41
|
+
)
|
|
42
|
+
if not namespace:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=500, detail="NAMESPACE is not set for API read mode"
|
|
45
|
+
)
|
|
46
|
+
api = _get_k8s_client()
|
|
47
|
+
try:
|
|
48
|
+
cm = api.read_namespaced_config_map(
|
|
49
|
+
name=configmap_name, namespace=namespace
|
|
50
|
+
)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
raise HTTPException(
|
|
53
|
+
status_code=500,
|
|
54
|
+
detail=f"Failed to read ConfigMap {namespace}/{configmap_name}: {e}", # noqa: E501
|
|
55
|
+
)
|
|
56
|
+
data = cm.data or {}
|
|
57
|
+
return dict(data)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
CONFIG_DIR = os.getenv("CONFIG_DIR", "/config")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read(config_dir: str = CONFIG_DIR) -> dict:
|
|
9
|
+
"""Read all files from the config directory and return as a dictionary.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
config_dir: Path to the configuration directory
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Dictionary mapping filenames to their content
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
FileNotFoundError: If the config directory doesn't exist or
|
|
19
|
+
isn't a directory
|
|
20
|
+
"""
|
|
21
|
+
path = pathlib.Path(config_dir)
|
|
22
|
+
if not path.exists() or not path.is_dir():
|
|
23
|
+
raise FileNotFoundError(f"Config directory not found: {config_dir}")
|
|
24
|
+
result = {}
|
|
25
|
+
for p in path.iterdir():
|
|
26
|
+
if p.is_file():
|
|
27
|
+
try:
|
|
28
|
+
content = p.read_text(encoding="utf-8")
|
|
29
|
+
except UnicodeDecodeError:
|
|
30
|
+
continue
|
|
31
|
+
result[p.name] = content
|
|
32
|
+
return result
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from fastapi import FastAPI, HTTPException
|
|
2
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import uvicorn
|
|
6
|
+
from . import config_dir, config_api
|
|
7
|
+
|
|
8
|
+
app = FastAPI()
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = os.getenv("CONFIG_DIR", "/config")
|
|
11
|
+
READ_MODE = os.getenv("READ_MODE", "volume").lower() # 'volume' or 'api'
|
|
12
|
+
CONFIGMAP_NAME = os.getenv("CONFIGMAP_NAME")
|
|
13
|
+
K8S_NAMESPACE = os.getenv("NAMESPACE") or os.getenv("K8S_NAMESPACE")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.get("/config")
|
|
17
|
+
def get_config():
|
|
18
|
+
if READ_MODE == "api":
|
|
19
|
+
data = config_api.read(CONFIGMAP_NAME, K8S_NAMESPACE)
|
|
20
|
+
else:
|
|
21
|
+
try:
|
|
22
|
+
data = config_dir.read(CONFIG_DIR)
|
|
23
|
+
except FileNotFoundError as e:
|
|
24
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
25
|
+
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
raise HTTPException(status_code=500, detail="Invalid config data")
|
|
28
|
+
|
|
29
|
+
status_code_raw = data.get("statusCode")
|
|
30
|
+
body = data.get("body")
|
|
31
|
+
|
|
32
|
+
if status_code_raw is None or body is None:
|
|
33
|
+
raise HTTPException(
|
|
34
|
+
status_code=500, detail="Missing required keys: statusCode or body"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
status_code = int(status_code_raw)
|
|
39
|
+
except Exception:
|
|
40
|
+
raise HTTPException(status_code=500, detail="Invalid statusCode value")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
parsed = json.loads(body)
|
|
44
|
+
return JSONResponse(content=parsed, status_code=status_code)
|
|
45
|
+
except Exception:
|
|
46
|
+
return PlainTextResponse(content=body, status_code=status_code)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.get("/health")
|
|
50
|
+
def health():
|
|
51
|
+
return {"status": "ok"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run():
|
|
55
|
+
|
|
56
|
+
uvicorn.run(
|
|
57
|
+
"app.main:app",
|
|
58
|
+
host="0.0.0.0",
|
|
59
|
+
port=int(os.getenv("PORT", "8000")),
|
|
60
|
+
reload=False,
|
|
61
|
+
)
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI, HTTPException
|
|
2
|
-
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
3
|
-
import os
|
|
4
|
-
import pathlib
|
|
5
|
-
import json
|
|
6
|
-
import uvicorn
|
|
7
|
-
|
|
8
|
-
app = FastAPI()
|
|
9
|
-
|
|
10
|
-
CONFIG_DIR = os.getenv("CONFIG_DIR", "/config")
|
|
11
|
-
READ_MODE = os.getenv("READ_MODE", "volume").lower() # 'volume' or 'api'
|
|
12
|
-
CONFIGMAP_NAME = os.getenv("CONFIGMAP_NAME")
|
|
13
|
-
K8S_NAMESPACE = os.getenv("NAMESPACE") or os.getenv("K8S_NAMESPACE")
|
|
14
|
-
|
|
15
|
-
_k8s_client = None
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _get_k8s_client():
|
|
19
|
-
global _k8s_client
|
|
20
|
-
if _k8s_client is not None:
|
|
21
|
-
return _k8s_client
|
|
22
|
-
try:
|
|
23
|
-
from kubernetes import client, config
|
|
24
|
-
|
|
25
|
-
# In-cluster config first; fallback to local kubeconfig for dev
|
|
26
|
-
try:
|
|
27
|
-
config.load_incluster_config()
|
|
28
|
-
except Exception:
|
|
29
|
-
config.load_kube_config()
|
|
30
|
-
_k8s_client = client.CoreV1Api()
|
|
31
|
-
return _k8s_client
|
|
32
|
-
except Exception as e:
|
|
33
|
-
raise HTTPException(
|
|
34
|
-
status_code=500, detail=f"Failed to init Kubernetes client: {e}"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def read_config_dir() -> dict:
|
|
39
|
-
path = pathlib.Path(CONFIG_DIR)
|
|
40
|
-
if not path.exists() or not path.is_dir():
|
|
41
|
-
raise FileNotFoundError(f"Config directory not found: {CONFIG_DIR}")
|
|
42
|
-
result = {}
|
|
43
|
-
for p in path.iterdir():
|
|
44
|
-
if p.is_file():
|
|
45
|
-
try:
|
|
46
|
-
content = p.read_text(encoding="utf-8")
|
|
47
|
-
except UnicodeDecodeError:
|
|
48
|
-
continue
|
|
49
|
-
result[p.name] = content
|
|
50
|
-
return result
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def read_config_via_api() -> dict:
|
|
54
|
-
if not CONFIGMAP_NAME:
|
|
55
|
-
raise HTTPException(
|
|
56
|
-
status_code=500,
|
|
57
|
-
detail="CONFIGMAP_NAME is not set for API read mode", # noqa: E501
|
|
58
|
-
)
|
|
59
|
-
if not K8S_NAMESPACE:
|
|
60
|
-
raise HTTPException(
|
|
61
|
-
status_code=500, detail="NAMESPACE is not set for API read mode"
|
|
62
|
-
)
|
|
63
|
-
api = _get_k8s_client()
|
|
64
|
-
try:
|
|
65
|
-
cm = api.read_namespaced_config_map(
|
|
66
|
-
name=CONFIGMAP_NAME, namespace=K8S_NAMESPACE
|
|
67
|
-
)
|
|
68
|
-
except Exception as e:
|
|
69
|
-
raise HTTPException(
|
|
70
|
-
status_code=500,
|
|
71
|
-
detail=f"Failed to read ConfigMap {K8S_NAMESPACE}/{CONFIGMAP_NAME}: {e}", # noqa: E501
|
|
72
|
-
)
|
|
73
|
-
data = cm.data or {}
|
|
74
|
-
# Return as filename -> string content just like volume mode
|
|
75
|
-
return dict(data)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@app.get("/config")
|
|
79
|
-
def get_config():
|
|
80
|
-
if READ_MODE == "api":
|
|
81
|
-
data = read_config_via_api()
|
|
82
|
-
else:
|
|
83
|
-
try:
|
|
84
|
-
data = read_config_dir()
|
|
85
|
-
except FileNotFoundError as e:
|
|
86
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
87
|
-
|
|
88
|
-
if not isinstance(data, dict):
|
|
89
|
-
raise HTTPException(status_code=500, detail="Invalid config data")
|
|
90
|
-
|
|
91
|
-
status_code_raw = data.get("statusCode")
|
|
92
|
-
body = data.get("body")
|
|
93
|
-
|
|
94
|
-
if status_code_raw is None or body is None:
|
|
95
|
-
raise HTTPException(
|
|
96
|
-
status_code=500, detail="Missing required keys: statusCode or body"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
status_code = int(status_code_raw)
|
|
101
|
-
except Exception:
|
|
102
|
-
raise HTTPException(status_code=500, detail="Invalid statusCode value")
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
parsed = json.loads(body)
|
|
106
|
-
return JSONResponse(content=parsed, status_code=status_code)
|
|
107
|
-
except Exception:
|
|
108
|
-
return PlainTextResponse(content=body, status_code=status_code)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@app.get("/health")
|
|
112
|
-
def health():
|
|
113
|
-
return {"status": "ok"}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def run():
|
|
117
|
-
|
|
118
|
-
uvicorn.run(
|
|
119
|
-
"app.main:app",
|
|
120
|
-
host="0.0.0.0",
|
|
121
|
-
port=int(os.getenv("PORT", "8000")),
|
|
122
|
-
reload=False,
|
|
123
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|