docs: add server port constraint implementation plan
This commit is contained in:
parent
114b141e2a
commit
7e5a8f89b5
1 changed files with 147 additions and 0 deletions
147
docs/superpowers/plans/2026-05-06-server-port-constraint.md
Normal file
147
docs/superpowers/plans/2026-05-06-server-port-constraint.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Server Port Constraint Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Ensure servers cannot share the same port by enforcing uniqueness at the database level and handling the constraint violation in the web UI.
|
||||
|
||||
**Architecture:** We will add a unique constraint to the `Server.port` column, generate an Alembic migration, and update the `/servers` POST route to catch `IntegrityError` when a port conflict occurs, returning a 409 status code.
|
||||
|
||||
**Tech Stack:** Python, Flask, SQLAlchemy, Alembic, Pytest
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add Unique Constraint to Server Port
|
||||
|
||||
**Files:**
|
||||
- Modify: `l4d2web/models.py`
|
||||
- Modify: `l4d2web/routes/server_routes.py`
|
||||
- Create: `l4d2web/alembic/versions/XXXX_make_server_port_unique.py` (via alembic)
|
||||
|
||||
- [ ] **Step 1: Update the database model**
|
||||
|
||||
Update `l4d2web/models.py` to add `unique=True` to the `port` column on the `Server` model.
|
||||
|
||||
```python
|
||||
class Server(Base):
|
||||
__tablename__ = "servers"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.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)
|
||||
port: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
|
||||
# ... rest of the columns
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Generate the Alembic migration**
|
||||
|
||||
Run: `PYTHONPATH=. alembic -c l4d2web/alembic.ini revision --autogenerate -m "make server port unique"`
|
||||
Expected: Creates a new migration file in `l4d2web/alembic/versions/`
|
||||
|
||||
- [ ] **Step 3: Update application logic**
|
||||
|
||||
Update `l4d2web/routes/server_routes.py` to catch the `IntegrityError` when creating a server.
|
||||
|
||||
```python
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
# ... other imports
|
||||
|
||||
@bp.post("/servers")
|
||||
@require_login
|
||||
def create_server() -> Response:
|
||||
# ... existing user check and payload extraction
|
||||
|
||||
with session_scope() as db:
|
||||
blueprint = db.scalar(
|
||||
select(BlueprintModel).where(
|
||||
BlueprintModel.id == int(payload["blueprint_id"]),
|
||||
BlueprintModel.user_id == user.id,
|
||||
)
|
||||
)
|
||||
if blueprint is None:
|
||||
return Response("blueprint not found", status=404)
|
||||
|
||||
server = Server(
|
||||
user_id=user.id,
|
||||
blueprint_id=blueprint.id,
|
||||
name=str(payload["name"]),
|
||||
port=int(payload["port"]),
|
||||
desired_state="stopped",
|
||||
actual_state="unknown",
|
||||
last_error="",
|
||||
)
|
||||
db.add(server)
|
||||
|
||||
try:
|
||||
db.flush()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
return Response("port already in use", status=409)
|
||||
|
||||
server_id = server.id
|
||||
|
||||
if json_response:
|
||||
return jsonify({"id": server_id}), 201
|
||||
return redirect(f"/servers/{server_id}")
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add l4d2web/models.py l4d2web/routes/server_routes.py l4d2web/alembic/versions/
|
||||
git commit -m "feat: enforce unique port constraint on servers"
|
||||
```
|
||||
|
||||
### Task 2: Write Verification Tests
|
||||
|
||||
**Files:**
|
||||
- Modify: `l4d2web/tests/test_servers.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add a test case to `l4d2web/tests/test_servers.py` that verifies the unique port constraint.
|
||||
|
||||
```python
|
||||
def test_create_server_duplicate_port(client, auth, db_session):
|
||||
auth.login()
|
||||
|
||||
# First, create a blueprint
|
||||
response = client.post(
|
||||
"/blueprints",
|
||||
data={"name": "my-blueprint", "arguments": "[]", "config": "[]"},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
# Then create the first server
|
||||
response = client.post(
|
||||
"/servers",
|
||||
data={"name": "server-1", "port": "27015", "blueprint_id": "1"},
|
||||
)
|
||||
assert response.status_code == 302
|
||||
|
||||
# Try to create a second server with the same port
|
||||
response = client.post(
|
||||
"/servers",
|
||||
data={"name": "server-2", "port": "27015", "blueprint_id": "1"},
|
||||
)
|
||||
assert response.status_code == 409
|
||||
assert b"port already in use" in response.data
|
||||
|
||||
# Verify the second server was not created
|
||||
from l4d2web.models import Server
|
||||
servers = db_session.query(Server).all()
|
||||
assert len(servers) == 1
|
||||
assert servers[0].name == "server-1"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it passes**
|
||||
|
||||
Run: `pytest l4d2web/tests/test_servers.py -v`
|
||||
Expected: PASS (It passes because we implemented the code in Task 1. We're doing this slightly out of TDD order to group the DB/Route changes together).
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add l4d2web/tests/test_servers.py
|
||||
git commit -m "test: add test for duplicate port constraint"
|
||||
```
|
||||
Loading…
Reference in a new issue