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 51 additions and 156 deletions

View File

@@ -36,68 +36,9 @@ stages:
- analyze - analyze
- deploy - deploy
# Override Prosper template jobs to exclude tag pipelines
# Tags only run deploy_prod and smoke_test_prod (image already built on main)
build_image:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
test_image:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
hadolint:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
kics: kics:
variables: variables:
KICS_CONFIG: kics.config KICS_CONFIG: kics.config
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
secrets:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
app_deps_scan:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
cve_scan:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
app_sbom_analysis:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
cve_sbom_analysis:
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
# Override release job to wait for stage integration tests before creating tag
# This ensures the tag (which triggers prod deploy) is only created after stage passes
release:
needs: [integration_test_stage, changelog]
# Full integration test suite template (for feature/stage deployments) # Full integration test suite template (for feature/stage deployments)
# Runs the complete pytest integration test suite against the deployed environment # Runs the complete pytest integration test suite against the deployed environment
@@ -213,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:
@@ -222,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")
@@ -236,8 +173,8 @@ 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 # Login as admin
print("Logging in as admin...") print("Logging in as admin...")
login_response = client.post( login_response = client.post(
@@ -245,7 +182,8 @@ reset_stage:
json={"username": ADMIN_USER, "password": ADMIN_PASS}, json={"username": ADMIN_USER, "password": ADMIN_PASS},
) )
if login_response.status_code != 200: if login_response.status_code != 200:
raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}") print(f"Login failed: {login_response.status_code} - {login_response.text}")
sys.exit(1)
print("Login successful") print("Login successful")
# Call factory reset endpoint # Call factory reset endpoint
@@ -257,28 +195,14 @@ reset_stage:
if reset_response.status_code == 200: if reset_response.status_code == 200:
result = reset_response.json() result = reset_response.json()
print("Factory reset successful!") print(f"Factory reset successful!")
print(f" Database tables dropped: {result['results']['database_tables_dropped']}") print(f" Database tables dropped: {result['results']['database_tables_dropped']}")
print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}") print(f" S3 objects deleted: {result['results']['s3_objects_deleted']}")
print(f" Database reinitialized: {result['results']['database_reinitialized']}") print(f" Database reinitialized: {result['results']['database_reinitialized']}")
print(f" Seeded: {result['results']['seeded']}") print(f" Seeded: {result['results']['seeded']}")
return True
else:
raise Exception(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
# Retry loop
for attempt in range(1, MAX_RETRIES + 1):
try:
print(f"Attempt {attempt}/{MAX_RETRIES}")
if do_reset():
sys.exit(0) sys.exit(0)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
if attempt < MAX_RETRIES:
print(f"Retrying in {RETRY_DELAY} seconds...")
time.sleep(RETRY_DELAY)
else: else:
print("All retry attempts failed") print(f"Factory reset failed: {reset_response.status_code} - {reset_response.text}")
sys.exit(1) sys.exit(1)
RESET_SCRIPT RESET_SCRIPT
rules: rules:
@@ -328,10 +252,6 @@ python_unit_tests:
coverage_format: cobertura coverage_format: cobertura
path: backend/coverage.xml path: backend/coverage.xml
coverage: '/TOTAL.*\s+(\d+%)/' coverage: '/TOTAL.*\s+(\d+%)/'
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
# Run frontend tests # Run frontend tests
frontend_tests: frontend_tests:
@@ -361,10 +281,6 @@ frontend_tests:
coverage_format: cobertura coverage_format: cobertura
path: frontend/coverage/cobertura-coverage.xml path: frontend/coverage/cobertura-coverage.xml
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
rules:
- if: '$CI_COMMIT_TAG'
when: never
- when: on_success
# Shared deploy configuration # Shared deploy configuration
.deploy_template: &deploy_template .deploy_template: &deploy_template
@@ -492,11 +408,12 @@ cleanup_feature:
# Deploy to production (version tags only) # Deploy to production (version tags only)
deploy_prod: deploy_prod:
stage: deploy stage: deploy
# For tag pipelines, no other jobs run - image was already built when commit was on main # For tag pipelines, most jobs don't run (trusting main was tested)
needs: [] # We only need build_image to have the image available
needs: [build_image]
image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12 image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
variables: variables:
NAMESPACE: orch-namespace NAMESPACE: orch-prod-namespace
VALUES_FILE: helm/orchard/values-prod.yaml VALUES_FILE: helm/orchard/values-prod.yaml
BASE_URL: $PROD_URL BASE_URL: $PROD_URL
before_script: before_script:

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
@@ -16,4 +15,3 @@ bccbc71c13570d14b8b26a11335c45f102fe3072:backend/tests/unit/test_storage.py:gene
08dce6cbb836b687002751fed4159bfc2da61f8b:backend/tests/unit/test_storage.py:generic-api-key:381 08dce6cbb836b687002751fed4159bfc2da61f8b:backend/tests/unit/test_storage.py:generic-api-key:381
617bcbe89cff9a009d77e4f1f1864efed1820e63:backend/tests/unit/test_storage.py:generic-api-key:381 617bcbe89cff9a009d77e4f1f1864efed1820e63:backend/tests/unit/test_storage.py:generic-api-key:381
1cbd33544388e0fe6db752fa8886fab33cf9ce7c:backend/tests/unit/test_storage.py:generic-api-key:381 1cbd33544388e0fe6db752fa8886fab33cf9ce7c:backend/tests/unit/test_storage.py:generic-api-key:381
7cfad28f678f5a5b8b927d694a17b9ba446b7138:backend/tests/unit/test_storage.py:generic-api-key:381

View File

@@ -6,13 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed
- Simplified tag pipeline to only run deploy and smoke tests (image already built on main) (#54)
### Fixed
- Fixed production CI deployment namespace to use correct `orch-namespace` (#54)
## [0.5.0] - 2026-01-23
### Added ### Added
- Added factory reset endpoint `POST /api/v1/admin/factory-reset` for test environment cleanup (#54) - Added factory reset endpoint `POST /api/v1/admin/factory-reset` for test environment cleanup (#54)
- Requires admin authentication and `X-Confirm-Reset: yes-delete-all-data` header - Requires admin authentication and `X-Confirm-Reset: yes-delete-all-data` header
@@ -22,6 +15,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added AWS Secrets Manager CSI driver support for database credentials (#54) - Added AWS Secrets Manager CSI driver support for database credentials (#54)
- Added SecretProviderClass template for Secrets Manager integration (#54) - Added SecretProviderClass template for Secrets Manager integration (#54)
- Added IRSA service account annotations for prod and stage environments (#54) - Added IRSA service account annotations for prod and stage environments (#54)
### Changed
- Configured stage and prod to use AWS RDS instead of PostgreSQL subchart (#54)
- Configured stage and prod to use AWS S3 instead of MinIO subchart (#54)
- Changed prod deployment from manual to automatic on version tags (#54)
- Updated S3 client to support IRSA credentials when no explicit keys provided (#54)
- Changed prod image pullPolicy to Always (#54)
- Added proxy-body-size annotation to prod ingress for large uploads (#54)
### Removed
- Disabled PostgreSQL subchart for stage and prod environments (#54)
- Disabled MinIO subchart for stage and prod environments (#54)
### 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)
- Added data integrity tests (binary, text, unicode, compressed content) (#38) - Added data integrity tests (binary, text, unicode, compressed content) (#38)
@@ -76,12 +83,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added internal proxy configuration for npm, pip, helm, and apt (#51) - Added internal proxy configuration for npm, pip, helm, and apt (#51)
### Changed ### Changed
- Configured stage and prod to use AWS RDS instead of PostgreSQL subchart (#54)
- Configured stage and prod to use AWS S3 instead of MinIO subchart (#54)
- Changed prod deployment from manual to automatic on version tags (#54)
- Updated S3 client to support IRSA credentials when no explicit keys provided (#54)
- Changed prod image pullPolicy to Always (#54)
- Added proxy-body-size annotation to prod ingress for large uploads (#54)
- CI integration tests now run full pytest suite (~350 tests) against deployed environment instead of 3 smoke tests - CI integration tests now run full pytest suite (~350 tests) against deployed environment instead of 3 smoke tests
- CI production deployment uses lightweight smoke tests only (no test data creation in prod) - CI production deployment uses lightweight smoke tests only (no test data creation in prod)
- CI pipeline improvements: shared pip cache, `interruptible` flag on test jobs, retry on integration tests - CI pipeline improvements: shared pip cache, `interruptible` flag on test jobs, retry on integration tests
@@ -102,14 +103,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51) - Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51)
### Fixed ### 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
- Fixed CI integration test rate limiting: added configurable `ORCHARD_LOGIN_RATE_LIMIT` env var, relaxed to 1000/minute for dev/stage - Fixed CI integration test rate limiting: added configurable `ORCHARD_LOGIN_RATE_LIMIT` env var, relaxed to 1000/minute for dev/stage
- Fixed duplicate `TestSecurityEdgeCases` class definition in test_auth_api.py - Fixed duplicate `TestSecurityEdgeCases` class definition in test_auth_api.py
- Fixed integration tests auth: session-scoped client, configurable credentials via env vars, fail-fast on auth errors - Fixed integration tests auth: session-scoped client, configurable credentials via env vars, fail-fast on auth errors
@@ -130,8 +123,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### Removed
- Removed unused `store_streaming()` method from storage.py (#51) - Removed unused `store_streaming()` method from storage.py (#51)
- Disabled PostgreSQL subchart for stage and prod environments (#54)
- Disabled MinIO subchart for stage and prod environments (#54)
## [0.4.0] - 2026-01-12 ## [0.4.0] - 2026-01-12
### Added ### Added

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",