Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/helm-install-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ jobs:
--set externalRedis.url="redis://redis:6379/0" \
--set externalRedis.existingSecret=redis-secret \
--set externalRedis.passwordKey=redis-password \
--set admin.enabled=true \
--set replicaCount=3 \
--set resources.limits.cpu=100m \
--set resources.requests.cpu=50m \
Expand Down Expand Up @@ -230,6 +231,67 @@ jobs:
echo "✓ All smoke tests passed!"
'

- name: Verify admin dashboard
run: |
set -euo pipefail
ENDPOINT="http://s3proxy-python:4433"
BUCKET="admin-ci"
KEY="admin-ci.txt"

echo "=== Upload an object through the proxy (so the dashboard has traffic) ==="
kubectl run admin-ci-upload -n s3proxy --rm -i --restart=Never \
--image=amazon/aws-cli:latest \
--env="AWS_ACCESS_KEY_ID=minioadmin" \
--env="AWS_SECRET_ACCESS_KEY=minioadmin" \
--env="AWS_DEFAULT_REGION=us-east-1" \
--command -- /bin/sh -c "
set -e
echo 'admin dashboard ci object' > /tmp/o.txt
aws --endpoint-url $ENDPOINT s3 mb s3://$BUCKET 2>/dev/null || true
aws --endpoint-url $ENDPOINT s3 cp /tmp/o.txt s3://$BUCKET/$KEY
aws --endpoint-url $ENDPOINT s3 cp s3://$BUCKET/$KEY /tmp/d.txt
test \"\$(cat /tmp/d.txt)\" = 'admin dashboard ci object'
echo UPLOAD_OK
"

echo "=== Port-forward the admin dashboard ==="
kubectl port-forward svc/s3proxy-python 4433:4433 -n s3proxy &
PF_PID=$!
trap "kill $PF_PID 2>/dev/null || true" EXIT
for i in $(seq 1 15); do curl -sf -o /dev/null http://localhost:4433/admin/login && break; sleep 1; done

echo "=== 1. Login page is served (unauthenticated) ==="
curl -sf http://localhost:4433/admin/login | grep -q "Sign in to the admin dashboard"
echo "✓ Login page served"

