4 Commits

Author SHA1 Message Date
Mondo Diaz
996e3ee4ce Fix concurrent upload race conditions and bump dev resources
- Add IntegrityError handling for concurrent artifact creation
- Add IntegrityError handling for concurrent tag creation with retry
- Add with_for_update() lock on tag lookup to prevent races
- Add database pool config env vars to Helm deployment template
- Bump dev environment resources (256Mi -> 512Mi memory for all services)
- Increase database pool settings for dev (10 connections, 20 overflow)
2026-01-27 21:14:38 +00:00
Mondo Diaz
aa853b5b32 Use CI variable for stage admin password
- Remove Secrets Manager config from values-stage.yaml
- Pass STAGE_ADMIN_PASSWORD via --set in deploy_stage
- Consistent with feature branch approach

Single source of truth: STAGE_ADMIN_PASSWORD CI variable is used by
deploy, reset, and integration test jobs.
2026-01-27 20:36:21 +00:00
Mondo Diaz
fe07638485 Merge branch 'feature/admin-password-env-var' into 'main'
Add configurable admin password via environment variable

Closes #87

See merge request esv/bsf/bsf-integration/orchard/orchard-mvp!46
2026-01-27 14:23:41 -06:00
Mondo Diaz
7120cf64f1 Add configurable admin password via environment variable 2026-01-27 14:23:40 -06:00
18 changed files with 378 additions and 60 deletions

7
.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Orchard Local Development Environment
# Copy this file to .env and customize as needed
# Note: .env is gitignored and will not be committed
# Admin account password (required for local development)
# This sets the initial admin password when the database is first created
ORCHARD_ADMIN_PASSWORD=changeme123

View File

