Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Rjvs committed Nov 15, 2024
0 parents commit caf7aaa
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 0 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/build.yaml
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
57 changes: 57 additions & 0 deletions Dockerfile
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"]
3 changes: 3 additions & 0 deletions README.md
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.
191 changes: 191 additions & 0 deletions diun2homer.py
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)
21 changes: 21 additions & 0 deletions docker-compose.yaml
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:
13 changes: 13 additions & 0 deletions pyproject.toml
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"

3 changes: 3 additions & 0 deletions requirements.txt
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

0 comments on commit caf7aaa

Please sign in to comment.