feat(docker): enhance Dockerfile with debugging outputs and install tree utility; update .dockerignore and MANIFEST.in for better packaging

This commit is contained in:
pacnpal
2025-01-28 22:36:22 -05:00
parent 64d09b8842
commit 0bc9dded41
6 changed files with 107 additions and 57 deletions

View File

@@ -41,7 +41,4 @@ ENV/
htmlcov/
# Project specific
rules_backup/
# Documentation
*.md
rules_backup/

View File

@@ -12,6 +12,7 @@ RUN apt-get update && \
libc6-dev \
python3-dev \
python3-pip \
tree \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& python3 -m pip install --no-cache-dir --upgrade "pip>=21.3" setuptools wheel
@@ -26,23 +27,27 @@ RUN mkdir -p /app/src/simpleguardhome && \
# Copy source code, maintaining directory structure
COPY . /app/
# Set execute permission for entrypoint script
RUN chmod +x /app/docker-entrypoint.sh && \
# Debug: Show the copied files and set execute permission for entrypoint script
RUN echo "Project structure:" && \
tree /app && \
echo "Package directory contents:" && \
ls -la /app/src/simpleguardhome/ && \
chmod +x /app/docker-entrypoint.sh && \
cp /app/docker-entrypoint.sh /usr/local/bin/
# Set PYTHONPATH
ENV PYTHONPATH=/app/src
# Install Python requirements
RUN pip install --no-cache-dir -r requirements.txt
# Install and verify the package
RUN set -e && \
# Install Python requirements and verify the package
RUN pip install --no-cache-dir -r requirements.txt && \
set -e && \
echo "Installing package..." && \
pip uninstall -y simpleguardhome || true && \
# Verify source files exist
echo "Verifying source files..." && \
ls -la /app/src/simpleguardhome/ && \
# Debug: Show package files
echo "Python path:" && \
python3 -c "import sys; print('\n'.join(sys.path))" && \
echo "Source directory contents:" && \
ls -R /app/src && \
# Install package in editable mode with compatibility mode enabled
pip install --use-pep517 -e . --config-settings editable_mode=compat && \
echo "Verifying installation..." && \
@@ -50,13 +55,15 @@ RUN set -e && \
# List all package files
echo "Package contents:" && \
find /app/src/simpleguardhome -type f -ls && \
# Verify import works
# Verify package can be imported
echo "Testing import..." && \
python3 -c "import simpleguardhome; from simpleguardhome.main import app; print(f'Package found at: {simpleguardhome.__file__}')" && \
echo "Package installation successful"
# Create rules backup directory with proper permissions
RUN mkdir -p /app/rules_backup && \
python3 -c "import simpleguardhome; print(f'Package found at: {simpleguardhome.__file__}')" && \
# Verify app can be imported
echo "Testing app import..." && \
python3 -c "from simpleguardhome.main import app; print('App imported successfully')" && \
echo "Package installation successful" && \
# Create rules backup directory with proper permissions
mkdir -p /app/rules_backup && \
chmod 777 /app/rules_backup
# Default environment variables

View File