@@ -15,6 +15,7 @@ variables:
STAGE_RDS_HOST: orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com
STAGE_RDS_DBNAME: postgres
STAGE_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:rds!cluster-a573672b-1a38-4665-a654-1b7df37b5297-IaeFQL"
STAGE_AUTH_SECRET_ARN: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orchard-stage-creds-SMqvQx"
STAGE_S3_BUCKET: orchard-artifacts-stage
AWS_REGION: us-gov-west-1
# Shared pip cache directory
@@ -117,6 +118,9 @@ release:
- pip install --index-url "$PIP_INDEX_URL" pytest pytest-asyncio httpx
script:
- cd backend
# Debug: Print environment variables for test configuration
- echo "ORCHARD_TEST_URL=$ORCHARD_TEST_URL"
- echo "ORCHARD_TEST_PASSWORD is set to '${ORCHARD_TEST_PASSWORD:-NOT SET}'"
# Run full integration test suite, excluding:
# - large/slow tests
# - requires_direct_s3 tests (can't access MinIO from outside K8s cluster)
@@ -196,14 +200,13 @@ release:
sys.exit(0)
PYTEST_SCRIPT
# Integration tests for stage deployment (full suite)
# Reset stage template - shared by pre and post test reset jobs
# Reset stage template - runs from CI runner, uses CI variable for auth
# Calls the /api/v1/admin/factory-reset endpoint which handles DB and S3 cleanup
.reset_stage_template: &reset_stage_template
stage: deploy
image: deps.global.bsf.tools/docker/python:3.12-slim
timeout: 5m
retry: 1 # Retry once on transient failures
retry: 1
before_script:
- pip install --index-url "$PIP_INDEX_URL" httpx
script:
@@ -216,19 +219,22 @@ release:
BASE_URL = os.environ.get("STAGE_URL", "")
ADMIN_USER = "admin"
ADMIN_PASS = "changeme123" # Default admin password
ADMIN_PASS = os.environ.get("STAGE_ADMIN_PASSWORD", "")
MAX_RETRIES = 3
RETRY_DELAY = 5 # seconds
RETRY_DELAY = 5
if not BASE_URL:
print("ERROR: STAGE_URL environment variable not set")
print("ERROR: STAGE_URL not set")
sys.exit(1)
if not ADMIN_PASS:
print("ERROR: STAGE_ADMIN_PASSWORD not set")
sys.exit(1)
print(f"=== Resetting stage environment at {BASE_URL} ===")
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",
@@ -238,7 +244,6 @@ release:
raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}")
print("Login successful")
# Call factory reset endpoint
print("Calling factory reset endpoint...")
reset_response = client.post(
"/api/v1/admin/factory-reset",
@@ -256,7 +261,6 @@ release:
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}")
@@ -280,12 +284,14 @@ reset_stage_pre:
<<: *reset_stage_template
needs: [deploy_stage]
# Integration tests for stage deployment (full suite)
# Integration tests for stage deployment
# Uses CI variable STAGE_ADMIN_PASSWORD (set in GitLab CI/CD settings)
integration_test_stage:
<<: *integration_test_template
needs: [reset_stage_pre]
variables:
ORCHARD_TEST_URL: $STAGE_URL
ORCHARD_TEST_PASSWORD: $STAGE_ADMIN_PASSWORD
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: on_success
@@ -297,11 +303,13 @@ reset_stage:
allow_failure: true # Don't fail pipeline if reset has issues
# Integration tests for feature deployment (full suite)
# Uses DEV_ADMIN_PASSWORD CI variable (same as deploy_feature)
integration_test_feature:
<<: *integration_test_template
needs: [deploy_feature]
variables:
ORCHARD_TEST_URL: https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools
ORCHARD_TEST_PASSWORD: $DEV_ADMIN_PASSWORD
rules:
- if: '$CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "main"'
when: on_success
@@ -422,6 +430,7 @@ deploy_stage:
--namespace $NAMESPACE \
-f $VALUES_FILE \
--set image.tag=git.linux-amd64-$CI_COMMIT_SHA \
--set orchard.auth.adminPassword=$STAGE_ADMIN_PASSWORD \
--wait \
--atomic \
--timeout 10m
@@ -453,6 +462,7 @@ deploy_feature:
--namespace $NAMESPACE \
-f $VALUES_FILE \
--set image.tag=git.linux-amd64-$CI_COMMIT_SHA \
--set orchard.auth.adminPassword=$DEV_ADMIN_PASSWORD \
--set ingress.hosts[0].host=orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \
--set ingress.tls[0].hosts[0]=orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \
--set ingress.tls[0].secretName=orchard-$CI_COMMIT_REF_SLUG-tls \

View File

