feat: enforce unique port constraint on servers

This commit is contained in:
mwiegand 2026-05-06 20:52:46 +02:00
parent 833ae318cf
commit fa002ce0f2
No known key found for this signature in database
4 changed files with 69 additions and 2 deletions

View file

@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View file

@ -0,0 +1,32 @@
"""make server port unique
Revision ID: b2c684fddbd3
Revises: 0001_initial
Create Date: 2026-05-06 20:52:35.109176
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b2c684fddbd3'
down_revision: Union[str, Sequence[str], None] = '0001_initial'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(None, 'servers', ['port'])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'servers', type_='unique')
# ### end Alembic commands ###

View file

@ -63,7 +63,7 @@ class Server(Base):
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
blueprint_id: Mapped[int] = mapped_column(ForeignKey("blueprints.id"), nullable=False) blueprint_id: Mapped[int] = mapped_column(ForeignKey("blueprints.id"), nullable=False)
name: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) name: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
port: Mapped[int] = mapped_column(Integer, nullable=False) port: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
desired_state: Mapped[str] = mapped_column(String(16), default="stopped", nullable=False) desired_state: Mapped[str] = mapped_column(String(16), default="stopped", nullable=False)
actual_state: Mapped[str] = mapped_column(String(16), default="unknown", nullable=False) actual_state: Mapped[str] = mapped_column(String(16), default="unknown", nullable=False)
actual_state_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) actual_state_updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

View file

@ -1,5 +1,6 @@
from flask import Blueprint, Response, jsonify, redirect, request from flask import Blueprint, Response, jsonify, redirect, request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from l4d2web.auth import current_user, require_login from l4d2web.auth import current_user, require_login
from l4d2web.db import session_scope from l4d2web.db import session_scope
@ -38,7 +39,13 @@ def create_server() -> Response:
last_error="", last_error="",
) )
db.add(server) db.add(server)
db.flush()
try:
db.flush()
except IntegrityError:
db.rollback()
return Response("port already in use", status=409)
server_id = server.id server_id = server.id
if json_response: if json_response: