Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
5f7ebd5
refactor: add debug prints
antidodo Mar 11, 2026
df0b2ac
refactor: add debug prints
antidodo Mar 11, 2026
aa0f91e
feat: propagate partner node statuses and log stop endpoint
Nightknight3000 Mar 12, 2026
4a48ecf
fix: set node ids to str
Nightknight3000 Mar 13, 2026
e9b0387
fix: add control print for nod status update to analysis
Nightknight3000 Mar 16, 2026
e4303e1
refactor: add error handling for partner node status updates
antidodo Mar 17, 2026
28ec410
refactor: remove debug prints
antidodo Mar 17, 2026
e002f0f
Merge branch 'main' into canary
antidodo Mar 31, 2026
4cfae06
Merge branch 'main' into canary
Nightknight3000 Apr 10, 2026
9d1a32a
feat: implement internal po logging
Nightknight3000 Apr 10, 2026
1cbc574
fix: error in custom log level
Nightknight3000 Apr 10, 2026
9b0d775
fix: numeric values of custom levels
Nightknight3000 Apr 10, 2026
5b29b04
feat: add JSON formatter for structured logging in po_logging.py
antidodo Apr 13, 2026
7b4867e
fix: add delay in status loop to prevent busy waiting
antidodo Apr 13, 2026
0a25132
fix: disable logging configuration in uvicorn so its also uses the de…
antidodo Apr 13, 2026
0b53bac
fix: update dependencies to latest versions in pyproject.toml
antidodo Apr 13, 2026
ed02e91
test: add unit tests
antidodo Apr 13, 2026
5520628
fix: escape dollar sign change because of deprecation
antidodo Apr 13, 2026
b848730
fix: use environment variable for NGINX image version with a default …
antidodo Apr 13, 2026
a119915
fix: enhance code base
antidodo Apr 14, 2026
fd3b37d
fix: update test cases for analysis and log entities to include new p…
antidodo Apr 14, 2026
a1f92ea
fix: improve pod log retrieval logic and validate UUIDs in analysis c…
antidodo Apr 14, 2026
11cc3f3
fix: optimize resource retrieval by removing unnecessary list convers…
antidodo Apr 14, 2026
d6ff04b
refactor: validate hub client init params and remove unused data sour…
antidodo Apr 16, 2026
72bd48d
docs: enhance docstrings across multiple modules for clarity and comp…
antidodo Apr 16, 2026
e88ad1d
fix: avoid failure on slow start
Nightknight3000 Apr 21, 2026
4bb19e1
fix: zombie creation after stuck state
Nightknight3000 Apr 21, 2026
9be9a4a
fix: consider firmly stuck analyses as failed
Nightknight3000 Apr 21, 2026
4d0e318
feat: bump po version to 0.5.0
Nightknight3000 Apr 21, 2026
435b57c
Merge branch 'refs/heads/main' into canary
antidodo May 5, 2026
a2ba75a
feat: ensure connectivity with hub if necessary in api endpoints
Nightknight3000 May 5, 2026
44519b5
feat: cleanup of running analyses deleted at hub
Nightknight3000 May 5, 2026
a8b7b1b
refactor: reorganize load functions in main
Nightknight3000 May 5, 2026
a844d63
refactor: improve status loop logging
Nightknight3000 May 5, 2026
65cab2c
fix: allow stopped as end status
Nightknight3000 May 5, 2026
d4057ae
feat: unsubscribe analyses from message-broker
Nightknight3000 May 5, 2026
6117d9f
fix: regenerate tokens during analysis stop process
Nightknight3000 May 6, 2026
1477caf
refactor: imports and constant
Nightknight3000 May 6, 2026
239f24e
feat: delete malformed analyses and apply clean up at loop end
Nightknight3000 May 6, 2026
2b5174f
refactor: init hub client naming
Nightknight3000 May 7, 2026
570f935
refactor: add status control prints
Nightknight3000 May 7, 2026
e8d6167
refactor: set po logs to debug mode
Nightknight3000 May 7, 2026
55bdb42
refactor: remove debug logs
Nightknight3000 May 7, 2026
cdb30c7
fix: correctly map internal stopped status
Nightknight3000 May 7, 2026
250b8c8
chore: update dependencies to latest versions in poetry.lock
antidodo May 11, 2026
c74a2e0
fix: temporarily disable analysis stop functionality for debugging
antidodo May 12, 2026
c349dcc
fix: comment out resource cleanup for debugging purposes
antidodo May 12, 2026
3d8385e
fix: raise condition of db model update after deployment, triggering …
antidodo May 12, 2026
038c238
refactor: ident
antidodo May 12, 2026
8fc9c4b
fix: update pod_ids
antidodo May 12, 2026
c983c61
fix: replace hub cleanup criteria with id instead of status
antidodo May 12, 2026
b09f8f3
fix: comment out hub validation for debugging purposes
antidodo May 12, 2026
293bb79
fix: clean_up_the_rest removed for debuging
antidodo May 12, 2026
672e864
refactor: add looging for clean up the rest
antidodo May 13, 2026
61dc5fe
fix: remove none cases log
antidodo May 13, 2026
1485ce2
fix: remove none cases log
antidodo May 13, 2026
19163e6
fix: extract analysis id from deployment name fix
antidodo May 13, 2026
fbbbc24
fix: reintroduce cleanup in status loop
antidodo May 13, 2026
4c6af92
refactor: ident
antidodo May 13, 2026
8d00023
fix: update Tests
antidodo May 13, 2026
6fdd8b2
feat: version bump to 0.5.5
antidodo May 13, 2026
56bec44
feat: address code rabbit review
antidodo May 13, 2026
d34319a
fix: update Tests
antidodo May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 92 additions & 92 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "Node-Pod-Orchestration"
version = "0.5.1"
version = "0.5.5"
description = ""
authors = ["Alexander Röhl <alexander.roehl@uni-tuebingen.de>", "David Hieber <david.hieber@uni-tuebingen.de>"]
license = "Apache 2.0"
Expand Down
90 changes: 64 additions & 26 deletions src/api/api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import time
import uvicorn
import os
from typing import Optional
import threading
from fastapi import APIRouter, FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from src.utils.hub_client import init_hub_client_with_client, get_node_id_by_client
from src.utils.hub_client import init_hub_client, get_node_id_by_client
from src.utils.other import extract_hub_envs
from src.api.oauth import valid_access_token
from src.resources.database.entity import Database
Expand All @@ -24,6 +25,7 @@

