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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: configmap_reader
3
- Version: 0.1.0
4
- Summary: Kafka Mock Messages Sender
3
+ Version: 0.2.0
4
+ Summary: microservice to read and return content of a configmap
5
5
  License: MIT
6
6
  License-File: LICENSE
7
7
  Keywords: kafka
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "configmap_reader"
3
- version = "0.1.0"
4
- description = "Kafka Mock Messages Sender"
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.1"
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
- )