2 Commits

Author SHA1 Message Date
Mondo Diaz
16140402c1 Fix reset_stage job to read STAGE_URL from environment 2026-01-21 22:21:16 +00:00
Mondo Diaz
00cc594cfd Add factory reset endpoint for stage environment cleanup (#54)
- Add POST /api/v1/admin/factory-reset endpoint requiring admin auth
  and X-Confirm-Reset header for safety
- Add delete_all() method to storage backend for bulk S3 cleanup
- Add reset_stage CI job that calls the endpoint after integration tests
- Automatically resets stage to clean state after each test run
2026-01-21 21:06:24 +00:00
4 changed files with 33 additions and 72 deletions

View File

@@ -154,7 +154,6 @@ reset_stage:
needs: [integration_test_stage] needs: [integration_test_stage]
image: deps.global.bsf.tools/docker/python:3.12-slim image: deps.global.bsf.tools/docker/python:3.12-slim
timeout: 5m timeout: 5m
retry: 1 # Retry once on transient failures
before_script: before_script:
- pip install --index-url "$PIP_INDEX_URL" httpx - pip install --index-url "$PIP_INDEX_URL" httpx
script: script:
@@ -163,13 +162,10 @@ reset_stage:
import httpx import httpx
import sys import sys
import os import os
import time
BASE_URL = os.environ.get("STAGE_URL", "") BASE_URL = os.environ.get("STAGE_URL", "")
ADMIN_USER = "admin" ADMIN_USER = "admin"
ADMIN_PASS = "changeme123" # Default admin password ADMIN_PASS = "changeme123" # Default admin password
MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds
if not BASE_URL: if not BASE_URL:
print("ERROR: STAGE_URL environment variable not set") print("ERROR: STAGE_URL environment variable not set")
@@ -177,50 +173,37 @@ reset_stage:
print(f"=== Resetting stage environment at {BASE_URL} ===") print(f"=== Resetting stage environment at {BASE_URL} ===")
def do_reset(): client = httpx.Client(base_url=BASE_URL, timeout=60.0)
with httpx.Client(base_url=BASE_URL, timeout=120.0) as client:
# Login as admin
print("Logging in as admin...")
login_response = client.post(
"/api/v1/auth/login",
json={"username": ADMIN_USER, "password": ADMIN_PASS},
)
if login_response.status_code != 200:
raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}")
print("Login successful")
# Call factory reset endpoint # Login as admin
print("Calling factory reset endpoint...") print("Logging in as admin...")
reset_response = client.post( login_response = client.post(
"/api/v1/admin/factory-reset", "/api/v1/auth/login",
headers={"X-Confirm-Reset": "yes-delete-all-data"}, json={"username": ADMIN_USER, "password": ADMIN_PASS},
) )
if login_response.status_code != 200:
print(f"Login failed: {login_response.status_code} - {login_response.text}")
sys.exit(1)
print("Login successful")
if reset_response.status_code == 200: # Call factory reset endpoint
result = reset_response.json() print("Calling factory reset endpoint...")
print("Factory reset successful!") reset_response = client.post(
print(f" Database tables dropped: {result['results']['database_tables_dropped']}") "/api/v1/admin/factory-reset",
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}") headers={"X-Confirm-Reset": "yes-delete-all-data"},
print(f" Database reinitialized: {result['results']['database_reinitialized']}") )
print(f" Seeded: {result['results']['seeded']}")
return True
else:
raise Exception(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
# Retry loop if reset_response.status_code == 200:
for attempt in range(1, MAX_RETRIES + 1): result = reset_response.json()
try: print(f"Factory reset successful!")
print(f"Attempt {attempt}/{MAX_RETRIES}") print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
if do_reset(): print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
sys.exit(0) print(f" Database reinitialized: {result['results']['database_reinitialized']}")
except Exception as e: print(f" Seeded: {result['results']['seeded']}")
print(f"Attempt {attempt} failed: {e}") sys.exit(0)
if attempt < MAX_RETRIES: else:
print(f"Retrying in {RETRY_DELAY} seconds...") print(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
time.sleep(RETRY_DELAY) sys.exit(1)
else:
print("All retry attempts failed")
sys.exit(1)
RESET_SCRIPT RESET_SCRIPT
rules: rules:
- if: '$CI_COMMIT_BRANCH == "main"' - if: '$CI_COMMIT_BRANCH == "main"'

View File

@@ -4,7 +4,6 @@
# False positive: s3_key is an attribute name in test assertions, not a secret # False positive: s3_key is an attribute name in test assertions, not a secret
# These are historical commits - files have since been deleted or updated with inline comments # These are historical commits - files have since been deleted or updated with inline comments
7e68baed0886a3c928644cd01aa3b39f92d4f976:backend/tests/test_duplicate_detection.py:generic-api-key:154 7e68baed0886a3c928644cd01aa3b39f92d4f976:backend/tests/test_duplicate_detection.py:generic-api-key:154
81458b3bcb5ace97109ba4c16f4afa6e55b1b8bd:backend/tests/test_duplicate_detection.py:generic-api-key:154
2f1891cf0126ec0e7d4c789d872a2cb2dd3a1745:backend/tests/unit/test_storage.py:generic-api-key:381 2f1891cf0126ec0e7d4c789d872a2cb2dd3a1745:backend/tests/unit/test_storage.py:generic-api-key:381
10d36947948de796f0bacea3827f4531529c405d:backend/tests/unit/test_storage.py:generic-api-key:381 10d36947948de796f0bacea3827f4531529c405d:backend/tests/unit/test_storage.py:generic-api-key:381
bccbc71c13570d14b8b26a11335c45f102fe3072:backend/tests/unit/test_storage.py:generic-api-key:381 bccbc71c13570d14b8b26a11335c45f102fe3072:backend/tests/unit/test_storage.py:generic-api-key:381

View File

@@ -28,16 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Disabled PostgreSQL subchart for stage and prod environments (#54) - Disabled PostgreSQL subchart for stage and prod environments (#54)
- Disabled MinIO subchart for stage and prod environments (#54) - Disabled MinIO subchart for stage and prod environments (#54)
### Fixed
- Fixed factory reset not creating default admin user after reset (#60)
- Admin user was only created at server startup, not after factory reset
- CI reset job would fail to login because admin user didn't exist
- Improved reset_stage CI job reliability (#60)
- Added application-level retry logic (3 attempts with 5s delay)
- Added job-level retry for transient failures
- Fixed httpx client to use proper context manager
- Increased timeout to 120s for reset operations
### Added ### Added
- Added comprehensive upload/download tests for size boundaries (1B to 1GB) (#38) - Added comprehensive upload/download tests for size boundaries (1B to 1GB) (#38)
- Added concurrent upload/download tests (2, 5, 10 parallel operations) (#38) - Added concurrent upload/download tests (2, 5, 10 parallel operations) (#38)

View File

@@ -6427,9 +6427,7 @@ def factory_reset(
detail="Factory reset requires X-Confirm-Reset header set to 'yes-delete-all-data'", detail="Factory reset requires X-Confirm-Reset header set to 'yes-delete-all-data'",
) )
# Capture username before we drop tables (user object will become invalid) logger.warning(f"Factory reset initiated by admin user: {current_user.username}")
admin_username = current_user.username
logger.warning(f"Factory reset initiated by admin user: {admin_username}")
results = { results = {
"database_tables_dropped": 0, "database_tables_dropped": 0,
@@ -6474,26 +6472,17 @@ def factory_reset(
# Step 3: Reinitialize database schema # Step 3: Reinitialize database schema
logger.info("Reinitializing database schema...") logger.info("Reinitializing database schema...")
from .database import init_db, SessionLocal from .database import init_db
init_db() init_db()
results["database_reinitialized"] = True results["database_reinitialized"] = True
# Step 4: Re-seed with default data (need fresh session after schema recreate) # Step 4: Re-seed with default data
logger.info("Seeding database with defaults...") logger.info("Seeding database with defaults...")
from .seed import seed_database from .seed import seed_database
from .auth import create_default_admin seed_database()
fresh_db = SessionLocal()
try:
# Create default admin user first (normally done at startup)
create_default_admin(fresh_db)
# Then seed other test data
seed_database(fresh_db)
fresh_db.commit()
finally:
fresh_db.close()
results["seeded"] = True results["seeded"] = True
logger.warning(f"Factory reset completed by {admin_username}") logger.warning(f"Factory reset completed by {current_user.username}")
return { return {
"status": "success", "status": "success",