Compare commits
9 Commits
16140402c1
...
fix/factor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0bb3ed569 | ||
|
|
1ac75e1017 | ||
|
|
693613f111 | ||
|
|
9da4ae8c0d | ||
|
|
7ffdc64364 | ||
|
|
6abc0c88b0 | ||
|
|
e96dc5cde8 | ||
|
|
cba5bac383 | ||
|
|
535280a783 |
@@ -154,6 +154,7 @@ 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:
|
||||||
@@ -162,10 +163,13 @@ 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")
|
||||||
@@ -173,37 +177,50 @@ reset_stage:
|
|||||||
|
|
||||||
print(f"=== Resetting stage environment at {BASE_URL} ===")
|
print(f"=== Resetting stage environment at {BASE_URL} ===")
|
||||||
|
|
||||||
client = httpx.Client(base_url=BASE_URL, timeout=60.0)
|
def do_reset():
|
||||||
|
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")
|
||||||
|
|
||||||
# Login as admin
|
# Call factory reset endpoint
|
||||||
print("Logging in as admin...")
|
print("Calling factory reset endpoint...")
|
||||||
login_response = client.post(
|
reset_response = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/admin/factory-reset",
|
||||||
json={"username": ADMIN_USER, "password": ADMIN_PASS},
|
headers={"X-Confirm-Reset": "yes-delete-all-data"},
|
||||||
)
|
)
|
||||||
if login_response.status_code != 200:
|
|
||||||
print(f"Login failed: {login_response.status_code} - {login_response.text}")
|
|
||||||
sys.exit(1)
|
|
||||||
print("Login successful")
|
|
||||||
|
|
||||||
# Call factory reset endpoint
|
if reset_response.status_code == 200:
|
||||||
print("Calling factory reset endpoint...")
|
result = reset_response.json()
|
||||||
reset_response = client.post(
|
print("Factory reset successful!")
|
||||||
"/api/v1/admin/factory-reset",
|
print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
|
||||||
headers={"X-Confirm-Reset": "yes-delete-all-data"},
|
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
|
||||||
)
|
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}")
|
||||||
|
|
||||||
if reset_response.status_code == 200:
|
# Retry loop
|
||||||
result = reset_response.json()
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
print(f"Factory reset successful!")
|
try:
|
||||||
print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
|
print(f"Attempt {attempt}/{MAX_RETRIES}")
|
||||||
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
|
if do_reset():
|
||||||
print(f" Database reinitialized: {result['results']['database_reinitialized']}")
|
sys.exit(0)
|
||||||
print(f" Seeded: {result['results']['seeded']}")
|
except Exception as e:
|
||||||
sys.exit(0)
|
print(f"Attempt {attempt} failed: {e}")
|
||||||
else:
|
if attempt < MAX_RETRIES:
|
||||||
print(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
|
print(f"Retrying in {RETRY_DELAY} seconds...")
|
||||||
sys.exit(1)
|
time.sleep(RETRY_DELAY)
|
||||||
|
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"'
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# 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
|
||||||
|
|||||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -28,6 +28,16 @@ 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)
|
||||||
|
|||||||
@@ -6427,7 +6427,9 @@ 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'",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.warning(f"Factory reset initiated by admin user: {current_user.username}")
|
# Capture username before we drop tables (user object will become invalid)
|
||||||
|
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,
|
||||||
@@ -6472,17 +6474,26 @@ 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
|
from .database import init_db, SessionLocal
|
||||||
init_db()
|
init_db()
|
||||||
results["database_reinitialized"] = True
|
results["database_reinitialized"] = True
|
||||||
|
|
||||||
# Step 4: Re-seed with default data
|
# Step 4: Re-seed with default data (need fresh session after schema recreate)
|
||||||
logger.info("Seeding database with defaults...")
|
logger.info("Seeding database with defaults...")
|
||||||
from .seed import seed_database
|
from .seed import seed_database
|
||||||
seed_database()
|
from .auth import create_default_admin
|
||||||
|
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 {current_user.username}")
|
logger.warning(f"Factory reset completed by {admin_username}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
Reference in New Issue
Block a user