@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added `ORCHARD_ADMIN_PASSWORD` environment variable to configure initial admin password (#87)
- When set, admin user is created with the specified password (no password change required)
- When not set, defaults to `changeme123` and requires password change on first login
- Added Helm chart support for admin password via multiple sources (#87):
- `orchard.auth.adminPassword` - plain value (creates K8s secret)
- `orchard.auth.existingSecret` - reference existing K8s secret
- `orchard.auth.secretsManager` - AWS Secrets Manager integration
- Added `.env.example` template for local development (#87)
- Added `.env` file support in docker-compose.local.yml (#87)
- Added Project Settings page accessible to project admins (#65)
- General settings section for editing description and visibility
- Access Management section (moved from project page)

View File

@@ -360,21 +360,36 @@ def create_default_admin(db: Session) -> Optional[User]:
"""Create the default admin user if no users exist.
Returns the created user, or None if users already exist.
The admin password can be set via ORCHARD_ADMIN_PASSWORD environment variable.
If not set, defaults to 'changeme123' and requires password change on first login.
"""
# Check if any users exist
user_count = db.query(User).count()
if user_count > 0:
return None
settings = get_settings()
# Use configured password or default
password = settings.admin_password if settings.admin_password else "changeme123"
# Only require password change if using the default password
must_change = not settings.admin_password
# Create default admin
auth_service = AuthService(db)
admin = auth_service.create_user(
username="admin",
password="changeme123",
password=password,
is_admin=True,
must_change_password=True,
must_change_password=must_change,
)
if settings.admin_password:
logger.info("Created default admin user with configured password")
else:
logger.info("Created default admin user with default password (changeme123)")
return admin

View File

@@ -53,6 +53,9 @@ class Settings(BaseSettings):
log_level: str = "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL
log_format: str = "auto" # "json", "standard", or "auto" (json in production)
# Initial admin user settings
admin_password: str = "" # Initial admin password (if empty, uses 'changeme123')
# JWT Authentication settings (optional, for external identity providers)
jwt_enabled: bool = False # Enable JWT token validation
jwt_secret: str = "" # Secret key for HS256, or leave empty for RS256 with JWKS

View File

@@ -406,14 +406,21 @@ def _create_or_update_tag(
"""
Create or update a tag, handling ref_count and history.
Uses SELECT FOR UPDATE to prevent race conditions during concurrent uploads.
Returns:
tuple of (tag, is_new, old_artifact_id)
- tag: The created/updated Tag object
- is_new: True if tag was created, False if updated
- old_artifact_id: Previous artifact_id if tag was updated, None otherwise
"""
# Use with_for_update() to lock the row and prevent race conditions
# during concurrent uploads to the same tag
existing_tag = (
db.query(Tag).filter(Tag.package_id == package_id, Tag.name == tag_name).first()
db.query(Tag)
.filter(Tag.package_id == package_id, Tag.name == tag_name)
.with_for_update()
.first()
)
if existing_tag:
@@ -447,7 +454,9 @@ def _create_or_update_tag(
# Same artifact, no change needed
return existing_tag, False, None
else:
# Create new tag
# Create new tag with race condition handling
from sqlalchemy.exc import IntegrityError
new_tag = Tag(
package_id=package_id,
name=tag_name,
@@ -455,7 +464,15 @@ def _create_or_update_tag(
created_by=user_id,
)
db.add(new_tag)
db.flush() # Get the tag ID
try:
db.flush() # Get the tag ID - may fail if concurrent insert happened
except IntegrityError:
# Another request created the tag concurrently
# Rollback the failed insert and retry as update
db.rollback()
logger.info(f"Tag '{tag_name}' created concurrently, retrying as update")
return _create_or_update_tag(db, package_id, tag_name, new_artifact_id, user_id)
# Record history for creation
history = TagHistory(
@@ -2608,6 +2625,8 @@ def upload_artifact(
# Create new artifact with ref_count=0
# NOTE: ref_count is managed by SQL triggers on tag INSERT/DELETE
# When a tag is created for this artifact, the trigger will increment ref_count
from sqlalchemy.exc import IntegrityError
artifact = Artifact(
id=storage_result.sha256,
size=storage_result.size,
@@ -2623,6 +2642,22 @@ def upload_artifact(
)
db.add(artifact)
# Flush to detect concurrent artifact creation race condition
try:
db.flush()
except IntegrityError:
# Another request created the artifact concurrently - fetch and use it
db.rollback()
logger.info(f"Artifact {storage_result.sha256[:12]}... created concurrently, fetching existing")
artifact = db.query(Artifact).filter(Artifact.id == storage_result.sha256).first()
if not artifact:
raise HTTPException(
status_code=500,
detail="Failed to create or fetch artifact record",
)
deduplicated = True
saved_bytes = storage_result.size
# Calculate upload duration
duration_ms = int((time.time() - start_time) * 1000)

View File

@@ -56,6 +56,26 @@ os.environ.setdefault("ORCHARD_S3_BUCKET", "test-bucket")
os.environ.setdefault("ORCHARD_S3_ACCESS_KEY_ID", "test")
os.environ.setdefault("ORCHARD_S3_SECRET_ACCESS_KEY", "test")
# =============================================================================
# Admin Credentials Helper
# =============================================================================
def get_admin_password() -> str:
"""Get the admin password for test authentication.
Returns the password from ORCHARD_TEST_PASSWORD environment variable,
or 'changeme123' as the default for local development.
"""
return os.environ.get("ORCHARD_TEST_PASSWORD", "changeme123")
def get_admin_username() -> str:
"""Get the admin username for test authentication."""
return os.environ.get("ORCHARD_TEST_USERNAME", "admin")
# Re-export factory functions for backward compatibility
from tests.factories import (
create_test_file,

View File

@@ -8,6 +8,8 @@ allow these tests to run. Production uses strict rate limits (5/minute).
import pytest
from uuid import uuid4
from tests.conftest import get_admin_password, get_admin_username
# Mark all tests in this module as auth_intensive (informational, not excluded from CI)
pytestmark = pytest.mark.auth_intensive
@@ -21,11 +23,11 @@ class TestAuthLogin:
"""Test successful login with default admin credentials."""
response = auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
assert response.status_code == 200
data = response.json()
assert data["username"] == "admin"
assert data["username"] == get_admin_username()
assert data["is_admin"] is True
assert "orchard_session" in response.cookies
@@ -34,7 +36,7 @@ class TestAuthLogin:
"""Test login with wrong password."""
response = auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "wrongpassword"},
json={"username": get_admin_username(), "password": "wrongpassword"},
)
assert response.status_code == 401
assert "Invalid username or password" in response.json()["detail"]
@@ -58,7 +60,7 @@ class TestAuthLogout:
# First login
login_response = auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
assert login_response.status_code == 200
@@ -84,13 +86,13 @@ class TestAuthMe:
# Login first
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
response = auth_client.get("/api/v1/auth/me")
assert response.status_code == 200
data = response.json()
assert data["username"] == "admin"
assert data["username"] == get_admin_username()
assert data["is_admin"] is True
assert "id" in data
assert "created_at" in data
@@ -119,7 +121,7 @@ class TestAuthChangePassword:
# Login as admin to create a test user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"pwchange_{uuid4().hex[:8]}"
auth_client.post(
@@ -162,7 +164,7 @@ class TestAuthChangePassword:
# Login as admin to create a test user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"pwwrong_{uuid4().hex[:8]}"
auth_client.post(
@@ -194,7 +196,7 @@ class TestAPIKeys:
# Login first
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
# Create API key
@@ -226,7 +228,7 @@ class TestAPIKeys:
# Login and create API key
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
create_response = auth_client.post(
"/api/v1/auth/keys",
@@ -242,12 +244,12 @@ class TestAPIKeys:
headers={"Authorization": f"Bearer {api_key}"},
)
assert response.status_code == 200
assert response.json()["username"] == "admin"
assert response.json()["username"] == get_admin_username()
# Clean up
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
auth_client.delete(f"/api/v1/auth/keys/{key_id}")
@@ -257,7 +259,7 @@ class TestAPIKeys:
# Login and create API key
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
create_response = auth_client.post(
"/api/v1/auth/keys",
@@ -288,14 +290,14 @@ class TestAdminUserManagement:
# Login as admin
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
response = auth_client.get("/api/v1/admin/users")
assert response.status_code == 200
users = response.json()
assert len(users) >= 1
assert any(u["username"] == "admin" for u in users)
assert any(u["username"] == get_admin_username() for u in users)
@pytest.mark.integration
def test_create_user(self, auth_client):
@@ -303,7 +305,7 @@ class TestAdminUserManagement:
# Login as admin
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
# Create new user
@@ -336,7 +338,7 @@ class TestAdminUserManagement:
# Login as admin
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
# Create a test user
@@ -362,7 +364,7 @@ class TestAdminUserManagement:
# Login as admin
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
# Create a test user
@@ -393,7 +395,7 @@ class TestAdminUserManagement:
# Login as admin and create non-admin user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"nonadmin_{uuid4().hex[:8]}"
auth_client.post(
@@ -423,7 +425,7 @@ class TestSecurityEdgeCases:
# Login as admin and create a user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"inactive_{uuid4().hex[:8]}"
auth_client.post(
@@ -451,7 +453,7 @@ class TestSecurityEdgeCases:
"""Test that short passwords are rejected when creating users."""
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
response = auth_client.post(
@@ -467,7 +469,7 @@ class TestSecurityEdgeCases:
# Create test user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"shortchange_{uuid4().hex[:8]}"
auth_client.post(
@@ -494,7 +496,7 @@ class TestSecurityEdgeCases:
"""Test that short passwords are rejected when resetting password."""
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
# Create a test user first
@@ -516,7 +518,7 @@ class TestSecurityEdgeCases:
"""Test that duplicate usernames are rejected."""
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"duplicate_{uuid4().hex[:8]}"
@@ -541,7 +543,7 @@ class TestSecurityEdgeCases:
# Login as admin and create an API key
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
create_response = auth_client.post(
"/api/v1/auth/keys",
@@ -572,7 +574,7 @@ class TestSecurityEdgeCases:
auth_client.cookies.clear()
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
auth_client.delete(f"/api/v1/auth/keys/{admin_key_id}")
@@ -582,7 +584,7 @@ class TestSecurityEdgeCases:
# Create a test user
auth_client.post(
"/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"},
json={"username": get_admin_username(), "password": get_admin_password()},
)
test_username = f"sessiontest_{uuid4().hex[:8]}"
auth_client.post(

View File

@@ -0,0 +1,95 @@
"""Unit tests for authentication module."""
import pytest
from unittest.mock import patch, MagicMock
class TestCreateDefaultAdmin:
"""Tests for the create_default_admin function."""
def test_create_default_admin_with_env_password(self):
"""Test that ORCHARD_ADMIN_PASSWORD env var sets admin password."""
from app.auth import create_default_admin, verify_password
# Create mock settings with custom password
mock_settings = MagicMock()
mock_settings.admin_password = "my-custom-password-123"
# Mock database session
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0 # No existing users
# Track the user that gets created
created_user = None
def capture_user(user):
nonlocal created_user
created_user = user
mock_db.add.side_effect = capture_user
with patch("app.auth.get_settings", return_value=mock_settings):
admin = create_default_admin(mock_db)
# Verify the user was created
assert mock_db.add.called
assert created_user is not None
assert created_user.username == "admin"
assert created_user.is_admin is True
# Password should NOT require change when set via env var
assert created_user.must_change_password is False
# Verify password was hashed correctly
assert verify_password("my-custom-password-123", created_user.password_hash)
def test_create_default_admin_with_default_password(self):
"""Test that default password 'changeme123' is used when env var not set."""
from app.auth import create_default_admin, verify_password
# Create mock settings with empty password (default)
mock_settings = MagicMock()
mock_settings.admin_password = ""
# Mock database session
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 0 # No existing users
# Track the user that gets created
created_user = None
def capture_user(user):
nonlocal created_user
created_user = user
mock_db.add.side_effect = capture_user
with patch("app.auth.get_settings", return_value=mock_settings):
admin = create_default_admin(mock_db)
# Verify the user was created
assert mock_db.add.called
assert created_user is not None
assert created_user.username == "admin"
assert created_user.is_admin is True
# Password SHOULD require change when using default
assert created_user.must_change_password is True
# Verify default password was used
assert verify_password("changeme123", created_user.password_hash)
def test_create_default_admin_skips_when_users_exist(self):
"""Test that no admin is created when users already exist."""
from app.auth import create_default_admin
# Create mock settings
mock_settings = MagicMock()
mock_settings.admin_password = "some-password"
# Mock database session with existing users
mock_db = MagicMock()
mock_db.query.return_value.count.return_value = 1 # Users exist
with patch("app.auth.get_settings", return_value=mock_settings):
result = create_default_admin(mock_db)
# Should return None and not create any user
assert result is None
assert not mock_db.add.called

View File

@@ -26,6 +26,8 @@ services:
- ORCHARD_REDIS_PORT=6379
# Higher rate limit for local development/testing
- ORCHARD_LOGIN_RATE_LIMIT=1000/minute
# Admin password - set in .env file or environment (see .env.example)
- ORCHARD_ADMIN_PASSWORD=${ORCHARD_ADMIN_PASSWORD:-}
depends_on:
postgres:
condition: service_healthy

View File

@@ -141,3 +141,16 @@ MinIO secret name
{{- printf "%s-s3-secret" (include "orchard.fullname" .) }}
{{- end }}
{{- end }}
{{/*
Auth secret name (for admin password)
*/}}
{{- define "orchard.auth.secretName" -}}
{{- if and .Values.orchard.auth .Values.orchard.auth.existingSecret }}
{{- .Values.orchard.auth.existingSecret }}
{{- else if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
{{- printf "%s-auth-credentials" (include "orchard.fullname" .) }}
{{- else }}
{{- printf "%s-auth-secret" (include "orchard.fullname" .) }}
{{- end }}
{{- end }}

View File

@@ -128,11 +128,39 @@ spec:
value: {{ .Values.orchard.rateLimit.login | quote }}
{{- end }}
{{- end }}
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
{{- if .Values.orchard.database.poolSize }}
- name: ORCHARD_DATABASE_POOL_SIZE
value: {{ .Values.orchard.database.poolSize | quote }}
{{- end }}
{{- if .Values.orchard.database.maxOverflow }}
- name: ORCHARD_DATABASE_MAX_OVERFLOW
value: {{ .Values.orchard.database.maxOverflow | quote }}
{{- end }}
{{- if .Values.orchard.database.poolTimeout }}
- name: ORCHARD_DATABASE_POOL_TIMEOUT
value: {{ .Values.orchard.database.poolTimeout | quote }}
{{- end }}
{{- if .Values.orchard.auth }}
{{- if or .Values.orchard.auth.secretsManager .Values.orchard.auth.existingSecret .Values.orchard.auth.adminPassword }}
- name: ORCHARD_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "orchard.auth.secretName" . }}
key: admin-password
{{- end }}
{{- end }}
{{- if or (and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled) (and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled) }}
volumeMounts:
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
- name: db-secrets
mountPath: /mnt/secrets-store
mountPath: /mnt/secrets-store/db
readOnly: true
{{- end }}
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
- name: auth-secrets
mountPath: /mnt/secrets-store/auth
readOnly: true
{{- end }}
{{- end }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
@@ -140,14 +168,24 @@ spec:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
{{- if or (and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled) (and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled) }}
volumes:
{{- if and .Values.orchard.database.secretsManager .Values.orchard.database.secretsManager.enabled }}
- name: db-secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: {{ include "orchard.fullname" . }}-db-secret
{{- end }}
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
- name: auth-secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: {{ include "orchard.fullname" . }}-auth-secret
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:

View File

@@ -25,3 +25,27 @@ spec:
- objectName: db-password
key: password
{{- end }}
---
{{- if and .Values.orchard.auth .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled }}
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: {{ include "orchard.fullname" . }}-auth-secret
labels:
{{- include "orchard.labels" . | nindent 4 }}
spec:
provider: aws
parameters:
objects: |
- objectName: "{{ .Values.orchard.auth.secretsManager.secretArn }}"
objectType: "secretsmanager"
jmesPath:
- path: admin_password
objectAlias: admin-password
secretObjects:
- secretName: {{ include "orchard.fullname" . }}-auth-credentials
type: Opaque
data:
- objectName: admin-password
key: admin-password
{{- end }}

View File

@@ -22,3 +22,15 @@ data:
access-key-id: {{ .Values.orchard.s3.accessKeyId | b64enc | quote }}
secret-access-key: {{ .Values.orchard.s3.secretAccessKey | b64enc | quote }}
{{- end }}
---
{{- if and .Values.orchard.auth .Values.orchard.auth.adminPassword (not .Values.orchard.auth.existingSecret) (not (and .Values.orchard.auth.secretsManager .Values.orchard.auth.secretsManager.enabled)) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "orchard.fullname" . }}-auth-secret
labels:
{{- include "orchard.labels" . | nindent 4 }}
type: Opaque
data:
admin-password: {{ .Values.orchard.auth.adminPassword | b64enc | quote }}
{{- end }}

View File

@@ -53,15 +53,16 @@ ingress:
hosts:
- orchard-dev.common.global.bsf.tools # Overridden by CI
# Lighter resources for ephemeral environments
# Resources for dev/feature environments
# Bumped to handle concurrent integration tests
# Note: memory requests must equal limits per cluster policy
resources:
limits:
cpu: 250m
memory: 256Mi
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
cpu: 200m
memory: 512Mi
livenessProbe:
httpGet:
@@ -90,6 +91,10 @@ orchard:
host: "0.0.0.0"
port: 8080
# Authentication settings
# Admin password is set via CI variable (DEV_ADMIN_PASSWORD) passed as --set flag
# This keeps the password out of version control
database:
host: ""
port: 5432
@@ -99,6 +104,10 @@ orchard:
sslmode: disable
existingSecret: ""
existingSecretPasswordKey: "password"
# Increased pool settings for concurrent integration tests
poolSize: 10
maxOverflow: 20
poolTimeout: 60
s3:
endpoint: ""
@@ -134,15 +143,16 @@ postgresql:
primary:
persistence:
enabled: false
# Resources with memory requests = limits per cluster policy
# Bumped resources for concurrent integration tests
# Note: memory requests must equal limits per cluster policy
resourcesPreset: "none"
resources:
limits:
cpu: 250m
memory: 256Mi
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
cpu: 200m
memory: 512Mi
# Volume permissions init container
volumePermissions:
resourcesPreset: "none"
@@ -168,15 +178,16 @@ minio:
defaultBuckets: "orchard-artifacts"
persistence:
enabled: false
# Resources with memory requests = limits per cluster policy
# Bumped resources for concurrent integration tests
# Note: memory requests must equal limits per cluster policy
resourcesPreset: "none" # Disable preset to use explicit resources
resources:
limits:
cpu: 250m
memory: 256Mi
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
cpu: 200m
memory: 512Mi
# Init container resources
defaultInitContainers:
volumePermissions:

View File

@@ -93,6 +93,13 @@ orchard:
host: "0.0.0.0"
port: 8080
# Authentication settings
auth:
# Admin password from AWS Secrets Manager
secretsManager:
enabled: true
secretArn: "arn:aws-us-gov:secretsmanager:us-gov-west-1:052673043337:secret:orch-prod-creds-0nhqkY"
# Database configuration - uses AWS Secrets Manager via CSI driver
database:
host: "orchard-prd.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com"

View File

@@ -95,6 +95,10 @@ orchard:
host: "0.0.0.0"
port: 8080
# Authentication settings
# Admin password is set via CI variable (STAGE_ADMIN_PASSWORD) passed as --set flag
# This keeps the password out of version control
# Database configuration - uses AWS Secrets Manager via CSI driver
database:
host: "orchard-stage.cluster-cvw3jzjkozoc.us-gov-west-1.rds.amazonaws.com"

View File

@@ -120,6 +120,17 @@ orchard:
mode: "presigned" # presigned, redirect, or proxy
presignedUrlExpiry: 3600 # Presigned URL expiry in seconds
# Authentication settings
auth:
# Option 1: Plain admin password (creates K8s secret)
adminPassword: ""
# Option 2: Use existing K8s secret (must have 'admin-password' key)
existingSecret: ""
# Option 3: AWS Secrets Manager
# secretsManager:
# enabled: false
# secretArn: "" # Secret must have 'admin_password' field
# PostgreSQL subchart configuration
postgresql:
enabled: true