From 7b8475955d81fe9cc53b302da93176cffc6dee8e Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Thu, 24 Oct 2024 11:54:35 +0300 Subject: [PATCH] feat(api): Add key/value store with namespace emulation For storing pipeline settings and variable and make them a bit more stateful we need reliable key/value store. Signed-off-by: Denys Fedoryshchenko --- api/db.py | 37 ++++++++++++++++++++++++++++++++++++- api/main.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/api/db.py b/api/db.py index ddf231d9..38de4c86 100644 --- a/api/db.py +++ b/api/db.py @@ -10,7 +10,8 @@ from beanie import init_beanie from fastapi_pagination.ext.motor import paginate from motor import motor_asyncio -from kernelci.api.models import Hierarchy, Node, parse_node_obj, EventHistory +from redis import asyncio as aioredis +from kernelci.api.models import EventHistory, Hierarchy, Node, parse_node_obj from .models import User, UserGroup @@ -45,6 +46,8 @@ class Database: def __init__(self, service='mongodb://db:27017', db_name='kernelci'): self._motor = motor_asyncio.AsyncIOMotorClient(service) + # TBD: Make redis host configurable + self._redis = aioredis.from_url('redis://redis:6379') self._db = self._motor[db_name] async def initialize_beanie(self): @@ -60,6 +63,38 @@ def _get_collection(self, model): col = self.COLLECTIONS[model] return self._db[col] + async def get_kv(self, namespace, key): + """ + Get value from redis key-value store + Create a keyname by concatenating namespace and key + """ + keyname = f"{namespace}:{key}" + return await self._redis.get(keyname) + + async def set_kv(self, namespace, key, value): + """ + Set value in redis key-value store + Create a keyname by concatenating namespace and key + """ + keyname = f"{namespace}:{key}" + return await self._redis.set(keyname, value) + + async def del_kv(self, namespace, key): + """ + Delete key from redis key-value store + Create a keyname by concatenating namespace and key + """ + keyname = f"{namespace}:{key}" + return await self._redis.delete(keyname) + + async def exists_kv(self, namespace, key): + """ + Check if key exists in redis key-value store + Create a keyname by concatenating namespace and key + """ + keyname = f"{namespace}:{key}" + return await self._redis.exists(keyname) + async def create_indexes(self): """Create indexes for models""" for model in self.COLLECTIONS: diff --git a/api/main.py b/api/main.py index 7154a71c..d6994d19 100644 --- a/api/main.py +++ b/api/main.py @@ -21,7 +21,8 @@ Request, Form, Header, - Query + Query, + Body, ) from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse from fastapi.security import OAuth2PasswordRequestForm @@ -780,6 +781,47 @@ async def put_nodes( return obj_list +# ----------------------------------------------------------------------------- +# Key/Value namespace enabled store +@app.get('/kv/{namespace}/{key}', response_model=Union[str, None]) +async def get_kv(namespace: str, key: str, + user: User = Depends(get_current_user)): + + """Get a key value pair from the store""" + metrics.add('http_requests_total', 1) + return await db.get_kv(namespace, key) + + +@app.post('/kv/{namespace}/{key}', response_model=Optional[str]) +async def post_kv(namespace: str, key: str, + value: Optional[str] = Body(default=None), + user: User = Depends(get_current_user)): + """Set a key-value pair in the store + namespace and key are part of the URL + value is part of the request body. + If value is not provided, we need to call delete_kv to remove the key. + """ + metrics.add('http_requests_total', 1) + if not value: + await db.del_kv(namespace, key) + return "OK" + ret = await db.set_kv(namespace, key, value) + if ret: + return "OK" + raise HTTPException(status_code=500, detail="Failed to set key-value pair") + + +# Delete a key-value pair from the store +@app.delete('/kv/{namespace}/{key}', response_model=Optional[str]) +async def delete_kv(namespace: str, key: str, + user: User = Depends(get_current_user)): + """Delete a key-value pair from the store""" + metrics.add('http_requests_total', 1) + await db.del_kv(namespace, key) + response = "Key-value pair deleted successfully" + return response + + # ----------------------------------------------------------------------------- # Pub/Sub