-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit caf7aaa
Showing
7 changed files
with
344 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
name: Build and Push Docker Image | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
tags: [ 'v*' ] | ||
pull_request: | ||
branches: [ main ] | ||
|
||
env: | ||
REGISTRY: ghcr.io | ||
IMAGE_NAME: ${{ github.repository }} | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
permissions: | ||
contents: read | ||
packages: write | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Log in to the Container registry | ||
if: github.event_name != 'pull_request' | ||
uses: docker/login-action@v3 | ||
with: | ||
registry: ${{ env.REGISTRY }} | ||
username: ${{ github.actor }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Extract metadata (tags, labels) for Docker | ||
id: meta | ||
uses: docker/metadata-action@v5 | ||
with: | ||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||
tags: | | ||
type=ref,event=branch | ||
type=ref,event=pr | ||
type=semver,pattern={{version}} | ||
type=semver,pattern={{major}}.{{minor}} | ||
type=sha,format=long | ||
- name: Set up Docker Buildx | ||
uses: docker/setup-buildx-action@v3 | ||
|
||
- name: Build and push Docker image | ||
uses: docker/build-push-action@v5 | ||
with: | ||
context: . | ||
push: ${{ github.event_name != 'pull_request' }} | ||
tags: ${{ steps.meta.outputs.tags }} | ||
labels: ${{ steps.meta.outputs.labels }} | ||
cache-from: type=gha | ||
cache-to: type=gha,mode=max |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Build stage | ||
FROM python:3.11-slim AS builder | ||
|
||
WORKDIR /app | ||
|
||
# Install build dependencies | ||
RUN apt-get update && \ | ||
apt-get install -y --no-install-recommends gcc && \ | ||
rm -rf /var/lib/apt/lists/* && \ | ||
apt-get clean && \ | ||
rm -rf /var/cache/apt/* && \ | ||
pip install --upgrade pip && \ | ||
pip install --upgrade uv | ||
|
||
# Copy dependency files | ||
COPY pyproject.toml ./ | ||
|
||
# Create virtual environment and install dependencies | ||
RUN python -m venv .venv && \ | ||
. .venv/bin/activate && \ | ||
uv pip install -e . | ||
|
||
# Final stage | ||
FROM python:3.11-slim | ||
|
||
WORKDIR /app | ||
|
||
# Create non-root user | ||
RUN useradd -m -u 1000 appuser | ||
|
||
# Copy virtual environment from builder | ||
COPY --from=builder /app/.venv ./.venv | ||
COPY --from=builder /app/pyproject.toml ./ | ||
|
||
# Copy application code | ||
COPY diun2homer.py ./ | ||
|
||
# Create data directory and set permissions | ||
RUN mkdir -p /app/data && \ | ||
chown -R appuser:appuser /app | ||
|
||
# Switch to non-root user | ||
USER appuser | ||
|
||
# Set environment variables | ||
ENV PATH="/app/.venv/bin:$PATH" \ | ||
PYTHONPATH="/app" \ | ||
PYTHONUNBUFFERED=1 | ||
|
||
# Volume for persistent data | ||
VOLUME /app/data | ||
|
||
# Expose port | ||
EXPOSE 8000 | ||
|
||
# Run the application | ||
CMD ["python", "diun2homer.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Diun to Homer | ||
|
||
A FastAPI server that receives events from [Diun](https://crazymax.dev/diun/) (Docker Image Update Notifier) via the webhook notifier and presents them in a format compatible with [Homer Dashboard](https://github.com/bastienwirtz/homer). This allows you to see your Docker image update notifications directly in your Homer dashboard. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
from fastapi import FastAPI, Request | ||
from pydantic import BaseModel | ||
from typing import List, Optional | ||
import sqlite3 | ||
from datetime import datetime | ||
import json | ||
import logging | ||
import os | ||
import traceback | ||
import sys | ||
|
||
app = FastAPI() | ||
|
||
# Configure logging based on DEBUG environment variable | ||
DEBUG = os.getenv('DEBUG', 'false').lower() == 'true' | ||
logging.basicConfig( | ||
level=logging.DEBUG if DEBUG else logging.INFO, | ||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||
handlers=[ | ||
logging.StreamHandler(sys.stdout), | ||
logging.FileHandler('diun2homer.log') | ||
] | ||
) | ||
logger = logging.getLogger('diun2homer') | ||
|
||
# Add this near the top of the file, after imports | ||
DATABASE_NAME = 'diun2homer.db' | ||
|
||
# Initialize SQLite database | ||
def init_db(): | ||
try: | ||
logger.info("Initializing database...") | ||
conn = sqlite3.connect(DATABASE_NAME) | ||
c = conn.cursor() | ||
c.execute(''' | ||
CREATE TABLE IF NOT EXISTS events | ||
(id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
image TEXT, | ||
status TEXT, | ||
platform TEXT, | ||
tag TEXT, | ||
message TEXT, | ||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP) | ||
''') | ||
conn.commit() | ||
conn.close() | ||
logger.info("Database initialization successful") | ||
except Exception as e: | ||
logger.error(f"Database initialization failed: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
raise | ||
|
||
# Diun webhook payload model | ||
class DiunPayload(BaseModel): | ||
status: str | ||
image: str | ||
platform: Optional[str] | ||
tag: Optional[str] | ||
message: str | ||
|
||
class Config: | ||
extra = "allow" # Allow additional fields for future compatibility | ||
|
||
# Convert Diun status to Homer message style | ||
def get_homer_style(status: str) -> str: | ||
status_map = { | ||
"new": "is-info", | ||
"update": "is-success", | ||
"error": "is-danger" | ||
} | ||
result = status_map.get(status.lower(), "is-warning") | ||
logger.debug(f"Mapped status '{status}' to homer style '{result}'") | ||
return result | ||
|
||
# Store diun event data | ||
def store_diun_payload(payload: DiunPayload): | ||
try: | ||
logger.info(f"Storing diun event data for image: {payload.image}") | ||
if DEBUG: | ||
logger.debug(f"Full payload: {payload.json(indent=2)}") | ||
|
||
conn = sqlite3.connect(DATABASE_NAME) | ||
c = conn.cursor() | ||
c.execute(''' | ||
INSERT INTO events (image, status, platform, tag, message) | ||
VALUES (?, ?, ?, ?, ?) | ||
''', ( | ||
payload.image, | ||
payload.status, | ||
payload.platform, | ||
payload.tag, | ||
payload.message | ||
)) | ||
conn.commit() | ||
conn.close() | ||
logger.info("Successfully stored diun event data") | ||
except Exception as e: | ||
logger.error(f"Failed to store diun event data: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
raise | ||
|
||
# Get stored events in Homer format | ||
def get_homer_messages() -> List[dict]: | ||
try: | ||
logger.info("Retrieving messages for Homer") | ||
conn = sqlite3.connect(DATABASE_NAME) | ||
c = conn.cursor() | ||
c.execute('SELECT image, status, message, timestamp FROM events ORDER BY timestamp DESC') | ||
rows = c.fetchall() | ||
conn.close() | ||
|
||
messages = [] | ||
for row in rows: | ||
image, status, message, timestamp = row | ||
message_data = { | ||
"style": get_homer_style(status), | ||
"title": image, | ||
"content": f"{message} ({timestamp})" | ||
} | ||
messages.append(message_data) | ||
|
||
logger.info(f"Retrieved {len(messages)} messages") | ||
if DEBUG: | ||
logger.debug(f"Full messages data: {json.dumps(messages, indent=2)}") | ||
return messages | ||
except Exception as e: | ||
logger.error(f"Failed to retrieve messages: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
raise | ||
|
||
# Initialize database on startup | ||
@app.on_event("startup") | ||
async def startup_event(): | ||
logger.info("Starting diun2homer") | ||
init_db() | ||
|
||
# Diun webhook endpoint | ||
@app.post("/diun") | ||
async def diun(payload: DiunPayload, request: Request): | ||
try: | ||
client_host = request.client.host | ||
logger.info(f"Received webhook from {client_host} for image: {payload.image}") | ||
if DEBUG: | ||
logger.debug(f"Request headers: {dict(request.headers)}") | ||
logger.debug(f"Raw payload: {await request.body()}") | ||
logger.debug(f"Parsed payload: {payload.json(indent=2)}") | ||
|
||
store_diun_payload(payload) | ||
logger.info("Successfully processed webhook") | ||
return {"status": "success"} | ||
except Exception as e: | ||
logger.error(f"Webhook processing failed: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
raise | ||
|
||
# Homer messages endpoint | ||
@app.get("/homer") | ||
async def homer(request: Request): | ||
try: | ||
client_host = request.client.host | ||
logger.info(f"Messages requested from {client_host}") | ||
if DEBUG: | ||
logger.debug(f"Request headers: {dict(request.headers)}") | ||
|
||
result = get_homer_messages() | ||
logger.info(f"Successfully returned {len(result)} messages") | ||
return result | ||
except Exception as e: | ||
logger.error(f"Messages request failed: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
raise | ||
|
||
# Health check endpoint | ||
@app.get("/health") | ||
async def health(): | ||
try: | ||
# Test database connection | ||
conn = sqlite3.connect(DATABASE_NAME) | ||
conn.cursor() | ||
conn.close() | ||
logger.info("Health check successful") | ||
return {"status": "healthy"} | ||
except Exception as e: | ||
logger.error(f"Health check failed: {str(e)}") | ||
logger.debug(f"Detailed error: {traceback.format_exc()}") | ||
return {"status": "unhealthy", "error": str(e)} | ||
|
||
if __name__ == "__main__": | ||
import uvicorn | ||
logger.info("Starting server...") | ||
uvicorn.run(app, host="0.0.0.0", port=8000) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
version: '3.8' | ||
|
||
services: | ||
diun2homer: | ||
image: ghcr.io/${GITHUB_REPOSITORY:Rjvs/diun2homer}:latest | ||
ports: | ||
- "8000:8000" | ||
volumes: | ||
- diun2homer-data:/app/data | ||
environment: | ||
- DEBUG=false | ||
restart: unless-stopped | ||
healthcheck: | ||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"] | ||
interval: 30s | ||
timeout: 10s | ||
retries: 3 | ||
start_period: 5s | ||
|
||
volumes: | ||
diun2homer-data: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[project] | ||
name = "diun2homer" | ||
version = "0.1.0" | ||
description = "FastAPI proxy receiving Diun webhooks and making them available to Homer" | ||
authors = [ | ||
{name = "Robert Spencer", email = "[email protected]"} | ||
] | ||
requires-python = ">=3.11" | ||
|
||
[build-system] | ||
requires = ["hatchling"] | ||
build-backend = "hatchling.build" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
fastapi>=0.109.0 | ||
uvicorn>=0.27.0 | ||
pydantic>=2.5.0 |