@@ -1,3 +1,18 @@
recursive-include src/simpleguardhome/templates *
# Include all package Python files
graft src/simpleguardhome
# Include package data files
include src/simpleguardhome/favicon.ico
recursive-include src/simpleguardhome *.py
include src/simpleguardhome/templates/*.html
# Include important project files
include README.md
include LICENSE
include requirements.txt
include pyproject.toml
include setup.py
# Exclude bytecode files
global-exclude *.py[cod]
global-exclude __pycache__
global-exclude *.so

View File

@@ -22,9 +22,21 @@ dependencies = [
]
[tool.setuptools]
# Using explicit package configuration
package-dir = {"" = "src"}
packages = ["simpleguardhome"]
include-package-data = true
# Include all package data
[tool.setuptools.package-data]
simpleguardhome = ["templates/*", "favicon.ico"]
"*" = ["*.ico", "templates/*.html"]
# Explicitly include the package data
[options.package_data]
simpleguardhome = [
"templates/*",
"favicon.ico"
]
# Make sure data files are included
[options]
include_package_data = true

View File

@@ -1,22 +1,23 @@
from pydantic_settings import BaseSettings
from typing import Optional
from pydantic_settings import BaseSettings # type: ignore
class Settings(BaseSettings):
"""Application settings using environment variables."""
ADGUARD_HOST: str = "http://localhost"
ADGUARD_PORT: int = 3000
ADGUARD_USERNAME: Optional[str] = None
ADGUARD_PASSWORD: Optional[str] = None
@property
def adguard_base_url(self) -> str:
"""Get the base URL for AdGuard Home API."""
return f"{self.ADGUARD_HOST}:{self.ADGUARD_PORT}"
class Config:
env_file = ".env"
settings = Settings()
settings = Settings()

View File

@@ -1,25 +1,33 @@
from fastapi import FastAPI, Request, Form, HTTPException, status
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pathlib import Path
import httpx
import logging
from pathlib import Path
from typing import Dict
from . import adguard
from .config import settings
from .adguard import (
AdGuardError,
AdGuardConnectionError,
AdGuardAPIError,
AdGuardValidationError,
FilterStatus,
FilterCheckHostResponse,
SetRulesRequest
import httpx # noqa: F401
from fastapi import ( # type: ignore # noqa: F401
FastAPI,
Form,
HTTPException,
Request,
status,
)
from fastapi.middleware.cors import CORSMiddleware # type: ignore
from fastapi.responses import HTMLResponse, JSONResponse # type: ignore
from fastapi.staticfiles import StaticFiles # type: ignore
from fastapi.templating import Jinja2Templates # type: ignore
from pydantic import BaseModel, Field
from . import adguard
from .adguard import (
AdGuardAPIError,
AdGuardConnectionError,
AdGuardError,
AdGuardValidationError,
FilterCheckHostResponse,
FilterStatus,
SetRulesRequest,
)
from .config import settings # noqa: F401
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@@ -53,13 +61,17 @@ app.mount("/static", StaticFiles(directory=str(Path(__file__).parent)), name="st
# Mount favicon.ico at root
static_files_path = Path(__file__).parent
app.mount("/favicon.ico", StaticFiles(directory=str(static_files_path)), name="favicon")
app.mount("/favicon.ico",
StaticFiles(directory=str(static_files_path)), name="favicon")
# Response models matching AdGuard spec
class ErrorResponse(BaseModel):
"""Error response model according to AdGuard spec."""
message: str = Field(..., description="The error message")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
"""Render the home page."""
@@ -68,6 +80,7 @@ async def home(request: Request):
{"request": request}
)
@app.get(
"/control/filtering/check_host",
response_model=FilterCheckHostResponse,
@@ -85,7 +98,7 @@ async def check_domain(name: str) -> FilterCheckHostResponse:
status_code=status.HTTP_400_BAD_REQUEST,
detail="Domain name is required"
)
logger.info(f"Checking domain: {name}")
try:
async with adguard.AdGuardClient() as client:
@@ -96,11 +109,12 @@ async def check_domain(name: str) -> FilterCheckHostResponse:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
) from e
except Exception as e:
logger.error(f"Error checking domain {name}: {str(e)}")
raise
@app.post(
"/control/filtering/set_rules",
response_model=Dict,
@@ -118,7 +132,7 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict:
status_code=status.HTTP_400_BAD_REQUEST,
detail="Rules are required"
)
# Extract domain from whitelist rule
rule = request.rules[0]
if not rule.startswith("@@||") or not rule.endswith("^"):
@@ -126,10 +140,10 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict:
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid whitelist rule format"
)
domain = rule[4:-1] # Remove @@|| prefix and ^ suffix
logger.info(f"Adding domain to whitelist: {domain}")
try:
async with adguard.AdGuardClient() as client:
success = await client.add_allowed_domain(domain)
@@ -144,11 +158,12 @@ async def add_to_whitelist(request: SetRulesRequest) -> Dict:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
) from e
except Exception as e:
logger.error(f"Error adding domain to whitelist: {str(e)}")
raise
@app.get(
"/control/filtering/status",
response_model=FilterStatus,
@@ -167,8 +182,9 @@ async def get_filtering_status() -> FilterStatus:
logger.error(f"Error getting filter status: {str(e)}")
raise
@app.exception_handler(AdGuardError)
async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse:
async def adguard_exception_handler(_request: Request, exc: AdGuardError) -> JSONResponse:
"""Handle AdGuard-related exceptions according to spec."""
if isinstance(exc, AdGuardConnectionError):
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
@@ -178,15 +194,16 @@ async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSON
status_code = status.HTTP_502_BAD_GATEWAY
else:
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return JSONResponse(
status_code=status_code,
content={"message": str(exc)}
)
def start():
"""Start the application using uvicorn."""
import uvicorn
import uvicorn # type: ignore
uvicorn.run(
app,
host="0.0.0.0",
@@ -194,5 +211,6 @@ def start():
reload=False # Disable reload in Docker
)
if __name__ == "__main__":
start()
start()