echo "=== 2. Auth is enforced ==="
CODE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4433/admin/api/status)
[ "$CODE" = "401" ] || { echo "✗ status API without auth returned $CODE (expected 401)"; exit 1; }
# The AWS secret key must NOT work as a dashboard password (fallback removed).
CODE=$(curl -s -o /dev/null -w '%{http_code}' -u minioadmin:minioadmin http://localhost:4433/admin/api/status)
[ "$CODE" = "401" ] || { echo "✗ AWS creds accepted as admin login ($CODE) — fallback not removed"; exit 1; }
echo "✓ Unauthenticated + AWS-cred requests rejected (401)"

echo "=== 3. Dashboard + status API work with admin credentials (admin/admin) ==="
CODE=$(curl -s -o /dev/null -w '%{http_code}' -u admin:admin http://localhost:4433/admin/)
[ "$CODE" = "200" ] || { echo "✗ dashboard returned $CODE (expected 200)"; exit 1; }
STATUS=$(curl -sf -u admin:admin http://localhost:4433/admin/api/status)
echo "$STATUS" | grep -q '"status":"Running"' || { echo "✗ status not Running: $STATUS"; exit 1; }
echo "✓ Dashboard + status API healthy"

echo "=== 4. Bucket listing API shows the uploaded object ==="
BUCKETS=$(curl -sf -u admin:admin "http://localhost:4433/admin/api/buckets/$BUCKET")
echo "$BUCKETS" | grep -q "\"$KEY\"" || { echo "✗ '$KEY' not in bucket listing: $BUCKETS"; exit 1; }
echo "✓ Bucket listing shows '$KEY'"

echo "=== 5. Object detail API reports encryption ==="
OBJ=$(curl -sf -u admin:admin "http://localhost:4433/admin/api/objects/$BUCKET/$KEY")
echo "$OBJ" | grep -q '"encrypted":true' || { echo "✗ object detail not encrypted: $OBJ"; exit 1; }
echo "✓ Object detail reports encrypted"

echo ""
echo "✓ Admin dashboard verified end-to-end"

- name: Show logs on failure
if: failure()
run: |
Expand Down
12 changes: 12 additions & 0 deletions chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ helm install s3proxy oci://ghcr.io/serversidehannes/s3proxy-python/charts/s3prox
| `secrets.existingSecrets.keys.encryptKey` | `S3PROXY_ENCRYPT_KEY` | Key name in existing secret |
| `secrets.existingSecrets.keys.awsAccessKeyId` | `AWS_ACCESS_KEY_ID` | Key name in existing secret |
| `secrets.existingSecrets.keys.awsSecretAccessKey` | `AWS_SECRET_ACCESS_KEY` | Key name in existing secret |
| `admin.enabled` | `false` | Enable the admin dashboard |
| `admin.path` | `/admin` | URL path prefix for the dashboard |
| `admin.username` | `admin` | Dashboard username (stored in the Secret; override in production) |
| `admin.password` | `admin` | Dashboard password (stored in the Secret; override in production) |
| `admin.existingSecret.name` | `""` | Pre-created secret holding admin credentials |
| `admin.existingSecret.usernameKey` | `S3PROXY_ADMIN_USERNAME` | Username key in the existing secret |
| `admin.existingSecret.passwordKey` | `S3PROXY_ADMIN_PASSWORD` | Password key in the existing secret |
| `admin.ingress.enabled` | `false` | Dedicated Ingress for the dashboard (keep off unless intentionally exposing it) |
| `admin.ingress.className` | `nginx` | Ingress class for the admin Ingress |
| `admin.ingress.host` | `""` | Hostname for the dashboard (required when enabled) |
| `admin.ingress.annotations` | `{}` | Annotations (e.g. IP allowlist) for the admin Ingress |
| `admin.ingress.tls` | `[]` | TLS config for the admin Ingress |
| `redis-ha.enabled` | `true` | Deploy embedded Redis HA |
| `redis-ha.replicas` | `1` | Redis replicas |
| `redis-ha.auth` | `false` | Enable Redis auth |
Expand Down
38 changes: 38 additions & 0 deletions chart/templates/admin-ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{{- if and .Values.admin.enabled .Values.admin.ingress.enabled -}}
{{- if not .Values.admin.ingress.host }}
{{- fail "admin.ingress.enabled is true but admin.ingress.host is empty — set a hostname for the admin dashboard" }}
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Chart.Name }}-admin
labels:
{{- include "s3proxy.labels" . | nindent 4 }}
{{- with .Values.admin.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.admin.ingress.className }}
{{- with .Values.admin.ingress.tls }}
tls:
{{- range . }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: {{ .Values.admin.ingress.host | quote }}
http:
paths:
- path: {{ .Values.admin.path }}
pathType: Prefix
backend:
service:
name: {{ .Chart.Name }}
port:
number: {{ .Values.service.port }}
{{- end }}
4 changes: 4 additions & 0 deletions chart/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ data:
{{- end }}
S3PROXY_REDIS_UPLOAD_TTL_HOURS: {{ .Values.externalRedis.uploadTtlHours | quote }}
S3PROXY_LOG_LEVEL: {{ .Values.logLevel | quote }}
{{- if .Values.admin.enabled }}
S3PROXY_ADMIN_UI: "true"
S3PROXY_ADMIN_PATH: {{ .Values.admin.path | quote }}
{{- end }}
21 changes: 20 additions & 1 deletion chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ spec:
- secretRef:
name: {{ printf "%s-secrets" .Chart.Name }}
{{- end }}
{{- /* Admin creds need an explicit env block only when they are NOT already
pulled in via the chart-secret envFrom above (i.e. an admin existingSecret
is set, or the app uses an existing secret so envFrom never fires). */ -}}
{{- $adminFromChartSecret := and .Values.admin.enabled (not .Values.admin.existingSecret.name) (not .Values.secrets.existingSecrets.enabled) }}
{{- $adminNeedsEnv := and .Values.admin.enabled (not $adminFromChartSecret) }}
{{- /* Determine if we need env section */ -}}
{{- $needsEnv := or .Values.secrets.existingSecrets.enabled (and (index .Values "redis-ha" "enabled") (index .Values "redis-ha" "auth")) (and (not (index .Values "redis-ha" "enabled")) .Values.externalRedis.existingSecret) }}
{{- $needsEnv := or .Values.secrets.existingSecrets.enabled $adminNeedsEnv (and (index .Values "redis-ha" "enabled") (index .Values "redis-ha" "auth")) (and (not (index .Values "redis-ha" "enabled")) .Values.externalRedis.existingSecret) }}
{{- if $needsEnv }}
env:
{{- /* App secrets from existing secret */ -}}
Expand All @@ -51,6 +56,20 @@ spec:
name: {{ .Values.secrets.existingSecrets.name }}
key: {{ .Values.secrets.existingSecrets.keys.awsSecretAccessKey }}
{{- end }}
{{- /* Admin dashboard credentials (when not pulled via the chart-secret envFrom) */ -}}
{{- if $adminNeedsEnv }}
{{- $adminSecret := .Values.admin.existingSecret.name | default (printf "%s-secrets" .Chart.Name) }}
- name: S3PROXY_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: {{ $adminSecret }}
key: {{ .Values.admin.existingSecret.usernameKey }}
- name: S3PROXY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: {{ $adminSecret }}
key: {{ .Values.admin.existingSecret.passwordKey }}
{{- end }}
{{- /* Redis password from redis-ha secret */ -}}
{{- if and (index .Values "redis-ha" "enabled") (index .Values "redis-ha" "auth") }}
- name: S3PROXY_REDIS_PASSWORD
Expand Down
13 changes: 11 additions & 2 deletions chart/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{{- if not .Values.secrets.existingSecrets.enabled }}
{{- $createApp := not .Values.secrets.existingSecrets.enabled -}}
{{- $createAdmin := and .Values.admin.enabled (not .Values.admin.existingSecret.name) -}}
{{- if or $createApp $createAdmin }}
# Creating static secrets (provide via helm --set or secure values file, not hardcoded)
# Secret keys created: S3PROXY_ENCRYPT_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
# Secret keys created: S3PROXY_ENCRYPT_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
# and (when the admin dashboard is enabled) S3PROXY_ADMIN_USERNAME / S3PROXY_ADMIN_PASSWORD
apiVersion: v1
kind: Secret
metadata:
Expand All @@ -9,7 +12,13 @@ metadata:
{{- include "s3proxy.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- if $createApp }}
S3PROXY_ENCRYPT_KEY: {{ .Values.secrets.encryptKey | quote }}
AWS_ACCESS_KEY_ID: {{ .Values.secrets.awsAccessKeyId | quote }}
AWS_SECRET_ACCESS_KEY: {{ .Values.secrets.awsSecretAccessKey | quote }}
{{- end }}
{{- if $createAdmin }}
S3PROXY_ADMIN_USERNAME: {{ .Values.admin.username | quote }}
S3PROXY_ADMIN_PASSWORD: {{ .Values.admin.password | quote }}
{{- end }}
{{- end }}
24 changes: 24 additions & 0 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ secrets:

logLevel: "DEBUG"

# Admin dashboard — served on the main service port at `admin.path`.
# Credentials are stored in a Secret. Override username/password in production,
# or point existingSecret at your own Secret holding the credential keys below.
admin:
enabled: false
path: "/admin"
username: "admin"
password: "admin"
existingSecret:
name: ""
usernameKey: "S3PROXY_ADMIN_USERNAME"
passwordKey: "S3PROXY_ADMIN_PASSWORD"

# Optional dedicated Ingress for the dashboard, separate from the S3 data-plane
# ingress above. OFF by default — the admin UI should not be internet-facing
# unless you explicitly expose it (ideally behind an internal ingress class,
# an IP allowlist, or VPN). Routes `admin.path` to the proxy service.
ingress:
enabled: false
className: "nginx"
host: ""
annotations: {}
tls: [] # e.g. [{ secretName: admin-tls, hosts: [admin.example.com] }]

resources:
requests:
cpu: "100m"
Expand Down
4 changes: 3 additions & 1 deletion e2e/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ services:
BUILD_PID=$$!

# Build esrally image with dependencies pre-installed (parallel)
# Pinned to 3.12 because yappi (esrally dep) has no 3.14 wheel yet
(
printf '%s\n' \
'FROM python:3.14-slim' \
'FROM python:3.12-slim' \
'RUN apt-get update && apt-get install -y --no-install-recommends gcc python3-dev git curl && rm -rf /var/lib/apt/lists/*' \
'RUN pip install --no-cache-dir esrally' \
| docker build -t esrally:latest -f - /tmp
Expand Down Expand Up @@ -419,6 +420,7 @@ services:
--set performance.memoryLimitMb=64 \
--set gateway.enabled=true \
--set ingress.enabled=true \
--set admin.enabled=true \
--set 'ingress.annotations.nginx\.ingress\.kubernetes\.io/proxy-body-size=256m' \
--set redis-ha.enabled=true \
--set redis-ha.persistentVolume.enabled=true \
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]

[tool.ruff.lint.per-file-ignores]
# Inline HTML/CSS/JS template has unavoidably long lines
"s3proxy/admin/templates.py" = ["E501"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
Expand Down
6 changes: 6 additions & 0 deletions s3proxy/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Admin dashboard for S3Proxy."""

from .collectors import record_request
from .router import create_admin_router

__all__ = ["create_admin_router", "record_request"]
Loading
Loading