diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..daf4f93 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,62 @@ +# JB Toolkit +.idea + +# Git +.git +.gitignore +.gitattributes + +# CI +.woodpecker/ + +# Virtual env +.venv/ + +# Docker +docker-compose.yml +Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +target/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Logs +**/*.log + +# Documentation +docs/_build/ + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# Project specific +.compose_repository/ +config/ +tests/ diff --git a/.editorconfig b/.editorconfig index 46daf4a..0aca624 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ trim_trailing_whitespace = true [*.py] indent_size = 2 + +[*.yaml] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 4b259c4..52364cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv .idea *.iml uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.woodpecker/dev.yaml b/.woodpecker/dev.yaml new file mode 100644 index 0000000..2ff2fdc --- /dev/null +++ b/.woodpecker/dev.yaml @@ -0,0 +1,14 @@ +steps: + - name: build + image: woodpeckerci/plugin-docker-buildx:5.2.2 + settings: + platforms: linux/amd64 + repo: hattori.ztsh.eu/iac/karl + registry: hattori.ztsh.eu + tags: dev-${CI_PIPELINE_NUMBER} + username: stawros + password: + from_secret: hattori-packages + when: + - event: pull_request + evaluate: 'CI_COMMIT_PULL_REQUEST_LABELS contains "ci-ready"' diff --git a/.woodpecker/latest.yaml b/.woodpecker/latest.yaml new file mode 100644 index 0000000..123d5aa --- /dev/null +++ b/.woodpecker/latest.yaml @@ -0,0 +1,14 @@ +steps: + - name: build + image: woodpeckerci/plugin-docker-buildx:5.2.2 + settings: + platforms: linux/amd64 + repo: hattori.ztsh.eu/iac/karl + registry: hattori.ztsh.eu + tags: latest + username: stawros + password: + from_secret: hattori-packages + when: + - event: [ tag, push, manual ] + branch: master diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d10579b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM ghcr.io/astral-sh/uv:0.9-python3.12-alpine AS builder + +WORKDIR /app + +RUN apk update \ + && apk add gcc python3-dev musl-dev linux-headers + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --no-install-workspace + +ADD . /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked + +FROM python:3.12-alpine3.22 + +RUN apk update --no-cache \ + && apk add --no-cache git + +COPY --from=builder --chown=app:app /app/.venv /app/.venv +COPY --from=builder --chown=app:app /app/src /app/src + +ENV PYTHONPATH="/app" + +EXPOSE 8081 + +WORKDIR /app + +ENTRYPOINT ["/app/.venv/bin/python"] + +CMD ["/app/src/karl/__init__.py"] diff --git a/app/api/__init__.py b/README.md similarity index 100% rename from app/api/__init__.py rename to README.md diff --git a/config/config.yaml b/config/config.yaml index c4fc44a..e895bb7 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,6 +1,6 @@ logging: level: "TRACE" - path: "logs/karl.log" + path: "../../logs/karl.log" app: host: "127.0.0.1" port: 8081 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ef1b364 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,22 @@ +services: + karl: + image: hattori.ztsh.eu/iac/karl:dev-36 + container_name: karl + restart: always + networks: + - web + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock + - ./data/config:/app/config + - ./data/logs:/app/logs + secrets: + - kp_secret + +secrets: + kp_secret: + file: ./data/kp_secret + +networks: + web: + external: true diff --git a/pyproject.toml b/pyproject.toml index ebe74fb..fa843df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Because name 'Jenkins' was already taken. Greatest composer ever. readme = "README.md" requires-python = ">=3.12" authors = [{ name = "Piotr Dec" }] + dependencies = [ "fastapi>=0.119.0", "uvicorn[standard]>=0.30.0", @@ -18,6 +19,7 @@ dependencies = [ "py-automapper>=2.2.0", "fastapi-utils>=0.8.0", "keyring>=25.6.0", + "keyring-backend>=0.1.0", ] [dependency-groups] @@ -31,11 +33,11 @@ dev = [ ] [project.scripts] -app = "app.main:run" +karl = "karl.main:run" -[tool.uv] -# uv automatycznie wykrywa dependencies z [project] -# Możesz dodać tu własne ustawienia cache/mirrors, jeśli potrzebne. +[build-system] +requires = ["uv_build>=0.8.23,<0.9.0"] +build-backend = "uv_build" [tool.ruff] line-length = 120 diff --git a/src/karl/__init__.py b/src/karl/__init__.py new file mode 100644 index 0000000..5dec085 --- /dev/null +++ b/src/karl/__init__.py @@ -0,0 +1,19 @@ +from config import get_settings + + +def main() -> None: + import uvicorn + + settings = get_settings() + uvicorn.run( + "karl.main:run", + factory=True, + host=settings.app.host, + port=settings.app.port, + reload=settings.app.reload, + log_config=None, + ) + + +if __name__ == "__main__": + main() diff --git a/app/core/__init__.py b/src/karl/api/__init__.py similarity index 100% rename from app/core/__init__.py rename to src/karl/api/__init__.py diff --git a/app/api/models.py b/src/karl/api/models.py similarity index 100% rename from app/api/models.py rename to src/karl/api/models.py diff --git a/app/api/v1.py b/src/karl/api/v1.py similarity index 85% rename from app/api/v1.py rename to src/karl/api/v1.py index a012834..9bbbfbc 100644 --- a/app/api/v1.py +++ b/src/karl/api/v1.py @@ -3,10 +3,10 @@ from fastapi import APIRouter, Depends from fastapi_utils.cbv import cbv from starlette.responses import JSONResponse, Response -from app.api.models import Request -from app.core.injects import AutowireSupport -from app.core.woodpecker import Woodpecker -from app.model.webhook import WoodpeckerEvent +from api.models import Request +from core.injects import AutowireSupport +from core.woodpecker import Woodpecker +from model.webhook import WoodpeckerEvent router = APIRouter() diff --git a/app/config/__init__.py b/src/karl/config/__init__.py similarity index 100% rename from app/config/__init__.py rename to src/karl/config/__init__.py diff --git a/app/config/settings.py b/src/karl/config/settings.py similarity index 87% rename from app/config/settings.py rename to src/karl/config/settings.py index 2017c14..79e66c9 100644 --- a/app/config/settings.py +++ b/src/karl/config/settings.py @@ -8,13 +8,13 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class LoggingConfig(BaseModel): level: str = "INFO" - path: Path = Path("logs/karl.log") + path: Path | None = None class AppConfig(BaseModel): host: str = "127.0.0.1" port: int = 8081 - reload: bool = True + reload: bool = False class GitConfig(BaseModel): @@ -43,6 +43,9 @@ class Settings(BaseSettings): if p.exists(): with p.open("r", encoding="utf-8") as fh: data = yaml.safe_load(fh) or {} + else: + import sys, os + sys.stderr.write(f"Warning: Config file {os.path.realpath(p)} not found.\n") return cls(**data) diff --git a/app/model/__init__.py b/src/karl/core/__init__.py similarity index 100% rename from app/model/__init__.py rename to src/karl/core/__init__.py diff --git a/app/core/injects.py b/src/karl/core/injects.py similarity index 74% rename from app/core/injects.py rename to src/karl/core/injects.py index 115ae95..623fa59 100644 --- a/app/core/injects.py +++ b/src/karl/core/injects.py @@ -1,6 +1,6 @@ from injectable import inject -from app.core.woodpecker import Woodpecker +from core.woodpecker import Woodpecker class AutowireSupport: diff --git a/app/core/router.py b/src/karl/core/router.py similarity index 100% rename from app/core/router.py rename to src/karl/core/router.py diff --git a/app/core/woodpecker.py b/src/karl/core/woodpecker.py similarity index 95% rename from app/core/woodpecker.py rename to src/karl/core/woodpecker.py index 1892c0b..2b66014 100644 --- a/app/core/woodpecker.py +++ b/src/karl/core/woodpecker.py @@ -6,10 +6,10 @@ from typing import Annotated from injectable import injectable, Autowired, autowired -from app.config import get_settings -from app.model.webhook import WoodpeckerEvent -from app.services import GitService, DockerService -from app.services.mo import Mo +from config import get_settings +from model.webhook import WoodpeckerEvent +from services import GitService, DockerService +from services.mo import Mo logger = logging.getLogger(__name__) diff --git a/app/main.py b/src/karl/main.py similarity index 78% rename from app/main.py rename to src/karl/main.py index 669d2d4..dbd5033 100644 --- a/app/main.py +++ b/src/karl/main.py @@ -3,8 +3,8 @@ import logging from fastapi import FastAPI from injectable import load_injection_container -from app.config import get_settings -from app.util.logging import HandlerFactory +from config import get_settings +from util.logging import HandlerFactory class KarlApplication: @@ -45,13 +45,13 @@ class KarlApplication: logging_logger.propagate = False def _set_middlewares(self, app: FastAPI): - from app.web.middlewares import LoggingMiddleware + from web.middlewares import LoggingMiddleware app.add_middleware(LoggingMiddleware) def _set_routes(self, app: FastAPI): - from app.core.router import router as core_router + from core.router import router as core_router app.include_router(core_router) - from app.api.v1 import router as api_v1_router + from api.v1 import router as api_v1_router app.include_router(api_v1_router, prefix="/api/v1", tags=["v1"]) pass @@ -61,17 +61,3 @@ class KarlApplication: def run(): return KarlApplication() - - -if __name__ == "__main__": - import uvicorn - - settings = get_settings() - uvicorn.run( - "app.main:run", - factory=True, - host=settings.app.host, - port=settings.app.port, - reload=settings.app.reload, - log_config=None, - ) diff --git a/app/util/__init__.py b/src/karl/model/__init__.py similarity index 100% rename from app/util/__init__.py rename to src/karl/model/__init__.py diff --git a/app/model/containers.py b/src/karl/model/containers.py similarity index 100% rename from app/model/containers.py rename to src/karl/model/containers.py diff --git a/app/model/healthcheck.py b/src/karl/model/healthcheck.py similarity index 100% rename from app/model/healthcheck.py rename to src/karl/model/healthcheck.py diff --git a/app/model/passwords.py b/src/karl/model/passwords.py similarity index 100% rename from app/model/passwords.py rename to src/karl/model/passwords.py diff --git a/app/model/webhook.py b/src/karl/model/webhook.py similarity index 100% rename from app/model/webhook.py rename to src/karl/model/webhook.py diff --git a/app/services/__init__.py b/src/karl/services/__init__.py similarity index 100% rename from app/services/__init__.py rename to src/karl/services/__init__.py diff --git a/app/services/containers.py b/src/karl/services/containers.py similarity index 94% rename from app/services/containers.py rename to src/karl/services/containers.py index a08e8c2..8803913 100644 --- a/app/services/containers.py +++ b/src/karl/services/containers.py @@ -5,7 +5,7 @@ import docker from docker.models.containers import Container from injectable import injectable -from app.model.containers import Tree, Compose, SimpleContainer +from model.containers import Tree, Compose, SimpleContainer logger = logging.getLogger(__name__) @@ -36,6 +36,7 @@ class DockerService: return self._tree def reload(self, compose_path: Path): + # TODO: Won't work in docker container cmd = ["sudo", "docker", "compose", "-f", str(compose_path), "up", "-d"] import subprocess try: diff --git a/app/services/mo.py b/src/karl/services/mo.py similarity index 95% rename from app/services/mo.py rename to src/karl/services/mo.py index 84792f7..9d5756e 100644 --- a/app/services/mo.py +++ b/src/karl/services/mo.py @@ -4,7 +4,7 @@ from typing import Annotated from injectable import injectable, autowired, Autowired -from app.services import Passwords +from services import Passwords class ValueTemplate(Template): diff --git a/app/services/passwords.py b/src/karl/services/passwords.py similarity index 94% rename from app/services/passwords.py rename to src/karl/services/passwords.py index e851d4e..a76dac8 100644 --- a/app/services/passwords.py +++ b/src/karl/services/passwords.py @@ -34,8 +34,10 @@ class KeyRequest: @injectable(singleton=True) class Passwords: def __init__(self): - from app.config import get_settings + from config import get_settings settings = get_settings() + import keyring_backend + keyring.set_keyring(keyring=keyring_backend.Backend()) with open(settings.kp.secret, "r") as fh: keyring.set_password("karl", "kp", fh.read().splitlines()[0]) diff --git a/app/services/system.py b/src/karl/services/system.py similarity index 100% rename from app/services/system.py rename to src/karl/services/system.py diff --git a/app/services/vcs.py b/src/karl/services/vcs.py similarity index 94% rename from app/services/vcs.py rename to src/karl/services/vcs.py index 0e97ec8..8796787 100644 --- a/app/services/vcs.py +++ b/src/karl/services/vcs.py @@ -1,7 +1,7 @@ from git import Repo, Remote from injectable import injectable -from app.config import GitConfig, get_settings +from config import GitConfig, get_settings @injectable(singleton=True) diff --git a/app/templates/index.html b/src/karl/templates/index.html similarity index 100% rename from app/templates/index.html rename to src/karl/templates/index.html diff --git a/app/web/__init__.py b/src/karl/util/__init__.py similarity index 100% rename from app/web/__init__.py rename to src/karl/util/__init__.py diff --git a/app/util/dicts.py b/src/karl/util/dicts.py similarity index 100% rename from app/util/dicts.py rename to src/karl/util/dicts.py diff --git a/app/util/logging.py b/src/karl/util/logging.py similarity index 90% rename from app/util/logging.py rename to src/karl/util/logging.py index 3e9e77a..f25ed4e 100644 --- a/app/util/logging.py +++ b/src/karl/util/logging.py @@ -67,6 +67,12 @@ class HandlerFactory: return handler def file_handler(prefix: str = ''): + if not file_path: + import sys + sys.stderr.write("No file path specified, skipping file logging...\n") + return None + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch(exist_ok=True) handler = TimedRotatingFileHandler(file_path, when='midnight', backupCount=30) handler.setFormatter(ApplicationFormatter(prefix)) handler.setLevel('TRACE') @@ -83,4 +89,5 @@ class HandlerFactory: handlers.append(console_handler(handler_prefix)) case _: raise ValueError(f"Unknown target: {target}") + handlers = [h for h in handlers if h is not None] return handlers diff --git a/src/karl/web/__init__.py b/src/karl/web/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/web/middlewares.py b/src/karl/web/middlewares.py similarity index 100% rename from app/web/middlewares.py rename to src/karl/web/middlewares.py diff --git a/tests/test_mo.py b/tests/test_mo.py index 01e63c0..b83529a 100644 --- a/tests/test_mo.py +++ b/tests/test_mo.py @@ -3,8 +3,8 @@ from pathlib import Path import pytest import yaml -from app.services import Passwords -from app.services.mo import Mo +from karl.services import Passwords +from karl.services.mo import Mo @pytest.fixture(scope='class')