Compare commits

...

26 commits

Author SHA1 Message Date
976d739b18
fix: Detach composes 2026-04-14 03:46:05 +02:00
ce74860ca3
fix: Another path fix 2026-04-14 03:44:13 +02:00
3224f8cfca
fix: Absolute path fix 2026-04-14 03:41:44 +02:00
bc592a2c6c
fix: Just another verbosity improvement 2026-04-14 03:38:18 +02:00
64726c56f3
fix: Just another verbosity improvement 2026-04-14 03:37:16 +02:00
88aa334846
fix: repr WoodpeckerEvent?!?! 2026-04-14 03:32:50 +02:00
d5f94e26c6
fix: repr WoodpeckerEvent??!! 2026-04-14 03:29:06 +02:00
53bbf001a0
fix: repr WoodpeckerEvent?? 2026-04-14 03:27:44 +02:00
239e51f0f8
fix: repr WoodpeckerEvent?! 2026-04-14 03:26:17 +02:00
7234560d16
fix: repr WoodpeckerEvent! 2026-04-14 03:24:04 +02:00
8da4bc4aaf
fix: repr WoodpeckerEvent? 2026-04-14 03:21:49 +02:00
7fa243a8d4
fix: repr WoodpeckerEvent 2026-04-14 03:19:07 +02:00
9a8bc44e09
fix: verbosity in ReloadService 2026-04-14 03:16:26 +02:00
4b8ea49c0c
fix: force .mo. reload in reload 2026-04-14 03:12:31 +02:00
08972266db Merge pull request 'feat: reload' (#32) from reload into develop
Reviewed-on: https://hattori.ztsh.eu/iac/karl/pulls/32
2026-04-14 02:50:29 +02:00
da1292a45f
fix: detached head check 2026-04-14 02:46:15 +02:00
a50df466d0
fix: moved services initialization to main 2026-04-14 02:27:31 +02:00
59c5844f5b
fix: api annotations again 2026-04-14 02:12:22 +02:00
cd62e256cf
fix: api annotations 2026-04-14 02:11:12 +02:00
dcba20ee94
fix: fixed import in injects 2026-04-14 02:07:16 +02:00
05931329a3
fix: moved ReloadService to core, fixed import 2026-04-14 02:06:23 +02:00
6ee6341a5e
fix: annotations 2026-04-14 02:03:31 +02:00
8f4dc486ac
fix: inject ReloadService to API class 2026-04-14 01:54:43 +02:00
604381348a
fix: logging in reload 2026-04-14 01:48:42 +02:00
0e19df5c3e
fix: settings location 2026-04-14 01:40:34 +02:00
e0bc04770b
feat: manual reload 2026-04-14 01:27:56 +02:00
10 changed files with 92 additions and 9 deletions

2
.gitignore vendored
View file

@ -14,6 +14,6 @@ uv.lock
**/*.kdbx* **/*.kdbx*
.compose_repository .compose_repository
__pycache__/ deployment/
**/dist/ **/dist/
**/*.log **/*.log

View file

@ -8,8 +8,7 @@ from starlette.responses import JSONResponse, Response
from karl.api.models import Request from karl.api.models import Request
from karl.core.injects import AutowireSupport from karl.core.injects import AutowireSupport
from karl.core.woodpecker import Woodpecker from karl.model.webhook import WoodpeckerEvent, ReloadEvent
from karl.model.webhook import WoodpeckerEvent
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,7 +20,6 @@ async def root():
@cbv(router) @cbv(router)
class APIv1: class APIv1:
woodpecker: Woodpecker = Depends(AutowireSupport.woodpecker)
bus: EventBus = Depends(AutowireSupport.bus) bus: EventBus = Depends(AutowireSupport.bus)
def __init__(self): def __init__(self):
@ -39,3 +37,10 @@ class APIv1:
async def ci(self, request: Request): async def ci(self, request: Request):
await self.bus.dispatch(mapper.map(request)) await self.bus.dispatch(mapper.map(request))
return Response(status_code=201) return Response(status_code=201)
@router.get("/reload", summary="Manual service reload")
async def reload(self, service: str = None) -> Response:
if service is None:
return Response(status_code=400)
await self.bus.dispatch(ReloadEvent(service=service))
return Response(status_code=201)

View file

@ -51,4 +51,8 @@ class Settings(BaseSettings):
@lru_cache @lru_cache
def get_settings() -> Settings: def get_settings() -> Settings:
return Settings.from_yaml() paths = ['deployment/config.yaml', 'config/config.yaml']
for path in paths:
if Path(path).exists():
return Settings.from_yaml(path)
raise Exception("Config file not found")

View file

@ -1,6 +1,7 @@
from bubus import EventBus from bubus import EventBus
from injectable import inject from injectable import inject
from karl.core.reload import ReloadService
from karl.core.woodpecker import Woodpecker from karl.core.woodpecker import Woodpecker
@ -10,6 +11,10 @@ class AutowireSupport:
def woodpecker(): def woodpecker():
return inject(Woodpecker) return inject(Woodpecker)
@staticmethod
def reload():
return inject(ReloadService)
@staticmethod @staticmethod
def bus(): def bus():
return inject(EventBus) return inject(EventBus)

48
src/karl/core/reload.py Normal file
View file

@ -0,0 +1,48 @@
import logging
from datetime import datetime
from pathlib import Path
from typing import Annotated
from bubus import EventBus
from injectable import injectable, autowired, Autowired
from karl import get_settings
from model.webhook import ReloadEvent, WoodpeckerEvent
from services import GitService
logger = logging.getLogger(__name__)
@injectable(singleton=True)
class ReloadService:
@autowired
def __init__(self, bus: Annotated[EventBus, Autowired]):
self._bus = bus
self._git = GitService()
bus.on(ReloadEvent, self.on_reload)
self.root_path = get_settings().git.path
logger.info("ReloadService initialized.")
async def on_reload(self, event: ReloadEvent):
try:
logger.info(f"Received ReloadEvent: {event.service}")
head = self._git.get_head()
file_path = Path(self.root_path) / f"files/{event.service}"
if not file_path.exists():
raise Exception(f"Service {event.service} not found: {file_path.absolute()} does not exist.")
logger.debug(f"Found service files at {file_path}: {', '.join([str(f) for f in list(file_path.iterdir())])}")
mos = list(file_path.glob('*.mo.*'))
logger.debug(f"Found {len(mos)} .mo files")
we = WoodpeckerEvent(
_id=-1,
commit=head.sha,
ref=head.branch,
message=f"Manual reload of {event.service}",
started=int(datetime.now().timestamp()),
files=[f"compose/{event.service}/docker-compose.yml"] + [str(pp).replace(str(self.root_path), '') for pp in mos]
)
logger.debug(f"Sending <WoodpeckerEvent commit={we.commit} ref={we.ref} message={we.message} started={we.started} files={we.files}")
await self._bus.dispatch(we)
except Exception as e:
logger.error(f"Reload error: {e}", exc_info=True)

View file

@ -3,6 +3,7 @@ import logging
from fastapi import FastAPI from fastapi import FastAPI
from injectable import load_injection_container from injectable import load_injection_container
from core.injects import AutowireSupport
from karl.config import get_settings from karl.config import get_settings
from karl.util.logging import HandlerFactory from karl.util.logging import HandlerFactory
@ -15,7 +16,7 @@ class KarlApplication:
_app = FastAPI(title="Karl", version="0.1.0") _app = FastAPI(title="Karl", version="0.1.0")
self._set_middlewares(_app) self._set_middlewares(_app)
self._set_routes(_app) self._set_routes(_app)
self._set_events(_app) self._init_services(_app)
self._app = _app self._app = _app
@ -55,8 +56,9 @@ class KarlApplication:
app.include_router(api_v1_router, prefix="/api/v1", tags=["v1"]) app.include_router(api_v1_router, prefix="/api/v1", tags=["v1"])
pass pass
def _set_events(self, app: FastAPI): def _init_services(self, app: FastAPI):
pass AutowireSupport.reload()
AutowireSupport.woodpecker()
def run(): def run():

7
src/karl/model/vcs.py Normal file
View file

@ -0,0 +1,7 @@
from dataclasses import dataclass
@dataclass
class Head:
sha: str
branch: str

View file

@ -10,3 +10,6 @@ class WoodpeckerEvent(BaseEvent):
message: str message: str
started: int started: int
files: List[str] files: List[str]
class ReloadEvent(BaseEvent):
service: str

View file

@ -17,4 +17,4 @@ class DockerService:
os.chdir(compose_path.parent) os.chdir(compose_path.parent)
self._client.compose.ps() self._client.compose.ps()
self._client.compose.down(remove_orphans=True) self._client.compose.down(remove_orphans=True)
self._client.compose.up() self._client.compose.up(detach=True)

View file

@ -2,6 +2,7 @@ from git import Repo, Remote
from injectable import injectable from injectable import injectable
from karl.config import GitConfig, get_settings from karl.config import GitConfig, get_settings
from model.vcs import Head
@injectable(singleton=True) @injectable(singleton=True)
@ -27,3 +28,11 @@ class GitService:
def checkout(self, sha: str): def checkout(self, sha: str):
self._origin.fetch() self._origin.fetch()
self._repo.git.checkout(sha) self._repo.git.checkout(sha)
def get_head(self) -> Head:
if self._repo.head.is_detached:
return Head(self._repo.head.object.hexsha, "detached")
return Head(
self._repo.active_branch.commit.hexsha,
self._repo.active_branch.name
)