logger = get_logger()


class PodOrchestrationAPI:
"""FastAPI application exposing the Pod Orchestration REST endpoints.

Expand All @@ -49,16 +51,11 @@ def __init__(self, database: Database, namespace: str = 'default'):
"""
self.database = database

client_id, client_secret, hub_url_core, hub_auth, enable_hub_logging, http_proxy, https_proxy = extract_hub_envs()
self.node_id = None
self.hub_client = None
self.enable_hub_logging = None
self._set_node_id_and_hub_client(max_attempts=100)

self.enable_hub_logging = enable_hub_logging
self.hub_client = init_hub_client_with_client(client_id,
client_secret,
hub_url_core,
hub_auth,
http_proxy,
https_proxy)
self.node_id = get_node_id_by_client(self.hub_client, client_id) if self.hub_client else None
self.namespace = namespace
app = FastAPI(title="FLAME PO",
docs_url="/api/docs",
Expand Down Expand Up @@ -329,12 +326,16 @@ def stop_all_analysis_call(self):
"""
try:
response = stop_analysis('all', self.database)
for analysis_id in self.database.get_analysis_ids():
stream_logs(AnalysisStoppedLog(analysis_id),
self.node_id,
self.enable_hub_logging,
self.database,
self.hub_client)
self._set_node_id_and_hub_client(max_attempts=5)
if self.node_id is not None:
for analysis_id in self.database.get_analysis_ids():
stream_logs(AnalysisStoppedLog(analysis_id),
self.node_id,
self.enable_hub_logging,
self.database,
self.hub_client)
else:
logger.warning(f"Couldn't forward logs for stopped analyses.")
return response
except Exception as e:
logger.error(f"Error stopping ALL analyzes: {repr(e)}")
Expand All @@ -354,11 +355,15 @@ def stop_analysis_call(self, analysis_id: str):
"""
try:
response = stop_analysis(analysis_id, self.database)
stream_logs(AnalysisStoppedLog(analysis_id),
self.node_id,
self.enable_hub_logging,
self.database,
self.hub_client)
self._set_node_id_and_hub_client(max_attempts=5)
if self.node_id is not None:
stream_logs(AnalysisStoppedLog(analysis_id),
self.node_id,
self.enable_hub_logging,
self.database,
self.hub_client)
else:
logger.warning(f"Couldn't forward logs for stopped analysis.")
return response
except Exception as e:
logger.error(f"Error stopping analysis: {repr(e)}")
Expand Down Expand Up @@ -414,7 +419,11 @@ def cleanup_call(self, cleanup_type: str):
HTTPException: 500 on any downstream failure (details in logs).
"""
try:
return cleanup(cleanup_type, self.database, self.namespace)
self._set_node_id_and_hub_client(max_attempts=5)
return cleanup(cleanup_type,
self.database,
self.hub_client,
self.namespace)
Comment on lines +422 to +426

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

cleanup_call forwards self.hub_client even when it is None.

When _set_node_id_and_hub_client(max_attempts=5) fails to resolve a client, self.hub_client stays None and is passed to cleanup(...). Combined with the destructive behavior in clean_up_the_rest (see comment in src/resources/utils.py), this means a DELETE /po/cleanup/... call during a Hub outage will run zombie cleanup with hub validation disabled (only a warning is logged). Consider failing fast with a 503 here when cleanup_type is 'all' or 'zombies' and hub_client is None, so an operator-triggered cleanup doesn't quietly skip the safety net.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/api.py` around lines 422 - 426, The code calls
self._set_node_id_and_hub_client(...) and then forwards self.hub_client into
cleanup(...) even when it remains None, which allows destructive cleanup paths
to run without Hub validation; after calling
_set_node_id_and_hub_client(max_attempts=5) in the method that returns
cleanup(cleanup_type,...), add a guard: if self.hub_client is None and
cleanup_type in ('all','zombies') then fail fast with a 503-style error (raise
the appropriate HTTP exception or return a 503 response) instead of invoking
cleanup; reference _set_node_id_and_hub_client, cleanup, cleanup_type,
self.hub_client and the destructive path in clean_up_the_rest when implementing
the check.

except Exception as e:
logger.error(f"Error cleaning up: {repr(e)}")
raise HTTPException(status_code=500, detail=f"Error cleaning up (see po logs).")
Expand All @@ -433,9 +442,14 @@ def stream_logs_call(self, body: CreateLogEntity):
HTTPException: 500 on any downstream failure (details in logs).
"""
try:
return stream_logs(body, self.node_id, self.enable_hub_logging, self.database, self.hub_client)
except Exception as e:
logger.error(f"Error streaming logs: {repr(e)}")
self._set_node_id_and_hub_client(max_attempts=5)
if self.node_id is not None:
return stream_logs(body, self.node_id, self.enable_hub_logging, self.database, self.hub_client)
else:
raise Exception
except Exception:
logger.error(f"Error streaming logs: node_id={self.node_id}, "
f"hub_client={self.hub_client}.")
raise HTTPException(status_code=500, detail=f"Error streaming logs (see po logs).")

def health_call(self):
Expand All @@ -452,3 +466,27 @@ def health_call(self):
raise RuntimeError("Main thread is not alive.")
else:
return {'status': "ok"}

def _set_node_id_and_hub_client(self, max_attempts: Optional[int] = None) -> None:
current_attempt = 1
client_id, client_secret, hub_url_core, hub_auth, enable_hub_logging, http_proxy, https_proxy = extract_hub_envs()
if (self.hub_client is None) or (get_node_id_by_client(self.hub_client, client_id) is None):
self.node_id = None
while self.node_id is None:
self.enable_hub_logging = enable_hub_logging
self.hub_client = init_hub_client(client_id,
client_secret,
hub_url_core,
hub_auth,
http_proxy,
https_proxy)
self.node_id = get_node_id_by_client(self.hub_client, client_id) if self.hub_client else None

if (max_attempts is not None) and (current_attempt >= max_attempts):
break
else:
current_attempt += 1
if current_attempt % 60 == 59:
logger.warning(f"Unable to connect to Hub, attempt {current_attempt}"
f"{'/' + str(max_attempts) if max_attempts is not None else ''}.")
time.sleep(1)
12 changes: 5 additions & 7 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import os
from threading import Thread

from dotenv import load_dotenv, find_dotenv
# load env
load_dotenv(find_dotenv())


from src.resources.database.entity import Database
from src.api.api import PodOrchestrationAPI
Expand All @@ -13,7 +9,12 @@
from src.utils.po_logging import get_logger


# connect logger
logger = get_logger()
# load cluster config
load_cluster_config()
# load env
load_dotenv(find_dotenv())


def main():
Expand All @@ -23,9 +24,6 @@ def main():
spawns the FastAPI server in a background thread, and starts the blocking
status monitoring loop on the main thread.
"""
# load cluster config
load_cluster_config()

if not os.getenv('NGINX_IMAGE'):
logger.warning("Environment variable 'NGINX_IMAGE' is not set, defaulting to 'nginx:1.29.8'.")

Expand Down
17 changes: 11 additions & 6 deletions src/resources/analysis/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from src.resources.database.db_models import AnalysisDB
from src.resources.database.entity import Database
from src.status.constants import AnalysisStatus
from src.utils.mb_client import delete_subscription


class Analysis(BaseModel):
Expand Down Expand Up @@ -55,15 +56,10 @@ def start(self, database: Database, namespace: str = 'default') -> None:
self.analysis_config['PROJECT_ID'] = self.project_id
self.analysis_config['DEPLOYMENT_NAME'] = self.deployment_name
self.namespace = namespace
self.pod_ids = create_analysis_deployment(name=self.deployment_name,
image=self.image_url,
env=self.analysis_config,
namespace=namespace)

database.create_analysis(analysis_id=self.analysis_id,
deployment_name=self.deployment_name,
project_id=self.project_id,
pod_ids=self.pod_ids,
pod_ids=None,
status=self.status,
log=self.log,
registry_url=self.registry_url,
Expand All @@ -74,6 +70,12 @@ def start(self, database: Database, namespace: str = 'default') -> None:
kong_token=self.kong_token,
restart_counter=self.restart_counter,
progress=self.progress)
self.pod_ids = create_analysis_deployment(name=self.deployment_name,
image=self.image_url,
env=self.analysis_config,
namespace=namespace)

database.update_deployment(self.deployment_name, pod_ids=json.dumps(self.pod_ids))

def stop(self,
database: Database,
Expand All @@ -94,6 +96,9 @@ def stop(self,
# Update the database
database.update_deployment(self.deployment_name, status=self.status)
database.update_deployment(self.deployment_name, log=self.log)
# end message-broker subscription
self.tokens = create_analysis_tokens(kong_token=self.kong_token, analysis_id=self.analysis_id)
delete_subscription(self.analysis_id, self.tokens['KEYCLOAK_TOKEN'])
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def read_db_analysis(analysis: AnalysisDB) -> Analysis:
Expand Down
8 changes: 6 additions & 2 deletions src/resources/log/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from pydantic import BaseModel

from src.status.constants import _MAX_RESTARTS
from src.status.constants import _MAX_RESTARTS, AnalysisStatus


class LogEntity(BaseModel):
Expand Down Expand Up @@ -99,4 +99,8 @@ def __init__(self, analysis_id: str) -> None:
log = (f"[flame -- POAPI: ANALYSISSTOPPED -- "
f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}] "
f"Info: The analysis was stopped either locally, or externally on another node.")
super().__init__(log=log, log_type="info", analysis_id=analysis_id, status="stopped", progress=0)
super().__init__(log=log,
log_type="info",
analysis_id=analysis_id,
status=AnalysisStatus.STOPPED.value,
progress=0)
Loading
Loading