feat: Implement container customization and add Portainer installation/removal playbooks.
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled

This commit is contained in:
2025-12-27 11:02:24 -05:00
parent 661d005fc7
commit 8affa0f8b7
30 changed files with 1767 additions and 22 deletions
+2
View File
@@ -13,6 +13,7 @@ from .docker_container import DockerContainer
from .docker_image import DockerImage
from .docker_volume import DockerVolume
from .docker_alert import DockerAlert
from .container_customization import ContainerCustomization
from .favorite_group import FavoriteGroup
from .favorite_container import FavoriteContainer
from .terminal_session import TerminalSession
@@ -39,6 +40,7 @@ __all__ = [
"DockerImage",
"DockerVolume",
"DockerAlert",
"ContainerCustomization",
"FavoriteGroup",
"FavoriteContainer",
"TerminalSession",
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .database import Base
class ContainerCustomization(Base):
__tablename__ = "container_customizations"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
host_id: Mapped[str] = mapped_column(String, ForeignKey("hosts.id", ondelete="CASCADE"), nullable=False)
container_id: Mapped[str] = mapped_column(String(64), nullable=False)
icon_key: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
icon_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
bg_color: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
__table_args__ = (
UniqueConstraint("user_id", "host_id", "container_id", name="uq_container_customizations_user_host_container"),
{"sqlite_autoincrement": True},
)
+40
View File
@@ -3,11 +3,14 @@ Uses SQLAlchemy 2.x async engine with SQLite + aiosqlite driver.
"""
from __future__ import annotations
import asyncio
import os
from pathlib import Path
from typing import AsyncGenerator
from urllib.parse import urlparse
from alembic import command
from alembic.config import Config
from sqlalchemy import event, MetaData
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base
@@ -129,5 +132,42 @@ async def init_db() -> None:
favorite_container,
) # noqa: F401
def _to_sync_database_url(db_url: str) -> str:
return db_url.replace("sqlite+aiosqlite:", "sqlite:")
def _run_alembic_upgrade() -> None:
# Try multiple locations for alembic.ini (dev vs Docker)
alembic_ini_paths = [
ROOT_DIR / "alembic.ini", # Dev: /path/to/project/alembic.ini
Path("/alembic.ini"), # Docker: /alembic.ini
Path("/app/alembic.ini"), # Docker alternative
]
alembic_ini = None
for path in alembic_ini_paths:
if path.exists():
alembic_ini = path
print(f"[DB] Found alembic.ini at: {alembic_ini}")
break
if not alembic_ini:
print(f"[DB] alembic.ini not found in any of: {[str(p) for p in alembic_ini_paths]}")
return
try:
cfg = Config(str(alembic_ini))
cfg.set_main_option("sqlalchemy.url", _to_sync_database_url(DATABASE_URL))
print(f"[DB] Running Alembic upgrade to head...")
command.upgrade(cfg, "head")
print(f"[DB] Alembic upgrade completed successfully")
except Exception as e:
print(f"[DB] Alembic upgrade failed: {e}")
raise
try:
await asyncio.to_thread(_run_alembic_upgrade)
except Exception as e:
print(f"[DB] Exception during Alembic migration: {e}")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)