Merge pull request 'CI: docker image & woodpecker' (#13) from dockerify into develop
Reviewed-on: https://hattori.ztsh.eu/iac/karl/pulls/13
This commit is contained in:
commit
846f22b8e1
40 changed files with 220 additions and 41 deletions
62
.dockerignore
Normal file
62
.dockerignore
Normal file
|
|
@ -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/
|
||||||
|
|
@ -8,3 +8,6 @@ trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.py]
|
[*.py]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.yaml]
|
||||||
|
indent_size = 2
|
||||||
|
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,3 +1,13 @@
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.12
|
||||||
14
.woodpecker/dev.yaml
Normal file
14
.woodpecker/dev.yaml
Normal file
|
|
@ -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"'
|
||||||
14
.woodpecker/latest.yaml
Normal file
14
.woodpecker/latest.yaml
Normal file
|
|
@ -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
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
|
|
@ -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"]
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
logging:
|
logging:
|
||||||
level: "TRACE"
|
level: "TRACE"
|
||||||
path: "logs/karl.log"
|
path: "../../logs/karl.log"
|
||||||
app:
|
app:
|
||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
port: 8081
|
port: 8081
|
||||||
|
|
|
||||||
22
docker-compose.yaml
Normal file
22
docker-compose.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -5,6 +5,7 @@ description = "Because name 'Jenkins' was already taken. Greatest composer ever.
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
authors = [{ name = "Piotr Dec" }]
|
authors = [{ name = "Piotr Dec" }]
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.119.0",
|
"fastapi>=0.119.0",
|
||||||
"uvicorn[standard]>=0.30.0",
|
"uvicorn[standard]>=0.30.0",
|
||||||
|
|
@ -18,6 +19,7 @@ dependencies = [
|
||||||
"py-automapper>=2.2.0",
|
"py-automapper>=2.2.0",
|
||||||
"fastapi-utils>=0.8.0",
|
"fastapi-utils>=0.8.0",
|
||||||
"keyring>=25.6.0",
|
"keyring>=25.6.0",
|
||||||
|
"keyring-backend>=0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|
@ -31,11 +33,11 @@ dev = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
app = "app.main:run"
|
karl = "karl.main:run"
|
||||||
|
|
||||||
[tool.uv]
|
[build-system]
|
||||||
# uv automatycznie wykrywa dependencies z [project]
|
requires = ["uv_build>=0.8.23,<0.9.0"]
|
||||||
# Możesz dodać tu własne ustawienia cache/mirrors, jeśli potrzebne.
|
build-backend = "uv_build"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
|
||||||
19
src/karl/__init__.py
Normal file
19
src/karl/__init__.py
Normal file
|
|
@ -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()
|
||||||
|
|
@ -3,10 +3,10 @@ from fastapi import APIRouter, Depends
|
||||||
from fastapi_utils.cbv import cbv
|
from fastapi_utils.cbv import cbv
|
||||||
from starlette.responses import JSONResponse, Response
|
from starlette.responses import JSONResponse, Response
|
||||||
|
|
||||||
from app.api.models import Request
|
from api.models import Request
|
||||||
from app.core.injects import AutowireSupport
|
from core.injects import AutowireSupport
|
||||||
from app.core.woodpecker import Woodpecker
|
from core.woodpecker import Woodpecker
|
||||||
from app.model.webhook import WoodpeckerEvent
|
from model.webhook import WoodpeckerEvent
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -8,13 +8,13 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
class LoggingConfig(BaseModel):
|
class LoggingConfig(BaseModel):
|
||||||
level: str = "INFO"
|
level: str = "INFO"
|
||||||
path: Path = Path("logs/karl.log")
|
path: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
host: str = "127.0.0.1"
|
host: str = "127.0.0.1"
|
||||||
port: int = 8081
|
port: int = 8081
|
||||||
reload: bool = True
|
reload: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GitConfig(BaseModel):
|
class GitConfig(BaseModel):
|
||||||
|
|
@ -43,6 +43,9 @@ class Settings(BaseSettings):
|
||||||
if p.exists():
|
if p.exists():
|
||||||
with p.open("r", encoding="utf-8") as fh:
|
with p.open("r", encoding="utf-8") as fh:
|
||||||
data = yaml.safe_load(fh) or {}
|
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)
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from injectable import inject
|
from injectable import inject
|
||||||
|
|
||||||
from app.core.woodpecker import Woodpecker
|
from core.woodpecker import Woodpecker
|
||||||
|
|
||||||
|
|
||||||
class AutowireSupport:
|
class AutowireSupport:
|
||||||
|
|
@ -6,10 +6,10 @@ from typing import Annotated
|
||||||
|
|
||||||
from injectable import injectable, Autowired, autowired
|
from injectable import injectable, Autowired, autowired
|
||||||
|
|
||||||
from app.config import get_settings
|
from config import get_settings
|
||||||
from app.model.webhook import WoodpeckerEvent
|
from model.webhook import WoodpeckerEvent
|
||||||
from app.services import GitService, DockerService
|
from services import GitService, DockerService
|
||||||
from app.services.mo import Mo
|
from services.mo import Mo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -3,8 +3,8 @@ import logging
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from injectable import load_injection_container
|
from injectable import load_injection_container
|
||||||
|
|
||||||
from app.config import get_settings
|
from config import get_settings
|
||||||
from app.util.logging import HandlerFactory
|
from util.logging import HandlerFactory
|
||||||
|
|
||||||
|
|
||||||
class KarlApplication:
|
class KarlApplication:
|
||||||
|
|
@ -45,13 +45,13 @@ class KarlApplication:
|
||||||
logging_logger.propagate = False
|
logging_logger.propagate = False
|
||||||
|
|
||||||
def _set_middlewares(self, app: FastAPI):
|
def _set_middlewares(self, app: FastAPI):
|
||||||
from app.web.middlewares import LoggingMiddleware
|
from web.middlewares import LoggingMiddleware
|
||||||
app.add_middleware(LoggingMiddleware)
|
app.add_middleware(LoggingMiddleware)
|
||||||
|
|
||||||
def _set_routes(self, app: FastAPI):
|
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)
|
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"])
|
app.include_router(api_v1_router, prefix="/api/v1", tags=["v1"])
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -61,17 +61,3 @@ class KarlApplication:
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
return KarlApplication()
|
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,
|
|
||||||
)
|
|
||||||
|
|
@ -5,7 +5,7 @@ import docker
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from injectable import injectable
|
from injectable import injectable
|
||||||
|
|
||||||
from app.model.containers import Tree, Compose, SimpleContainer
|
from model.containers import Tree, Compose, SimpleContainer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@ class DockerService:
|
||||||
return self._tree
|
return self._tree
|
||||||
|
|
||||||
def reload(self, compose_path: Path):
|
def reload(self, compose_path: Path):
|
||||||
|
# TODO: Won't work in docker container
|
||||||
cmd = ["sudo", "docker", "compose", "-f", str(compose_path), "up", "-d"]
|
cmd = ["sudo", "docker", "compose", "-f", str(compose_path), "up", "-d"]
|
||||||
import subprocess
|
import subprocess
|
||||||
try:
|
try:
|
||||||
|
|
@ -4,7 +4,7 @@ from typing import Annotated
|
||||||
|
|
||||||
from injectable import injectable, autowired, Autowired
|
from injectable import injectable, autowired, Autowired
|
||||||
|
|
||||||
from app.services import Passwords
|
from services import Passwords
|
||||||
|
|
||||||
|
|
||||||
class ValueTemplate(Template):
|
class ValueTemplate(Template):
|
||||||
|
|
@ -34,8 +34,10 @@ class KeyRequest:
|
||||||
@injectable(singleton=True)
|
@injectable(singleton=True)
|
||||||
class Passwords:
|
class Passwords:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
from app.config import get_settings
|
from config import get_settings
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
import keyring_backend
|
||||||
|
keyring.set_keyring(keyring=keyring_backend.Backend())
|
||||||
|
|
||||||
with open(settings.kp.secret, "r") as fh:
|
with open(settings.kp.secret, "r") as fh:
|
||||||
keyring.set_password("karl", "kp", fh.read().splitlines()[0])
|
keyring.set_password("karl", "kp", fh.read().splitlines()[0])
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from git import Repo, Remote
|
from git import Repo, Remote
|
||||||
from injectable import injectable
|
from injectable import injectable
|
||||||
|
|
||||||
from app.config import GitConfig, get_settings
|
from config import GitConfig, get_settings
|
||||||
|
|
||||||
|
|
||||||
@injectable(singleton=True)
|
@injectable(singleton=True)
|
||||||
|
|
@ -67,6 +67,12 @@ class HandlerFactory:
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def file_handler(prefix: str = ''):
|
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 = TimedRotatingFileHandler(file_path, when='midnight', backupCount=30)
|
||||||
handler.setFormatter(ApplicationFormatter(prefix))
|
handler.setFormatter(ApplicationFormatter(prefix))
|
||||||
handler.setLevel('TRACE')
|
handler.setLevel('TRACE')
|
||||||
|
|
@ -83,4 +89,5 @@ class HandlerFactory:
|
||||||
handlers.append(console_handler(handler_prefix))
|
handlers.append(console_handler(handler_prefix))
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unknown target: {target}")
|
raise ValueError(f"Unknown target: {target}")
|
||||||
|
handlers = [h for h in handlers if h is not None]
|
||||||
return handlers
|
return handlers
|
||||||
0
src/karl/web/__init__.py
Normal file
0
src/karl/web/__init__.py
Normal file
|
|
@ -3,8 +3,8 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from app.services import Passwords
|
from karl.services import Passwords
|
||||||
from app.services.mo import Mo
|
from karl.services.mo import Mo
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='class')
|
@pytest.fixture(scope='class')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue